package at.hannibal2.skyhanni.utils import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.data.HypixelData import at.hannibal2.skyhanni.data.IslandType import at.hannibal2.skyhanni.data.MayorElection import at.hannibal2.skyhanni.data.TitleManager import at.hannibal2.skyhanni.features.dungeon.DungeonAPI import at.hannibal2.skyhanni.mixins.transformers.AccessorGuiEditSign import at.hannibal2.skyhanni.test.TestBingo import at.hannibal2.skyhanni.utils.NEUItems.getItemStackOrNull import at.hannibal2.skyhanni.utils.StringUtils.capAtMinecraftLength import at.hannibal2.skyhanni.utils.StringUtils.removeColor import at.hannibal2.skyhanni.utils.StringUtils.toDashlessUUID import at.hannibal2.skyhanni.utils.renderables.Renderable import com.google.gson.JsonPrimitive import io.github.moulberry.moulconfig.observer.Observer import io.github.moulberry.moulconfig.observer.Property import io.github.moulberry.notenoughupdates.util.SkyBlockTime import net.minecraft.client.Minecraft import net.minecraft.client.gui.inventory.GuiEditSign import net.minecraft.entity.EntityLivingBase import net.minecraft.entity.SharedMonsterAttributes import net.minecraft.event.ClickEvent import net.minecraft.event.HoverEvent import net.minecraft.launchwrapper.Launch import net.minecraft.util.ChatComponentText import net.minecraftforge.fml.common.FMLCommonHandler import java.awt.Color import java.lang.reflect.Field import java.lang.reflect.Modifier import java.text.DecimalFormat import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Collections import java.util.Timer import java.util.TimerTask import java.util.regex.Matcher import kotlin.properties.ReadWriteProperty import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty import kotlin.reflect.KProperty0 import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds object LorenzUtils { val onHypixel get() = (HypixelData.hypixelLive || HypixelData.hypixelAlpha) && Minecraft.getMinecraft().thePlayer != null val isOnAlphaServer get() = onHypixel && HypixelData.hypixelAlpha val inSkyBlock get() = onHypixel && HypixelData.skyBlock val inDungeons get() = inSkyBlock && DungeonAPI.inDungeon() val skyBlockIsland get() = HypixelData.skyBlockIsland val skyBlockArea get() = if (inSkyBlock) HypixelData.skyBlockArea else "?" val inKuudraFight get() = skyBlockIsland == IslandType.KUUDRA_ARENA val noTradeMode get() = HypixelData.noTrade val isStrandedProfile get() = HypixelData.stranded val isBingoProfile get() = inSkyBlock && (HypixelData.bingo || TestBingo.testBingo) val lastWorldSwitch get() = HypixelData.joinedWorld // TODO log based on chat category (error, warning, debug, user error, normal) private val log = LorenzLogger("chat/mod_sent") var lastButtonClicked = 0L private const val DEBUG_PREFIX = "[SkyHanni Debug] §7" private const val USER_ERROR_PREFIX = "§c[SkyHanni] " private val ERROR_PREFIX by lazy { "§c[SkyHanni-${SkyHanniMod.version}] " } private const val CHAT_PREFIX = "[SkyHanni] " /** * Sends a debug message to the chat and the console. * This is only sent if the debug feature is enabled. * * @param message The message to be sent * * @see DEBUG_PREFIX */ fun debug(message: String) { if (SkyHanniMod.feature.dev.debug.enabled && internalChat(DEBUG_PREFIX + message)) { consoleLog("[Debug] $message") } } /** * Sends a message to the user that they did something incorrectly. * We should tell them what to do instead as well. * * @param message The message to be sent * * @see USER_ERROR_PREFIX */ fun userError(message: String) { internalChat(USER_ERROR_PREFIX + message) } /** * Sends a message to the user that an error occurred caused by something in the code. * This should be used for errors that are not caused by the user. * * Why deprecate this? Even if this message is descriptive for the user and the developer, * we don't want inconsitencies in errors, and we would need to search * for the code line where this error gets printed any way. * so it's better to use the stack trace still. * * @param message The message to be sent * @param prefix Whether to prefix the message with the error prefix, default true * * @see ERROR_PREFIX */ @Deprecated( "Do not send the user a non clickable non stacktrace containing error message.", ReplaceWith("ErrorManager") ) fun error(message: String) { println("error: '$message'") internalChat(ERROR_PREFIX + message) } /** * Sends a message to the user * @param message The message to be sent * @param prefix Whether to prefix the message with the chat prefix, default true * @param prefixColor Color that the prefix should be, default yellow (§e) * * @see CHAT_PREFIX */ fun chat(message: String, prefix: Boolean = true, prefixColor: String = "§e") { if (prefix) { internalChat(prefixColor + CHAT_PREFIX + message) } else { internalChat(message) } } private fun internalChat(message: String): Boolean { log.log(message) val minecraft = Minecraft.getMinecraft() if (minecraft == null) { consoleLog(message.removeColor()) return false } val thePlayer = minecraft.thePlayer if (thePlayer == null) { consoleLog(message.removeColor()) return false } thePlayer.addChatMessage(ChatComponentText(message)) return true } fun SimpleDateFormat.formatCurrentTime(): String = this.format(System.currentTimeMillis()) fun stripVanillaMessage(originalMessage: String): String { var message = originalMessage while (message.startsWith("§r")) { message = message.substring(2) } while (message.endsWith("§r")) { message = message.substring(0, message.length - 2) } return message } fun Double.round(decimals: Int): Double { var multiplier = 1.0 repeat(decimals) { multiplier *= 10 } val result = kotlin.math.round(this * multiplier) / multiplier val a = result.toString() val b = toString() return if (a.length > b.length) this else result } fun Float.round(decimals: Int): Float { var multiplier = 1.0 repeat(decimals) { multiplier *= 10 } val result = kotlin.math.round(this * multiplier) / multiplier val a = result.toString().length val b = toString().length return if (a > b) this else result.toFloat() } // TODO replace all calls with regex @Deprecated("Do not use complicated string operations", ReplaceWith("Regex")) fun String.between(start: String, end: String): String = this.split(start, end)[1] // TODO use derpy() on every use case val EntityLivingBase.baseMaxHealth: Int get() = this.getEntityAttribute(SharedMonsterAttributes.maxHealth).baseValue.toInt() fun formatPercentage(percentage: Double): String = formatPercentage(percentage, "0.00") fun formatPercentage(percentage: Double, format: String?): String = DecimalFormat(format).format(percentage * 100).replace(',', '.') + "%" fun formatInteger(i: Int): String = formatInteger(i.toLong()) fun formatInteger(l: Long): String = NumberFormat.getIntegerInstance().format(l) fun formatDouble(d: Double, round: Int = 1): String { val numberInstance = NumberFormat.getNumberInstance() numberInstance.maximumFractionDigits = round return numberInstance.format(d.round(round)) } fun consoleLog(text: String) { SkyHanniMod.consoleLog(text) } fun getPointsForDojoRank(rank: String): Int { return when (rank) { "S" -> 1000 "A" -> 800 "B" -> 600 "C" -> 400 "D" -> 200 "F" -> 0 else -> 0 } } fun > List>.sorted(): List> { return sortedBy { (_, value) -> value } } fun > Map.sorted(): Map { return toList().sorted().toMap() } fun > Map.sortedDesc(): Map { return toList().sorted().reversed().toMap() } fun getSBMonthByName(month: String): Int { var monthNr = 0 for (i in 1..12) { val monthName = SkyBlockTime.monthName(i) if (month == monthName) { monthNr = i } } return monthNr } fun getPlayerUuid() = getRawPlayerUuid().toDashlessUUID() fun getRawPlayerUuid() = Minecraft.getMinecraft().thePlayer.uniqueID fun getPlayerName(): String = Minecraft.getMinecraft().thePlayer.name fun MutableList>.addAsSingletonList(text: E) { add(Collections.singletonList(text)) } // (key -> value) -> (sorting value -> key item icon) fun fillTable(list: MutableList>, data: MutableMap, Pair>) { val keys = data.mapValues { (_, v) -> v.first }.sortedDesc().keys val renderer = Minecraft.getMinecraft().fontRendererObj val longest = keys.map { it.first }.maxOfOrNull { renderer.getStringWidth(it.removeColor()) } ?: 0 for (pair in keys) { val (name, second) = pair var displayName = name while (renderer.getStringWidth(displayName.removeColor()) < longest) { displayName += " " } data[pair]!!.second.getItemStackOrNull()?.let { list.add(listOf(it, "$displayName $second")) } } } fun setTextIntoSign(text: String) { val gui = Minecraft.getMinecraft().currentScreen if (gui !is AccessorGuiEditSign) return gui.tileSign.signText[0] = ChatComponentText(text) } fun addTextIntoSign(addedText: String) { val gui = Minecraft.getMinecraft().currentScreen if (gui !is AccessorGuiEditSign) return val lines = gui.tileSign.signText val index = gui.editLine val text = lines[index].unformattedText + addedText lines[index] = ChatComponentText(text.capAtMinecraftLength(90)) } /** * Sends a message to the user that they can click and run a command * @param message The message to be sent * @param command The command to be executed when the message is clicked * @param prefix Whether to prefix the message with the chat prefix, default true * @param prefixColor Color that the prefix should be, default yellow (§e) * * @see CHAT_PREFIX */ fun clickableChat(message: String, command: String, prefix: Boolean = true, prefixColor: String = "§e") { val msgPrefix = if (prefix) prefixColor + CHAT_PREFIX else "" val text = ChatComponentText(msgPrefix + message) val fullCommand = "/" + command.removePrefix("/") text.chatStyle.chatClickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, fullCommand) text.chatStyle.chatHoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, ChatComponentText("§eExecute $fullCommand")) Minecraft.getMinecraft().thePlayer.addChatMessage(text) } /** * Sends a message to the user that they can click and run a command * @param message The message to be sent * @param hover The message to be shown when the message is hovered * @param command The command to be executed when the message is clicked * @param prefix Whether to prefix the message with the chat prefix, default true * @param prefixColor Color that the prefix should be, default yellow (§e) * * @see CHAT_PREFIX */ fun hoverableChat( message: String, hover: List, command: String? = null, prefix: Boolean = true, prefixColor: String = "§e" ) { val msgPrefix = if (prefix) prefixColor + CHAT_PREFIX else "" val text = ChatComponentText(msgPrefix + message) text.chatStyle.chatHoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, ChatComponentText(hover.joinToString("\n"))) command?.let { text.chatStyle.chatClickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/${it.removePrefix("/")}") } Minecraft.getMinecraft().thePlayer.addChatMessage(text) } fun Map.moveEntryToTop(matcher: (Map.Entry) -> Boolean): Map { val entry = entries.find(matcher) if (entry != null) { val newMap = linkedMapOf(entry.key to entry.value) newMap.putAll(this) return newMap } return this } private var lastMessageSent = 0L fun sendCommandToServer(command: String) { sendMessageToServer("/$command") } fun sendMessageToServer(message: String) { if (System.currentTimeMillis() > lastMessageSent + 1_000) { lastMessageSent = System.currentTimeMillis() val thePlayer = Minecraft.getMinecraft().thePlayer thePlayer.sendChatMessage(message) } } // MoulConfig is in Java, I don't want to downgrade this logic fun onChange(vararg properties: Property, observer: Observer) { for (property in properties) { property.whenChanged { a, b -> observer.observeChange(a, b) } } } fun onToggle(vararg properties: Property, observer: Runnable) { onChange(*properties) { _, _ -> observer.run() } } fun Property.onToggle(observer: Runnable) { whenChanged { _, _ -> observer.run() } } fun Property.afterChange(observer: T.() -> Unit) { whenChanged { _, new -> observer(new) } } fun Map.editCopy(function: MutableMap.() -> Unit) = toMutableMap().also { function(it) }.toMap() fun List.editCopy(function: MutableList.() -> Unit) = toMutableList().also { function(it) }.toList() fun colorCodeToRarity(colorCode: Char): String { return when (colorCode) { 'f' -> "Common" 'a' -> "Uncommon" '9' -> "Rare" '5' -> "Epic" '6' -> "Legendary" 'd' -> "Mythic" 'b' -> "Divine" '4' -> "Supreme" // legacy items else -> "Special" } } inline fun > MutableList>.addSelector( prefix: String, getName: (T) -> String, isCurrent: (T) -> Boolean, crossinline onChange: (T) -> Unit, ) { add(buildSelector(prefix, getName, isCurrent, onChange)) } inline fun > buildSelector( prefix: String, getName: (T) -> String, isCurrent: (T) -> Boolean, crossinline onChange: (T) -> Unit ) = buildList { add(prefix) for (entry in enumValues()) { val display = getName(entry) if (isCurrent(entry)) { add("§a[$display]") } else { add("§e[") add(Renderable.link("§e$display") { onChange(entry) }) add("§e]") } add(" ") } } inline fun MutableList>.addButton( prefix: String, getName: String, crossinline onChange: () -> Unit, tips: List = emptyList(), ) { val onClick = { if ((System.currentTimeMillis() - lastButtonClicked) > 150) { // funny thing happen if I don't do that onChange() SoundUtils.playClickSound() lastButtonClicked = System.currentTimeMillis() } } add(buildList { add(prefix) add("§a[") if (tips.isEmpty()) { add(Renderable.link("§e$getName", false, onClick)) } else { add(Renderable.clickAndHover("§e$getName", tips, false, onClick)) } add("§a]") }) } // TODO nea? // fun dynamic(block: () -> KMutableProperty0?): ReadWriteProperty { // return object : ReadWriteProperty { // override fun getValue(thisRef: Any?, property: KProperty<*>): T? { // return block()?.get() // } // // override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { // if (value != null) // block()?.set(value) // } // } // } fun dynamic(root: KProperty0, child: KMutableProperty1) = object : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): T? { val rootObj = root.get() ?: return null return child.get(rootObj) } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { if (value == null) return val rootObj = root.get() ?: return child.set(rootObj, value) } } fun List.nextAfter(after: String, skip: Int = 1) = nextAfter({ it == after }, skip) fun List.nextAfter(after: (String) -> Boolean, skip: Int = 1): String? { var missing = -1 for (line in this) { if (after(line)) { missing = skip - 1 continue } if (missing == 0) { return line } if (missing != -1) { missing-- } } return null } fun GuiEditSign.isRancherSign(): Boolean { if (this !is AccessorGuiEditSign) return false val tileSign = (this as AccessorGuiEditSign).tileSign return (tileSign.signText[1].unformattedText.removeColor() == "^^^^^^" && tileSign.signText[2].unformattedText.removeColor() == "Set your" && tileSign.signText[3].unformattedText.removeColor() == "speed cap!") } fun IslandType.isInIsland() = inSkyBlock && (skyBlockIsland == this || this == IslandType.CATACOMBS && inDungeons) fun MutableMap.addOrPut(key: K, number: Int): Int { val currentValue = this[key] ?: 0 val newValue = currentValue + number this[key] = newValue return newValue } fun MutableMap.addOrPut(key: K, number: Long): Long { val currentValue = this[key] ?: 0L val newValue = currentValue + number this[key] = newValue return newValue } fun MutableMap.addOrPut(key: K, number: Double): Double { val currentValue = this[key] ?: 0.0 val newValue = currentValue + number this[key] = newValue return newValue } fun MutableMap.sumAllValues(): Double { if (values.isEmpty()) return 0.0 return when (values.first()) { is Double -> values.sumOf { it.toDouble() } is Float -> values.sumOf { it.toDouble() } is Long -> values.sumOf { it.toLong() }.toDouble() else -> values.sumOf { it.toInt() }.toDouble() } } /** transfer string colors from the config to java.awt.Color */ fun String.toChromaColor() = Color(SpecialColour.specialToChromaRGB(this), true) fun List.getOrNull(index: Int): E? { return if (index in indices) { get(index) } else null } fun T?.toSingletonListOrEmpty(): List { if (this == null) return emptyList() return listOf(this) } fun Field.makeAccessible() = also { isAccessible = true } // Taken and modified from Skytils @JvmStatic fun T.equalsOneOf(vararg other: T): Boolean { for (obj in other) { if (this == obj) return true } return false } infix fun MutableMap.put(pairs: Pair) { this[pairs.first] = pairs.second } fun Field.removeFinal(): Field { javaClass.getDeclaredField("modifiers").makeAccessible().set(this, modifiers and (Modifier.FINAL.inv())) return this } fun List.indexOfFirst(vararg args: T) = args.map { indexOf(it) }.firstOrNull { it != -1 } private val recalculateDerpy = RecalculatingValue(1.seconds) { MayorElection.isPerkActive("Derpy", "DOUBLE MOBS HP!!!") } val isDerpy get() = recalculateDerpy.getValue() fun Int.derpy() = if (isDerpy) this / 2 else this fun runDelayed(duration: Duration, runnable: () -> Unit) { Timer().schedule(object : TimerTask() { override fun run() { runnable() } }, duration.inWholeMilliseconds) } val JsonPrimitive.asIntOrNull get() = takeIf { it.isNumber }?.asInt fun T.transformIf(condition: T.() -> Boolean, transofmration: T.() -> T) = if (condition()) transofmration(this) else this fun T.conditionalTransform(condition: Boolean, ifTrue: T.() -> Any, ifFalse: T.() -> Any) = if (condition) ifTrue(this) else ifFalse(this) fun sendTitle(text: String, duration: Duration, height: Double = 1.8) { TitleManager.sendTitle(text, duration, height) } @Deprecated("Dont use this approach at all. check with regex or equals instead.", ReplaceWith("Regex or equals")) fun Iterable.anyContains(element: String) = any { it.contains(element) } inline fun > enumValueOfOrNull(name: String): T? { val enums = enumValues() return enums.firstOrNull { it.name == name } } inline fun > enumValueOf(name: String) = enumValueOfOrNull(name) ?: kotlin.error("Unknown enum constant for ${enumValues().first().name.javaClass.simpleName}: '$name'") fun isInDevEnviromen() = Launch.blackboard.get("fml.deobfuscatedEnvironment") as Boolean fun shutdownMinecraft(reason: String? = null) { System.err.println("SkyHanni-${SkyHanniMod.version} forced the game to shutdown.") reason?.let { System.err.println("Reason: $it") } FMLCommonHandler.instance().handleExit(-1) } /** * Get the group, otherwise, return null * @param groupName The group name in the pattern */ fun Matcher.groupOrNull(groupName: String): String? { return runCatching { this.group(groupName) }.getOrNull() } }