diff options
Diffstat (limited to 'src')
15 files changed, 970 insertions, 40 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index b615238db..5caf50566 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -298,6 +298,7 @@ import at.hannibal2.skyhanni.features.mining.HighlightMiningCommissionMobs import at.hannibal2.skyhanni.features.mining.KingTalismanHelper import at.hannibal2.skyhanni.features.mining.MiningCommissionsBlocksColor import at.hannibal2.skyhanni.features.mining.MiningNotifications +import at.hannibal2.skyhanni.features.mining.TunnelsMaps import at.hannibal2.skyhanni.features.mining.crystalhollows.CrystalHollowsNamesInCore import at.hannibal2.skyhanni.features.mining.crystalhollows.CrystalHollowsWalls import at.hannibal2.skyhanni.features.mining.eventtracker.MiningEventDisplay @@ -610,6 +611,7 @@ class SkyHanniMod { loadModule(DungeonMilestonesDisplay) loadModule(DungeonDeathCounter()) loadModule(DungeonCleanEnd()) + loadModule(TunnelsMaps()) loadModule(DungeonBossMessages()) loadModule(DungeonBossHideDamageSplash()) loadModule(UniqueGiftingOpportunitiesFeatures) diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt index c3b011d0e..5c85c97ec 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt @@ -159,6 +159,17 @@ class ConfigManager { .create() var configDirectory = File("config/skyhanni") + + inline fun <reified T> GsonBuilder.registerTypeAdapter( + crossinline write: (JsonWriter, T) -> Unit, + crossinline read: (JsonReader) -> T, + ): GsonBuilder { + this.registerTypeAdapter(T::class.java, object : TypeAdapter<T>() { + override fun write(out: JsonWriter, value: T) = write(out, value) + override fun read(reader: JsonReader) = read(reader) + }.nullSafe()) + return this + } } val features get() = jsonHolder[ConfigFileType.FEATURES] as Features diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java index b529a7c91..6d8bd06ab 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/MiningConfig.java @@ -48,6 +48,9 @@ public class MiningConfig { public MiningNotificationsConfig notifications = new MiningNotificationsConfig(); @Expose + @Category(name = "Tunnel Maps", desc = "Settings for the Tunnel Maps") + public TunnelMapsConfig tunnelMaps = new TunnelMapsConfig(); + @Expose @ConfigOption(name = "Commissions Blocks Color", desc = "") @Accordion public CommissionsBlocksColorConfig commissionsBlocksColor = new CommissionsBlocksColorConfig(); diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java new file mode 100644 index 000000000..99f36a429 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/TunnelMapsConfig.java @@ -0,0 +1,80 @@ +package at.hannibal2.skyhanni.config.features.mining; + +import at.hannibal2.skyhanni.config.FeatureToggle; +import at.hannibal2.skyhanni.config.core.config.Position; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorColour; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorKeybind; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; +import io.github.notenoughupdates.moulconfig.observer.Property; +import org.lwjgl.input.Keyboard; + +public class TunnelMapsConfig { + + @Expose + @ConfigOption(name = "Enable", desc = "Enables the tunnel maps, which give you a path to any location you want. Open the Inventory to select a destination.") + @ConfigEditorBoolean + @FeatureToggle + public boolean enable = true; + + @ConfigLink(owner = TunnelMapsConfig.class, field = "enable") + public Position position = new Position(20, 20); + + @Expose + @ConfigOption(name = "Auto Commission", desc = "Takes the first collector commission as target when opening the commissions inventory, also works when completing commissions.") + @ConfigEditorBoolean + public boolean autoCommission = false; + + @Expose + @ConfigOption(name = "Campfire Hotkey", desc = "Hotkey to warp to the campfire, if the travel scroll is not unlocked shows a path to the campfire.") + @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE) + public int campfireKey = Keyboard.KEY_NONE; + + @Expose + @ConfigOption(name = "Travel Scroll", desc = "Lets the mod know that you have unlocked the travel scroll to basecamp.") + @ConfigEditorBoolean + public boolean travelScroll = false; + + @Expose + @ConfigOption(name = "Next Spot Hotkey", desc = "Hotkey to select the next spot.") + @ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE) + public int nextSpotHotkey = Keyboard.KEY_NONE; + + @Expose + @ConfigOption(name = "Dynamic Path Colour", desc = "Instead of the selected color use the color of the target as line colour.") + @ConfigEditorBoolean + public boolean dynamicPathColour = true; + + @Expose + @ConfigOption(name = "Path Colour", desc = "The colour for the paths, if the dynamic colour option is turned off.") + @ConfigEditorColour + public String pathColour = "0:255:0:255:0"; + + @Expose + @ConfigOption(name = "Text Size", desc = "Size of the waypoint texts.") + @ConfigEditorSlider(minValue = 0.5f, maxValue = 2.5f, minStep = 0.1f) + public float textSize = 1.0f; + + @Expose + @ConfigOption(name = "Path width", desc = "Size of the path lines.") + @ConfigEditorSlider(minValue = 1f, maxValue = 15f, minStep = 1f) + public float pathWidth = 4.0f; + + @Expose + @ConfigOption(name = "Distance at First", desc = "Shows the distance at the first edge instead of the end.") + @ConfigEditorBoolean + public boolean distanceFirst = false; + + @Expose + @ConfigOption(name = "Compact Gemstone", desc = "Only shows the icon for gemstones in the selection list.") + @ConfigEditorBoolean + public Property<Boolean> compactGemstone = Property.of(false); + + @Expose + @ConfigOption(name = "Exclude Fairy", desc = "Excludes the fairy soul spots from the selection list.") + @ConfigEditorBoolean + public Property<Boolean> excludeFairy = Property.of(false); +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt b/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt index ae5a03878..97039022a 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/EntityMovementData.kt @@ -1,11 +1,16 @@ package at.hannibal2.skyhanni.data import at.hannibal2.skyhanni.events.EntityMoveEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.LorenzWarpEvent import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent +import at.hannibal2.skyhanni.utils.DelayedRun import at.hannibal2.skyhanni.utils.LorenzUtils import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.StringUtils.matches import at.hannibal2.skyhanni.utils.getLorenzVec +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern import net.minecraft.client.Minecraft import net.minecraft.entity.Entity import net.minecraftforge.fml.common.eventhandler.SubscribeEvent @@ -41,6 +46,24 @@ class EntityMovementData { } } + private val warpingPattern by RepoPattern.pattern( + "warping", + "§7Warping...|" + + "§7Warping you to your SkyBlock island...|" + + "§7Warping using transfer token...|" + + "§7Finding player...|" + + "§7Sending a visit request..." + ) + + @SubscribeEvent + fun onChat(event: LorenzChatEvent) { + if (!LorenzUtils.inSkyBlock) return + if (!warpingPattern.matches(event.message)) return + DelayedRun.runNextTick { + LorenzWarpEvent().postAndCatch() + } + } + @SubscribeEvent fun onWorldChange(event: LorenzWorldChangeEvent) { entityLocation.clear() diff --git a/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt new file mode 100644 index 000000000..b6e6f7b8e --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/model/Graph.kt @@ -0,0 +1,180 @@ +package at.hannibal2.skyhanni.data.model + +import at.hannibal2.skyhanni.config.ConfigManager.Companion.registerTypeAdapter +import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.fromJson +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.annotations.Expose +import java.util.PriorityQueue + +@JvmInline +value class Graph( + @Expose + val graph: List<GraphNode>, +) : List<GraphNode> { + override val size + get() = graph.size + + override fun contains(element: GraphNode) = graph.contains(element) + + override fun containsAll(elements: Collection<GraphNode>) = graph.containsAll(elements) + + override fun get(index: Int) = graph.get(index) + + override fun isEmpty() = graph.isEmpty() + + override fun indexOf(element: GraphNode) = graph.indexOf(element) + + override fun iterator(): Iterator<GraphNode> = graph.iterator() + override fun listIterator() = graph.listIterator() + + override fun listIterator(index: Int) = graph.listIterator(index) + + override fun subList(fromIndex: Int, toIndex: Int) = graph.subList(fromIndex, toIndex) + + override fun lastIndexOf(element: GraphNode) = graph.lastIndexOf(element) + + companion object { + val gson = GsonBuilder().setPrettyPrinting() + /* ConfigManager.createBaseGsonBuilder() */.registerTypeAdapter<Graph>({ out, value -> + out.beginObject() + value.forEach { + out.name(it.id.toString()).beginObject() + out.name("Position").value(with(it.position) { "$x:$y:$z" }) + if (it.name != null) { + out.name("Name").value(it.name) + } + out.name("Neighbours") + out.beginObject() + it.neighbours.forEach { (node, weight) -> + val id = node.id.toString() + out.name(id).value(weight) + } + out.endObject() + out.endObject() + } + out.endObject() + }, { reader -> + reader.beginObject() + val list = mutableListOf<GraphNode>() + val neigbourMap = mutableMapOf<GraphNode, List<Pair<Int, Double>>>() + while (reader.hasNext()) { + val id = reader.nextName().toInt() + reader.beginObject() + var position: LorenzVec? = null + var name: String? = null + var neighbors = mutableListOf<Pair<Int, Double>>() + while (reader.hasNext()) { + when (reader.nextName()) { + "Position" -> { + position = reader.nextString().split(":").let { parts -> + LorenzVec(parts[0].toDouble(), parts[1].toDouble(), parts[2].toDouble()) + } + } + + "Neighbours" -> { + reader.beginObject() + while (reader.hasNext()) { + val nId = reader.nextName().toInt() + val distance = reader.nextDouble() + neighbors.add(nId to distance) + } + reader.endObject() + } + + "Name" -> { + name = reader.nextString() + } + + } + } + val node = GraphNode(id, position!!, name) + list.add(node) + neigbourMap[node] = neighbors + reader.endObject() + } + neigbourMap.forEach { (node, edge) -> + node.neighbours = edge.associate { (id, distance) -> + list.first { it.id == id } to distance + } + } + reader.endObject() + Graph(list) + }).create() + + fun fromJson(json: String): Graph = gson.fromJson<Graph>(json) + fun fromJson(json: JsonElement): Graph = gson.fromJson<Graph>(json) + } +} + +class GraphNode(val id: Int, val position: LorenzVec, val name: String? = null) { + + /** Keys are the neighbours and value the edge weight (e.g. Distance) */ + lateinit var neighbours: Map<GraphNode, Double> + + override fun hashCode(): Int { + return id + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GraphNode + + if (id != other.id) return false + + return true + } +} + +fun Graph.findShortestPathAsGraph(start: GraphNode, end: GraphNode): Graph = + this.findShortestPathAsGraphWithDistance(start, end).first + +fun Graph.findShortestPathAsGraphWithDistance(start: GraphNode, end: GraphNode): Pair<Graph, Double> { + val distances = mutableMapOf<GraphNode, Double>() + val previous = mutableMapOf<GraphNode, GraphNode>() + val visited = mutableSetOf<GraphNode>() + val queue = PriorityQueue<GraphNode>(compareBy { distances.getOrDefault(it, Double.MAX_VALUE) }) + + distances[start] = 0.0 + queue.add(start) + + while (queue.isNotEmpty()) { + val current = queue.poll() + if (current == end) break + + visited.add(current) + + current.neighbours.forEach { (neighbour, weight) -> + if (neighbour !in visited) { + val newDistance = distances.getValue(current) + weight + if (newDistance < distances.getOrDefault(neighbour, Double.MAX_VALUE)) { + distances[neighbour] = newDistance + previous[neighbour] = current + queue.add(neighbour) + } + } + } + } + + return Graph(buildList { + var current = end + while (current != start) { + add(current) + current = previous[current] ?: return Graph(emptyList()) to 0.0 + } + add(start) + }.reversed()) to distances[end]!! +} + +fun Graph.findShortestPath(start: GraphNode, end: GraphNode): List<LorenzVec> = + this.findShortestPathAsGraph(start, end).toPositionsList() + +fun Graph.findShortestDistance(start: GraphNode, end: GraphNode): Double = + this.findShortestPathAsGraphWithDistance(start, end).second + +fun Graph.toPositionsList() = this.map { it.position } + +fun Graph.toJson(): String = Graph.gson.toJson(this) diff --git a/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt new file mode 100644 index 000000000..170dcabe9 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/events/LorenzWarpEvent.kt @@ -0,0 +1,3 @@ +package at.hannibal2.skyhanni.events + +class LorenzWarpEvent : LorenzEvent() diff --git a/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt index 3b2b426fe..fc006b049 100644 --- a/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt +++ b/src/main/java/at/hannibal2/skyhanni/events/RepositoryReloadEvent.kt @@ -9,7 +9,7 @@ import java.lang.reflect.Type class RepositoryReloadEvent(val repoLocation: File, val gson: Gson) : LorenzEvent() { - inline fun <reified T : Any> getConstant(constant: String, type: Type? = null): T = try { + inline fun <reified T : Any> getConstant(constant: String, type: Type? = null, gson: Gson = this.gson): T = try { RepoManager.setlastConstant(constant) if (!repoLocation.exists()) throw RepoError("Repo folder does not exist!") RepoUtils.getConstant(repoLocation, constant, gson, T::class.java, type) diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt new file mode 100644 index 000000000..32e5864af --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/TunnelsMaps.kt @@ -0,0 +1,451 @@ +package at.hannibal2.skyhanni.features.mining + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.data.model.Graph +import at.hannibal2.skyhanni.data.model.GraphNode +import at.hannibal2.skyhanni.data.model.findShortestDistance +import at.hannibal2.skyhanni.data.model.findShortestPathAsGraphWithDistance +import at.hannibal2.skyhanni.events.ConfigLoadEvent +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.LorenzKeyPressEvent +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.LorenzToolTipEvent +import at.hannibal2.skyhanni.events.LorenzWarpEvent +import at.hannibal2.skyhanni.events.RepositoryReloadEvent +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.filterNotNullKeys +import at.hannibal2.skyhanni.utils.ColorUtils.getFirstColorCode +import at.hannibal2.skyhanni.utils.ColorUtils.toChromaColor +import at.hannibal2.skyhanni.utils.ConditionalUtils.onToggle +import at.hannibal2.skyhanni.utils.DelayedRun +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceSqToPlayer +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzColor.Companion.toLorenzColor +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName +import at.hannibal2.skyhanni.utils.RenderUtils +import at.hannibal2.skyhanni.utils.RenderUtils.draw3DPathWithWaypoint +import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.fromNow +import at.hannibal2.skyhanni.utils.StringUtils.anyMatches +import at.hannibal2.skyhanni.utils.StringUtils.matchFirst +import at.hannibal2.skyhanni.utils.StringUtils.matches +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.Color +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +class TunnelsMaps { + + private val config get() = SkyHanniMod.feature.mining.tunnelMaps + + private var graph: Graph = Graph(emptyList()) + private lateinit var campfire: GraphNode + + private var goalReached = false + private var prevGoal: GraphNode? = null + private var goal: GraphNode? = null + set(value) { + prevGoal = field + field = value + } + + private var closedNote: GraphNode? = null + private var path: Pair<Graph, Double>? = null + + private var possibleLocations = mapOf<String, List<GraphNode>>() + private val cooldowns = mutableMapOf<GraphNode, SimpleTimeMark>() + private var active: String = "" + + private lateinit var fairySouls: Map<String, GraphNode> + private lateinit var newGemstones: Map<String, List<GraphNode>> + private lateinit var oldGemstones: Map<String, List<GraphNode>> + private lateinit var normalLocations: Map<String, List<GraphNode>> + + private var locationDisplay: List<Renderable> = emptyList() + + private fun getNext(name: String = active): GraphNode? { + fairySouls[name]?.let { + goalReached = false + return it + } + + val closed = closedNote ?: return null + val list = possibleLocations[name] ?: return null + + val offCooldown = list.filter { cooldowns[it]?.isInPast() != false } + val best = offCooldown.minByOrNull { graph.findShortestDistance(closed, it) } ?: list.minBy { + cooldowns[it] ?: SimpleTimeMark.farPast() + } + if (cooldowns[best]?.isInPast() != false) { + cooldowns[best] = 5.0.seconds.fromNow() + } + goalReached = false + return best + } + + private fun hasNext(name: String = active): Boolean { + val list = possibleLocations[name] ?: return false + return list.size > 1 + } + + private val oldGemstonePattern by RepoPattern.pattern( + "mining.tunnels.maps.gem.old", ".*(?:Ruby|Amethyst|Jade|Sapphire|Amber|Topaz).*" + ) + private val newGemstonePattern by RepoPattern.pattern( + "mining.tunnels.maps.gem.new", ".*(?:Aquamarine|Onyx|Citrine|Peridot).*" + ) + private val commissionInvPattern by RepoPattern.pattern( + "mining.commission.inventory", "Commissions" + ) + private val glacitePattern by RepoPattern.pattern( + "mining.commisson.reward.glacite", + "§7- §b\\d+ Glacite Powder" + ) + private val collectorCommissionPattern by RepoPattern.pattern( + "mining.commisson.collector", + "§9(?<what>\\w+(?: \\w+)?) Collector" + ) + private val invalidGoalPattern by RepoPattern.pattern( + "mining.commisson.collector.invalid", + "Glacite|Scrap" + ) + private val completedPattern by RepoPattern.pattern( + "mining.commisson.completed", + "§a§lCOMPLETED" + ) + + private val translateTable = mutableMapOf<String, String>() + + /** @return Errors with an empty String */ + private fun getGenericName(input: String): String = translateTable.getOrPut(input) { + possibleLocations.keys.firstOrNull() { it.uppercase().removeColor().contains(input.uppercase()) } ?: "" + } + + private var clickTranslate = mapOf<Int, String>() + + @SubscribeEvent + fun onInventoryFullyOpened(event: InventoryFullyOpenedEvent) { + if (!isEnabled()) return + clickTranslate = mapOf() + if (!commissionInvPattern.matches(event.inventoryName)) return + clickTranslate = event.inventoryItems.mapNotNull { (slotId, item) -> + val lore = item.getLore() + if (!glacitePattern.anyMatches(lore)) return@mapNotNull null + if (completedPattern.anyMatches(lore)) return@mapNotNull null + val type = lore.matchFirst(collectorCommissionPattern) { + group("what") + } ?: return@mapNotNull null + if (invalidGoalPattern.matches(type)) return@mapNotNull null + val mapName = getGenericName(type) + if (mapName.isEmpty()) { + ErrorManager.logErrorStateWithData( + "Unknown Collection Commission: $type", "$type can't be found in the graph.", + "type" to type, + "graphNames" to possibleLocations.keys, + "lore" to lore + ) + null + } else { + slotId to getGenericName(type) + } + }.toMap() + if (config.autoCommission) { + clickTranslate.values.firstOrNull()?.let { + setActiveAndGoal(it) + } + } + } + + @SubscribeEvent + fun onRenderItemTooltip(event: LorenzToolTipEvent) { + if (!isEnabled()) return + clickTranslate[event.slot.slotIndex]?.let { + event.toolTip.add("§e§lRight Click §r§eto for Tunnel Maps.") + } + } + + @SubscribeEvent + fun onGuiContainerSlotClick(event: GuiContainerEvent.SlotClickEvent) { + if (!isEnabled()) return + if (event.clickedButton != 1) return + clickTranslate[event.slotId]?.let { + setActiveAndGoal(it) + } + } + + @SubscribeEvent + fun onRepoReload(event: RepositoryReloadEvent) { + graph = event.getConstant<Graph>("TunnelsGraph", gson = Graph.gson) + possibleLocations = graph.groupBy { it.name }.filterNotNullKeys().mapValues { (_, value) -> + value + } + val fairy = mutableMapOf<String, GraphNode>() + val oldGemstone = mutableMapOf<String, List<GraphNode>>() + val newGemstone = mutableMapOf<String, List<GraphNode>>() + val other = mutableMapOf<String, List<GraphNode>>() + possibleLocations.forEach { (key, value) -> + when { + key.contains("Campfire") -> campfire = value.first() + key.contains("Fairy") -> fairy[key] = value.first() + newGemstonePattern.matches(key) -> newGemstone[key] = value + oldGemstonePattern.matches(key) -> oldGemstone[key] = value + else -> other[key] = value + } + } + fairySouls = fairy + this.newGemstones = newGemstone + this.oldGemstones = oldGemstone + normalLocations = other + translateTable.clear() + DelayedRun.runNextTick { // Needs to be delayed since the config may not be loaded + locationDisplay = generateLocationsDisplay() + } + } + + @SubscribeEvent + fun onConfigLoad(event: ConfigLoadEvent) { + onToggle( + config.compactGemstone, + config.excludeFairy + ) { + locationDisplay = generateLocationsDisplay() + } + } + + @SubscribeEvent + fun onRenderDisplay(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) { + if (!isEnabled()) return + val display = buildList<Renderable> { + if (active.isNotEmpty()) { + if (goal == campfire && active != campfire.name) { + add(Renderable.string("§6Override for ${campfire.name}")) + add(Renderable.clickable(Renderable.string("§eMake §f$active §eactive"), onClick = { + goal = getNext() + })) + } else { + add( + Renderable.clickAndHover( + Renderable.string("§6Active: §f$active"), + listOf("§eClick to disable current Waypoint"), + onClick = ::clearPath + ) + ) + if (hasNext()) { + add(Renderable.clickable(Renderable.string("§eNext Spot"), onClick = { + goal = getNext() + })) + } else { + add(Renderable.string("")) + } + } + } else { + add(Renderable.string("")) + add(Renderable.string("")) + } + addAll(locationDisplay) + } + config.position.renderRenderables(display, posLabel = "TunnelsMaps") + } + + private fun generateLocationsDisplay() = buildList { + add(Renderable.string("§6Loactions:")) + add( + Renderable.multiClickAndHover( + campfire.name!!, listOf( + "§eLeft Click to set active", "§eRight Click for override" + ), click = mapOf( + 0 to guiSetActive(campfire.name!!), 1 to ::campfireOverride + ) + ) + ) + if (!config.excludeFairy.get()) { + add(Renderable.hoverable(Renderable.horizontalContainer(listOf(Renderable.string("§dFairy Souls")) + fairySouls.map { + val name = it.key.removePrefix("§dFairy Soul ") + Renderable.clickable(Renderable.string("§d[${name}]"), onClick = guiSetActive(it.key)) + }), Renderable.string("§dFairy Souls"))) + } + if (config.compactGemstone.get()) { + add( + Renderable.table( + listOf( + newGemstones.map(::toCompactGemstoneName), oldGemstones.map(::toCompactGemstoneName) + ) + ) + ) + } else { + addAll(newGemstones.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + addAll(oldGemstones.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + } + addAll(normalLocations.map { + Renderable.clickable(Renderable.string(it.key), onClick = guiSetActive(it.key)) + }) + } + + private fun toCompactGemstoneName(it: Map.Entry<String, List<GraphNode>>): Renderable = Renderable.clickAndHover( + Renderable.string((it.key.getFirstColorCode()?.let { "§$it" } ?: "") + ("ROUGH_".plus( + it.key.removeColor().removeSuffix("stone") + ).asInternalName().itemName.takeWhile { it != ' ' }.removeColor()), + horizontalAlign = RenderUtils.HorizontalAlignment.CENTER + ), + tips = listOf(it.key), + onClick = guiSetActive(it.key), + ) + + private fun campfireOverride() { + goalReached = false + goal = campfire + } + + private fun setActiveAndGoal(it: String) { + active = it + goal = getNext() + } + + private fun guiSetActive(it: String): () -> Unit = { + setActiveAndGoal(it) + } + + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (!isEnabled()) return + if (checkGoalReached()) return + val prevClosed = closedNote + closedNote = graph.minBy { it.position.distanceSqToPlayer() } + val closest = closedNote ?: return + val goal = goal ?: return + if (closest == prevClosed && goal == prevGoal) return + val (path, distance) = graph.findShortestPathAsGraphWithDistance(closest, goal) + val first = path.firstOrNull() + val second = path.getOrNull(1) + + val playerPosition = LocationUtils.playerLocation() + val nodeDistance = first?.let { playerPosition.distance(it.position) } ?: 0.0 + if (first != null && second != null) { + val direct = playerPosition.distance(second.position) + val firstPath = first.neighbours[second] ?: 0.0 + val around = nodeDistance + firstPath + if (direct < around) { + this.path = Graph(path.drop(1)) to (distance - firstPath + direct) + return + } + } + this.path = path to (distance + nodeDistance) + } + + private fun checkGoalReached(): Boolean { + if (goalReached) return true + val goal = goal ?: return false + val distance = goal.position.distanceSqToPlayer() + goalReached = distance < if (goal == campfire) { + 15.0 * 15.0 + } else { + 6.0 * 6.0 + } + if (goalReached) { + if (goal == campfire && active != campfire.name) { + this.goal = getNext() + } else { + cooldowns[goal] = 60.0.seconds.fromNow() + clearPath() + } + return true + } + return false + } + + private fun clearPath() { + path = null + goal = null + } + + @SubscribeEvent + fun onRenderWorld(event: LorenzRenderWorldEvent) { + if (!isEnabled()) return + val path = path?.takeIf { it.first.isNotEmpty() } ?: return + event.draw3DPathWithWaypoint( + path.first, + getPathColor(), + config.pathWidth.toInt(), + true, + bezierPoint = 2.0, + textSize = config.textSize.toDouble() + ) + event.drawDynamicText( + if (config.distanceFirst) { + path.first.first() + } else { + path.first.last() + }.position, + "§e${path.second.roundToInt()}m", + config.textSize.toDouble(), + yOff = 10f + ) + } + + private fun getPathColor(): Color = if (config.dynamicPathColour) { + goal?.name?.getFirstColorCode()?.toLorenzColor()?.takeIf { it != LorenzColor.WHITE }?.toColor() + } else { + null + } ?: config.pathColour.toChromaColor() + + @SubscribeEvent + fun onKeyPress(event: LorenzKeyPressEvent) { + if (!isEnabled()) return + campfireKey(event) + nextSpotKey(event) + } + + private fun campfireKey(event: LorenzKeyPressEvent) { + if (event.keyCode != config.campfireKey) return + if (config.travelScroll) { + ChatUtils.sendMessageToServer("/warp basecamp") + } else { + campfireOverride() + } + } + + @SubscribeEvent + fun onLorenzWarp(event: LorenzWarpEvent) { + if (!isEnabled()) return + if (goal != null) { + DelayedRun.runNextTick { + goal = getNext() + } + } + } + + private var nextSpotDelay = SimpleTimeMark.farPast() + + private fun nextSpotKey(event: LorenzKeyPressEvent) { + if (event.keyCode != config.nextSpotHotkey) return + if (!nextSpotDelay.isInPast()) return + nextSpotDelay = 0.5.seconds.fromNow() + goal = getNext() + } + + val areas = setOf( + "Glacite Tunnels", "Dwarven Base Camp", "Glacite Lake", "Fossil Research Center" + ) + + private fun isEnabled() = + IslandType.DWARVEN_MINES.isInIsland() && config.enable && areas.contains(LorenzUtils.skyBlockArea) +} diff --git a/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt b/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt index b81a25599..0ae712f9d 100644 --- a/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt +++ b/src/main/java/at/hannibal2/skyhanni/test/SkyHanniDebugsAndTests.kt @@ -7,6 +7,7 @@ import at.hannibal2.skyhanni.config.ConfigManager import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator import at.hannibal2.skyhanni.config.core.config.Position import at.hannibal2.skyhanni.data.HypixelData +import at.hannibal2.skyhanni.data.model.Graph import at.hannibal2.skyhanni.events.GuiKeyPressEvent import at.hannibal2.skyhanni.events.GuiRenderEvent import at.hannibal2.skyhanni.events.LorenzChatEvent @@ -43,6 +44,7 @@ import at.hannibal2.skyhanni.utils.NEUItems.getPriceOrNull import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators import at.hannibal2.skyhanni.utils.OSUtils import at.hannibal2.skyhanni.utils.ReflectionUtils.makeAccessible +import at.hannibal2.skyhanni.utils.RenderUtils.draw3DPathWithWaypoint import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables import at.hannibal2.skyhanni.utils.RenderUtils.renderString @@ -52,6 +54,7 @@ import at.hannibal2.skyhanni.utils.SoundUtils import at.hannibal2.skyhanni.utils.renderables.Renderable import at.hannibal2.skyhanni.utils.renderables.Renderable.Companion.renderBounds import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import net.minecraft.client.Minecraft import net.minecraft.init.Blocks import net.minecraft.init.Items @@ -59,6 +62,7 @@ import net.minecraft.item.ItemStack import net.minecraft.nbt.NBTTagCompound import net.minecraftforge.common.MinecraftForge import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.Color import java.io.File import kotlin.time.Duration.Companion.seconds @@ -116,9 +120,14 @@ class SkyHanniDebugsAndTests { ChatUtils.chat("set test waypoint") } + var path: Graph = Graph(emptyList()) + fun testCommand(args: Array<String>) { SoundUtils.playBeepSound() - + runBlocking { + val input = OSUtils.readFromClipboard() ?: return@runBlocking + path = Graph.fromJson(input) + } // val a = Thread { OSUtils.copyToClipboard("123") } // val b = Thread { OSUtils.copyToClipboard("456") } // a.start() @@ -425,6 +434,11 @@ class SkyHanniDebugsAndTests { } @SubscribeEvent + fun onTestGraphPath(event: LorenzRenderWorldEvent) { + event.draw3DPathWithWaypoint(path, Color.GREEN, 8, true) + } + + @SubscribeEvent fun onShowInternalName(event: LorenzToolTipEvent) { if (!LorenzUtils.inSkyBlock) return if (!debugConfig.showInternalName) return diff --git a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt index 53ae224d9..915677bb6 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt @@ -285,4 +285,28 @@ object CollectionUtils { } return destination } + + inline fun <T, R> Iterable<T>.zipWithNext3(transform: (a: T, b: T, c: T) -> R): List<R> { + val iterator = iterator() + if (!iterator.hasNext()) return emptyList() + var one = iterator.next() + if (!iterator.hasNext()) return emptyList() + var two = iterator.next() + val result = mutableListOf<R>() + while (iterator.hasNext()) { + val next = iterator.next() + result.add(transform(one, two, next)) + one = two + two = next + } + return result + } + + fun <T> Iterable<T>.zipWithNext3(): List<Triple<T, T, T>> { + return zipWithNext3 { a, b, c -> Triple(a, b, c) } + } + + fun <K, V : Any> Map<K?, V>.filterNotNullKeys(): Map<K, V> { + return filterKeys { it != null } as Map<K, V> + } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/ColorUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/ColorUtils.kt index 97a1342c1..e5ff47073 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/ColorUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/ColorUtils.kt @@ -9,6 +9,8 @@ object ColorUtils { fun String.toChromaColor() = Color(toChromaColorInt(), true) fun String.toChromaColorInt() = SpecialColour.specialToChromaRGB(this) + fun String.getFirstColorCode() = this.takeIf { it.firstOrNull() == '§' }?.getOrNull(1) + fun getRed(colour: Int) = colour shr 16 and 0xFF fun getGreen(colour: Int) = colour shr 8 and 0xFF diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LorenzColor.kt b/src/main/java/at/hannibal2/skyhanni/utils/LorenzColor.kt index 16fe8cd6e..08de7038a 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LorenzColor.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LorenzColor.kt @@ -81,5 +81,13 @@ enum class LorenzColor(val chatColorCode: Char, private val color: Color, privat EnumDyeColor.BROWN -> GOLD EnumDyeColor.BLACK -> BLACK } + + fun Char.toLorenzColor(): LorenzColor? = entries.firstOrNull { it.chatColorCode == this } ?: run { + ErrorManager.logErrorWithData( + Exception("Unknown chat color: $this"), + "Unknown chat color: $this" + ) + null + } } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt index 55be8cd3e..dc143a841 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt @@ -136,6 +136,7 @@ data class LorenzVec( fun clone(): LorenzVec = LorenzVec(x, y, z) fun toDoubleArray(): Array<Double> = arrayOf(x, y, z) + fun toFloatArray(): Array<Float> = arrayOf(x.toFloat(), y.toFloat(), z.toFloat()) fun equalsIgnoreY(other: LorenzVec) = x == other.x && z == other.z diff --git a/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt index d17831fb5..fb97a8357 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt @@ -5,13 +5,19 @@ import at.hannibal2.skyhanni.data.GuiEditManager import at.hannibal2.skyhanni.data.GuiEditManager.Companion.getAbsX import at.hannibal2.skyhanni.data.GuiEditManager.Companion.getAbsY import at.hannibal2.skyhanni.data.GuiEditManager.Companion.getDummySize +import at.hannibal2.skyhanni.data.model.Graph +import at.hannibal2.skyhanni.data.model.toPositionsList import at.hannibal2.skyhanni.events.GuiContainerEvent import at.hannibal2.skyhanni.events.GuiRenderItemEvent import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent import at.hannibal2.skyhanni.events.RenderGuiItemOverlayEvent import at.hannibal2.skyhanni.features.misc.RoundedRectangleOutlineShader import at.hannibal2.skyhanni.features.misc.RoundedRectangleShader +import at.hannibal2.skyhanni.test.GriffinUtils.drawWaypointFilled import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.CollectionUtils.zipWithNext3 +import at.hannibal2.skyhanni.utils.ColorUtils.getFirstColorCode +import at.hannibal2.skyhanni.utils.LorenzColor.Companion.toLorenzColor import at.hannibal2.skyhanni.utils.renderables.Renderable import at.hannibal2.skyhanni.utils.renderables.RenderableUtils.renderXAligned import at.hannibal2.skyhanni.utils.shader.ShaderManager @@ -35,6 +41,7 @@ import net.minecraft.util.MathHelper import net.minecraft.util.ResourceLocation import org.lwjgl.opengl.GL11 import java.awt.Color +import java.nio.FloatBuffer import kotlin.math.cos import kotlin.math.max import kotlin.math.sin @@ -66,8 +73,9 @@ object RenderUtils { private val beaconBeam = ResourceLocation("textures/entity/beacon_beam.png") - private val matrixBuffer = GLAllocation.createDirectFloatBuffer(16) - private val colourBuffer = GLAllocation.createDirectFloatBuffer(16) + private val matrixBuffer: FloatBuffer = GLAllocation.createDirectFloatBuffer(16); + private val colourBuffer: FloatBuffer = GLAllocation.createDirectFloatBuffer(16) + private val bezier2Buffer: FloatBuffer = GLAllocation.createDirectFloatBuffer(9) infix fun Slot.highlight(color: LorenzColor) { highlight(color.toColor()) @@ -1017,43 +1025,10 @@ object RenderUtils { } } - fun LorenzRenderWorldEvent.draw3DLine(p1: LorenzVec, p2: LorenzVec, color: Color, lineWidth: Int, depth: Boolean) { - GlStateManager.disableCull() - - val render = Minecraft.getMinecraft().renderViewEntity - val worldRenderer = Tessellator.getInstance().worldRenderer - val realX = render.lastTickPosX + (render.posX - render.lastTickPosX) * partialTicks - val realY = render.lastTickPosY + (render.posY - render.lastTickPosY) * partialTicks - val realZ = render.lastTickPosZ + (render.posZ - render.lastTickPosZ) * partialTicks - GlStateManager.pushMatrix() - GlStateManager.translate(-realX, -realY, -realZ) - GlStateManager.disableTexture2D() - GlStateManager.enableBlend() - GlStateManager.disableAlpha() - GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0) - GL11.glLineWidth(lineWidth.toFloat()) - if (!depth) { - GL11.glDisable(GL11.GL_DEPTH_TEST) - GlStateManager.depthMask(false) - } - GlStateManager.color(color.red / 255f, color.green / 255f, color.blue / 255f, color.alpha / 255f) - worldRenderer.begin(GL11.GL_LINE_STRIP, DefaultVertexFormats.POSITION) - worldRenderer.pos(p1.x, p1.y, p1.z).endVertex() - worldRenderer.pos(p2.x, p2.y, p2.z).endVertex() - Tessellator.getInstance().draw() - GlStateManager.translate(realX, realY, realZ) - if (!depth) { - GL11.glEnable(GL11.GL_DEPTH_TEST) - GlStateManager.depthMask(true) + fun LorenzRenderWorldEvent.draw3DLine(p1: LorenzVec, p2: LorenzVec, color: Color, lineWidth: Int, depth: Boolean) = + LineDrawer.draw3D(partialTicks) { + draw3DLine(p1, p2, color, lineWidth, depth) } - GlStateManager.disableBlend() - GlStateManager.enableAlpha() - GlStateManager.enableTexture2D() - GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f) - GlStateManager.popMatrix() - GlStateManager.disableLighting() - GlStateManager.enableDepth() - } fun LorenzRenderWorldEvent.exactLocation(entity: Entity) = exactLocation(entity, partialTicks) @@ -1232,6 +1207,159 @@ object RenderUtils { ) } + fun LorenzRenderWorldEvent.draw3DPathWithWaypoint( + path: Graph, + colorLine: Color, + lineWidth: Int, + depth: Boolean, + startAtEye: Boolean = true, + textSize: Double = 1.0, + waypointColor: Color = + (path.lastOrNull()?.name?.getFirstColorCode()?.toLorenzColor() ?: LorenzColor.WHITE).toColor(), + bezierPoint: Double = 1.0, + ) { + if (path.isEmpty()) return + val points = if (startAtEye) { + listOf( + this.exactPlayerEyeLocation() + + Minecraft.getMinecraft().thePlayer.getLook(this.partialTicks) + .toLorenzVec()/* .rotateXZ(-Math.PI / 72.0) */.times(2) + ) + } else { + emptyList() + } + path.toPositionsList().map { it.add(0.5, 0.5, 0.5) } + LineDrawer.draw3D(partialTicks) { + drawPath( + points, + colorLine, + lineWidth, + depth, + bezierPoint + ) + } + path.filter { it.name?.isNotEmpty() == true }.forEach { + this.drawDynamicText(it.position, it.name!!, textSize) + } + val last = path.last() + drawWaypointFilled(last.position, waypointColor, seeThroughBlocks = true) + } + + class LineDrawer @PublishedApi internal constructor(val tessellator: Tessellator) { + val worldRenderer = tessellator.worldRenderer + fun drawPath(path: List<LorenzVec>, color: Color, lineWidth: Int, depth: Boolean, bezierPoint: Double = 1.0) { + if (bezierPoint < 0) { + path.zipWithNext().forEach { + draw3DLine(it.first, it.second, color, lineWidth, depth) + } + } else { + val pathLines = path.zipWithNext() + pathLines.forEachIndexed { index, it -> + val reduce = it.second.minus(it.first).normalize().times(bezierPoint) + draw3DLine( + if (index != 0) it.first + reduce else it.first, + if (index != pathLines.lastIndex) it.second - reduce else it.second, + color, + lineWidth, + depth + ) + } + path.zipWithNext3().forEach { + val p1 = it.second.minus(it.second.minus(it.first).normalize().times(bezierPoint)) + val p3 = it.second.minus(it.second.minus(it.third).normalize().times(bezierPoint)) + val p2 = it.second + drawBezier2(p1, p2, p3, color, lineWidth, depth) + } + } + } + + fun draw3DLine(p1: LorenzVec, p2: LorenzVec, color: Color, lineWidth: Int, depth: Boolean) { + GL11.glLineWidth(lineWidth.toFloat()) + if (!depth) { + GL11.glDisable(GL11.GL_DEPTH_TEST) + GlStateManager.depthMask(false) + } + GlStateManager.color(color.red / 255f, color.green / 255f, color.blue / 255f, color.alpha / 255f) + worldRenderer.begin(GL11.GL_LINE_STRIP, DefaultVertexFormats.POSITION) + worldRenderer.pos(p1.x, p1.y, p1.z).endVertex() + worldRenderer.pos(p2.x, p2.y, p2.z).endVertex() + tessellator.draw() + if (!depth) { + GL11.glEnable(GL11.GL_DEPTH_TEST) + GlStateManager.depthMask(true) + } + } + + fun drawBezier2( + p1: LorenzVec, + p2: LorenzVec, + p3: LorenzVec, + color: Color, + lineWidth: Int, + depth: Boolean, + segments: Int = 30, + ) { + GL11.glLineWidth(lineWidth.toFloat()) + if (!depth) { + GL11.glDisable(GL11.GL_DEPTH_TEST) + GlStateManager.depthMask(false) + } + GlStateManager.color(color.red / 255f, color.green / 255f, color.blue / 255f, color.alpha / 255f) + val ctrlpoints = p1.toFloatArray() + p2.toFloatArray() + p3.toFloatArray() + bezier2Buffer.clear() + ctrlpoints.forEach { + bezier2Buffer.put(it) + } + bezier2Buffer.flip() + GL11.glMap1f( + GL11.GL_MAP1_VERTEX_3, 0.0f, 1.0f, 3, 3, + bezier2Buffer + ) + + GL11.glEnable(GL11.GL_MAP1_VERTEX_3) + + GL11.glBegin(GL11.GL_LINE_STRIP) + for (i in 0..segments) { + GL11.glEvalCoord1f(i.toFloat() / segments.toFloat()) + } + GL11.glEnd() + if (!depth) { + GL11.glEnable(GL11.GL_DEPTH_TEST) + GlStateManager.depthMask(true) + } + } + + companion object { + inline fun draw3D( + partialTicks: Float = 0F, + crossinline quads: LineDrawer.() -> Unit, + ) { + + GlStateManager.enableBlend() + GlStateManager.disableLighting() + GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 1, 0) + GlStateManager.disableTexture2D() + GlStateManager.disableCull() + GlStateManager.disableAlpha() + + val tessellator = Tessellator.getInstance() + + GlStateManager.pushMatrix() + RenderUtils.translate(getViewerPos(partialTicks).negated()) + getViewerPos(partialTicks) + + quads.invoke(LineDrawer(Tessellator.getInstance())) + + GlStateManager.popMatrix() + + GlStateManager.enableAlpha() + GlStateManager.enableTexture2D() + GlStateManager.enableCull() + GlStateManager.disableBlend() + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f) + } + } + } + class QuadDrawer @PublishedApi internal constructor(val tessellator: Tessellator) { val worldRenderer = tessellator.worldRenderer inline fun draw( |