From d2f240ff0ca0d27f417f837e706c781a98c31311 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 28 Aug 2024 19:04:24 +0200 Subject: Refactor source layout Introduce compat source sets and move all kotlin sources to the main directory [no changelog] --- src/main/kotlin/features/FeatureManager.kt | 120 +++++++++ src/main/kotlin/features/FirmamentFeature.kt | 23 ++ src/main/kotlin/features/chat/AutoCompletions.kt | 57 ++++ src/main/kotlin/features/chat/ChatLinks.kt | 161 +++++++++++ src/main/kotlin/features/chat/QuickCommands.kt | 100 +++++++ src/main/kotlin/features/debug/DebugLogger.kt | 13 + src/main/kotlin/features/debug/DebugView.kt | 38 +++ .../kotlin/features/debug/DeveloperFeatures.kt | 55 ++++ src/main/kotlin/features/debug/MinorTrolling.kt | 27 ++ src/main/kotlin/features/debug/PowerUserTools.kt | 193 +++++++++++++ .../kotlin/features/diana/AncestralSpadeSolver.kt | 131 +++++++++ src/main/kotlin/features/diana/DianaWaypoints.kt | 35 +++ .../kotlin/features/diana/NearbyBurrowsSolver.kt | 144 ++++++++++ .../events/anniversity/AnniversaryFeatures.kt | 224 ++++++++++++++++ .../features/events/carnival/CarnivalFeatures.kt | 17 ++ .../features/events/carnival/MinesweeperHelper.kt | 276 +++++++++++++++++++ .../kotlin/features/fixes/CompatibliltyFeatures.kt | 51 ++++ src/main/kotlin/features/fixes/Fixes.kt | 71 +++++ .../kotlin/features/inventory/CraftingOverlay.kt | 66 +++++ .../features/inventory/ItemRarityCosmetics.kt | 85 ++++++ src/main/kotlin/features/inventory/PriceData.kt | 51 ++++ .../features/inventory/SaveCursorPosition.kt | 66 +++++ src/main/kotlin/features/inventory/SlotLocking.kt | 203 ++++++++++++++ .../features/inventory/buttons/InventoryButton.kt | 85 ++++++ .../inventory/buttons/InventoryButtonEditor.kt | 184 +++++++++++++ .../inventory/buttons/InventoryButtonTemplates.kt | 35 +++ .../features/inventory/buttons/InventoryButtons.kt | 88 ++++++ .../storageoverlay/StorageBackingHandle.kt | 53 ++++ .../inventory/storageoverlay/StorageData.kt | 21 ++ .../inventory/storageoverlay/StorageOverlay.kt | 154 +++++++++++ .../storageoverlay/StorageOverlayCustom.kt | 98 +++++++ .../storageoverlay/StorageOverlayScreen.kt | 296 ++++++++++++++++++++ .../storageoverlay/StorageOverviewScreen.kt | 123 +++++++++ .../inventory/storageoverlay/StoragePageSlot.kt | 66 +++++ .../inventory/storageoverlay/VirtualInventory.kt | 65 +++++ src/main/kotlin/features/mining/Histogram.kt | 81 ++++++ src/main/kotlin/features/mining/PickaxeAbility.kt | 176 ++++++++++++ .../features/mining/PristineProfitTracker.kt | 133 +++++++++ .../kotlin/features/notifications/Notifications.kt | 7 + .../kotlin/features/texturepack/AlwaysPredicate.kt | 17 ++ .../kotlin/features/texturepack/AndPredicate.kt | 26 ++ .../kotlin/features/texturepack/BakedModelExtra.kt | 9 + .../features/texturepack/BakedOverrideData.kt | 8 + .../features/texturepack/CustomBlockTextures.kt | 295 ++++++++++++++++++++ .../texturepack/CustomGlobalArmorOverrides.kt | 106 ++++++++ .../features/texturepack/CustomGlobalTextures.kt | 167 ++++++++++++ .../texturepack/CustomModelOverrideParser.kt | 74 +++++ .../features/texturepack/CustomSkyBlockTextures.kt | 114 ++++++++ .../features/texturepack/DisplayNamePredicate.kt | 22 ++ .../texturepack/ExtraAttributesPredicate.kt | 268 +++++++++++++++++++ .../texturepack/FirmamentModelPredicate.kt | 8 + .../texturepack/FirmamentModelPredicateParser.kt | 8 + .../kotlin/features/texturepack/ItemPredicate.kt | 32 +++ .../texturepack/JsonUnbakedModelFirmExtra.kt | 10 + .../kotlin/features/texturepack/LorePredicate.kt | 19 ++ .../features/texturepack/ModelOverrideData.kt | 7 + .../features/texturepack/ModelOverrideFilterSet.kt | 19 ++ .../kotlin/features/texturepack/NotPredicate.kt | 18 ++ .../kotlin/features/texturepack/NumberMatcher.kt | 125 +++++++++ .../kotlin/features/texturepack/OrPredicate.kt | 26 ++ .../kotlin/features/texturepack/PetPredicate.kt | 66 +++++ .../kotlin/features/texturepack/RarityMatcher.kt | 69 +++++ .../kotlin/features/texturepack/StringMatcher.kt | 159 +++++++++++ src/main/kotlin/features/world/FairySouls.kt | 131 +++++++++ src/main/kotlin/features/world/NPCWaypoints.kt | 40 +++ .../kotlin/features/world/NavigableWaypoint.kt | 22 ++ src/main/kotlin/features/world/NavigationHelper.kt | 121 +++++++++ src/main/kotlin/features/world/NpcWaypointGui.kt | 68 +++++ src/main/kotlin/features/world/Waypoints.kt | 297 +++++++++++++++++++++ 69 files changed, 6223 insertions(+) create mode 100644 src/main/kotlin/features/FeatureManager.kt create mode 100644 src/main/kotlin/features/FirmamentFeature.kt create mode 100644 src/main/kotlin/features/chat/AutoCompletions.kt create mode 100644 src/main/kotlin/features/chat/ChatLinks.kt create mode 100644 src/main/kotlin/features/chat/QuickCommands.kt create mode 100644 src/main/kotlin/features/debug/DebugLogger.kt create mode 100644 src/main/kotlin/features/debug/DebugView.kt create mode 100644 src/main/kotlin/features/debug/DeveloperFeatures.kt create mode 100644 src/main/kotlin/features/debug/MinorTrolling.kt create mode 100644 src/main/kotlin/features/debug/PowerUserTools.kt create mode 100644 src/main/kotlin/features/diana/AncestralSpadeSolver.kt create mode 100644 src/main/kotlin/features/diana/DianaWaypoints.kt create mode 100644 src/main/kotlin/features/diana/NearbyBurrowsSolver.kt create mode 100644 src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt create mode 100644 src/main/kotlin/features/events/carnival/CarnivalFeatures.kt create mode 100644 src/main/kotlin/features/events/carnival/MinesweeperHelper.kt create mode 100644 src/main/kotlin/features/fixes/CompatibliltyFeatures.kt create mode 100644 src/main/kotlin/features/fixes/Fixes.kt create mode 100644 src/main/kotlin/features/inventory/CraftingOverlay.kt create mode 100644 src/main/kotlin/features/inventory/ItemRarityCosmetics.kt create mode 100644 src/main/kotlin/features/inventory/PriceData.kt create mode 100644 src/main/kotlin/features/inventory/SaveCursorPosition.kt create mode 100644 src/main/kotlin/features/inventory/SlotLocking.kt create mode 100644 src/main/kotlin/features/inventory/buttons/InventoryButton.kt create mode 100644 src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt create mode 100644 src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt create mode 100644 src/main/kotlin/features/inventory/buttons/InventoryButtons.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageData.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt create mode 100644 src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt create mode 100644 src/main/kotlin/features/mining/Histogram.kt create mode 100644 src/main/kotlin/features/mining/PickaxeAbility.kt create mode 100644 src/main/kotlin/features/mining/PristineProfitTracker.kt create mode 100644 src/main/kotlin/features/notifications/Notifications.kt create mode 100644 src/main/kotlin/features/texturepack/AlwaysPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/AndPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/BakedModelExtra.kt create mode 100644 src/main/kotlin/features/texturepack/BakedOverrideData.kt create mode 100644 src/main/kotlin/features/texturepack/CustomBlockTextures.kt create mode 100644 src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt create mode 100644 src/main/kotlin/features/texturepack/CustomGlobalTextures.kt create mode 100644 src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt create mode 100644 src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt create mode 100644 src/main/kotlin/features/texturepack/DisplayNamePredicate.kt create mode 100644 src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt create mode 100644 src/main/kotlin/features/texturepack/ItemPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt create mode 100644 src/main/kotlin/features/texturepack/LorePredicate.kt create mode 100644 src/main/kotlin/features/texturepack/ModelOverrideData.kt create mode 100644 src/main/kotlin/features/texturepack/ModelOverrideFilterSet.kt create mode 100644 src/main/kotlin/features/texturepack/NotPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/NumberMatcher.kt create mode 100644 src/main/kotlin/features/texturepack/OrPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/PetPredicate.kt create mode 100644 src/main/kotlin/features/texturepack/RarityMatcher.kt create mode 100644 src/main/kotlin/features/texturepack/StringMatcher.kt create mode 100644 src/main/kotlin/features/world/FairySouls.kt create mode 100644 src/main/kotlin/features/world/NPCWaypoints.kt create mode 100644 src/main/kotlin/features/world/NavigableWaypoint.kt create mode 100644 src/main/kotlin/features/world/NavigationHelper.kt create mode 100644 src/main/kotlin/features/world/NpcWaypointGui.kt create mode 100644 src/main/kotlin/features/world/Waypoints.kt (limited to 'src/main/kotlin/features') diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt new file mode 100644 index 0000000..19b91de --- /dev/null +++ b/src/main/kotlin/features/FeatureManager.kt @@ -0,0 +1,120 @@ + + +package moe.nea.firmament.features + +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.generated.AllSubscriptions +import moe.nea.firmament.events.FeaturesInitializedEvent +import moe.nea.firmament.events.FirmamentEvent +import moe.nea.firmament.events.subscription.Subscription +import moe.nea.firmament.features.chat.AutoCompletions +import moe.nea.firmament.features.chat.ChatLinks +import moe.nea.firmament.features.chat.QuickCommands +import moe.nea.firmament.features.debug.DebugView +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.MinorTrolling +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.features.diana.DianaWaypoints +import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures +import moe.nea.firmament.features.events.carnival.CarnivalFeatures +import moe.nea.firmament.features.fixes.CompatibliltyFeatures +import moe.nea.firmament.features.fixes.Fixes +import moe.nea.firmament.features.inventory.CraftingOverlay +import moe.nea.firmament.features.inventory.ItemRarityCosmetics +import moe.nea.firmament.features.inventory.PriceData +import moe.nea.firmament.features.inventory.SaveCursorPosition +import moe.nea.firmament.features.inventory.SlotLocking +import moe.nea.firmament.features.inventory.buttons.InventoryButtons +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay +import moe.nea.firmament.features.mining.PickaxeAbility +import moe.nea.firmament.features.mining.PristineProfitTracker +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures +import moe.nea.firmament.features.world.FairySouls +import moe.nea.firmament.features.world.Waypoints +import moe.nea.firmament.util.data.DataHolder + +object FeatureManager : DataHolder(serializer(), "features", ::Config) { + @Serializable + data class Config( + val enabledFeatures: MutableMap = mutableMapOf() + ) + + private val features = mutableMapOf() + + val allFeatures: Collection get() = features.values + + private var hasAutoloaded = false + + init { + autoload() + } + + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(MinorTrolling) + loadFeature(FairySouls) + loadFeature(AutoCompletions) + // TODO: loadFeature(FishingWarning) + loadFeature(SlotLocking) + loadFeature(StorageOverlay) + loadFeature(PristineProfitTracker) + loadFeature(CraftingOverlay) + loadFeature(PowerUserTools) + loadFeature(Waypoints) + loadFeature(ChatLinks) + loadFeature(InventoryButtons) + loadFeature(CompatibliltyFeatures) + loadFeature(AnniversaryFeatures) + loadFeature(QuickCommands) + loadFeature(SaveCursorPosition) + loadFeature(CustomSkyBlockTextures) + loadFeature(PriceData) + loadFeature(Fixes) + loadFeature(DianaWaypoints) + loadFeature(ItemRarityCosmetics) + loadFeature(PickaxeAbility) + loadFeature(CarnivalFeatures) + if (Firmament.DEBUG) { + loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } + allFeatures.forEach { it.config } + FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) + hasAutoloaded = true + } + } + + fun subscribeEvents() { + AllSubscriptions.provideSubscriptions { + subscribeSingleEvent(it) + } + } + + private fun subscribeSingleEvent(it: Subscription) { + it.eventBus.subscribe(false, it.invoke) + } + + fun loadFeature(feature: FirmamentFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } + + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] + + + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } + +} diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt new file mode 100644 index 0000000..2cfc4fd --- /dev/null +++ b/src/main/kotlin/features/FirmamentFeature.kt @@ -0,0 +1,23 @@ + + +package moe.nea.firmament.features + +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.gui.config.ManagedConfig + +// TODO: remove this entire feature system and revamp config +interface FirmamentFeature : SubscriptionOwner { + val identifier: String + val defaultEnabled: Boolean + get() = true + var isEnabled: Boolean + get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled + set(value) { + FeatureManager.setEnabled(identifier, value) + } + override val delegateFeature: FirmamentFeature + get() = this + val config: ManagedConfig? get() = null + fun onLoad() {} + +} diff --git a/src/main/kotlin/features/chat/AutoCompletions.kt b/src/main/kotlin/features/chat/AutoCompletions.kt new file mode 100644 index 0000000..9144898 --- /dev/null +++ b/src/main/kotlin/features/chat/AutoCompletions.kt @@ -0,0 +1,57 @@ + + +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.arguments.StringArgumentType.string +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.MaskCommands +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC + +object AutoCompletions : FirmamentFeature { + + object TConfig : ManagedConfig(identifier) { + val provideWarpTabCompletion by toggle("warp-complete") { true } + val replaceWarpIsByWarpIsland by toggle("warp-is") { true } + } + + override val config: ManagedConfig? + get() = TConfig + override val identifier: String + get() = "auto-completions" + + @Subscribe + fun onMaskCommands(event: MaskCommands) { + if (TConfig.provideWarpTabCompletion) { + event.mask("warp") + } + } + + @Subscribe + fun onCommandEvent(event: CommandEvent) { + if (!TConfig.provideWarpTabCompletion) return + event.deleteCommand("warp") + event.register("warp") { + thenArgument("to", string()) { toArg -> + suggestsList { + RepoManager.neuRepo.constants?.islands?.warps?.flatMap { listOf(it.warp) + it.aliases } ?: listOf() + } + thenExecute { + val warpName = get(toArg) + if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) { + MC.sendServerCommand("warp island") + } else { + MC.sendServerCommand("warp $warpName") + } + } + } + } + } +} diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt new file mode 100644 index 0000000..f2cb78a --- /dev/null +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -0,0 +1,161 @@ + + +package moe.nea.firmament.features.chat + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.utils.io.jvm.javaio.toInputStream +import java.net.URL +import java.util.Collections +import moe.nea.jarvis.api.Point +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlin.math.min +import net.minecraft.client.gui.screen.ChatScreen +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.NativeImageBackedTexture +import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ModifyChatEvent +import moe.nea.firmament.events.ScreenRenderPostEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +object ChatLinks : FirmamentFeature { + override val identifier: String + get() = "chat-links" + + object TConfig : ManagedConfig(identifier) { + val enableLinks by toggle("links-enabled") { true } + val imageEnabled by toggle("image-enabled") { true } + val allowAllHosts by toggle("allow-all-hosts") { false } + val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" } + val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() } + val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) } + } + + private fun isHostAllowed(host: String) = + TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) } + + private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) + + override val config get() = TConfig + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex() + + data class Image( + val texture: Identifier, + val width: Int, + val height: Int, + ) + + val imageCache: MutableMap> = + Collections.synchronizedMap(mutableMapOf>()) + + private fun tryCacheUrl(url: String) { + if (!isUrlAllowed(url)) { + return + } + if (url in imageCache) { + return + } + imageCache[url] = Firmament.coroutineScope.async { + try { + val response = Firmament.httpClient.get(URL(url)) + if (response.status.value == 200) { + val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob) + val image = NativeImage.read(inputStream) + val texture = MC.textureManager.registerDynamicTexture( + "dynamic_image_preview", + NativeImageBackedTexture(image) + ) + Image(texture, image.width, image.height) + } else + null + } catch (exc: Exception) { + exc.printStackTrace() + null + } + } + } + + val imageExtensions = listOf("jpg", "png", "gif", "jpeg") + fun isImageUrl(url: String): Boolean { + return (url.substringAfterLast('.').lowercase() in imageExtensions) + } + + @Subscribe + @OptIn(ExperimentalCoroutinesApi::class) + fun onRender(it: ScreenRenderPostEvent) { + if (!TConfig.imageEnabled) return + if (it.screen !is ChatScreen) return + val hoveredComponent = + MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return + val hoverEvent = hoveredComponent.hoverEvent ?: return + val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return + if (!isImageUrl(url)) return + val imageFuture = imageCache[url] ?: return + if (!imageFuture.isCompleted) return + val image = imageFuture.getCompleted() ?: return + it.drawContext.matrices.push() + val pos = TConfig.position + pos.applyTransformations(it.drawContext.matrices) + val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) + it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.drawTexture( + image.texture, + 0, + 0, + 1F, + 1F, + image.width, + image.height, + image.width, + image.height, + ) + it.drawContext.matrices.pop() + } + + @Subscribe + fun onModifyChat(it: ModifyChatEvent) { + if (!TConfig.enableLinks) return + it.replaceWith = it.replaceWith.transformEachRecursively { child -> + val text = child.string + if ("://" !in text) return@transformEachRecursively child + val s = Text.empty().setStyle(child.style) + var index = 0 + while (index < text.length) { + val nextMatch = urlRegex.find(text, index) + if (nextMatch == null) { + s.append(Text.literal(text.substring(index, text.length))) + break + } + val range = nextMatch.groups[0]!!.range + val url = nextMatch.groupValues[0] + s.append(Text.literal(text.substring(index, range.first))) + s.append( + Text.literal(url).setStyle( + Style.EMPTY.withUnderline(true).withColor( + Formatting.AQUA + ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) + .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) + ) + ) + if (isImageUrl(url)) + tryCacheUrl(url) + index = range.last + 1 + } + s + } + } +} diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt new file mode 100644 index 0000000..5944b92 --- /dev/null +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -0,0 +1,100 @@ + + +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.context.CommandContext +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData + +object QuickCommands : FirmamentFeature { + override val identifier: String + get() = "quick-commands" + + fun removePartialPrefix(text: String, prefix: String): String? { + var lf: String? = null + for (i in 1..prefix.length) { + if (text.startsWith(prefix.substring(0, i))) { + lf = text.substring(i) + } + } + return lf + } + + val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") + val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") + + @Subscribe + fun onCommands(it: CommandEvent) { + it.register("join") { + thenArgument("what", RestArgumentType) { what -> + thenExecute { + val what = this[what] + if (!SBData.isOnSkyblock) { + MC.sendCommand("join $what") + return@thenExecute + } + val joinName = getNameForFloor(what.replace(" ", "").lowercase()) + if (joinName == null) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) + } else { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", + joinName)) + MC.sendCommand("joininstance $joinName") + } + } + } + thenExecute { + source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) + } + } + } + + fun CommandContext.getNameForFloor(w: String): String? { + val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier") + if (kuudraLevel != null) { + val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst { + it.startsWith( + kuudraLevel, + true + ) + } + if (l !in kuudraLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", + kuudraLevel)) + return null + } + return "KUUDRA_${kuudraLevelNames[l]}" + } + val masterLevel = removePartialPrefix(w, "master") + val normalLevel = + removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons") + val dungeonLevel = masterLevel ?: normalLevel + if (dungeonLevel != null) { + val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst { + it.startsWith( + dungeonLevel, + true + ) + } + if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) { + return "CATACOMBS_ENTRANCE" + } + if (l !in dungeonLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", + kuudraLevel)) + return null + } + return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" + } + return null + } +} diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt new file mode 100644 index 0000000..ab06030 --- /dev/null +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -0,0 +1,13 @@ + +package moe.nea.firmament.features.debug + +import net.minecraft.text.Text +import moe.nea.firmament.util.MC + +class DebugLogger(val tag: String) { + fun isEnabled() = DeveloperFeatures.isEnabled // TODO: allow filtering by tag + fun log(text: () -> String) { + if (!isEnabled()) return + MC.sendChat(Text.literal(text())) + } +} diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt new file mode 100644 index 0000000..7e1b8ec --- /dev/null +++ b/src/main/kotlin/features/debug/DebugView.kt @@ -0,0 +1,38 @@ + + +package moe.nea.firmament.features.debug + +import moe.nea.firmament.Firmament +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.TimeMark + +object DebugView : FirmamentFeature { + private data class StoredVariable( + val obj: T, + val timer: TimeMark, + ) + + private val storedVariables: MutableMap> = sortedMapOf() + override val identifier: String + get() = "debug-view" + override val defaultEnabled: Boolean + get() = Firmament.DEBUG + + fun showVariable(label: String, obj: T) { + synchronized(this) { + storedVariables[label] = StoredVariable(obj, TimeMark.now()) + } + } + + fun recalculateDebugWidget() { + } + + override fun onLoad() { + TickEvent.subscribe { + synchronized(this) { + recalculateDebugWidget() + } + } + } +} diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt new file mode 100644 index 0000000..20c0cfd --- /dev/null +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -0,0 +1,55 @@ + + +package moe.nea.firmament.features.debug + +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import kotlin.io.path.absolute +import kotlin.io.path.exists +import net.minecraft.client.MinecraftClient +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.errorBoundary +import moe.nea.firmament.util.iterate + +object DeveloperFeatures : FirmamentFeature { + override val identifier: String + get() = "developer" + override val config: TConfig + get() = TConfig + override val defaultEnabled: Boolean + get() = Firmament.DEBUG + + val gradleDir = + Path.of(".").absolute() + .iterate { it.parent } + .find { it.resolve("settings.gradle.kts").exists() } + + object TConfig : ManagedConfig("developer") { + val autoRebuildResources by toggle("auto-rebuild") { false } + } + + @JvmStatic + fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture { + val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) { + val builder = ProcessBuilder("./gradlew", ":processResources") + builder.directory(gradleDir.toFile()) + builder.inheritIO() + val process = builder.start() + MC.player?.sendMessage(Text.translatable("firmament.dev.resourcerebuild.start")) + val startTime = TimeMark.now() + process.toHandle().onExit().thenApply { + MC.player?.sendMessage(Text.stringifiedTranslatable("firmament.dev.resourcerebuild.done", startTime.passedTime())) + Unit + } + } else { + CompletableFuture.completedFuture(Unit) + } + return reloadFuture.thenCompose { client.reloadResources() } + } +} + diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt new file mode 100644 index 0000000..32035a6 --- /dev/null +++ b/src/main/kotlin/features/debug/MinorTrolling.kt @@ -0,0 +1,27 @@ + + +package moe.nea.firmament.features.debug + +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ModifyChatEvent +import moe.nea.firmament.features.FirmamentFeature + + +// In memorian Dulkir +object MinorTrolling : FirmamentFeature { + override val identifier: String + get() = "minor-trolling" + + val trollers = listOf("nea89o", "lrg89") + val t = "From(?: \\[[^\\]]+])? ([^:]+): (.*)".toRegex() + + @Subscribe + fun onTroll(it: ModifyChatEvent) { + val m = t.matchEntire(it.unformattedString) ?: return + val (_, name, text) = m.groupValues + if (name !in trollers) return + if (!text.startsWith("c:")) return + it.replaceWith = Text.literal(text.substring(2).replace("&", "§")) + } +} diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt new file mode 100644 index 0000000..7893eff --- /dev/null +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -0,0 +1,193 @@ + + +package moe.nea.firmament.features.debug + +import net.minecraft.block.SkullBlock +import net.minecraft.block.entity.SkullBlockEntity +import net.minecraft.component.DataComponentTypes +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.text.Text +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.EntityHitResult +import net.minecraft.util.hit.HitResult +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.CustomItemModelEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.skyBlockId + +object PowerUserTools : FirmamentFeature { + override val identifier: String + get() = "power-user" + + object TConfig : ManagedConfig(identifier) { + val showItemIds by toggle("show-item-id") { false } + val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") + val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id") + val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data") + val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") + val copyEntityData by keyBindingWithDefaultUnbound("entity-data") + } + + override val config + get() = TConfig + + var lastCopiedStack: Pair? = null + set(value) { + field = value + if (value != null) + lastCopiedStackViewTime = true + } + var lastCopiedStackViewTime = false + + override fun onLoad() { + TickEvent.subscribe { + if (!lastCopiedStackViewTime) + lastCopiedStack = null + lastCopiedStackViewTime = false + } + ScreenChangeEvent.subscribe { + lastCopiedStack = null + } + } + + fun debugFormat(itemStack: ItemStack): Text { + return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + } + + @Subscribe + fun onEntityInfo(event: WorldKeyboardEvent) { + if (!event.matches(TConfig.copyEntityData)) return + val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity + if (target == null) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.fail")) + return + } + showEntity(target) + } + + fun showEntity(target: Entity) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) + MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) + MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) + if (target is LivingEntity) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) + for (armorItem in target.armorItems) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) + } + } + MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size)) + target.passengerList.forEach { + showEntity(it) + } + } + + + @Subscribe + fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { + if (it.screen !is AccessorHandledScreen) return + val item = it.screen.focusedItemStack ?: return + if (it.matches(TConfig.copyItemId)) { + val sbId = item.skyBlockId + if (sbId == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail")) + return + } + ClipboardUtils.setTextContent(sbId.neuItem) + lastCopiedStack = + Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) + } else if (it.matches(TConfig.copyTexturePackId)) { + val model = CustomItemModelEvent.getModelIdentifier(item) + if (model == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) + return + } + ClipboardUtils.setTextContent(model.toString()) + lastCopiedStack = + Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) + } else if (it.matches(TConfig.copyNbtData)) { + // TODO: copy full nbt + val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toString() ?: "" + ClipboardUtils.setTextContent(nbt) + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) + } else if (it.matches(TConfig.copySkullTexture)) { + if (item.item != Items.PLAYER_HEAD) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) + return + } + val profile = item.get(DataComponentTypes.PROFILE) + if (profile == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) + return + } + val skullTexture = CustomSkyBlockTextures.getSkullTexture(profile) + if (skullTexture == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) + return + } + ClipboardUtils.setTextContent(skullTexture.toString()) + lastCopiedStack = + Pair( + item, + Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString()) + ) + println("Copied skull id: $skullTexture") + } + } + + @Subscribe + fun onCopyWorldInfo(it: WorldKeyboardEvent) { + if (it.matches(TConfig.copySkullTexture)) { + val p = MC.camera ?: return + val blockHit = p.raycast(20.0, 0.0f, false) ?: return + if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return + } + val blockAt = p.world.getBlockState(blockHit.blockPos)?.block + val entity = p.world.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return + } + val id = CustomSkyBlockTextures.getSkullTexture(entity.owner!!) + if (id == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + } else { + ClipboardUtils.setTextContent(id.toString()) + MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString())) + } + } + } + + @Subscribe + fun addItemId(it: ItemTooltipEvent) { + if (TConfig.showItemIds) { + val id = it.stack.skyBlockId ?: return + it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem)) + } + val (item, text) = lastCopiedStack ?: return + if (!ItemStack.areEqual(item, it.stack)) { + lastCopiedStack = null + return + } + lastCopiedStackViewTime = true + it.lines.add(text) + } + + +} diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt new file mode 100644 index 0000000..39ca6d3 --- /dev/null +++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt @@ -0,0 +1,131 @@ + +package moe.nea.firmament.features.diana + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.particle.ParticleTypes +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.WarpUtil +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId + +object AncestralSpadeSolver : SubscriptionOwner { + var lastDing = TimeMark.farPast() + private set + private val pitches = mutableListOf() + val particlePositions = mutableListOf() + var nextGuess: Vec3d? = null + private set + + val ancestralSpadeId = SkyblockId("ANCESTRAL_SPADE") + private var lastTeleportAttempt = TimeMark.farPast() + + fun isEnabled() = + DianaWaypoints.TConfig.ancestralSpadeSolver + && SBData.skyblockLocation == SkyBlockIsland.HUB + && MC.player?.inventory?.containsAny { it.skyBlockId == ancestralSpadeId } == true // TODO: add a reactive property here + + @Subscribe + fun onKeyBind(event: WorldKeyboardEvent) { + if (!isEnabled()) return + if (!event.matches(DianaWaypoints.TConfig.ancestralSpadeTeleport)) return + + if (lastTeleportAttempt.passedTime() < 3.seconds) return + WarpUtil.teleportToNearestWarp(SkyBlockIsland.HUB, nextGuess ?: return) + lastTeleportAttempt = TimeMark.now() + } + + @Subscribe + fun onParticleSpawn(event: ParticleSpawnEvent) { + if (!isEnabled()) return + if (event.particleEffect != ParticleTypes.DRIPPING_LAVA) return + if (event.offset.x != 0.0F || event.offset.y != 0F || event.offset.z != 0F) + return + particlePositions.add(event.position) + if (particlePositions.size > 20) { + particlePositions.removeFirst() + } + } + + @Subscribe + fun onPlaySound(event: SoundReceiveEvent) { + if (!isEnabled()) return + if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return + + if (lastDing.passedTime() > 1.seconds) { + particlePositions.clear() + pitches.clear() + } + lastDing = TimeMark.now() + + pitches.add(event.pitch) + if (pitches.size > 20) { + pitches.removeFirst() + } + + if (particlePositions.size < 3) { + return + } + + val averagePitchDelta = + if (pitches.isEmpty()) return + else pitches + .zipWithNext { a, b -> b - a } + .average() + + val soundDistanceEstimate = (Math.E / averagePitchDelta) - particlePositions.first().distanceTo(event.position) + + if (soundDistanceEstimate > 1000) { + return + } + + val lastParticleDirection = particlePositions + .takeLast(3) + .let { (a, _, b) -> b.subtract(a) } + .normalize() + + nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate)) + } + + @Subscribe + fun onWorldRender(event: WorldRenderLastEvent) { + if (!isEnabled()) return + RenderInWorldContext.renderInWorld(event) { + nextGuess?.let { + color(1f, 1f, 0f, 0.5f) + tinyBlock(it, 1f) + color(1f, 1f, 0f, 1f) + tracer(it, lineWidth = 3f) + } + if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) { + color(0f, 1f, 0f, 0.7f) + line(particlePositions) + } + } + } + + @Subscribe + fun onSwapWorld(event: WorldReadyEvent) { + nextGuess = null + particlePositions.clear() + pitches.clear() + lastDing = TimeMark.farPast() + } + + override val delegateFeature: FirmamentFeature + get() = DianaWaypoints + +} diff --git a/src/main/kotlin/features/diana/DianaWaypoints.kt b/src/main/kotlin/features/diana/DianaWaypoints.kt new file mode 100644 index 0000000..0a34eaa --- /dev/null +++ b/src/main/kotlin/features/diana/DianaWaypoints.kt @@ -0,0 +1,35 @@ + +package moe.nea.firmament.features.diana + +import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.events.UseBlockEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig + +object DianaWaypoints : FirmamentFeature { + override val identifier get() = "diana" + override val config get() = TConfig + + object TConfig : ManagedConfig(identifier) { + val ancestralSpadeSolver by toggle("ancestral-spade") { true } + val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport") + val nearbyWaypoints by toggle("nearby-waypoints") { true } + } + + override fun onLoad() { + UseBlockEvent.subscribe { + NearbyBurrowsSolver.onBlockClick(it.hitResult.blockPos) + } + AttackBlockEvent.subscribe { + NearbyBurrowsSolver.onBlockClick(it.blockPos) + } + } +} + + diff --git a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt new file mode 100644 index 0000000..7158bb9 --- /dev/null +++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt @@ -0,0 +1,144 @@ + +package moe.nea.firmament.features.diana + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.particle.ParticleTypes +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Position +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.mutableMapWithMaxSize +import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld + +object NearbyBurrowsSolver : SubscriptionOwner { + + + private val recentlyDugBurrows: MutableMap = mutableMapWithMaxSize(20) + private val recentEnchantParticles: MutableMap = mutableMapWithMaxSize(500) + private var lastBlockClick: BlockPos? = null + + enum class BurrowType { + START, MOB, TREASURE + } + + val burrows = mutableMapOf() + + @Subscribe + fun onChatEvent(event: ProcessChatEvent) { + val lastClickedBurrow = lastBlockClick ?: return + if (event.unformattedString.startsWith("You dug out a Griffin Burrow!") || + event.unformattedString.startsWith(" ☠ You were killed by") || + event.unformattedString.startsWith("You finished the Griffin burrow chain!") + ) { + markAsDug(lastClickedBurrow) + burrows.remove(lastClickedBurrow) + } + } + + + fun wasRecentlyDug(blockPos: BlockPos): Boolean { + val lastDigTime = recentlyDugBurrows[blockPos] ?: TimeMark.farPast() + return lastDigTime.passedTime() < 10.seconds + } + + fun markAsDug(blockPos: BlockPos) { + recentlyDugBurrows[blockPos] = TimeMark.now() + } + + fun wasRecentlyEnchanted(blockPos: BlockPos): Boolean { + val lastEnchantTime = recentEnchantParticles[blockPos] ?: TimeMark.farPast() + return lastEnchantTime.passedTime() < 4.seconds + } + + fun markAsEnchanted(blockPos: BlockPos) { + recentEnchantParticles[blockPos] = TimeMark.now() + } + + @Subscribe + fun onParticles(event: ParticleSpawnEvent) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + + val position: BlockPos = event.position.toBlockPos().down() + + if (wasRecentlyDug(position)) return + + val isEven50Spread = (event.offset.x == 0.5f && event.offset.z == 0.5f) + + if (event.particleEffect.type == ParticleTypes.ENCHANT) { + if (event.count == 5 && event.speed == 0.05F && event.offset.y == 0.4F && isEven50Spread) { + markAsEnchanted(position) + } + return + } + + if (!wasRecentlyEnchanted(position)) return + + if (event.particleEffect.type == ParticleTypes.ENCHANTED_HIT + && event.count == 4 + && event.speed == 0.01F + && event.offset.y == 0.1f + && isEven50Spread + ) { + burrows[position] = BurrowType.START + } + if (event.particleEffect.type == ParticleTypes.CRIT + && event.count == 3 + && event.speed == 0.01F + && event.offset.y == 0.1F + && isEven50Spread + ) { + burrows[position] = BurrowType.MOB + } + if (event.particleEffect.type == ParticleTypes.DRIPPING_LAVA + && event.count == 2 + && event.speed == 0.01F + && event.offset.y == 0.1F + && event.offset.x == 0.35F && event.offset.z == 0.35f + ) { + burrows[position] = BurrowType.TREASURE + } + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + renderInWorld(event) { + for ((location, burrow) in burrows) { + when (burrow) { + BurrowType.START -> color(.2f, .8f, .2f, 0.4f) + BurrowType.MOB -> color(0.3f, 0.4f, 0.9f, 0.4f) + BurrowType.TREASURE -> color(1f, 0.7f, 0.2f, 0.4f) + } + block(location) + } + } + } + + @Subscribe + fun onSwapWorld(worldReadyEvent: WorldReadyEvent) { + burrows.clear() + recentEnchantParticles.clear() + recentlyDugBurrows.clear() + lastBlockClick = null + } + + fun onBlockClick(blockPos: BlockPos) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + burrows.remove(blockPos) + lastBlockClick = blockPos + } + + override val delegateFeature: FirmamentFeature + get() = DianaWaypoints +} + +fun Position.toBlockPos(): BlockPos { + return BlockPos(MathHelper.floor(x), MathHelper.floor(y), MathHelper.floor(z)) +} diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt new file mode 100644 index 0000000..8926a95 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt @@ -0,0 +1,224 @@ + +package moe.nea.firmament.features.events.anniversity + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.jarvis.api.Point +import kotlin.time.Duration.Companion.seconds +import net.minecraft.entity.passive.PigEntity +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityInteractionEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.hud.MoulConfigHud +import moe.nea.firmament.rei.SBItemEntryDefinition +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.useMatch + +object AnniversaryFeatures : FirmamentFeature { + override val identifier: String + get() = "anniversary" + + object TConfig : ManagedConfig(identifier) { + val enableShinyPigTracker by toggle("shiny-pigs") {true} + val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) } + } + + override val config: ManagedConfig? + get() = TConfig + + data class ClickedPig( + val clickedAt: TimeMark, + val startLocation: BlockPos, + val pigEntity: PigEntity + ) { + @Bind("timeLeft") + fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration + } + + val clickedPigs = ObservableList(mutableListOf()) + var lastClickedPig: PigEntity? = null + + val pigDuration = 90.seconds + + @Subscribe + fun onTick(event: TickEvent) { + clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration } + } + + val pattern = "SHINY! You extracted (?.*) from the piglet's orb!".toPattern() + + @Subscribe + fun onChat(event: ProcessChatEvent) { + if(!TConfig.enableShinyPigTracker)return + if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") { + val pig = lastClickedPig ?: return + // TODO: store proper location based on the orb location, maybe + val startLocation = pig.blockPos ?: return + clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig)) + lastClickedPig = null + } + if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") { + val player = MC.player ?: return + val lowest = + clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return + clickedPigs.remove(lowest) + } + pattern.useMatch(event.unformattedString) { + val reward = group("reward") + val parsedReward = parseReward(reward) + addReward(parsedReward) + PigCooldown.rewards.atOnce { + PigCooldown.rewards.clear() + rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) } + } + } + } + + fun addReward(reward: Reward) { + val it = rewards.listIterator() + while (it.hasNext()) { + val merged = reward.mergeWith(it.next()) ?: continue + it.set(merged) + return + } + rewards.add(reward) + } + + val rewards = mutableListOf() + + fun ObservableList.atOnce(block: () -> Unit) { + val oldObserver = observer + observer = null + block() + observer = oldObserver + update() + } + + sealed interface Reward { + fun mergeWith(other: Reward): Reward? + data class EXP(val amount: Double, val skill: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is EXP && other.skill == skill) + return EXP(amount + other.amount, skill) + return null + } + } + + data class Coins(val amount: Double) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Coins) + return Coins(other.amount + amount) + return null + } + } + + data class Items(val amount: Int, val item: SkyblockId) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Items && other.item == item) + return Items(amount + other.amount, item) + return null + } + } + + data class Unknown(val text: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + return null + } + } + } + + val expReward = "\\+(?$SHORT_NUMBER_FORMAT) (?[^ ]+) XP".toPattern() + val coinReward = "\\+(?$SHORT_NUMBER_FORMAT) coins".toPattern() + val itemReward = "(?:(?[0-9]+)x )?(?.*)".toPattern() + fun parseReward(string: String): Reward { + expReward.useMatch(string) { + val exp = parseShortNumber(group("exp")) + val kind = group("kind") + return Reward.EXP(exp, kind) + } + coinReward.useMatch(string) { + val coins = parseShortNumber(group("amount")) + return Reward.Coins(coins) + } + itemReward.useMatch(string) { + val amount = group("amount")?.toIntOrNull() ?: 1 + val name = group("name") + val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch + return Reward.Items(amount, item) + } + return Reward.Unknown(string) + } + + @Subscribe + fun onWorldClear(event: WorldReadyEvent) { + lastClickedPig = null + clickedPigs.clear() + } + + @Subscribe + fun onEntityClick(event: EntityInteractionEvent) { + if (event.entity is PigEntity) { + lastClickedPig = event.entity + } + } + + @Subscribe + fun init(event: WorldReadyEvent) { + PigCooldown.forceInit() + } + + object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) { + override fun shouldRender(): Boolean { + return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker + } + + @Bind("pigs") + fun getPigs() = clickedPigs + + class DisplayReward(val backedBy: Reward) { + @Bind + fun count(): String { + return when (backedBy) { + is Reward.Coins -> backedBy.amount + is Reward.EXP -> backedBy.amount + is Reward.Items -> backedBy.amount + is Reward.Unknown -> 0 + }.toString() + } + + val itemStack = if (backedBy is Reward.Items) { + SBItemEntryDefinition.getEntry(backedBy.item, backedBy.amount) + } else { + SBItemEntryDefinition.getEntry(SkyblockId.NULL) + } + + @Bind + fun name(): String { + return when (backedBy) { + is Reward.Coins -> "Coins" + is Reward.EXP -> backedBy.skill + is Reward.Items -> itemStack.value.asItemStack().name.string + is Reward.Unknown -> backedBy.text + } + } + + @Bind + fun isKnown() = backedBy !is Reward.Unknown + } + + @get:Bind("rewards") + val rewards = ObservableList(mutableListOf()) + + } + +} diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt new file mode 100644 index 0000000..1e6d97a --- /dev/null +++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt @@ -0,0 +1,17 @@ + +package moe.nea.firmament.features.events.carnival + +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig + +object CarnivalFeatures : FirmamentFeature { + object TConfig : ManagedConfig(identifier) { + val enableBombSolver by toggle("bombs-solver") { true } + val displayTutorials by toggle("tutorials") { true } + } + + override val config: ManagedConfig? + get() = TConfig + override val identifier: String + get() = "carnival" +} diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt new file mode 100644 index 0000000..06caf86 --- /dev/null +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -0,0 +1,276 @@ + +package moe.nea.firmament.features.events.carnival + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import java.util.UUID +import net.minecraft.block.Blocks +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.text.ClickEvent +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import net.minecraft.world.WorldAccess +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.debug.DebugLogger +import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.item.createSkullItem +import mo