aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCalMWolfs <94038482+CalMWolfs@users.noreply.github.com>2023-06-19 21:39:11 +1000
committerGitHub <noreply@github.com>2023-06-19 21:39:11 +1000
commitda2d2def82ac819dbfa9457b0eb180bba4491278 (patch)
tree561a70b4089def74da0d7019d0629fc3ae5d362c /src
parent54cbf2fc5177ffc6b219a72be8032789ff3514b7 (diff)
parent24ac5fb6dbdb793ce0eeac92c80c24f64f0868de (diff)
downloadskyhanni-da2d2def82ac819dbfa9457b0eb180bba4491278.tar.gz
skyhanni-da2d2def82ac819dbfa9457b0eb180bba4491278.tar.bz2
skyhanni-da2d2def82ac819dbfa9457b0eb180bba4491278.zip
Merge branch 'beta' into frozen_treasure_tracker
Diffstat (limited to 'src')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt6
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/Storage.java18
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/Chat.java7
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/Slayer.java27
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/ApiDataLoader.kt15
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/SlayerAPI.kt8
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/chat/ArachneChatMessageHider.kt50
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/NonGodPotEffectDisplay.kt10
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerItemProfitTracker.kt1
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerRngMeterDisplay.kt189
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt2
11 files changed, 310 insertions, 23 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
index aaa3231f6..5eb0eec1b 100644
--- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
+++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
@@ -12,6 +12,7 @@ import at.hannibal2.skyhanni.features.bazaar.BazaarBestSellMethod
import at.hannibal2.skyhanni.features.bazaar.BazaarCancelledBuyOrderClipboard
import at.hannibal2.skyhanni.features.bazaar.BazaarOrderHelper
import at.hannibal2.skyhanni.features.bingo.*
+import at.hannibal2.skyhanni.features.chat.ArachneChatMessageHider
import at.hannibal2.skyhanni.features.chat.ChatFilter
import at.hannibal2.skyhanni.features.chat.PlayerDeathMessages
import at.hannibal2.skyhanni.features.chat.playerchat.PlayerChatFilter
@@ -42,7 +43,6 @@ import at.hannibal2.skyhanni.features.minion.MinionCollectLogic
import at.hannibal2.skyhanni.features.minion.MinionFeatures
import at.hannibal2.skyhanni.features.misc.*
import at.hannibal2.skyhanni.features.misc.discordrpc.DiscordRPCManager
-import at.hannibal2.skyhanni.features.misc.GhostCounter
import at.hannibal2.skyhanni.features.misc.items.EstimatedItemValue
import at.hannibal2.skyhanni.features.misc.items.EstimatedWardrobePrice
import at.hannibal2.skyhanni.features.misc.tabcomplete.PlayerTabComplete
@@ -94,7 +94,7 @@ import org.apache.logging.log4j.Logger
clientSideOnly = true,
useMetadata = true,
guiFactory = "at.hannibal2.skyhanni.config.ConfigGuiForgeInterop",
- version = "0.18.Beta.20",
+ version = "0.18.Beta.21",
)
class SkyHanniMod {
@Mod.EventHandler
@@ -296,8 +296,10 @@ class SkyHanniMod {
loadModule(DetectBrokenHyperion())
loadModule(RestorePieceOfWizardPortalLore())
loadModule(QuickModMenuSwitch)
+ loadModule(ArachneChatMessageHider())
loadModule(ShowItemUuid())
loadModule(FrozenTreasureTracker())
+ loadModule(SlayerRngMeterDisplay())
loadModule(GhostCounter)
init()
diff --git a/src/main/java/at/hannibal2/skyhanni/config/Storage.java b/src/main/java/at/hannibal2/skyhanni/config/Storage.java
index c0a003aad..4a95871cb 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/Storage.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/Storage.java
@@ -274,5 +274,23 @@ public class Storage {
public boolean hidden;
}
}
+
+ @Expose
+ public Map<String, SlayerRngMeterStorage> slayerRngMeter = new HashMap<>();
+
+ public static class SlayerRngMeterStorage {
+
+ @Expose
+ public long currentMeter = -1;
+
+ @Expose
+ public long gainPerBoss = -1;
+
+ @Expose
+ public long goalNeeded = -1;
+
+ @Expose
+ public String itemGoal = "?";
+ }
}
} \ No newline at end of file
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/Chat.java b/src/main/java/at/hannibal2/skyhanni/config/features/Chat.java
index bd60e0eee..f149fc624 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/Chat.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/Chat.java
@@ -122,10 +122,15 @@ public class Chat {
"except for players who are nearby or during dungeons/a Kuudra fight.")
@ConfigEditorBoolean
public boolean hideFarDeathMessages = false;
- //TODO jawbus + x
+ //TODO jawbus + thunder
@Expose
@ConfigOption(name = "Compact Potion Message", desc = "Shorten chat messages about player potion effects.")
@ConfigEditorBoolean
public boolean compactPotionMessage = true;
+
+ @Expose
+ @ConfigOption(name = "Arachne Hider", desc = "Hide chat messages about the Arachne Fight while outside of §eArachne's Sanctuary§7.")
+ @ConfigEditorBoolean
+ public boolean hideArachneMessages = false;
}
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/Slayer.java b/src/main/java/at/hannibal2/skyhanni/config/features/Slayer.java
index 00392bcc4..852b54970 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/Slayer.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/Slayer.java
@@ -144,6 +144,33 @@ public class Slayer {
}
@Expose
+ @ConfigOption(name = "RNG Meter Display", desc = "")
+ @Accordion
+ public RngMeterDisplay rngMeterDisplay = new RngMeterDisplay();
+
+ public static class RngMeterDisplay {
+
+ @Expose
+ @ConfigOption(name = "Enabled", desc = "Display amount of bosses needed until next rng meter drop.")
+ @ConfigEditorBoolean
+ public boolean enabled = true;
+
+ @Expose
+ @ConfigOption(name = "Warn Empty", desc = "Warn when no item is set in the rng meter.")
+ @ConfigEditorBoolean
+ public boolean warnEmpty = false;
+
+ @Expose
+ @ConfigOption(name = "Hide Chat", desc = "Hide the rng meter message from chat if current item is selected.")
+ @ConfigEditorBoolean
+ public boolean hideChat = true;
+
+ @Expose
+ public Position pos = new Position(410, 110, false, true);
+
+ }
+
+ @Expose
@ConfigOption(name = "Broken Wither Impact",
desc = "Warns when right-clicking with a Wither Impact weapon (e.g. Hyperion) no longer gains combat exp. " +
"Kill a mob with melee-hits to fix this hypixel bug. §cOnly works while doing slayers!"
diff --git a/src/main/java/at/hannibal2/skyhanni/data/ApiDataLoader.kt b/src/main/java/at/hannibal2/skyhanni/data/ApiDataLoader.kt
index 82938994a..0d412f6da 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/ApiDataLoader.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/ApiDataLoader.kt
@@ -1,7 +1,6 @@
package at.hannibal2.skyhanni.data
import at.hannibal2.skyhanni.SkyHanniMod
-import at.hannibal2.skyhanni.events.LorenzChatEvent
import at.hannibal2.skyhanni.events.ProfileApiDataLoadedEvent
import at.hannibal2.skyhanni.events.ProfileJoinEvent
import at.hannibal2.skyhanni.utils.APIUtil
@@ -36,19 +35,6 @@ class ApiDataLoader {
}
@SubscribeEvent
- fun onStatusBar(event: LorenzChatEvent) {
- val message = event.message
- if (message.startsWith("§aYour new API key is §r§b")) {
- SkyHanniMod.feature.storage.apiKey = message.substring(26)
- LorenzUtils.chat("§b[SkyHanni] A new API Key has been detected and installed")
-
- if (currentProfileName != "") {
- updateApiData()
- }
- }
- }
-
- @SubscribeEvent
fun onProfileJoin(event: ProfileJoinEvent) {
currentProfileName = event.name
updateApiData()
@@ -99,7 +85,6 @@ class ApiDataLoader {
LorenzUtils.error("§c[SkyHanni] Invalid API key from $modName")
}
}
- LorenzUtils.error("§c[SkyHanni] SkyHanni has no API key set. Please run /api new")
}
}
diff --git a/src/main/java/at/hannibal2/skyhanni/data/SlayerAPI.kt b/src/main/java/at/hannibal2/skyhanni/data/SlayerAPI.kt
index 1c5c76d92..7e44bd5cf 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/SlayerAPI.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/SlayerAPI.kt
@@ -27,7 +27,7 @@ object SlayerAPI {
var questStartTime = 0L
var isInSlayerArea = false
- private var latestSlayerCategory = ""
+ var latestSlayerCategory = ""
private var latestProgressChangeTime = 0L
var latestWrongAreaWarning = 0L
private var latestSlayerProgress = ""
@@ -106,10 +106,14 @@ object SlayerAPI {
if (event.phase != TickEvent.Phase.START) return
if (!LorenzUtils.inSkyBlock) return
+ // wait with sending SlayerChangeEvent until profile is detected
+ if (ProfileStorageData.profileSpecific == null) return
+
val slayerQuest = ScoreboardData.sidebarLinesFormatted.nextAfter("Slayer Quest") ?: ""
if (slayerQuest != latestSlayerCategory) {
- SlayerChangeEvent(latestSlayerCategory, slayerQuest).postAndCatch()
+ val old = latestSlayerCategory
latestSlayerCategory = slayerQuest
+ SlayerChangeEvent(old, latestSlayerCategory).postAndCatch()
}
val slayerProgress = ScoreboardData.sidebarLinesFormatted.nextAfter("Slayer Quest", 2) ?: ""
diff --git a/src/main/java/at/hannibal2/skyhanni/features/chat/ArachneChatMessageHider.kt b/src/main/java/at/hannibal2/skyhanni/features/chat/ArachneChatMessageHider.kt
new file mode 100644
index 000000000..d870197c7
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/chat/ArachneChatMessageHider.kt
@@ -0,0 +1,50 @@
+package at.hannibal2.skyhanni.features.chat
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.events.LorenzChatEvent
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class ArachneChatMessageHider {
+ private val config get() = SkyHanniMod.feature.chat
+ private var hideArachneDeadMessage = false
+ private val arachneCallingPattern = "§4☄ §r.* §r§eplaced an §r§9Arachne's Calling§r§e!.*".toPattern()
+ private val arachneCrystalPattern = "§4☄ §r.* §r§eplaced an Arachne Crystal! Something is awakening!".toPattern()
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+ if (!isEnabled()) return
+ if (LorenzUtils.skyBlockIsland != IslandType.SPIDER_DEN) return
+ if (LorenzUtils.skyBlockArea == "Arachne's Sanctuary") return
+
+ if (shouldHide(event.message)) {
+ event.blockedReason = "arachne"
+ }
+ }
+
+ private fun shouldHide(message: String): Boolean {
+
+ arachneCallingPattern.matchMatcher(message) {
+ return true
+ }
+ arachneCrystalPattern.matchMatcher(message) {
+ return true
+ }
+
+ if (message == "§c[BOSS] Arachne§r§f: Ahhhh...A Calling...") return true
+ if (message == "§c[BOSS] Arachne§r§f: The Era of Spiders begins now.") return true
+
+ if (message == "§a§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬") {
+ hideArachneDeadMessage = !hideArachneDeadMessage
+ return true
+ }
+ if (message == " §r§6§lARACHNE DOWN!") {
+ hideArachneDeadMessage = true
+ }
+ return hideArachneDeadMessage
+ }
+
+ fun isEnabled() = LorenzUtils.inSkyBlock && config.hideArachneMessages
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/NonGodPotEffectDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/NonGodPotEffectDisplay.kt
index 6549efaa9..fabff46ba 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/misc/NonGodPotEffectDisplay.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/NonGodPotEffectDisplay.kt
@@ -182,9 +182,13 @@ class NonGodPotEffectDisplay {
for (line in lines) {
for (effect in NonGodPotEffect.values()) {
if (line.startsWith(effect.displayName)) {
- val duration = TimeUtils.getMillis(line.split("§f")[1])
- effectDuration[effect] = System.currentTimeMillis() + duration
- update()
+ try {
+ val duration = TimeUtils.getMillis(line.split("§f")[1])
+ effectDuration[effect] = System.currentTimeMillis() + duration
+ update()
+ } catch (e: IndexOutOfBoundsException) {
+ LorenzUtils.debug("Error while reading non god pot effects from tab list! line: '$line'")
+ }
}
}
patternEffectsCount.matchMatcher(line) {
diff --git a/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerItemProfitTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerItemProfitTracker.kt
index f4e380fc4..c04d662e2 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerItemProfitTracker.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerItemProfitTracker.kt
@@ -211,6 +211,7 @@ object SlayerItemProfitTracker {
lastClickDelay = System.currentTimeMillis() + 500
} else {
itemProfit.hidden = !hidden
+ lastClickDelay = System.currentTimeMillis()
}
update()
}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerRngMeterDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerRngMeterDisplay.kt
new file mode 100644
index 000000000..1d41aaeeb
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/slayer/SlayerRngMeterDisplay.kt
@@ -0,0 +1,189 @@
+package at.hannibal2.skyhanni.features.slayer
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.config.Storage
+import at.hannibal2.skyhanni.data.ProfileStorageData
+import at.hannibal2.skyhanni.data.SlayerAPI
+import at.hannibal2.skyhanni.data.TitleUtils
+import at.hannibal2.skyhanni.events.GuiRenderEvent
+import at.hannibal2.skyhanni.events.InventoryOpenEvent
+import at.hannibal2.skyhanni.events.LorenzChatEvent
+import at.hannibal2.skyhanni.events.SlayerChangeEvent
+import at.hannibal2.skyhanni.utils.ItemUtils.getInternalName
+import at.hannibal2.skyhanni.utils.ItemUtils.getLore
+import at.hannibal2.skyhanni.utils.ItemUtils.nameWithEnchantment
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
+import at.hannibal2.skyhanni.utils.NumberUtil.formatNumber
+import at.hannibal2.skyhanni.utils.RenderUtils.renderString
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import at.hannibal2.skyhanni.utils.StringUtils.removeWordsAtEnd
+import io.github.moulberry.notenoughupdates.util.Constants
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import kotlin.math.ceil
+
+class SlayerRngMeterDisplay {
+ private val config get() = SkyHanniMod.feature.slayer.rngMeterDisplay
+ private var display = ""
+ private val inventoryNamePattern = "(?<name>.*) RNG Meter".toPattern()
+ private val updatePattern = " §dRNG Meter §f- §d(?<exp>.*) Stored XP".toPattern()
+ private val changedItemPattern = "§aYou set your §r.* RNG Meter §r§ato drop §r.*§a!".toPattern()
+ private var lastItemDroppedTime = 0L
+
+ private var tick = 0
+
+ @SubscribeEvent
+ fun onTick(event: TickEvent.ClientTickEvent) {
+ if (event.phase != TickEvent.Phase.START) return
+
+ if (!isEnabled()) return
+ tick++
+
+ if (tick % 20 == 0) {
+ if (lastItemDroppedTime != 0L) {
+ if (System.currentTimeMillis() > lastItemDroppedTime + 4_000) {
+ lastItemDroppedTime = 0L
+ update()
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onSlayerChange(event: SlayerChangeEvent) {
+ update()
+ }
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+
+ if (!isEnabled()) return
+
+ if (config.hideChat) {
+ if (SlayerAPI.isInSlayerArea) {
+ changedItemPattern.matchMatcher(event.message) {
+ event.blockedReason = "slayer_rng_meter"
+ }
+ }
+ }
+
+ val currentMeter = updatePattern.matchMatcher(event.message) {
+ group("exp").formatNumber()
+ } ?: return
+
+ val storage = getStorage() ?: return
+ val old = storage.currentMeter
+ storage.currentMeter = currentMeter
+
+ if (old != -1L) {
+ val item = storage.itemGoal
+ val hasItemSelected = item != "" && item != "?"
+ if (!hasItemSelected) {
+ if (config.warnEmpty) {
+ LorenzUtils.warning("§c[Skyhanni] No Slayer RNG Meter Item selected!")
+ TitleUtils.sendTitle("§cNo RNG Meter Item!", 3_000)
+ }
+ }
+ var blockChat = config.hideChat && hasItemSelected
+ val diff = currentMeter - old
+ if (diff > 0) {
+ storage.gainPerBoss = diff
+ } else {
+ storage.itemGoal = ""
+ blockChat = false
+ val from = old.addSeparators()
+ val to = storage.goalNeeded.addSeparators()
+
+ var rawPercentage = old.toDouble() / storage.goalNeeded
+ if (rawPercentage > 1) rawPercentage = 1.0
+ val percentage = LorenzUtils.formatPercentage(rawPercentage)
+ LorenzUtils.chat("§e[SkyHanni] §dRNG Meter §7dropped at §e$percentage §7XP ($from/${to}§7)")
+ lastItemDroppedTime = System.currentTimeMillis()
+ }
+ if (blockChat) {
+ event.blockedReason = "slayer_rng_meter"
+ }
+ }
+ update()
+ }
+
+ private fun getStorage(): Storage.ProfileSpecific.SlayerRngMeterStorage? {
+ return ProfileStorageData.profileSpecific?.slayerRngMeter?.getOrPut(getCurrentSlayer()) {
+ Storage.ProfileSpecific.SlayerRngMeterStorage()
+ }
+ }
+
+ private fun getCurrentSlayer() = SlayerAPI.latestSlayerCategory.removeWordsAtEnd(1).removeColor()
+
+ @SubscribeEvent
+ fun onInventoryOpen(event: InventoryOpenEvent) {
+ if (!isEnabled()) return
+
+ val name = inventoryNamePattern.matchMatcher(event.inventoryName) {
+ group("name")
+ } ?: return
+
+ if (name != getCurrentSlayer()) return
+
+ val storage = getStorage() ?: return
+
+ val selectedItem = event.inventoryItems.values.find { item -> item.getLore().any { it.contains("§aSELECTED") } }
+ if (selectedItem == null) {
+ storage.itemGoal = ""
+ storage.goalNeeded = -1
+ } else {
+ storage.itemGoal = selectedItem.nameWithEnchantment
+ val jsonObject = Constants.RNGSCORE["slayer"].asJsonObject.get(getCurrentSlayer()).asJsonObject
+ storage.goalNeeded = jsonObject.get(selectedItem.getInternalName()).asLong
+ }
+ update()
+ }
+
+ private fun update() {
+ display = drawDisplay()
+ }
+
+ private fun drawDisplay(): String {
+ val storage = getStorage() ?: return ""
+
+ if (SlayerAPI.latestSlayerCategory.let {
+ it.endsWith(" I") || it.endsWith(" II")
+ }) {
+ return ""
+ }
+ val latestSlayerCategory = SlayerAPI.latestSlayerCategory
+ latestSlayerCategory.endsWith(" I")
+
+ with(storage) {
+ if (itemGoal == "?") return "§cOpen RNG Meter Inventory!"
+ if (itemGoal == "") {
+ return if (lastItemDroppedTime != 0L) {
+ "§a§lRNG Item dropped!"
+ } else {
+ "§eNo RNG Item selected!"
+ }
+ }
+ if (currentMeter == -1L || gainPerBoss == -1L) return "§cKill the slayer boss 2 times!"
+
+ val missing = goalNeeded - currentMeter + gainPerBoss
+ var timesMissing = missing.toDouble() / gainPerBoss
+ if (timesMissing < 1) timesMissing = 1.0
+ timesMissing = ceil(timesMissing)
+
+ return "$itemGoal §7in §e${timesMissing.toInt().addSeparators()} §7bosses!"
+ }
+ }
+
+ @SubscribeEvent
+ fun onRenderOverlay(event: GuiRenderEvent.GameOverlayRenderEvent) {
+ if (!isEnabled()) return
+ if (!SlayerAPI.isInSlayerArea) return
+ if (!SlayerAPI.hasActiveSlayerQuest()) return
+
+ config.pos.renderString(display, posLabel = "Rng Meter Display")
+ }
+
+ fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt
index 90dcddf9b..917ec5c88 100644
--- a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt
+++ b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt
@@ -105,4 +105,6 @@ object StringUtils {
"$format$text"
}
}
+
+ fun String.removeWordsAtEnd(i: Int) = split(" ").dropLast(i).joinToString(" ")
} \ No newline at end of file