aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/features')
-rw-r--r--src/main/kotlin/features/FeatureManager.kt132
-rw-r--r--src/main/kotlin/features/FirmamentFeature.kt23
-rw-r--r--src/main/kotlin/features/chat/AutoCompletions.kt33
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt101
-rw-r--r--src/main/kotlin/features/chat/CopyChat.kt21
-rw-r--r--src/main/kotlin/features/chat/PartyCommands.kt8
-rw-r--r--src/main/kotlin/features/chat/QuickCommands.kt55
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt193
-rw-r--r--src/main/kotlin/features/debug/DebugLogger.kt11
-rw-r--r--src/main/kotlin/features/debug/DebugView.kt29
-rw-r--r--src/main/kotlin/features/debug/DeveloperFeatures.kt78
-rw-r--r--src/main/kotlin/features/debug/ExportedTestConstantMeta.kt27
-rw-r--r--src/main/kotlin/features/debug/MinorTrolling.kt10
-rw-r--r--src/main/kotlin/features/debug/PowerUserTools.kt191
-rw-r--r--src/main/kotlin/features/debug/SkinPreviews.kt91
-rw-r--r--src/main/kotlin/features/debug/SoundVisualizer.kt65
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt256
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ItemExporter.kt250
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt87
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt318
-rw-r--r--src/main/kotlin/features/diana/AncestralSpadeSolver.kt35
-rw-r--r--src/main/kotlin/features/diana/DianaWaypoints.kt36
-rw-r--r--src/main/kotlin/features/diana/NearbyBurrowsSolver.kt19
-rw-r--r--src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt396
-rw-r--r--src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt65
-rw-r--r--src/main/kotlin/features/events/carnival/CarnivalFeatures.kt13
-rw-r--r--src/main/kotlin/features/events/carnival/MinesweeperHelper.kt55
-rw-r--r--src/main/kotlin/features/fixes/CompatibliltyFeatures.kt41
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt77
-rw-r--r--src/main/kotlin/features/garden/HideComposterNoises.kt34
-rw-r--r--src/main/kotlin/features/inventory/CraftingOverlay.kt33
-rw-r--r--src/main/kotlin/features/inventory/ItemHotkeys.kt10
-rw-r--r--src/main/kotlin/features/inventory/ItemRarityCosmetics.kt37
-rw-r--r--src/main/kotlin/features/inventory/JunkHighlighter.kt30
-rw-r--r--src/main/kotlin/features/inventory/PetFeatures.kt551
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt149
-rw-r--r--src/main/kotlin/features/inventory/REIDependencyWarner.kt32
-rw-r--r--src/main/kotlin/features/inventory/SaveCursorPosition.kt113
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt372
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt48
-rw-r--r--src/main/kotlin/features/inventory/WardrobeKeybinds.kt80
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt162
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt231
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt5
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt158
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt24
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt105
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt84
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt370
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt82
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt68
-rw-r--r--src/main/kotlin/features/items/BlockZapperOverlay.kt139
-rw-r--r--src/main/kotlin/features/items/BonemerangOverlay.kt94
-rw-r--r--src/main/kotlin/features/items/EtherwarpOverlay.kt234
-rw-r--r--src/main/kotlin/features/items/recipes/ArrowWidget.kt38
-rw-r--r--src/main/kotlin/features/items/recipes/ComponentWidget.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/EntityWidget.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/FireWidget.kt19
-rw-r--r--src/main/kotlin/features/items/recipes/ItemList.kt308
-rw-r--r--src/main/kotlin/features/items/recipes/ItemSlotWidget.kt141
-rw-r--r--src/main/kotlin/features/items/recipes/MoulConfigWidget.kt46
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeRegistry.kt116
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeScreen.kt129
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeWidget.kt37
-rw-r--r--src/main/kotlin/features/items/recipes/RenderableRecipe.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt77
-rw-r--r--src/main/kotlin/features/items/recipes/TooltipWidget.kt30
-rw-r--r--src/main/kotlin/features/macros/ComboProcessor.kt103
-rw-r--r--src/main/kotlin/features/macros/HotkeyAction.kt40
-rw-r--r--src/main/kotlin/features/macros/KeyComboTrie.kt73
-rw-r--r--src/main/kotlin/features/macros/MacroData.kt19
-rw-r--r--src/main/kotlin/features/macros/MacroUI.kt293
-rw-r--r--src/main/kotlin/features/macros/RadialMenu.kt153
-rw-r--r--src/main/kotlin/features/mining/CommissionFeatures.kt10
-rw-r--r--src/main/kotlin/features/mining/Histogram.kt3
-rw-r--r--src/main/kotlin/features/mining/HotmPresets.kt69
-rw-r--r--src/main/kotlin/features/mining/MiningBlockInfoUi.kt53
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt112
-rw-r--r--src/main/kotlin/features/mining/PristineProfitTracker.kt47
-rw-r--r--src/main/kotlin/features/misc/CustomCapes.kt172
-rw-r--r--src/main/kotlin/features/misc/Devs.kt42
-rw-r--r--src/main/kotlin/features/misc/Hud.kt75
-rw-r--r--src/main/kotlin/features/misc/LicenseViewer.kt136
-rw-r--r--src/main/kotlin/features/misc/ModAnnouncer.kt80
-rw-r--r--src/main/kotlin/features/notifications/Notifications.kt7
-rw-r--r--src/main/kotlin/features/world/ColeWeightCompat.kt125
-rw-r--r--src/main/kotlin/features/world/FairySouls.kt214
-rw-r--r--src/main/kotlin/features/world/FirmWaypointManager.kt168
-rw-r--r--src/main/kotlin/features/world/FirmWaypoints.kt37
-rw-r--r--src/main/kotlin/features/world/NavigableWaypoint.kt2
-rw-r--r--src/main/kotlin/features/world/NavigationHelper.kt24
-rw-r--r--src/main/kotlin/features/world/NpcWaypointGui.kt3
-rw-r--r--src/main/kotlin/features/world/TemporaryWaypoints.kt72
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt345
94 files changed, 7570 insertions, 1890 deletions
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt
index 0f5ebf8..d474d65 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -1,123 +1,25 @@
package moe.nea.firmament.features
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.serializer
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.events.FeaturesInitializedEvent
import moe.nea.firmament.events.FirmamentEvent
import moe.nea.firmament.events.subscription.Subscription
import moe.nea.firmament.events.subscription.SubscriptionList
-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.PetFeatures
-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.world.FairySouls
-import moe.nea.firmament.features.world.Waypoints
-import moe.nea.firmament.util.data.DataHolder
-
-object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
- @Serializable
- data class Config(
- val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
- )
-
- private val features = mutableMapOf<String, FirmamentFeature>()
-
- val allFeatures: Collection<FirmamentFeature> 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(PetFeatures)
- loadFeature(SaveCursorPosition)
- 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() {
- SubscriptionList.allLists.forEach {
- it.provideSubscriptions {
- it.owner.javaClass.classes.forEach {
- runCatching { it.getDeclaredField("INSTANCE").get(null) }
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+object FeatureManager {
+
+ fun subscribeEvents() {
+ SubscriptionList.allLists.forEach { list ->
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ ErrorUtil.catch("Error while loading events from $list") {
+ list.provideSubscriptions {
+ subscribeSingleEvent(it)
+ }
}
- subscribeSingleEvent(it)
- }
- }
- }
-
- private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
- it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", 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()
- }
+ }
+ }
+ private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
+ it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke)
+ }
}
diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt
deleted file mode 100644
index 2cfc4fd..0000000
--- a/src/main/kotlin/features/FirmamentFeature.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-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
index 9e0de40..f13fe7e 100644
--- a/src/main/kotlin/features/chat/AutoCompletions.kt
+++ b/src/main/kotlin/features/chat/AutoCompletions.kt
@@ -1,6 +1,15 @@
package moe.nea.firmament.features.chat
+import com.mojang.brigadier.Message
import com.mojang.brigadier.arguments.StringArgumentType.string
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.exceptions.BuiltInExceptions
+import com.mojang.brigadier.exceptions.CommandExceptionType
+import com.mojang.brigadier.exceptions.CommandSyntaxException
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType
+import kotlin.concurrent.thread
+import net.minecraft.SharedConstants
+import net.minecraft.commands.BrigadierExceptions
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.suggestsList
@@ -8,21 +17,21 @@ 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
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
-object AutoCompletions : FirmamentFeature {
+object AutoCompletions {
+ @Config
object TConfig : ManagedConfig(identifier, Category.CHAT) {
val provideWarpTabCompletion by toggle("warp-complete") { true }
val replaceWarpIsByWarpIsland by toggle("warp-is") { true }
}
- override val config: ManagedConfig?
- get() = TConfig
- override val identifier: String
+ val identifier: String
get() = "auto-completions"
@Subscribe
@@ -44,12 +53,20 @@ object AutoCompletions : FirmamentFeature {
thenExecute {
val warpName = get(toArg)
if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) {
- MC.sendServerCommand("warp island")
+ MC.sendCommand("warp island")
} else {
- MC.sendServerCommand("warp $warpName")
+ redirectToServer()
}
}
}
}
}
+
+ fun CommandContext<*>.redirectToServer() {
+ val message = tr(
+ "firmament.warp.auto-complete.internal-throw",
+ "This is an internal syntax exception that should not show up in gameplay, used to pass on a command to the server"
+ )
+ throw CommandSyntaxException(CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand(), message)
+ }
}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
index f85825b..aca7af8 100644
--- a/src/main/kotlin/features/chat/ChatLinks.kt
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -1,47 +1,48 @@
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.net.URI
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
+import kotlinx.coroutines.future.await
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 net.minecraft.client.gui.screens.ChatScreen
+import com.mojang.blaze3d.platform.NativeImage
+import net.minecraft.client.renderer.texture.DynamicTexture
+import net.minecraft.network.chat.ClickEvent
+import net.minecraft.network.chat.HoverEvent
+import net.minecraft.network.chat.Style
+import net.minecraft.network.chat.Component
+import net.minecraft.ChatFormatting
+import net.minecraft.resources.ResourceLocation
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.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.net.HttpUtil
import moe.nea.firmament.util.render.drawTexture
import moe.nea.firmament.util.transformEachRecursively
import moe.nea.firmament.util.unformattedString
-object ChatLinks : FirmamentFeature {
- override val identifier: String
+object ChatLinks {
+ val identifier: String
get() = "chat-links"
+ @Config
object TConfig : ManagedConfig(identifier, Category.CHAT) {
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) }
+ val position by position("position", 16 * 20, 9 * 20) { Vector2i(0, 0) }
}
private fun isHostAllowed(host: String) =
@@ -49,12 +50,11 @@ object ChatLinks : FirmamentFeature {
private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
- override val config get() = TConfig
- val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
+ val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex()
val nextTexId = AtomicInteger(0)
data class Image(
- val texture: Identifier,
+ val texture: ResourceLocation,
val width: Int,
val height: Int,
)
@@ -71,18 +71,16 @@ object ChatLinks : FirmamentFeature {
}
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 texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
- MC.textureManager.registerTexture(
- texId,
- NativeImageBackedTexture(image)
- )
- Image(texId, image.width, image.height)
- } else
- null
+ val inputStream = HttpUtil.request(url)
+ .forInputStream()
+ .await()
+ val image = NativeImage.read(inputStream)
+ val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
+ MC.textureManager.register(
+ texId,
+ DynamicTexture({ texId.path }, image)
+ )
+ Image(texId, image.width, image.height)
} catch (exc: Exception) {
exc.printStackTrace()
null
@@ -101,19 +99,19 @@ object ChatLinks : FirmamentFeature {
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
+ MC.inGameHud.chat.getClickedComponentStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return
+ val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return
+ val value = hoverEvent.value
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()
+ it.drawContext.pose().pushMatrix()
val pos = TConfig.position
- pos.applyTransformations(it.drawContext.matrices)
+ pos.applyTransformations(JarvisIntegration.jarvis, it.drawContext.pose())
val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
- it.drawContext.matrices.scale(scale, scale, 1F)
+ it.drawContext.pose().scale(scale, scale)
it.drawContext.drawTexture(
image.texture,
0,
@@ -125,7 +123,7 @@ object ChatLinks : FirmamentFeature {
image.width,
image.height,
)
- it.drawContext.matrices.pop()
+ it.drawContext.pose().popMatrix()
}
@Subscribe
@@ -134,23 +132,24 @@ object ChatLinks : FirmamentFeature {
it.replaceWith = it.replaceWith.transformEachRecursively { child ->
val text = child.string
if ("://" !in text) return@transformEachRecursively child
- val s = Text.empty().setStyle(child.style)
+ val s = Component.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)))
+ val url = nextMatch?.groupValues[0]
+ val uri = runCatching { url?.let(::URI) }.getOrNull()
+ if (nextMatch == null || url == null || uri == null) {
+ s.append(Component.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(Component.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))
+ Component.literal(url).setStyle(
+ Style.EMPTY.withUnderlined(true).withColor(
+ ChatFormatting.AQUA
+ ).withHoverEvent(HoverEvent.ShowText(Component.literal(url)))
+ .withClickEvent(ClickEvent.OpenUrl(uri))
)
)
if (isImageUrl(url))
diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt
new file mode 100644
index 0000000..6bef99f
--- /dev/null
+++ b/src/main/kotlin/features/chat/CopyChat.kt
@@ -0,0 +1,21 @@
+package moe.nea.firmament.features.chat
+
+import net.minecraft.util.FormattedCharSequence
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.reconstitute
+
+
+object CopyChat {
+ val identifier: String
+ get() = "copy-chat"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.CHAT) {
+ val copyChat by toggle("copy-chat") { false }
+ }
+
+ fun orderedTextToString(orderedText: FormattedCharSequence): String {
+ return orderedText.reconstitute().string
+ }
+}
diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt
index de3a0d9..85daf51 100644
--- a/src/main/kotlin/features/chat/PartyCommands.kt
+++ b/src/main/kotlin/features/chat/PartyCommands.kt
@@ -5,17 +5,18 @@ import com.mojang.brigadier.StringReader
import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.LiteralCommandNode
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.util.math.BlockPos
+import net.minecraft.core.BlockPos
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.PartyMessageReceivedEvent
import moe.nea.firmament.events.ProcessChatEvent
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.useMatch
@@ -79,7 +80,7 @@ object PartyCommands {
register("coords") {
executes {
- val p = MC.player?.blockPos ?: BlockPos.ORIGIN
+ val p = MC.player?.blockPosition() ?: BlockPos.ZERO
MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
0
}
@@ -89,6 +90,7 @@ object PartyCommands {
// TODO: at TPS command
}
+ @Config
object TConfig : ManagedConfig("party-commands", Category.CHAT) {
val enable by toggle("enable") { false }
val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds }
diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt
index 7963171..b857f8a 100644
--- a/src/main/kotlin/features/chat/QuickCommands.kt
+++ b/src/main/kotlin/features/chat/QuickCommands.kt
@@ -5,9 +5,9 @@ import com.mojang.brigadier.context.CommandContext
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals
-import net.minecraft.command.CommandRegistryAccess
-import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket
-import net.minecraft.text.Text
+import net.minecraft.commands.CommandBuildContext
+import net.minecraft.network.protocol.game.ClientboundCommandsPacket
+import net.minecraft.network.chat.Component
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
import moe.nea.firmament.commands.RestArgumentType
@@ -15,18 +15,19 @@ 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.gui.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.tr
-object QuickCommands : FirmamentFeature {
- override val identifier: String
+object QuickCommands {
+ val identifier: String
get() = "quick-commands"
+ @Config
object TConfig : ManagedConfig("quick-commands", Category.CHAT) {
val enableJoin by toggle("join") { true }
val enableDh by toggle("dh") { true }
@@ -43,10 +44,14 @@ object QuickCommands : FirmamentFeature {
val dispatcher = CommandDispatcher<FabricClientCommandSource>()
ClientCommandInternals.setActiveDispatcher(dispatcher)
ClientCommandRegistrationCallback.EVENT.invoker()
- .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries,
- network.enabledFeatures))
+ .register(
+ dispatcher, CommandBuildContext.simple(
+ network.registryAccess,
+ network.enabledFeatures()
+ )
+ )
ClientCommandInternals.finalizeInit()
- network.onCommandTree(lastPacket)
+ network.handleCommands(lastPacket)
} catch (ex: Exception) {
ClientCommandInternals.setActiveDispatcher(fallback)
throw ex
@@ -64,7 +69,7 @@ object QuickCommands : FirmamentFeature {
return lf
}
- var lastReceivedTreePacket: CommandTreeS2CPacket? = null
+ var lastReceivedTreePacket: ClientboundCommandsPacket? = null
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
@@ -98,16 +103,20 @@ object QuickCommands : FirmamentFeature {
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
+ source.sendFeedback(Component.translatableEscape("firmament.quick-commands.join.unknown", what))
} else {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
- joinName))
+ source.sendFeedback(
+ Component.translatableEscape(
+ "firmament.quick-commands.join.success",
+ joinName
+ )
+ )
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
- source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
+ source.sendFeedback(Component.translatable("firmament.quick-commands.join.explain"))
}
}
}
@@ -122,8 +131,12 @@ object QuickCommands : FirmamentFeature {
)
}
if (l !in kuudraLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
- kuudraLevel))
+ source.sendFeedback(
+ Component.translatableEscape(
+ "firmament.quick-commands.join.unknown-kuudra",
+ kuudraLevel
+ )
+ )
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
@@ -143,8 +156,12 @@ object QuickCommands : FirmamentFeature {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
- kuudraLevel))
+ source.sendFeedback(
+ Component.translatableEscape(
+ "firmament.quick-commands.join.unknown-catacombs",
+ kuudraLevel
+ )
+ )
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
new file mode 100644
index 0000000..dc77115
--- /dev/null
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -0,0 +1,193 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.commands.arguments.ResourceKeyArgument
+import net.minecraft.core.component.DataComponentType
+import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.decoration.ArmorStand
+import net.minecraft.world.item.ItemStack
+import net.minecraft.nbt.Tag
+import net.minecraft.nbt.NbtOps
+import net.minecraft.core.registries.Registries
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.math.GChainReconciliation
+import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle
+import moe.nea.firmament.util.mc.NbtPrism
+import moe.nea.firmament.util.tr
+
+object AnimatedClothingScanner {
+
+ data class LensOfFashionTheft<T>(
+ val prism: NbtPrism,
+ val component: DataComponentType<T>,
+ ) {
+ fun observe(itemStack: ItemStack): Collection<Tag> {
+ val x = itemStack.get(component) ?: return listOf()
+ val nbt = component.codecOrThrow().encodeStart(NbtOps.INSTANCE, x).orThrow
+ return prism.access(nbt)
+ }
+ }
+
+ var lens: LensOfFashionTheft<*>? = null
+ var subject: Entity? = null
+ var history: MutableList<String> = mutableListOf()
+ val metaHistory: MutableList<List<String>> = mutableListOf()
+
+ @OptIn(ExperimentalStdlibApi::class)
+ @Subscribe
+ fun onUpdate(event: EntityUpdateEvent) {
+ val s = subject ?: return
+ if (event.entity != s) return
+ val l = lens ?: return
+ if (event is EntityUpdateEvent.EquipmentUpdate) {
+ event.newEquipment.forEach {
+ val formatted = (l.observe(it.second)).joinToString()
+ history.add(formatted)
+ // TODO: add a slot filter
+ }
+ }
+ }
+
+ fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> {
+ return metaHistory.fold(history, reducer).shortenCycle()
+ }
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("stealthisfit") {
+ thenLiteral("clear") {
+ thenExecute {
+ subject = null
+ metaHistory.clear()
+ history.clear()
+ MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history"))
+ }
+ }
+ thenLiteral("copy") {
+ thenExecute {
+ val history = reduceHistory { a, b -> a + b }
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history"))
+ }
+ thenLiteral("deduplicated") {
+ thenExecute {
+ val history = reduceHistory { a, b ->
+ (a.toMutableSet() + b).toList()
+ }
+ copyHistory(history)
+ MC.sendChat(
+ tr(
+ "firmament.fitstealer.copied.deduplicated",
+ "Copied the deduplicated history"
+ )
+ )
+ }
+ }
+ thenLiteral("merged") {
+ thenExecute {
+ val history = reduceHistory(GChainReconciliation::reconcileCycles)
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history"))
+ }
+ }
+ }
+ thenLiteral("target") {
+ thenLiteral("self") {
+ thenExecute {
+ toggleObserve(MC.player!!)
+ }
+ }
+ thenLiteral("pet") {
+ thenExecute {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.stealingpet",
+ "Observing nearest marker armourstand"
+ )
+ )
+ val p = MC.player!!
+ val nearestPet = p.level.getEntitiesOfClass(
+ ArmorStand::class.java,
+ p.boundingBox.inflate(10.0),
+ { it.isMarker })
+ .minBy { it.distanceToSqr(p) }
+ toggleObserve(nearestPet)
+ }
+ }
+ thenExecute {
+ val ent = MC.instance.crosshairPickEntity
+ if (ent == null) {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.notargetundercursor",
+ "No entity under cursor"
+ )
+ )
+ } else {
+ toggleObserve(ent)
+ }
+ }
+ }
+ thenLiteral("path") {
+ thenArgument(
+ "component",
+ ResourceKeyArgument.key(Registries.DATA_COMPONENT_TYPE)
+ ) { component ->
+ thenArgument("path", NbtPrism.Argument) { path ->
+ thenExecute {
+ lens = LensOfFashionTheft(
+ get(path),
+ MC.unsafeGetRegistryEntry(get(component))!!,
+ )
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.lensset",
+ "Analyzing path ${get(path)} for component ${get(component).location()}"
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun copyHistory(toCopy: List<String>) {
+ ClipboardUtils.setTextContent(toCopy.joinToString("\n"))
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ subject = null
+ if (history.isNotEmpty()) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ }
+
+ private fun toggleObserve(entity: Entity?) {
+ subject = if (subject == null) entity else null
+ if (subject == null) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ MC.sendChat(
+ subject?.let {
+ tr(
+ "firmament.fitstealer.targeted",
+ "Observing the equipment of ${it.name}."
+ )
+ } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
+ )
+ }
+}
diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt
index 2c6b962..2c8ced0 100644
--- a/src/main/kotlin/features/debug/DebugLogger.kt
+++ b/src/main/kotlin/features/debug/DebugLogger.kt
@@ -1,24 +1,29 @@
package moe.nea.firmament.features.debug
import kotlinx.serialization.serializer
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TestUtil
import moe.nea.firmament.util.collections.InstanceList
+import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.DataHolder
class DebugLogger(val tag: String) {
companion object {
val allInstances = InstanceList<DebugLogger>("DebugLogger")
}
+
+ @Config
object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf)
init {
allInstances.add(this)
}
- fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag)
+ fun isEnabled() = TestUtil.isInTest || EnabledLogs.data.contains(tag)
+ fun log(text: String) = log { text }
fun log(text: () -> String) {
if (!isEnabled()) return
- MC.sendChat(Text.literal(text()))
+ MC.sendChat(Component.literal(text()))
}
}
diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt
deleted file mode 100644
index ee54260..0000000
--- a/src/main/kotlin/features/debug/DebugView.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-package moe.nea.firmament.features.debug
-
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-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<T>(
- val obj: T,
- val timer: TimeMark,
- )
-
- private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf()
- override val identifier: String
- get() = "debug-view"
- override val defaultEnabled: Boolean
- get() = Firmament.DEBUG
-
- fun <T : Any?> showVariable(label: String, obj: T) {
- synchronized(this) {
- storedVariables[label] = StoredVariable(obj, TimeMark.now())
- }
- }
-
-}
diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt
index 8f0c25c..8638bb6 100644
--- a/src/main/kotlin/features/debug/DeveloperFeatures.kt
+++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt
@@ -3,33 +3,38 @@ package moe.nea.firmament.features.debug
import java.io.File
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.ClassNode
+import org.spongepowered.asm.mixin.Mixin
import kotlinx.serialization.json.encodeToStream
import kotlin.io.path.absolute
import kotlin.io.path.exists
-import net.minecraft.client.MinecraftClient
-import net.minecraft.text.Text
+import net.minecraft.client.Minecraft
+import net.minecraft.network.chat.Component
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.DebugInstantiateEvent
import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.init.MixinPlugin
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.asm.AsmAnnotationUtil
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.iterate
-object DeveloperFeatures : FirmamentFeature {
- override val identifier: String
+object DeveloperFeatures {
+ val DEVELOPER_SUBCOMMAND: String = "dev"
+ 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() }
+ @Config
object TConfig : ManagedConfig("developer", Category.DEV) {
val autoRebuildResources by toggle("auto-rebuild") { false }
}
@@ -42,6 +47,42 @@ object DeveloperFeatures : FirmamentFeature {
}
@Subscribe
+ fun loadAllMixinClasses(event: DebugInstantiateEvent) {
+ val allMixinClasses = mutableSetOf<String>()
+ MixinPlugin.instances.forEach { plugin ->
+ val prefix = plugin.mixinPackage + "."
+ val classes = plugin.mixins.map { prefix + it }
+ allMixinClasses.addAll(classes)
+ for (cls in classes) {
+ val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use {
+ val node = ClassNode()
+ ClassReader(it).accept(node, 0)
+ val mixins = mutableListOf<Mixin>()
+ (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach {
+ val annotationType = Type.getType(it.desc)
+ val mixinType = Type.getType(Mixin::class.java)
+ if (mixinType == annotationType) {
+ mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it))
+ }
+ }
+ mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } }
+ }
+ for (target in targets)
+ try {
+ Firmament.logger.debug("Loading ${target} to force instantiate ${cls}")
+ Class.forName(target, true, javaClass.classLoader)
+ } catch (ex: Throwable) {
+ Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex)
+ }
+ }
+ }
+ Firmament.logger.info("Forceloaded all Firmament mixins:")
+ val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet()
+ applied.forEach { Firmament.logger.info(" - ${it}") }
+ require(allMixinClasses == applied)
+ }
+
+ @Subscribe
fun dumpMissingTranslations(tickEvent: TickEvent) {
val toDump = missingTranslations ?: return
missingTranslations = null
@@ -51,24 +92,27 @@ object DeveloperFeatures : FirmamentFeature {
}
@JvmStatic
- fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> {
- val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) {
+ fun hookOnBeforeResourceReload(client: Minecraft): CompletableFuture<Void> {
+ val reloadFuture = if (TConfig.autoRebuildResources && Firmament.DEBUG && gradleDir != null) {
val builder = ProcessBuilder("./gradlew", ":processResources")
builder.directory(gradleDir.toFile())
builder.inheritIO()
val process = builder.start()
- MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start"))
+ MC.sendChat(Component.translatable("firmament.dev.resourcerebuild.start"))
val startTime = TimeMark.now()
process.toHandle().onExit().thenApply {
- MC.sendChat(Text.stringifiedTranslatable(
- "firmament.dev.resourcerebuild.done",
- startTime.passedTime()))
+ MC.sendChat(
+ Component.translatableEscape(
+ "firmament.dev.resourcerebuild.done",
+ startTime.passedTime()
+ )
+ )
Unit
}
} else {
CompletableFuture.completedFuture(Unit)
}
- return reloadFuture.thenCompose { client.reloadResources() }
+ return reloadFuture.thenCompose { client.reloadResourcePacks() }
}
}
diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
new file mode 100644
index 0000000..a2b42fd
--- /dev/null
+++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.debug
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import net.minecraft.SharedConstants
+import moe.nea.firmament.Firmament
+
+data class ExportedTestConstantMeta(
+ val dataVersion: Int,
+ val modVersion: Optional<String>,
+) {
+ companion object {
+ val current = ExportedTestConstantMeta(
+ SharedConstants.getCurrentVersion().dataVersion().version,
+ Optional.of("Firmament ${Firmament.version.friendlyString}")
+ )
+
+ val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create {
+ it.group(
+ Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion),
+ Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion),
+ ).apply(it, ::ExportedTestConstantMeta)
+ }
+ val SOURCE_CODEC = CODEC.fieldOf("source").codec()
+ }
+}
diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt
index 32035a6..7936521 100644
--- a/src/main/kotlin/features/debug/MinorTrolling.kt
+++ b/src/main/kotlin/features/debug/MinorTrolling.kt
@@ -2,15 +2,13 @@
package moe.nea.firmament.features.debug
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
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
+object MinorTrolling {
+ val identifier: String
get() = "minor-trolling"
val trollers = listOf("nea89o", "lrg89")
@@ -22,6 +20,6 @@ object MinorTrolling : FirmamentFeature {
val (_, name, text) = m.groupValues
if (name !in trollers) return
if (!text.startsWith("c:")) return
- it.replaceWith = Text.literal(text.substring(2).replace("&", "§"))
+ it.replaceWith = Component.literal(text.substring(2).replace("&", "§"))
}
}
diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt
index 225bc13..145ea35 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -2,43 +2,55 @@ package moe.nea.firmament.features.debug
import com.mojang.serialization.JsonOps
import kotlin.jvm.optionals.getOrNull
-import net.minecraft.block.SkullBlock
-import net.minecraft.block.entity.SkullBlockEntity
-import net.minecraft.component.DataComponentTypes
-import net.minecraft.component.type.ProfileComponent
-import net.minecraft.entity.Entity
-import net.minecraft.entity.LivingEntity
-import net.minecraft.item.ItemStack
-import net.minecraft.item.Items
+import net.minecraft.world.level.block.SkullBlock
+import net.minecraft.world.level.block.entity.SkullBlockEntity
+import net.minecraft.core.component.DataComponents
+import net.minecraft.world.item.component.ResolvableProfile
+import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.LivingEntity
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.nbt.ListTag
import net.minecraft.nbt.NbtOps
-import net.minecraft.text.Text
-import net.minecraft.text.TextCodecs
-import net.minecraft.util.Identifier
-import net.minecraft.util.hit.BlockHitResult
-import net.minecraft.util.hit.EntityHitResult
-import net.minecraft.util.hit.HitResult
+import net.minecraft.advancements.critereon.NbtPredicate
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.ComponentSerialization
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.world.Nameable
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.EntityHitResult
+import net.minecraft.world.phys.HitResult
import moe.nea.firmament.annotations.Subscribe
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.SlotRenderEvents
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldKeyboardEvent
-import moe.nea.firmament.features.FirmamentFeature
-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.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.mc.IntrospectableItemModelManager
+import moe.nea.firmament.util.mc.SNbtFormatter
import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.iterableArmorItems
import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.unsafeNbt
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
-object PowerUserTools : FirmamentFeature {
- override val identifier: String
+object PowerUserTools {
+ val identifier: String
get() = "power-user"
+ @Config
object TConfig : ManagedConfig(identifier, Category.DEV) {
val showItemIds by toggle("show-item-id") { false }
val copyItemId by keyBindingWithDefaultUnbound("copy-item-id")
@@ -48,22 +60,26 @@ object PowerUserTools : FirmamentFeature {
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack")
+ val copyTitle by keyBindingWithDefaultUnbound("copy-title")
+ val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack")
+ val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe")
+ val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location")
+ val highlightNonOverlayItems by toggle("highlight-non-overlay") { false }
+ val dontHighlightSemicolonItems by toggle("dont-highlight-semicolon-items") { false }
+ val showSlotNumbers by keyBindingWithDefaultUnbound("slot-numbers")
+ val autoCopyAnimatedSkins by toggle("copy-animated-skins") { false }
}
- override val config
- get() = TConfig
-
- var lastCopiedStack: Pair<ItemStack, Text>? = null
+ var lastCopiedStack: Pair<ItemStack, Component>? = null
set(value) {
field = value
- if (value != null) lastCopiedStackViewTime = true
+ if (value != null) lastCopiedStackViewTime = 2
}
- var lastCopiedStackViewTime = false
+ var lastCopiedStackViewTime = 0
@Subscribe
fun resetLastCopiedStack(event: TickEvent) {
- if (!lastCopiedStackViewTime) lastCopiedStack = null
- lastCopiedStackViewTime = false
+ if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null
}
@Subscribe
@@ -71,39 +87,58 @@ object PowerUserTools : FirmamentFeature {
lastCopiedStack = null
}
- fun debugFormat(itemStack: ItemStack): Text {
- return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString())
+ fun debugFormat(itemStack: ItemStack): Component {
+ return Component.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString())
+ }
+
+ @Subscribe
+ fun onRender(event: SlotRenderEvents.After) {
+ if (TConfig.showSlotNumbers.isPressed()) {
+ event.context.drawString(
+ MC.font,
+ event.slot.index.toString(), event.slot.x, event.slot.y, 0xFF00FF00.toInt(), true
+ )
+ event.context.drawString(
+ MC.font,
+ event.slot.containerSlot.toString(), event.slot.x, event.slot.y + MC.font.lineHeight, 0xFFFF0000.toInt(), true
+ )
+ }
}
@Subscribe
fun onEntityInfo(event: WorldKeyboardEvent) {
if (!event.matches(TConfig.copyEntityData)) return
- val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity
+ val target = (MC.instance.hitResult as? EntityHitResult)?.entity
if (target == null) {
- MC.sendChat(Text.translatable("firmament.poweruser.entity.fail"))
+ MC.sendChat(Component.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))
+ val nbt = NbtPredicate.getEntityTagToCompare(target)
+ nbt.remove("Inventory")
+ nbt.put("StyledName", ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, target.feedbackDisplayName).orThrow)
+ println(SNbtFormatter.prettify(nbt))
+ ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt))
+ MC.sendChat(Component.translatable("firmament.poweruser.entity.type", target.type))
+ MC.sendChat(Component.translatable("firmament.poweruser.entity.name", target.name))
+ MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.position", target.position))
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(Component.translatable("firmament.poweruser.entity.armor"))
+ for ((slot, armorItem) in target.iterableArmorItems) {
+ MC.sendChat(Component.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem)))
}
}
- MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size))
- target.passengerList.forEach {
+ MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.passengers", target.passengers.size))
+ target.passengers.forEach {
showEntity(it)
}
}
// TODO: leak this through some other way, maybe.
- lateinit var getSkullId: (profile: ProfileComponent) -> Identifier?
+ lateinit var getSkullId: (profile: ResolvableProfile) -> ResourceLocation?
@Subscribe
fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) {
@@ -112,58 +147,74 @@ object PowerUserTools : FirmamentFeature {
if (it.matches(TConfig.copyItemId)) {
val sbId = item.skyBlockId
if (sbId == null) {
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skyblockid.fail"))
return
}
ClipboardUtils.setTextContent(sbId.neuItem)
lastCopiedStack =
- Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem))
+ Pair(item, Component.translatableEscape("firmament.tooltip.copied.skyblockid", sbId.neuItem))
} else if (it.matches(TConfig.copyTexturePackId)) {
- val model = CustomItemModelEvent.getModelIdentifier(item) // TODO: remove global texture overrides, maybe
+ val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager {
+ override fun hasModel_firmament(identifier: ResourceLocation): Boolean {
+ return true
+ }
+ }).getOrNull() // TODO: remove global texture overrides, maybe
if (model == null) {
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.modelid.fail"))
return
}
ClipboardUtils.setTextContent(model.toString())
lastCopiedStack =
- Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString()))
+ Pair(item, Component.translatableEscape("firmament.tooltip.copied.modelid", model.toString()))
} else if (it.matches(TConfig.copyNbtData)) {
// TODO: copy full nbt
- val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "<empty>"
+ val nbt = item.get(DataComponents.CUSTOM_DATA)?.unsafeNbt?.toPrettyString() ?: "<empty>"
ClipboardUtils.setTextContent(nbt)
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.nbt"))
} else if (it.matches(TConfig.copyLoreData)) {
val list = mutableListOf(item.displayNameAccordingToNbt)
list.addAll(item.loreAccordingToNbt)
ClipboardUtils.setTextContent(list.joinToString("\n") {
- TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString()
+ ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString()
})
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.lore"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.lore"))
} 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"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-skull"))
return
}
- val profile = item.get(DataComponentTypes.PROFILE)
+ val profile = item.get(DataComponents.PROFILE)
if (profile == null) {
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile"))
+ lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-profile"))
return
}
val skullTexture = getSkullId(profile)
if (skullTexture == null) {
- lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture"))
+ lastCopiedStack = Pair(item, Component.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()))
+ Pair(item, Component.translatableEscape("firmament.tooltip.copied.skull-id", skullTexture.toString()))
println("Copied skull id: $skullTexture")
} else if (it.matches(TConfig.copyItemStack)) {
- ClipboardUtils.setTextContent(
- ItemStack.CODEC
- .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item)
- .orThrow.toPrettyString())
- lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack"))
+ val nbt = ItemStack.CODEC
+ .encodeStart(MC.currentOrDefaultRegistries.createSerializationContext(NbtOps.INSTANCE), item)
+ .orThrow
+ ClipboardUtils.setTextContent(nbt.toPrettyString())
+ lastCopiedStack = Pair(item, Component.translatableEscape("firmament.tooltip.copied.stack"))
+ } else if (it.matches(TConfig.copyTitle) && it.screen is AbstractContainerScreen<*>) {
+ val allTitles = ListTag()
+ val inventoryNames =
+ it.screen.menu.slots
+ .mapNotNullTo(mutableSetOf()) { it.container }
+ .filterIsInstance<Nameable>()
+ .map { it.name }
+ for (it in listOf(it.screen.title) + inventoryNames) {
+ allTitles.add(ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!)
+ }
+ ClipboardUtils.setTextContent(allTitles.toPrettyString())
+ MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles"))
}
}
@@ -171,23 +222,23 @@ object PowerUserTools : FirmamentFeature {
fun onCopyWorldInfo(it: WorldKeyboardEvent) {
if (it.matches(TConfig.copySkullTexture)) {
val p = MC.camera ?: return
- val blockHit = p.raycast(20.0, 0.0f, false) ?: return
+ val blockHit = p.pick(20.0, 0.0f, false) ?: return
if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) {
- MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
+ MC.sendChat(Component.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"))
+ val blockAt = p.level.getBlockState(blockHit.blockPos)?.block
+ val entity = p.level.getBlockEntity(blockHit.blockPos)
+ if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.ownerProfile == null) {
+ MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail"))
return
}
- val id = getSkullId(entity.owner!!)
+ val id = getSkullId(entity.ownerProfile!!)
if (id == null) {
- MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
+ MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail"))
} else {
ClipboardUtils.setTextContent(id.toString())
- MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString()))
+ MC.sendChat(Component.translatableEscape("firmament.tooltip.copied.skull", id.toString()))
}
}
}
@@ -196,14 +247,14 @@ object PowerUserTools : FirmamentFeature {
fun addItemId(it: ItemTooltipEvent) {
if (TConfig.showItemIds) {
val id = it.stack.skyBlockId ?: return
- it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem))
+ it.lines.add(Component.translatableEscape("firmament.tooltip.skyblockid", id.neuItem).grey())
}
val (item, text) = lastCopiedStack ?: return
- if (!ItemStack.areEqual(item, it.stack)) {
+ if (!ItemStack.matches(item, it.stack)) {
lastCopiedStack = null
return
}
- lastCopiedStackViewTime = true
+ lastCopiedStackViewTime = 0
it.lines.add(text)
}
diff --git a/src/main/kotlin/features/debug/SkinPreviews.kt b/src/main/kotlin/features/debug/SkinPreviews.kt
new file mode 100644
index 0000000..56c63db
--- /dev/null
+++ b/src/main/kotlin/features/debug/SkinPreviews.kt
@@ -0,0 +1,91 @@
+package moe.nea.firmament.features.debug
+
+import com.mojang.authlib.GameProfile
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.core.component.DataComponents
+import net.minecraft.world.item.component.ResolvableProfile
+import net.minecraft.world.entity.EquipmentSlot
+import net.minecraft.world.entity.LivingEntity
+import net.minecraft.world.phys.Vec3
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.IsSlotProtectedEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.json.toJsonArray
+import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.rawSkyBlockId
+import moe.nea.firmament.util.toTicks
+import moe.nea.firmament.util.tr
+
+
+object SkinPreviews {
+
+ // TODO: add pet support
+ @Subscribe
+ fun onEntityUpdate(event: EntityUpdateEvent) {
+ if (!isRecording) return
+ if (event.entity.position != pos)
+ return
+ val entity = event.entity as? LivingEntity ?: return
+ val stack = entity.getItemBySlot(EquipmentSlot.HEAD) ?: return
+ val profile = stack.get(DataComponents.PROFILE)?.partialProfile() ?: return
+ if (profile == animation.lastOrNull()) return
+ animation.add(profile)
+ val shortened = animation.shortenCycle()
+ if (shortened.size <= (animation.size / 2).coerceAtLeast(1) && lastDiscard.passedTime() > 2.seconds) {
+ val tickEstimation = (lastDiscard.passedTime() / animation.size).toTicks()
+ val skinName = if (skinColor != null) "${skinId}_${skinColor?.replace(" ", "_")?.uppercase()}" else skinId!!
+ val json =
+ buildJsonObject {
+ put("ticks", tickEstimation)
+ put(
+ "textures",
+ shortened.map {
+ it.id.toString() + ":" + it.properties()["textures"].first().value()
+ }.toJsonArray()
+ )
+ }
+ MC.sendChat(
+ tr(
+ "firmament.dev.skinpreviews.done",
+ "Observed a total of ${animation.size} elements, which could be shortened to a cycle of ${shortened.size}. Copying JSON array. Estimated ticks per frame: $tickEstimation."
+ )
+ )
+ isRecording = false
+ ClipboardUtils.setTextContent(JsonPrimitive(skinName).toString() + ":" + json.toString())
+ }
+ }
+
+ var animation = mutableListOf<GameProfile>()
+ var pos = Vec3(-1.0, 72.0, -101.25)
+ var isRecording = false
+ var skinColor: String? = null
+ var skinId: String? = null
+ var lastDiscard = TimeMark.farPast()
+
+ @Subscribe
+ fun onActivate(event: IsSlotProtectedEvent) {
+ if (!PowerUserTools.TConfig.autoCopyAnimatedSkins) return
+ val lastLine = event.itemStack.loreAccordingToNbt.lastOrNull()?.string
+ if (lastLine != "Right-click to preview!" && lastLine != "Click to preview!") return
+ lastDiscard = TimeMark.now()
+ val stackName = event.itemStack.displayNameAccordingToNbt.string
+ if (stackName == "FIRE SALE!") {
+ skinColor = null
+ skinId = event.itemStack.rawSkyBlockId
+ } else {
+ skinColor = stackName
+ }
+ animation.clear()
+ isRecording = true
+ MC.sendChat(tr("firmament.dev.skinpreviews.start", "Starting to observe items"))
+ }
+}
diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt
new file mode 100644
index 0000000..37b248a
--- /dev/null
+++ b/src/main/kotlin/features/debug/SoundVisualizer.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object SoundVisualizer {
+
+ var showSounds = false
+
+ var sounds = mutableListOf<SoundReceiveEvent>()
+
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("sounds") {
+ thenExecute {
+ showSounds = !showSounds
+ if (!showSounds) {
+ sounds.clear()
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ sounds.clear()
+ }
+
+ @Subscribe
+ fun onRender(event: WorldRenderLastEvent) {
+ RenderInWorldContext.renderInWorld(event) {
+ sounds.forEach { event ->
+ withFacingThePlayer(event.position) {
+ text(
+ Component.literal(event.sound.value().location.toString()).also {
+ if (event.cancelled)
+ it.red()
+ },
+ verticalAlign = RenderInWorldContext.VerticalAlign.CENTER,
+ )
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onSoundReceive(event: SoundReceiveEvent) {
+ if (!showSounds) return
+ if (sounds.size > 1000) {
+ sounds.subList(0, 200).clear()
+ }
+ sounds.add(event)
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
new file mode 100644
index 0000000..d12d667
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
@@ -0,0 +1,256 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import net.minecraft.client.player.AbstractClientPlayer
+import net.minecraft.world.entity.decoration.ArmorStand
+import net.minecraft.core.ClientAsset
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.ItemNameLookup
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.async.waitForTextInput
+import moe.nea.firmament.util.ifDropLast
+import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.removeColorCodes
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.unformattedString
+import moe.nea.firmament.util.useMatch
+
+object ExportRecipe {
+
+
+ val xNames = "123"
+ val yNames = "ABC"
+
+ val slotIndices = (0..<9).map {
+ val x = it % 3
+ val y = it / 3
+
+ (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10
+ }
+ val resultSlot = 25
+ val craftingTableSlut = resultSlot - 2
+
+ @Subscribe
+ fun exportNpcLocation(event: WorldKeyboardEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) {
+ return
+ }
+ val entity = MC.instance.crosshairPickEntity
+ if (entity == null) {
+ MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export"))
+ return
+ }
+ Firmament.coroutineScope.launch {
+ val guessName = entity.level.getEntitiesOfClass(
+ ArmorStand::class.java,
+ entity.boundingBox.inflate(0.1),
+ { !it.name.string.contains("CLICK") })
+ .firstOrNull()?.customName?.string
+ ?: ""
+ val reply = waitForTextInput("$guessName (NPC)", "Export stub")
+ val id = generateName(reply)
+ ItemExporter.exportStub(id, "§9$reply") {
+ val playerEntity = entity as? AbstractClientPlayer
+ val textureUrl = (playerEntity?.skin?.body as? ClientAsset.DownloadedTexture)?.url
+ if (textureUrl != null)
+ it.setSkullOwner(playerEntity.uuid, textureUrl)
+ }
+ ItemExporter.modifyJson(id) {
+ val mutJson = it.toMutableMap()
+ mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown")
+ mutJson["x"] = JsonPrimitive(entity.blockX)
+ mutJson["y"] = JsonPrimitive(entity.blockY)
+ mutJson["z"] = JsonPrimitive(entity.blockZ)
+ JsonObject(mutJson)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) {
+ return
+ }
+ val title = event.screen.title.string
+ val sellSlot = event.screen.getSlotByIndex(49, false)?.item
+ val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false)
+ if (craftingTableSlot?.item?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") {
+ slotIndices.forEach { (_, index) ->
+ event.screen.getSlotByIndex(index, false)?.item?.let(ItemExporter::ensureExported)
+ }
+ val inputs = slotIndices.associate { (name, index) ->
+ val id = event.screen.getSlotByIndex(index, false)?.item?.takeIf { !it.isEmpty() }?.let {
+ "${it.skyBlockId?.neuItem}:${it.count}"
+ } ?: ""
+ name to JsonPrimitive(id)
+ }
+ val output = event.screen.getSlotByIndex(resultSlot, false)?.item!!
+ val overrideOutputId = output.skyBlockId!!.neuItem
+ val count = output.count
+ val recipe = JsonObject(
+ inputs + mapOf(
+ "type" to JsonPrimitive("crafting"),
+ "count" to JsonPrimitive(count),
+ "overrideOutputId" to JsonPrimitive(overrideOutputId)
+ )
+ )
+ ItemExporter.appendRecipe(output.skyBlockId!!, recipe)
+ MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported."))
+ return
+ } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt
+ ?: listOf()).any { it.string == "Click to buyback!" }
+ ) {
+ val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC")
+ if (!ItemExporter.isExported(shopId)) {
+ // TODO: export location + skin of last clicked npc
+ ItemExporter.exportStub(shopId, "§9$title (NPC)")
+ }
+ for (index in (9..9 * 5)) {
+ val item = event.screen.getSlotByIndex(index, false)?.item ?: continue
+ val skyblockId = item.skyBlockId ?: continue
+ val costLines = item.loreAccordingToNbt
+ .map { it.string.trim() }
+ .dropWhile { !it.startsWith("Cost") }
+ .dropWhile { it == "Cost" }
+ .takeWhile { it != "Click to trade!" }
+ .takeWhile { it != "Stock" }
+ .filter { !it.isBlank() }
+ .map { it.removePrefix("Cost: ") }
+
+
+ val costs = costLines.mapNotNull { lineText ->
+ val line = findStackableItemByName(lineText)
+ if (line == null) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.itemshop.fail",
+ "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}"
+ ).red()
+ )
+ }
+ line
+ }
+
+
+ ItemExporter.appendRecipe(
+ shopId, JsonObject(
+ mapOf(
+ "type" to JsonPrimitive("npc_shop"),
+ "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }),
+ "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"),
+ )
+ )
+ )
+ }
+ MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete."))
+ } else {
+ MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found"))
+ }
+ }
+
+ private val coinRegex = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern()
+ private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern()
+ private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern()
+
+ private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".toPattern()
+
+ fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? {
+ var id = ItemNameLookup.guessItemByName(name, true)
+ if (id == null && fallbackToGenerated) {
+ id = generateName(name)
+ }
+ return id
+ }
+
+ fun skill(name: String): SkyblockId {
+ return SkyblockId("SKYBLOCK_SKILL_${name}")
+ }
+
+ fun generateName(name: String): SkyblockId {
+ return SkyblockId(name.uppercase().replace(" ", "_").replace(Regex("[^A-Z_]+"), ""))
+ }
+
+ fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? {
+ val properName = name.removeColorCodes().trim()
+ if (properName == "FREE" || properName == "This Chest is Free!") {
+ return Pair(SkyBlockItems.COINS, 0.0)
+ }
+ coinRegex.useMatch(properName) {
+ return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount")))
+ }
+ etherialRewardPattern.useMatch(properName) {
+ val id = when (val id = group("what")) {
+ "Copper" -> SkyblockId("SKYBLOCK_COPPER")
+ "Bits" -> SkyblockId("SKYBLOCK_BIT")
+ "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN")
+ "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING")
+ "Gold Essence" -> SkyblockId("ESSENCE_GOLD")
+ "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE")
+ "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL")
+ "Pelts" -> SkyblockId("SKYBLOCK_PELT")
+ "Fine Flour" -> SkyblockId("FINE_FLOUR")
+ else -> {
+ id.ifDropLast(" Experience") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" XP") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" Powder") {
+ SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}")
+ } ?: id.ifDropLast(" Essence") {
+ SkyblockId("ESSENCE_${generateName(it).neuItem}")
+ } ?: generateName(id)
+ }
+ }
+ return Pair(id, parseShortNumber(group("amount")))
+ }
+ essenceRegex.useMatch(properName) {
+ return Pair(
+ SkyblockId("ESSENCE_${group("essence").uppercase()}"),
+ parseShortNumber(group("count"))
+ )
+ }
+ stackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ reverseStackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ numberedItemRegex.useMatch(properName) {
+ val item = findForName(group("what"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+
+ return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) }
+ }
+
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
new file mode 100644
index 0000000..f664da3
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
@@ -0,0 +1,250 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.notExists
+import kotlin.io.path.readText
+import kotlin.io.path.relativeTo
+import kotlin.io.path.writeText
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.nbt.StringTag
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+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.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.SlotRenderEvents
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.RepoDownloadManager
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.LegacyTagParser
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.setSkyBlockId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+
+object ItemExporter {
+
+ fun exportItem(itemStack: ItemStack): Component {
+ nonOverlayCache.clear()
+ val exporter = LegacyItemExporter.createExporter(itemStack)
+ var json = exporter.exportJson()
+ val fileName = json.jsonObject["internalname"]?.jsonPrimitive?.takeIf { it.isString }?.content
+ if (fileName == null) {
+ return tr(
+ "firmament.repoexport.nointernalname",
+ "Could not find internal name to export for this item (null.json)"
+ )
+ }
+ val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json")
+ itemFile.createParentDirectories()
+ if (itemFile.exists()) {
+ val existing = try {
+ Firmament.json.decodeFromString<JsonObject>(itemFile.readText())
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ JsonObject(mapOf())
+ }
+ val mut = json.jsonObject.toMutableMap()
+ for (prop in existing) {
+ if (prop.key !in mut || mut[prop.key]!!.let {
+ (it is JsonPrimitive && (it.content.isEmpty() || it.content == "0")) || (it is JsonArray && it.isEmpty()) || (it is JsonObject && it.isEmpty())
+ })
+ mut[prop.key] = prop.value
+ }
+ json = JsonObject(mut)
+ }
+ val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json)
+ itemFile.writeText(jsonFormatted)
+ val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay")
+ .resolve(ExportedTestConstantMeta.current.dataVersion.toString())
+ .resolve("${fileName}.snbt")
+ overlayFile.createParentDirectories()
+ overlayFile.writeText(exporter.exportModernSnbt().toPrettyString())
+ return tr(
+ "firmament.repoexport.success",
+ "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${
+ exporter.warnings.joinToString(
+ ""
+ ) { "\nWarning: $it" }
+ }"
+ )
+ }
+
+ fun pathFor(skyBlockId: SkyblockId) =
+ RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json")
+
+ fun isExported(skyblockId: SkyblockId) =
+ pathFor(skyblockId).exists()
+
+ fun ensureExported(itemStack: ItemStack) {
+ if (!isExported(itemStack.skyBlockId ?: return))
+ MC.sendChat(exportItem(itemStack))
+ }
+
+ fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) {
+ val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText())
+ val newJson = modify(oldJson)
+ pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson)))
+ }
+
+ fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) {
+ modifyJson(skyblockId) { oldJson ->
+ val mutableJson = oldJson.toMutableMap()
+ val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList()
+ recipes.add(recipe)
+ mutableJson["recipes"] = JsonArray(recipes)
+ JsonObject(mutableJson)
+ }
+ }
+
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("reexportlore") {
+ thenArgument("itemid", RestArgumentType) { itemid ->
+ suggests { ctx, builder ->
+ val spaceIndex = builder.remaining.lastIndexOf(" ")
+ val (before, after) =
+ if (spaceIndex < 0) Pair("", builder.remaining)
+ else Pair(
+ builder.remaining.substring(0, spaceIndex + 1),
+ builder.remaining.substring(spaceIndex + 1)
+ )
+ RepoManager.neuRepo.items.items.keys
+ .asSequence()
+ .filter { it.startsWith(after, ignoreCase = true) }
+ .forEach {
+ builder.suggest(before + it)
+ }
+
+ builder.buildFuture()
+ }
+ thenExecute {
+ for (itemid in get(itemid).split(" ").map { SkyblockId(it) }) {
+ if (pathFor(itemid).notExists()) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.fail",
+ "Could not find json file to relore for ${itemid}"
+ )
+ )
+ }
+ fixLoreNbtFor(itemid)
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore",
+ "Updated lore / display name for $itemid"
+ )
+ )
+ }
+ }
+ }
+ thenLiteral("all") {
+ thenExecute {
+ var i = 0
+ val chunkSize = 100
+ val items = RepoManager.neuRepo.items.items.keys
+ Firmament.coroutineScope.launch {
+ items.chunked(chunkSize).forEach { key ->
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.progress",
+ "Updated lore / display for ${i * chunkSize} / ${items.size}."
+ )
+ )
+ i++
+ key.forEach {
+ fixLoreNbtFor(SkyblockId(it))
+ }
+ }
+ MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated."))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun fixLoreNbtFor(itemid: SkyblockId) {
+ modifyJson(itemid) {
+ val mutJson = it.toMutableMap()
+ val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content)
+ val display = legacyTag.getCompoundOrEmpty("display")
+ legacyTag.put("display", display)
+ display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content)
+ display.put(
+ "Lore",
+ (mutJson["lore"] as JsonArray).map { StringTag.valueOf(it.jsonPrimitive.content) }
+ .toNbtList()
+ )
+ mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString())
+ JsonObject(mutJson)
+ }
+ }
+
+ @Subscribe
+ fun onKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) {
+ val itemStack = event.screen.focusedItemStack ?: return
+ PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack))
+ }
+ }
+
+ val nonOverlayCache = mutableMapOf<SkyblockId, Boolean>()
+
+ @Subscribe
+ fun onRender(event: SlotRenderEvents.Before) {
+ if (!PowerUserTools.TConfig.highlightNonOverlayItems) {
+ return
+ }
+ val stack = event.slot.item ?: return
+ val id = event.slot.item.skyBlockId?.neuItem
+ if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return
+ val sbId = stack.skyBlockId ?: return
+ val isExported = nonOverlayCache.getOrPut(sbId) {
+ RepoManager.overlayData.getOverlayFiles(sbId).isNotEmpty() || // This extra case is here so that an export works immediately, without repo reload
+ RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay")
+ .resolve(ExportedTestConstantMeta.current.dataVersion.toString())
+ .resolve("${stack.skyBlockId}.snbt")
+ .exists()
+ }
+ if (!isExported)
+ event.context.drawGuiTexture(
+ Firmament.identifier("selected_pet_background"),
+ event.slot.x, event.slot.y, 16, 16,
+ )
+ }
+
+ fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) {
+ exportItem(ItemStack(Items.PLAYER_HEAD).also {
+ it.displayNameAccordingToNbt = Component.literal(title)
+ it.loreAccordingToNbt = listOf(Component.literal(""))
+ it.setSkyBlockId(skyblockId)
+ extra(it) // LOL
+ })
+ MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId"))
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
new file mode 100644
index 0000000..2bc51bc
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
@@ -0,0 +1,87 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.Serializable
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.resources.ResourceLocation
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.ItemCache
+import moe.nea.firmament.util.StringUtil.camelWords
+import moe.nea.firmament.util.mc.loadItemFromNbt
+
+/**
+ * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json)
+ */
+object LegacyItemData {
+ @Serializable
+ data class ItemData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ val stackSize: Int,
+ val variations: List<Variation> = listOf()
+ ) {
+ val properId = if (name.contains(":")) name else "minecraft:$name"
+
+ fun allVariants() =
+ variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0)
+ }
+
+ @Serializable
+ data class Variation(
+ val metadata: Int, val displayName: String
+ )
+
+ data class LegacyItemType(
+ val name: String,
+ val metadata: Short
+ ) {
+ override fun toString(): String {
+ return "$name:$metadata"
+ }
+ }
+
+ @Serializable
+ data class EnchantmentData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ )
+
+ inline fun <reified T : Any> getLegacyData(name: String) =
+ Firmament.tryDecodeJsonFromStream<T>(
+ LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!!
+ ).getOrThrow()
+
+ val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments")
+ val enchantmentLut = enchantmentData.associateBy { ResourceLocation.withDefaultNamespace(it.name) }
+
+ val itemDat = getLegacyData<List<ItemData>>("items")
+
+ @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread.
+ val itemLut = itemDat.flatMap { item ->
+ item.allVariants().map { legacyItemType ->
+ val nbt = ItemCache.convert189ToModern(CompoundTag().apply {
+ putString("id", legacyItemType.name)
+ putByte("Count", 1)
+ putShort("Damage", legacyItemType.metadata)
+ })!!
+ nbt.remove("components")
+ val stack = loadItemFromNbt(nbt) ?: error("Could not transform $legacyItemType: $nbt")
+ stack.item to legacyItemType
+ }
+ }.toMap()
+
+ @Serializable
+ data class LegacyEffect(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ val type: String
+ )
+
+ val effectList = getLegacyData<List<LegacyEffect>>("effects")
+ .associateBy {
+ it.name.camelWords().map { it.trim().lowercase() }.joinToString("_")
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
new file mode 100644
index 0000000..7b855d1
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
@@ -0,0 +1,318 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlin.concurrent.thread
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.core.component.DataComponents
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.nbt.ByteTag
+import net.minecraft.nbt.CompoundTag
+import net.minecraft.nbt.Tag
+import net.minecraft.nbt.IntTag
+import net.minecraft.nbt.ListTag
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.StringTag
+import net.minecraft.tags.ItemTags
+import net.minecraft.network.chat.Component
+import net.minecraft.util.Unit
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientStartedEvent
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.HypixelPetInfo
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.StringUtil.words
+import moe.nea.firmament.util.directLiteralStringContent
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.getLegacyFormatString
+import moe.nea.firmament.util.json.toJsonArray
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.modifyExtraAttributes
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.transformEachRecursively
+import moe.nea.firmament.util.unformattedString
+
+class LegacyItemExporter private constructor(var itemStack: ItemStack) {
+ init {
+ require(!itemStack.isEmpty)
+ itemStack.count = 1
+ }
+
+ var lore = itemStack.loreAccordingToNbt
+ val originalId = itemStack.extraAttributes.getString("id")
+ var name = itemStack.displayNameAccordingToNbt
+ val extraAttribs = itemStack.extraAttributes.copy()
+ val legacyNbt = CompoundTag()
+ val warnings = mutableListOf<String>()
+
+ // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so
+
+ fun preprocess() {
+ // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui
+ extraAttribs.remove("timestamp")
+ extraAttribs.remove("uuid")
+ extraAttribs.remove("modifier")
+ extraAttribs.getString("petInfo").ifPresent { petInfoJson ->
+ var petInfo = Firmament.json.decodeFromString<HypixelPetInfo>(petInfoJson)
+ petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null)
+ extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo))
+ }
+ itemStack.skyBlockId?.let {
+ extraAttribs.putString("id", it.neuItem)
+ }
+ trimLore()
+ itemStack.loreAccordingToNbt = itemStack.item.defaultInstance.loreAccordingToNbt
+ itemStack.remove(DataComponents.CUSTOM_NAME)
+ }
+
+ fun trimLore() {
+ val rarityIdx = lore.indexOfLast {
+ val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull()
+ firstWordInLine?.let(Rarity::fromString) != null
+ }
+ if (rarityIdx >= 0) {
+ lore = lore.subList(0, rarityIdx + 1)
+ }
+
+ trimStats()
+
+ deleteLineUntilNextSpace { it.startsWith("Held Item: ") }
+ deleteLineUntilNextSpace { it.startsWith("Progress to Level ") }
+ deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") }
+ deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") }
+ collapseWhitespaces()
+
+ name = name.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}")
+ Component.literal(string).setStyle(it.style)
+ }
+
+ if (lore.isEmpty())
+ lore = listOf(Component.empty())
+ }
+
+ private fun trimStats() {
+ val lore = this.lore.toMutableList()
+ for (index in lore.indices) {
+ val value = lore[index]
+ val statLine = SBItemStack.parseStatLine(value)
+ if (statLine == null) break
+ val v = value.copy()
+ require(value.directLiteralStringContent == "")
+ v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") }
+ val last = v.siblings.last()
+ v.siblings[v.siblings.lastIndex] =
+ Component.literal(last.directLiteralStringContent!!.trimEnd())
+ .setStyle(last.style)
+ lore[index] = v
+ }
+ this.lore = lore
+ }
+
+ fun collapseWhitespaces() {
+ lore = (listOf(null as Component?) + lore).zipWithNext()
+ .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() }
+ .map { it.second!! }
+ }
+
+ fun deleteLineUntilNextSpace(search: (String) -> Boolean) {
+ val idx = lore.indexOfFirst { search(it.unformattedString) }
+ if (idx < 0) return
+ val l = lore.toMutableList()
+ val p = l.subList(idx, l.size)
+ val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() }
+ if (nextBlank < 0)
+ p.clear()
+ else
+ p.subList(0, nextBlank).clear()
+ lore = l
+ }
+
+ fun processNbt() {
+ // TODO: calculate hideflags
+ legacyNbt.put("HideFlags", IntTag.valueOf(254))
+ copyUnbreakable()
+ copyItemModel()
+ copyPotion()
+ copyExtraAttributes()
+ copyLegacySkullNbt()
+ copyDisplay()
+ copyColour()
+ copyEnchantments()
+ copyEnchantGlint()
+ // TODO: copyDisplay
+ }
+
+ private fun copyPotion() {
+ val effects = itemStack.get(DataComponents.POTION_CONTENTS) ?: return
+ legacyNbt.put("CustomPotionEffects", ListTag().also {
+ effects.allEffects.forEach { effect ->
+ val effectId = effect.effect.unwrapKey().get().location().path
+ val duration = effect.duration
+ val legacyId = LegacyItemData.effectList[effectId]!!
+
+ it.add(CompoundTag().apply {
+ put("Ambient", ByteTag.valueOf(false))
+ put("Duration", IntTag.valueOf(duration))
+ put("Id", ByteTag.valueOf(legacyId.id.toByte()))
+ put("Amplifier", ByteTag.valueOf(effect.amplifier.toByte()))
+ })
+ }
+ })
+ }
+
+ fun CompoundTag.getOrPutCompound(name: String): CompoundTag {
+ val compound = getCompoundOrEmpty(name)
+ put(name, compound)
+ return compound
+ }
+
+ private fun copyColour() {
+ if (!itemStack.`is`(ItemTags.DYEABLE)) {
+ itemStack.remove(DataComponents.DYED_COLOR)
+ return
+ }
+ val leatherTint = itemStack.componentsPatch.get(DataComponents.DYED_COLOR)?.getOrNull() ?: return
+ legacyNbt.getOrPutCompound("display").put("color", IntTag.valueOf(leatherTint.rgb))
+ }
+
+ private fun copyItemModel() {
+ val itemModel = itemStack.get(DataComponents.ITEM_MODEL) ?: return
+ legacyNbt.put("ItemModel", StringTag.valueOf(itemModel.toString()))
+ }
+
+ private fun copyDisplay() {
+ legacyNbt.getOrPutCompound("display").apply {
+ put("Lore", lore.map { StringTag.valueOf(it.getLegacyFormatString(trimmed = true)) }.toNbtList())
+ putString("Name", name.getLegacyFormatString(trimmed = true))
+ }
+ }
+
+ fun exportModernSnbt(): Tag {
+ val overlay = ItemStack.CODEC.encodeStart(MC.currentOrDefaultRegistryNbtOps, itemStack.copy().also {
+ it.modifyExtraAttributes { attribs ->
+ originalId.ifPresent { attribs.putString("id", it) }
+ }
+ }).orThrow
+ val overlayWithVersion =
+ ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay)
+ .orThrow
+ return overlayWithVersion
+ }
+
+ fun prepare() {
+ preprocess()
+ processNbt()
+ itemStack.extraAttributes = extraAttribs
+ }
+
+ fun exportJson(): JsonElement {
+ return buildJsonObject {
+ val (itemId, damage) = legacyifyItemStack()
+ put("itemid", itemId)
+ put("displayname", name.getLegacyFormatString(trimmed = true))
+ put("nbttag", legacyNbt.toLegacyString())
+ put("damage", damage)
+ put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray())
+ val sbId = itemStack.skyBlockId
+ if (sbId == null)
+ warnings.add("Could not find skyblock id")
+ put("internalname", sbId?.neuItem)
+ put("clickcommand", "")
+ put("crafttext", "")
+ put("modver", "Firmament ${Firmament.version.friendlyString}")
+ put("infoType", "")
+ put("info", JsonArray(listOf()))
+ }
+
+ }
+
+ companion object {
+ fun createExporter(itemStack: ItemStack): LegacyItemExporter {
+ return LegacyItemExporter(itemStack.copy()).also { it.prepare() }
+ }
+
+ @Subscribe
+ fun load(event: ClientStartedEvent) {
+ thread(start = true, name = "ItemExporter Meta Load Thread") {
+ LegacyItemData.itemLut
+ }
+ }
+ }
+
+ fun copyEnchantGlint() {
+ if (itemStack.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) == true) {
+ val ench = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", ench)
+ }
+ }
+
+ private fun copyUnbreakable() {
+ if (itemStack.get(DataComponents.UNBREAKABLE) == Unit.INSTANCE) {
+ legacyNbt.putBoolean("Unbreakable", true)
+ }
+ }
+
+ fun copyEnchantments() {
+ val enchantments = itemStack.get(DataComponents.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return
+ val enchTag = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", enchTag)
+ enchantments.entrySet().forEach { entry ->
+ val id = entry.key.unwrapKey().get().location()
+ val legacyId = LegacyItemData.enchantmentLut[id]
+ if (legacyId == null) {
+ warnings.add("Could not find legacy enchantment id for ${id}")
+ return@forEach
+ }
+ enchTag.add(CompoundTag().apply {
+ putShort("lvl", entry.intValue.toShort())
+ putShort(
+ "id",
+ legacyId.id.toShort()
+ )
+ })
+ }
+ }
+
+ fun copyExtraAttributes() {
+ legacyNbt.put("ExtraAttributes", extraAttribs)
+ }
+
+ fun copyLegacySkullNbt() {
+ val profile = itemStack.get(DataComponents.PROFILE) ?: return
+ legacyNbt.put("SkullOwner", CompoundTag().apply {
+ putString("Id", profile.partialProfile().id.toString())
+ putBoolean("hypixelPopulated", true)
+ put("Properties", CompoundTag().apply {
+ profile.partialProfile().properties().forEach { prop, value ->
+ val list = getListOrEmpty(prop)
+ put(prop, list)
+ list.add(CompoundTag().apply {
+ value.signature?.let {
+ putString("Signature", it)
+ }
+ putString("Value", value.value)
+ putString("Name", value.name)
+ })
+ }
+ })
+ })
+ }
+
+ fun legacyifyItemStack(): LegacyItemData.LegacyItemType {
+ // TODO: add a default here
+ if (itemStack.item == Items.LINGERING_POTION || itemStack.item == Items.SPLASH_POTION)
+ return LegacyItemData.LegacyItemType("potion", 16384)
+ return LegacyItemData.itemLut[itemStack.item]!!
+ }
+}
diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
index ff85c00..c12a2c6 100644
--- a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
+++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
@@ -1,33 +1,30 @@
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 net.minecraft.core.particles.ParticleTypes
+import net.minecraft.sounds.SoundEvents
+import net.minecraft.world.phys.Vec3
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
import moe.nea.firmament.util.skyblock.SkyBlockItems
-object AncestralSpadeSolver : SubscriptionOwner {
+object AncestralSpadeSolver {
var lastDing = TimeMark.farPast()
private set
private val pitches = mutableListOf<Float>()
- val particlePositions = mutableListOf<Vec3d>()
- var nextGuess: Vec3d? = null
+ val particlePositions = mutableListOf<Vec3>()
+ var nextGuess: Vec3? = null
private set
private var lastTeleportAttempt = TimeMark.farPast()
@@ -35,7 +32,11 @@ object AncestralSpadeSolver : SubscriptionOwner {
fun isEnabled() =
DianaWaypoints.TConfig.ancestralSpadeSolver
&& SBData.skyblockLocation == SkyBlockIsland.HUB
- && MC.player?.inventory?.containsAny { it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE } == true // TODO: add a reactive property here
+ && MC.player?.inventory?.hasAnyMatching {
+ it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE ||
+ it.skyBlockId == SkyBlockItems.ARCHAIC_SPADE ||
+ it.skyBlockId == SkyBlockItems.DEIFIC_SPADE
+ } == true // TODO: add a reactive property here
@Subscribe
fun onKeyBind(event: WorldKeyboardEvent) {
@@ -62,7 +63,7 @@ object AncestralSpadeSolver : SubscriptionOwner {
@Subscribe
fun onPlaySound(event: SoundReceiveEvent) {
if (!isEnabled()) return
- if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return
+ if (!SoundEvents.NOTE_BLOCK_HARP.`is`(event.sound.value().location)) return
if (lastDing.passedTime() > 1.seconds) {
particlePositions.clear()
@@ -96,7 +97,7 @@ object AncestralSpadeSolver : SubscriptionOwner {
.let { (a, _, b) -> b.subtract(a) }
.normalize()
- nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate))
+ nextGuess = event.position.add(lastParticleDirection.scale(soundDistanceEstimate))
}
@Subscribe
@@ -106,13 +107,11 @@ object AncestralSpadeSolver : SubscriptionOwner {
nextGuess?.let {
tinyBlock(it, 1f, 0x80FFFFFF.toInt())
// TODO: replace this
- color(1f, 1f, 0f, 1f)
- tracer(it, lineWidth = 3f)
+ tracer(it, lineWidth = 3f, color = 0x80FFFFFF.toInt())
}
if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) {
// TODO: replace this // TODO: add toggle
- color(0f, 1f, 0f, 0.7f)
- line(particlePositions)
+ line(particlePositions, color = 0x80FFFFFF.toInt())
}
}
}
@@ -124,8 +123,4 @@ object AncestralSpadeSolver : SubscriptionOwner {
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
index 6d87262..650e6f9 100644
--- a/src/main/kotlin/features/diana/DianaWaypoints.kt
+++ b/src/main/kotlin/features/diana/DianaWaypoints.kt
@@ -3,29 +3,29 @@ package moe.nea.firmament.features.diana
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.AttackBlockEvent
import moe.nea.firmament.events.UseBlockEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object DianaWaypoints : FirmamentFeature {
- override val identifier get() = "diana"
- override val config get() = TConfig
+object DianaWaypoints {
+ val identifier get() = "diana"
- object TConfig : ManagedConfig(identifier, Category.EVENTS) {
- val ancestralSpadeSolver by toggle("ancestral-spade") { true }
- val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport")
- val nearbyWaypoints by toggle("nearby-waypoints") { true }
- }
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+ val ancestralSpadeSolver by toggle("ancestral-spade") { true }
+ val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport")
+ val nearbyWaypoints by toggle("nearby-waypoints") { true }
+ }
- @Subscribe
- fun onBlockUse(event: UseBlockEvent) {
- NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos)
- }
+ @Subscribe
+ fun onBlockUse(event: UseBlockEvent) {
+ NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos)
+ }
- @Subscribe
- fun onBlockAttack(event: AttackBlockEvent) {
- NearbyBurrowsSolver.onBlockClick(event.blockPos)
- }
+ @Subscribe
+ fun onBlockAttack(event: AttackBlockEvent) {
+ NearbyBurrowsSolver.onBlockClick(event.blockPos)
+ }
}
diff --git a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
index 2fb4002..35af660 100644
--- a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
+++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
@@ -2,22 +2,20 @@ package moe.nea.firmament.features.diana
import me.shedaniel.math.Color
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 net.minecraft.core.particles.ParticleTypes
+import net.minecraft.core.BlockPos
+import net.minecraft.util.Mth
+import net.minecraft.core.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.collections.mutableMapWithMaxSize
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
-object NearbyBurrowsSolver : SubscriptionOwner {
+object NearbyBurrowsSolver {
private val recentlyDugBurrows: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(20)
@@ -65,7 +63,7 @@ object NearbyBurrowsSolver : SubscriptionOwner {
fun onParticles(event: ParticleSpawnEvent) {
if (!DianaWaypoints.TConfig.nearbyWaypoints) return
- val position: BlockPos = event.position.toBlockPos().down()
+ val position: BlockPos = event.position.toBlockPos().below()
if (wasRecentlyDug(position)) return
@@ -134,11 +132,8 @@ object NearbyBurrowsSolver : SubscriptionOwner {
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))
+ return BlockPos(Mth.floor(x()), Mth.floor(y()), Mth.floor(z()))
}
diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
index 5151862..955d23b 100644
--- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
+++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
@@ -1,224 +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 org.joml.Vector2i
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.entity.passive.PigEntity
-import net.minecraft.util.math.BlockPos
+import net.minecraft.world.entity.animal.Pig
+import net.minecraft.network.chat.Component
+import net.minecraft.core.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.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.repo.SBItemStack
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.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
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, Category.EVENTS) {
- val enableShinyPigTracker by toggle("shiny-pigs") {true}
- val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) }
- }
+object AnniversaryFeatures {
+ val identifier: String
+ get() = "anniversary"
- override val config: ManagedConfig?
- get() = TConfig
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+ val enableShinyPigTracker by toggle("shiny-pigs") { true }
+ val trackPigCooldown by position("pig-hud", 200, 300) { Vector2i(100, 200) }
+ }
- data class ClickedPig(
+ data class ClickedPig(
val clickedAt: TimeMark,
val startLocation: BlockPos,
- val pigEntity: PigEntity
- ) {
- @Bind("timeLeft")
- fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
- }
-
- val clickedPigs = ObservableList<ClickedPig>(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 (?<reward>.*) 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<Reward>()
-
- fun <T> ObservableList<T>.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 = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
- val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern()
- val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
- fun parseReward(string: String): Reward {
- expReward.useMatch<Unit>(string) {
- val exp = parseShortNumber(group("exp"))
- val kind = group("kind")
- return Reward.EXP(exp, kind)
- }
- coinReward.useMatch<Unit>(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) {
- SBItemStack(backedBy.item, backedBy.amount)
- } else {
- SBItemStack(SkyblockId.NULL)
- }
-
- @Bind
- fun name(): String {
- return when (backedBy) {
- is Reward.Coins -> "Coins"
- is Reward.EXP -> backedBy.skill
- is Reward.Items -> itemStack.asImmutableItemStack().name.string
- is Reward.Unknown -> backedBy.text
- }
- }
-
- @Bind
- fun isKnown() = backedBy !is Reward.Unknown
- }
-
- @get:Bind("rewards")
- val rewards = ObservableList<DisplayReward>(mutableListOf())
-
- }
+ val pigEntity: Pig
+ ) {
+ @Bind("timeLeft")
+ fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
+ }
+
+ val clickedPigs = ObservableList<ClickedPig>(mutableListOf())
+ var lastClickedPig: Pig? = null
+
+ val pigDuration = 90.seconds
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration }
+ }
+
+ val pattern = "SHINY! You extracted (?<reward>.*) 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.blockPosition() ?: 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.distToCenterSqr(player.position) } ?: 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<Reward>()
+
+ fun <T> ObservableList<T>.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 = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
+ val coinReward = "(?i)\\+(?<amount>$SHORT_NUMBER_FORMAT) Coins".toPattern()
+ val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
+ fun parseReward(string: String): Reward {
+ expReward.useMatch<Unit>(string) {
+ val exp = parseShortNumber(group("exp"))
+ val kind = group("kind")
+ return Reward.EXP(exp, kind)
+ }
+ coinReward.useMatch<Unit>(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 Pig) {
+ 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) {
+ SBItemStack(backedBy.item, backedBy.amount)
+ } else {
+ SBItemStack(SkyblockId.NULL)
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ @Bind
+ fun name(): Component {
+ return when (backedBy) {
+ is Reward.Coins -> Component.literal("Coins")
+ is Reward.EXP -> Component.literal(backedBy.skill)
+ is Reward.Items -> itemStack.asImmutableItemStack().hoverName
+ is Reward.Unknown -> Component.literal(backedBy.text)
+ }
+ }
+
+ @Bind
+ fun isKnown() = backedBy !is Reward.Unknown
+ }
+
+ @get:Bind("rewards")
+ val rewards = ObservableList<DisplayReward>(mutableListOf())
+
+ }
}
diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt
new file mode 100644
index 0000000..9b87cc6
--- /dev/null
+++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.features.events.anniversity
+
+import java.util.Optional
+import me.shedaniel.math.Color
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.world.entity.player.Player
+import net.minecraft.network.chat.Style
+import net.minecraft.ChatFormatting
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object CenturyRaffleFeatures {
+ @Config
+ object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) {
+ val highlightPlayersForSlice by toggle("highlight-cake-players") { true }
+// val highlightAllPlayers by toggle("highlight-all-cake-players") { true }
+ }
+
+ val cakeIcon = "⛃"
+
+ val cakeColors = listOf(
+ CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, ChatFormatting.BLUE),
+ CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, ChatFormatting.YELLOW),
+ CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, ChatFormatting.GREEN),
+ CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, ChatFormatting.RED),
+ CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, ChatFormatting.LIGHT_PURPLE),
+ )
+
+ data class CakeTeam(
+ val id: SkyblockId,
+ val formatting: ChatFormatting,
+ ) {
+ val searchedTextRgb = formatting.color!!
+ val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0)
+ val tintOverlay by lazy {
+ TintedOverlayTexture().setColor(brightenedRgb)
+ }
+ }
+
+ val sliceToColor = cakeColors.associateBy { it.id }
+
+ @Subscribe
+ fun onEntityRender(event: EntityRenderTintEvent) {
+ if (!TConfig.highlightPlayersForSlice) return
+ val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return
+ // TODO: cache the requested color
+ val player = event.entity as? Player ?: return
+ val cakeColor: Style = player.feedbackDisplayName.visit(
+ { style, text ->
+ if (text == cakeIcon) Optional.of(style)
+ else Optional.empty()
+ }, Style.EMPTY).getOrNull() ?: return
+ if (cakeColor.color?.value == requestedCakeTeam.searchedTextRgb) {
+ event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
index 840fb8c..3f149ff 100644
--- a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
+++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
@@ -1,17 +1,16 @@
package moe.nea.firmament.features.events.carnival
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object CarnivalFeatures : FirmamentFeature {
- object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+object CarnivalFeatures {
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
val enableBombSolver by toggle("bombs-solver") { true }
val displayTutorials by toggle("tutorials") { true }
}
- override val config: ManagedConfig?
- get() = TConfig
- override val identifier: String
+ 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
index 1824225..64b3814 100644
--- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
+++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
@@ -2,17 +2,17 @@
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.platform.MoulConfigPlatform
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 net.minecraft.world.level.block.Blocks
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.network.chat.ClickEvent
+import net.minecraft.network.chat.Component
+import net.minecraft.core.BlockPos
+import net.minecraft.world.level.LevelAccessor
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.AttackBlockEvent
@@ -45,7 +45,6 @@ object MinesweeperHelper {
enum class Piece(
- @get:Bind("fruitName")
val fruitName: String,
val points: Int,
val specialAbility: String,
@@ -118,24 +117,26 @@ object MinesweeperHelper {
val textureUrl = "http://textures.minecraft.net/texture/$textureHash"
val itemStack = createSkullItem(UUID.randomUUID(), textureUrl)
.setSkyBlockFirmamentUiId("MINESWEEPER_$name")
+ @get:Bind("fruitName")
+ val textFruitName = Component.literal(fruitName)
@Bind
- fun getIcon() = ModernItemStack.of(itemStack)
+ fun getIcon() = MoulConfigPlatform.wrap(itemStack)
- @Bind
- fun pieceLabel() = fruitColor.formattingCode + fruitName
+ @get:Bind("pieceLabel")
+ val pieceLabel = Component.literal(fruitColor.formattingCode + fruitName)
- @Bind
- fun boardLabel() = "§a$totalPerBoard§7/§rboard"
+ @get:Bind("boardLabel")
+ val boardLabel = Component.literal("§a$totalPerBoard§7/§rboard")
- @Bind("description")
- fun getDescription() = buildString {
+ @get:Bind("description")
+ val getDescription = Component.literal(buildString {
append(specialAbility)
if (points >= 0) {
append(" Default points: $points.")
}
}
- }
+) }
object TutorialScreen {
@get:Bind("pieces")
@@ -158,7 +159,7 @@ object MinesweeperHelper {
;
@Bind("itemType")
- fun getItemStack() = ModernItemStack.of(ItemStack(itemType))
+ fun getItemStack() = MoulConfigPlatform.wrap(ItemStack(itemType))
companion object {
val id = SkyblockId("CARNIVAL_SHOVEL")
@@ -175,10 +176,10 @@ object MinesweeperHelper {
) {
fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y)
- fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block
- fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND
- fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE
- fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS
+ fun getBlock(world: LevelAccessor) = world.getBlockState(toBlockPos()).block
+ fun isUnopened(world: LevelAccessor) = getBlock(world) == Blocks.SAND
+ fun isOpened(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE
+ fun isScorched(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE_STAIRS
companion object {
fun fromBlockPos(blockPos: BlockPos): BoardPosition? {
@@ -221,8 +222,8 @@ object MinesweeperHelper {
@Subscribe
fun onChat(event: ProcessChatEvent) {
if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
- MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
+ MC.sendChat(Component.translatable("firmament.carnival.tutorial.minesweeper").withStyle {
+ it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial"))
})
}
if (!CarnivalFeatures.TConfig.enableBombSolver) {
@@ -259,7 +260,7 @@ object MinesweeperHelper {
val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
gs.lastClickedPosition = boardPosition
- gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
+ gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandItem)
}
@Subscribe
@@ -267,7 +268,7 @@ object MinesweeperHelper {
val gs = gameState ?: return
RenderInWorldContext.renderInWorld(event) {
for ((pos, bombCount) in gs.nearbyBombs) {
- this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3"))
+ this.text(pos.toBlockPos().above().center, Component.literal("§a$bombCount \uD83D\uDCA3"))
}
}
}
diff --git a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
deleted file mode 100644
index 76f6ed4..0000000
--- a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package moe.nea.firmament.features.fixes
-
-import net.minecraft.particle.ParticleTypes
-import net.minecraft.util.math.Vec3d
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.ParticleSpawnEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.compatloader.CompatLoader
-
-object CompatibliltyFeatures : FirmamentFeature {
- override val identifier: String
- get() = "compatibility"
-
- object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) {
- val enhancedExplosions by toggle("explosion-enabled") { false }
- val explosionSize by integer("explosion-power", 10, 50) { 1 }
- }
-
- override val config: ManagedConfig?
- get() = TConfig
-
- interface ExplosiveApiWrapper {
- fun spawnParticle(vec3d: Vec3d, power: Float)
-
- companion object : CompatLoader<ExplosiveApiWrapper>(ExplosiveApiWrapper::class.java)
- }
-
- private val explosiveApiWrapper = ExplosiveApiWrapper.singleInstance
-
- @Subscribe
- fun onExplosion(it: ParticleSpawnEvent) {
- if (TConfig.enhancedExplosions &&
- it.particleEffect.type == ParticleTypes.EXPLOSION_EMITTER &&
- explosiveApiWrapper != null
- ) {
- it.cancel()
- explosiveApiWrapper.spawnParticle(it.position, 2F)
- }
- }
-}
diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 7030319..ae4abd7 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -1,57 +1,72 @@
package moe.nea.firmament.features.fixes
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
-import net.minecraft.client.MinecraftClient
-import net.minecraft.client.option.KeyBinding
-import net.minecraft.text.Text
+import net.minecraft.client.Minecraft
+import net.minecraft.client.KeyMapping
+import net.minecraft.network.chat.Component
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.WorldKeyboardEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
-object Fixes : FirmamentFeature {
- override val identifier: String
+object Fixes {
+ val identifier: String
get() = "fixes"
+ @Config
object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
- val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
+ val autoSprintUnderWater by toggle("auto-sprint-underwater") { true }
+ var autoSprintHudToggle by toggle("auto-sprint-hud-toggle") { true }
+ val autoSprintHud by position("auto-sprint-hud", 80, 10) { Vector2i() }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
+ val peekChatScroll by toggle("peek-chat-scroll") { false }
val hidePotionEffects by toggle("hide-mob-effects") { false }
+ val hidePotionEffectsHud by toggle("hide-potion-effects-hud") { false }
+ val noHurtCam by toggle("disable-hurt-cam") { false }
+ val hideSlotHighlights by toggle("hide-slot-highlights") { false }
+ val hideRecipeBook by toggle("hide-recipe-book") { false }
+ val hideOffHand by toggle("hide-off-hand") { false }
}
- override val config: ManagedConfig
- get() = TConfig
-
fun handleIsPressed(
- keyBinding: KeyBinding,
- cir: CallbackInfoReturnable<Boolean>
+ keyBinding: KeyMapping,
+ cir: CallbackInfoReturnable<Boolean>
) {
- if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
- cir.returnValue = true
+ if (keyBinding !== Minecraft.getInstance().options.keySprint) return
+ if (!TConfig.autoSprint) return
+ val player = MC.player ?: return
+ if (player.isSprinting) return
+ if (!TConfig.autoSprintUnderWater && player.isInWater) return
+ cir.returnValue = true
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
- if (!TConfig.autoSprintKeyBinding.isBound) return
- it.context.matrices.push()
- TConfig.autoSprintHud.applyTransformations(it.context.matrices)
- it.context.drawText(
- MC.font, Text.translatable(
- if (TConfig.autoSprint)
- "firmament.fixes.auto-sprint.on"
- else if (MC.player?.isSprinting == true)
- "firmament.fixes.auto-sprint.sprinting"
- else
- "firmament.fixes.auto-sprint.not-sprinting"
- ), 0, 0, -1, false
+ if (!TConfig.autoSprintKeyBinding.isBound || !TConfig.autoSprintHudToggle) return
+ it.context.pose().pushMatrix()
+ TConfig.autoSprintHud.applyTransformations(it.context.pose())
+ it.context.drawString(
+ MC.font, (
+ if (MC.player?.isSprinting == true) {
+ Component.translatable("firmament.fixes.auto-sprint.sprinting")
+ } else if (TConfig.autoSprint) {
+ if (!TConfig.autoSprintUnderWater && MC.player?.isInWater == true)
+ tr("firmament.fixes.auto-sprint.under-water", "In Water")
+ else
+ Component.translatable("firmament.fixes.auto-sprint.on")
+ } else {
+ Component.translatable("firmament.fixes.auto-sprint.not-sprinting")
+ }
+ ), 0, 0, -1, true
)
- it.context.matrices.pop()
+ it.context.pose().popMatrix()
}
@Subscribe
@@ -64,4 +79,8 @@ object Fixes : FirmamentFeature {
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
+
+ fun shouldScrollPeekedChat(): Boolean {
+ return TConfig.peekChatScroll
+ }
}
diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt
new file mode 100644
index 0000000..edd511f
--- /dev/null
+++ b/src/main/kotlin/features/garden/HideComposterNoises.kt
@@ -0,0 +1,34 @@
+package moe.nea.firmament.features.garden
+
+import net.minecraft.world.entity.animal.wolf.WolfSoundVariants
+import net.minecraft.sounds.SoundEvent
+import net.minecraft.sounds.SoundEvents
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+
+object HideComposterNoises {
+ @Config
+ object TConfig : ManagedConfig("composter", Category.GARDEN) {
+ val hideComposterNoises by toggle("no-more-noises") { false }
+ }
+
+ val composterSoundEvents: List<SoundEvent> = listOf(
+ SoundEvents.PISTON_EXTEND,
+ SoundEvents.WATER_AMBIENT,
+ SoundEvents.CHICKEN_EGG,
+ SoundEvents.WOLF_SOUNDS[WolfSoundVariants.SoundSet.CLASSIC]!!.growlSound().value(),
+ )
+
+ @Subscribe
+ fun onNoise(event: SoundReceiveEvent) {
+ if (!TConfig.hideComposterNoises) return
+ if (SBData.skyblockLocation == SkyBlockIsland.GARDEN) {
+ if (event.sound.value() in composterSoundEvents)
+ event.cancel()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt
index d2c79fd..5241f54 100644
--- a/src/main/kotlin/features/inventory/CraftingOverlay.kt
+++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt
@@ -1,20 +1,20 @@
package moe.nea.firmament.features.inventory
import io.github.moulberry.repo.data.NEUCraftingRecipe
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
-import net.minecraft.item.ItemStack
-import net.minecraft.util.Formatting
+import net.minecraft.client.gui.screens.inventory.ContainerScreen
+import net.minecraft.world.item.ItemStack
+import net.minecraft.ChatFormatting
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.skyblockId
-object CraftingOverlay : FirmamentFeature {
+object CraftingOverlay {
- private var screen: GenericContainerScreen? = null
+ private var screen: ContainerScreen? = null
private var recipe: NEUCraftingRecipe? = null
private var useNextScreen = false
private val craftingOverlayIndices = listOf(
@@ -24,7 +24,7 @@ object CraftingOverlay : FirmamentFeature {
)
val CRAFTING_SCREEN_NAME = "Craft Item"
- fun setOverlay(screen: GenericContainerScreen?, recipe: NEUCraftingRecipe) {
+ fun setOverlay(screen: ContainerScreen?, recipe: NEUCraftingRecipe) {
this.screen = screen
if (screen == null) {
useNextScreen = true
@@ -34,7 +34,7 @@ object CraftingOverlay : FirmamentFeature {
@Subscribe
fun onScreenChange(event: ScreenChangeEvent) {
- if (useNextScreen && event.new is GenericContainerScreen
+ if (useNextScreen && event.new is ContainerScreen
&& event.new.title?.string == "Craft Item"
) {
useNextScreen = false
@@ -42,18 +42,19 @@ object CraftingOverlay : FirmamentFeature {
}
}
- override val identifier: String
+ val identifier: String
get() = "crafting-overlay"
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onSlotRender(event: SlotRenderEvents.After) {
val slot = event.slot
val recipe = this.recipe ?: return
- if (slot.inventory != screen?.screenHandler?.inventory) return
- val recipeIndex = craftingOverlayIndices.indexOf(slot.index)
+ if (slot.container != screen?.menu?.container) return
+ val recipeIndex = craftingOverlayIndices.indexOf(slot.containerSlot)
if (recipeIndex < 0) return
val expectedItem = recipe.inputs[recipeIndex]
- val actualStack = slot.stack ?: ItemStack.EMPTY!!
+ val actualStack = slot.item ?: ItemStack.EMPTY!!
val actualEntry = SBItemStack(actualStack)
if ((actualEntry.skyblockId != expectedItem.skyblockId || actualEntry.getStackSize() < expectedItem.amount)
&& expectedItem.amount.toInt() != 0
@@ -66,15 +67,15 @@ object CraftingOverlay : FirmamentFeature {
0x80FF0000.toInt()
)
}
- if (!slot.hasStack()) {
+ if (!slot.hasItem()) {
val itemStack = SBItemStack(expectedItem)?.asImmutableItemStack() ?: return
- event.context.drawItem(itemStack, event.slot.x, event.slot.y)
- event.context.drawStackOverlay(
+ event.context.renderItem(itemStack, event.slot.x, event.slot.y)
+ event.context.renderItemDecorations(
MC.font,
itemStack,
event.slot.x,
event.slot.y,
- "${Formatting.RED}${expectedItem.amount.toInt()}"
+ "${ChatFormatting.RED}${expectedItem.amount.toInt()}"
)
}
}
diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt
index 4aa8202..e9d0631 100644
--- a/src/main/kotlin/features/inventory/ItemHotkeys.kt
+++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt
@@ -2,22 +2,26 @@ package moe.nea.firmament.features.inventory
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.HypixelStaticData
-import moe.nea.firmament.repo.ItemCache
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.isBroken
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.asBazaarStock
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.focusedItemStack
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
object ItemHotkeys {
+ @Config
object TConfig : ManagedConfig("item-hotkeys", Category.INVENTORY) {
val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface")
}
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) {
if (!event.matches(TConfig.openGlobalTradeInterface)) {
@@ -26,7 +30,7 @@ object ItemHotkeys {
var item = event.screen.focusedItemStack ?: return
val skyblockId = item.skyBlockId ?: return
item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item
- if (HypixelStaticData.hasBazaarStock(skyblockId)) {
+ if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) {
MC.sendCommand("bz ${item.getSearchName()}")
} else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) {
MC.sendCommand("ahs ${item.getSearchName()}")
diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
index fdc378a..9712067 100644
--- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
+++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
@@ -1,45 +1,38 @@
package moe.nea.firmament.features.inventory
import java.awt.Color
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.RenderLayer
-import net.minecraft.item.ItemStack
-import net.minecraft.util.Formatting
-import net.minecraft.util.Identifier
+import net.minecraft.client.renderer.RenderPipelines
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.world.item.ItemStack
+import net.minecraft.resources.ResourceLocation
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HotbarItemRenderEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.collections.lastNotNullOfOrNull
-import moe.nea.firmament.util.collections.memoizeIdentity
-import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.skyblock.Rarity
-import moe.nea.firmament.util.unformattedString
-object ItemRarityCosmetics : FirmamentFeature {
- override val identifier: String
+object ItemRarityCosmetics {
+ val identifier: String
get() = "item-rarity-cosmetics"
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val showItemRarityBackground by toggle("background") { false }
val showItemRarityInHotbar by toggle("background-hotbar") { false }
}
- override val config: ManagedConfig
- get() = TConfig
-
private val rarityToColor = Rarity.colourMap.mapValues {
- val c = Color(it.value.colorValue!!)
+ val c = Color(it.value.color!!)
c.rgb
}
- fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) {
+ fun drawItemStackRarity(drawContext: GuiGraphics, x: Int, y: Int, item: ItemStack) {
val rarity = Rarity.fromItem(item) ?: return
val rgb = rarityToColor[rarity] ?: 0xFF00FF80.toInt()
- drawContext.drawGuiTexture(
- RenderLayer::getGuiTextured,
- Identifier.of("firmament:item_rarity_background"),
+ drawContext.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ ResourceLocation.parse("firmament:item_rarity_background"),
x, y,
16, 16,
rgb
@@ -50,7 +43,7 @@ object ItemRarityCosmetics : FirmamentFeature {
@Subscribe
fun onRenderSlot(it: SlotRenderEvents.Before) {
if (!TConfig.showItemRarityBackground) return
- val stack = it.slot.stack ?: return
+ val stack = it.slot.item ?: return
drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack)
}
diff --git a/src/main/kotlin/features/inventory/JunkHighlighter.kt b/src/main/kotlin/features/inventory/JunkHighlighter.kt
new file mode 100644
index 0000000..15bdcfa
--- /dev/null
+++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt
@@ -0,0 +1,30 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SlotRenderEvents
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
+import moe.nea.firmament.util.useMatch
+
+object JunkHighlighter {
+ val identifier: String
+ get() = "junk-highlighter"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val junkRegex by string("regex") { "" }
+ val highlightBind by keyBinding("highlight") { GLFW.GLFW_KEY_LEFT_CONTROL }
+ }
+
+ @Subscribe
+ fun onDrawSlot(event: SlotRenderEvents.After) {
+ if (!TConfig.highlightBind.isPressed() || TConfig.junkRegex.isEmpty()) return
+ val junkRegex = TConfig.junkRegex.toPattern()
+ val slot = event.slot
+ junkRegex.useMatch(slot.item.getSearchName()) {
+ event.context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0xffff0000.toInt())
+ }
+ }
+}
diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt
index 5ca10f7..e0bb4b1 100644
--- a/src/main/kotlin/features/inventory/PetFeatures.kt
+++ b/src/main/kotlin/features/inventory/PetFeatures.kt
@@ -1,40 +1,561 @@
package moe.nea.firmament.features.inventory
-import net.minecraft.util.Identifier
+import java.util.regex.Matcher
+import org.joml.Vector2i
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.item.ItemStack
+import net.minecraft.network.chat.Component
+import net.minecraft.ChatFormatting
+import net.minecraft.util.StringRepresentable
+import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.ProfileSwitchEvent
+import moe.nea.firmament.events.SlotClickEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.jarvis.JarvisIntegration
+import moe.nea.firmament.repo.ExpLadders
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.FirmFormatters.formatPercent
+import moe.nea.firmament.util.FirmFormatters.shortFormat
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.formattedString
+import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.skyblock.TabListAPI
+import moe.nea.firmament.util.skyblockUUID
+import moe.nea.firmament.util.titleCase
+import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
+import moe.nea.firmament.util.withColor
-object PetFeatures : FirmamentFeature {
- override val identifier: String
+object PetFeatures {
+ val identifier: String
get() = "pets"
- override val config: ManagedConfig?
- get() = TConfig
-
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val highlightEquippedPet by toggle("highlight-pet") { true }
+ val petOverlay by toggle("pet-overlay") { false }
+ val petOverlayHud by position("pet-overlay-hud", 80, 10) {
+ Vector2i()
+ }
+ val petOverlayHudStyle by choice("pet-overlay-hud-style") { PetOverlayHudStyles.PLAIN_NO_BACKGROUND }
}
- val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern()
+ enum class PetOverlayHudStyles : StringRepresentable {
+ PLAIN_NO_BACKGROUND,
+ COLOUR_NO_BACKGROUND,
+ PLAIN_BACKGROUND,
+ COLOUR_BACKGROUND,
+ ICON_ONLY;
+
+ override fun getSerializedName() : String {
+ return name
+ }
+ }
+
+ private val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern()
+ private val autopetPattern =
+ "§cAutopet §eequipped your §7\\[Lvl (\\d{1,3})\\] §([fa956d])([\\w\\s]+)§e! §aVIEW RULE".toPattern()
+ private val petItemPattern = "§aYour pet is now holding (§[fa956d][\\w\\s]+)§a.".toPattern()
+ private val petLevelUpPattern = "§aYour §([fa956d])([\\w\\s]+) §aleveled up to level §9(\\d+)§a!".toPattern()
+ private val petMap = HashMap<String, ParsedPet>()
+ private var currentPetUUID: String = ""
+ private var tempTabPet: ParsedPet? = null
+ private var tempChatPet: ParsedPet? = null
+
+ @Subscribe
+ fun onProfileSwitch(event: ProfileSwitchEvent) {
+ petMap.clear()
+ currentPetUUID = ""
+ tempTabPet = null
+ tempChatPet = null
+ }
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
- if (!TConfig.highlightEquippedPet) return
- val stack = event.slot.stack
- if (stack.petData?.active == true)
- petMenuTitle.useMatch(MC.screenName ?: return) {
+ // Cache pets
+ petMenuTitle.useMatch(MC.screenName ?: return) {
+ val stack = event.slot.item
+ if (!stack.isEmpty) cachePet(stack)
+ if (stack.petData?.active == true) {
+ if (currentPetUUID == "") currentPetUUID = stack.skyblockUUID.toString()
+ // Highlight active pet feature
+ if (!TConfig.highlightEquippedPet) return
event.context.drawGuiTexture(
- event.slot.x, event.slot.y, 0, 16, 16,
- Identifier.of("firmament:selected_pet_background")
+ Firmament.identifier("selected_pet_background"),
+ event.slot.x, event.slot.y, 16, 16,
)
}
+ }
+ }
+
+ private fun cachePet(stack: ItemStack) {
+ // Cache information about a pet
+ if (stack.skyblockUUID == null) return
+ if (petMap.containsKey(stack.skyblockUUID.toString()) &&
+ petMap[stack.skyblockUUID.toString()]?.isComplete == true) return
+
+ val pet = PetParser.parsePetMenuSlot(stack) ?: return
+ petMap[stack.skyblockUUID.toString()] = pet
+ }
+
+ @Subscribe
+ fun onSlotClick(event: SlotClickEvent) {
+ // Check for switching/removing pet manually
+ petMenuTitle.useMatch(MC.screenName ?: return) {
+ if (event.slot.container is Inventory) return
+ if (event.button != 0 && event.button != 1) return
+ val petData = event.stack.petData ?: return
+ if (petData.active == true) {
+ currentPetUUID = "None"
+ return
+ }
+ if (event.button != 0) return
+ if (!petMap.containsKey(event.stack.skyblockUUID.toString())) cachePet(event.stack)
+ currentPetUUID = event.stack.skyblockUUID.toString()
+ }
}
+ @Subscribe
+ fun onChatEvent(event: ProcessChatEvent) {
+ // Handle AutoPet
+ var matcher = autopetPattern.matcher(event.text.formattedString())
+ if (matcher.matches()) {
+ val tempMap = petMap.filter { (uuid, pet) ->
+ pet.name == matcher.group(3) &&
+ pet.rarity == PetParser.reversePetColourMap[matcher.group(2)] &&
+ pet.level <= matcher.group(1).toInt()
+ }
+ if (tempMap.isNotEmpty()) {
+ currentPetUUID = tempMap.keys.first()
+ } else {
+ tempChatPet = PetParser.parsePetChatMessage(matcher.group(3), matcher.group(2), matcher.group(1).toInt())
+ currentPetUUID = ""
+ }
+ tempTabPet = null
+ return
+ }
+ // Handle changing pet item
+ // This is needed for when pet item can't be found in tab list
+ matcher = petItemPattern.matcher(event.text.formattedString())
+ if (matcher.matches()) {
+ petMap[currentPetUUID]?.petItem = matcher.group(1)
+ tempTabPet?.petItem = matcher.group(1)
+ tempChatPet?.petItem = matcher.group(1)
+ // TODO: Handle tier boost pet items if required
+ // I'm not rich enough to be able to test tier boosts
+ }
+ // Handle pet levelling up
+ // This is needed for when pet level can't be found in tab list
+ matcher = petLevelUpPattern.matcher(event.text.formattedString())
+ if (matcher.matches()) {
+ val tempPet =
+ PetParser.parsePetChatMessage(matcher.group(2), matcher.group(1), matcher.group(3).toInt()) ?: return
+ val tempMap = petMap.filter { (uuid, pet) ->
+ pet.name == tempPet.name &&
+ pet.rarity == tempPet.rarity &&
+ pet.level <= tempPet.level
+ }
+ if (tempMap.isNotEmpty()) petMap[tempMap.keys.first()]?.update(tempPet)
+ if (tempTabPet?.name == tempPet.name && tempTabPet?.rarity == tempPet.rarity) {
+ tempTabPet?.update(tempPet)
+ }
+ if (tempChatPet?.name == tempPet.name && tempChatPet?.rarity == tempPet.rarity) tempChatPet?.update(tempPet)
+ }
+ }
+ private fun renderLinesAndBackground(it: HudRenderEvent, lines: List<Component>) {
+ // Render background for the hud
+ if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND ||
+ TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) {
+ var maxWidth = 0
+ lines.forEach { if (MC.font.width(it) > maxWidth) maxWidth = MC.font.width(it.unformattedString) }
+ val height = if (MC.font.lineHeight * lines.size > 32) MC.font.lineHeight * lines.size else 32
+ it.context.fill(0, -3, 40 + maxWidth, height + 2, 0x80000000.toInt())
+ }
+
+ // Render text for the hud
+ lines.forEachIndexed { index, line ->
+ it.context.drawString(
+ MC.font,
+ line.copy().withColor(ChatFormatting.GRAY),
+ 36,
+ MC.font.lineHeight * index,
+ -1,
+ true
+ )
+ }
+ }
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.petOverlay || !SBData.isOnSkyblock) return
+
+ // Possibly handle Montezuma as a future feature? Could track how many pieces have been found etc
+ // Would likely need to be a separate config toggle though since that has
+ // very different usefulness/purpose to the pet hud outside of rift
+ if (SBData.skyblockLocation == SkyBlockIsland.RIFT) return
+
+ // Initial data
+ var pet: ParsedPet? = null
+ // Do not render the HUD if there is no pet active
+ if (currentPetUUID == "None") return
+ // Get active pet from cache
+ if (currentPetUUID != "") pet = petMap[currentPetUUID]
+ // Parse tab widget for pet data
+ val tabPet = PetParser.parseTabWidget(TabListAPI.getWidgetLines(TabListAPI.WidgetName.PET))
+ if (pet == null && tabPet == null && tempTabPet == null && tempChatPet == null) {
+ // No data on current pet
+ it.context.pose().pushMatrix()
+ TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose())
+ val lines = mutableListOf<Component>()
+ lines.add(Component.literal("" + ChatFormatting.WHITE + "Unknown Pet"))
+ lines.add(Component.literal("Open Pets Menu To Fix"))
+ renderLinesAndBackground(it, lines)
+ it.context.pose().popMatrix()
+ return
+ }
+ if (pet == null) {
+ // Pet is only known through tab widget or chat message, potentially saved from tab widget elsewhere
+ // (e.g. another server or before removing the widget from the tab list)
+ pet = tabPet ?: tempTabPet ?: tempChatPet ?: return
+ if (tempTabPet == null) tempTabPet = tabPet
+ }
+
+ // Update pet based on tab widget if needed
+ if (tabPet != null && pet.name == tabPet.name && pet.rarity == tabPet.rarity) {
+ if (tabPet.level > pet.level) {
+ // Level has increased since caching
+ pet.level = tabPet.level
+ pet.currentExp = tabPet.currentExp
+ pet.expForNextLevel = tabPet.expForNextLevel
+ pet.totalExp = tabPet.totalExp
+ } else if (tabPet.currentExp > pet.currentExp) {
+ // Exp has increased since caching, level has not
+ pet.currentExp = tabPet.currentExp
+ pet.totalExp = tabPet.totalExp
+ }
+ if (tabPet.petItem != pet.petItem && tabPet.petItem != "Unknown") {
+ // Pet item has changed since caching
+ pet.petItem = tabPet.petItem
+ pet.petItemStack = tabPet.petItemStack
+ }
+ }
+
+ // Set the text for the HUD
+
+ val lines = mutableListOf<Component>()
+
+ if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_NO_BACKGROUND ||
+ TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) {
+ // Colour Style
+ lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name)
+ .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE)))
+
+ lines.add(Component.literal(pet.petItem))
+ if (pet.level != pet.maxLevel) {
+ // Exp data
+ lines.add(
+ Component.literal(
+ "" + ChatFormatting.YELLOW + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}" +
+ ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW +
+ "${shortFormat(pet.expForNextLevel)} " + ChatFormatting.GOLD +
+ "(${formatPercent(pet.currentExp / pet.expForNextLevel)})"
+ )
+ )
+ lines.add(
+ Component.literal(
+ "" + ChatFormatting.YELLOW + "Required L100: ${shortFormat(pet.totalExp)}" +
+ ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW +
+ "${shortFormat(pet.expForMax)} " + ChatFormatting.GOLD +
+ "(${formatPercent(pet.totalExp / pet.expForMax)})"
+ )
+ )
+ } else {
+ // Overflow Exp data
+ lines.add(Component.literal(
+ "" + ChatFormatting.AQUA + ChatFormatting.BOLD + "MAX LEVEL"
+ ))
+ lines.add(Component.literal(
+ "" + ChatFormatting.GOLD + "+" + ChatFormatting.YELLOW + "${shortFormat(pet.overflowExp)} XP"
+ ))
+ }
+ } else if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_NO_BACKGROUND ||
+ TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND) {
+ // Plain Style
+ lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name)
+ .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE)))
+
+ lines.add(Component.literal(if (pet.petItem != "None" && pet.petItem != "Unknown")
+ pet.petItem.substring(2) else pet.petItem))
+ if (pet.level != pet.maxLevel) {
+ // Exp data
+ lines.add(
+ Component.literal(
+ "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}/" +
+ "${shortFormat(pet.expForNextLevel)} " +
+ "(${formatPercent(pet.currentExp / pet.expForNextLevel)})"
+ )
+ )
+ lines.add(
+ Component.literal(
+ "Required L100: ${shortFormat(pet.totalExp)}/${shortFormat(pet.expForMax)} " +
+ "(${formatPercent(pet.totalExp / pet.expForMax)})"
+ )
+ )
+ } else {
+ // Overflow Exp data
+ lines.add(Component.literal(
+ "MAX LEVEL"
+ ))
+ lines.add(Component.literal(
+ "+${shortFormat(pet.overflowExp)} XP"
+ ))
+ }
+ }
+
+ // Render HUD
+
+ it.context.pose().pushMatrix()
+ TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose())
+
+ renderLinesAndBackground(it, lines)
+
+ // Draw the ItemStack
+ it.context.pose().pushMatrix()
+ it.context.pose().translate(-0.5F, -0.5F)
+ it.context.pose().scale(2f, 2f)
+ it.context.renderItem(pet.petItemStack.value, 0, 0)
+ it.context.pose().popMatrix()
+
+ it.context.pose().popMatrix()
+ }
+}
+
+object PetParser {
+ private val petNamePattern = " §7\\[Lvl (\\d{1,3})] §([fa956d])([\\w\\s]+)".toPattern()
+ private val petItemPattern = " (§[fa956dbc4][\\s\\w]+)".toPattern()
+ private val petExpPattern = " §e((?:\\d{1,3}[,.]?)+\\d*[kM]?)§6\\/§e((?:\\d{1,3}[,.]?)+\\d*[kM]?) XP §6\\(\\d+(?:.\\d+)?%\\)".toPattern()
+ private val petOverflowExpPattern = " §6\\+§e((?:\\d{1,3}[,.])+\\d*[kM]?) XP".toPattern()
+ private val katPattern = " Kat:.*".toPattern()
+
+ val reversePetColourMap = mapOf(
+ "f" to Rarity.COMMON,
+ "a" to Rarity.UNCOMMON,
+ "9" to Rarity.RARE,
+ "5" to Rarity.EPIC,
+ "6" to Rarity.LEGENDARY,
+ "d" to Rarity.MYTHIC
+ )
+
+ val found = HashMap<String, Matcher>()
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun parsePetChatMessage(name: String, rarityCode: String, level: Int) : ParsedPet? {
+ val petId = name.uppercase().replace(" ", "_")
+ val petRarity = reversePetColourMap[rarityCode] ?: Rarity.COMMON
+
+ val neuRarity = petRarity.neuRepoRarity ?: return null
+ val expLadder = ExpLadders.getExpLadder(petId, neuRarity)
+
+ var currentExp = 0.0
+ val expForNextLevel: Double
+ if (found.containsKey("exp")) {
+ currentExp = parseShortNumber(found.getValue("exp").group(1))
+ expForNextLevel = parseShortNumber(found.getValue("exp").group(2))
+ } else {
+ expForNextLevel = expLadder.getPetExpForLevel(level + 1).toDouble() -
+ expLadder.getPetExpForLevel(level).toDouble()
+ }
+
+ val totalExpBeforeLevel = expLadder.getPetExpForLevel(level).toDouble()
+ val totalExp = totalExpBeforeLevel + currentExp
+ val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100
+ val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble()
+ val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() }
+
+ return ParsedPet(
+ name,
+ petRarity,
+ level,
+ -1,
+ expLadder,
+ currentExp,
+ expForNextLevel,
+ totalExp,
+ totalExpBeforeLevel,
+ expForMax,
+ 0.0,
+ "Unknown",
+ petItemStack,
+ false
+ )
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun parseTabWidget(lines: List<Component>): ParsedPet? {
+ found.clear()
+ for (line in lines.reversed()) {
+ if (!found.containsKey("kat")) {
+ val matcher = katPattern.matcher(line.formattedString())
+ if (matcher.matches()) {
+ found["kat"] = matcher
+ continue
+ }
+ }
+ if (!found.containsKey("exp")) {
+ val matcher = petExpPattern.matcher(line.formattedString())
+ if (matcher.matches()) {
+ found["exp"] = matcher
+ continue
+ }
+ }
+ if (!found.containsKey("exp")) {
+ val matcher = petOverflowExpPattern.matcher(line.formattedString())
+ if (matcher.matches()) {
+ found["overflow"] = matcher
+ continue
+ }
+ }
+ if (!found.containsKey("item")) {
+ val matcher = petItemPattern.matcher(line.formattedString())
+ if (matcher.matches()) {
+ found["item"] = matcher
+ continue
+ }
+ }
+ if (!found.containsKey("name")) {
+ val matcher = petNamePattern.matcher(line.formattedString())
+ if (matcher.matches()) {
+ found["name"] = matcher
+ continue
+ }
+ }
+ }
+ if (!found.containsKey("name")) return null
+
+ val petName = titleCase(found.getValue("name").group(3))
+ val petRarity = reversePetColourMap.getValue(found.getValue("name").group(2))
+ val petId = petName.uppercase().replace(" ", "_")
+
+ val petLevel = found.getValue("name").group(1).toInt()
+
+ val neuRarity = petRarity.neuRepoRarity ?: return null
+ val expLadder = ExpLadders.getExpLadder(petId, neuRarity)
+
+ var currentExp = 0.0
+ val expForNextLevel: Double
+ if (found.containsKey("exp")) {
+ currentExp = parseShortNumber(found.getValue("exp").group(1))
+ expForNextLevel = parseShortNumber(found.getValue("exp").group(2))
+ } else {
+ expForNextLevel = expLadder.getPetExpForLevel(petLevel + 1).toDouble() -
+ expLadder.getPetExpForLevel(petLevel).toDouble()
+ }
+
+ val overflowExp: Double = if (found.containsKey("overflow"))
+ parseShortNumber(found.getValue("overflow").group(1)) else 0.0
+
+ val totalExpBeforeLevel = expLadder.getPetExpForLevel(petLevel).toDouble()
+ val totalExp = totalExpBeforeLevel + currentExp
+ val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100
+ val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble()
+ val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() }
+
+
+ var petItem = "Unknown"
+ if (found.containsKey("item")) {
+ petItem = found.getValue("item").group(1)
+ }
+
+ return ParsedPet(
+ petName,
+ petRarity,
+ petLevel,
+ maxLevel,
+ expLadder,
+ currentExp,
+ expForNextLevel,
+ totalExp,
+ totalExpBeforeLevel,
+ expForMax,
+ overflowExp,
+ petItem,
+ petItemStack,
+ false
+ )
+ }
+
+ fun parsePetMenuSlot(stack: ItemStack) : ParsedPet? {
+ val petData = stack.petData ?: return null
+ val expData = petData.level
+ val overflow = if (expData.expTotal - expData.expRequiredForMaxLevel > 0)
+ (expData.expTotal - expData.expRequiredForMaxLevel).toDouble() else 0.0
+ val petItem = if (stack.petData?.heldItem != null)
+ RepoManager.neuRepo.items.items.getValue(stack.petData?.heldItem).displayName else "None"
+ return ParsedPet(
+ titleCase(petData.type),
+ Rarity.fromNeuRepo(petData.tier) ?: Rarity.COMMON,
+ expData.currentLevel,
+ expData.maxLevel,
+ ExpLadders.getExpLadder(petData.skyblockId.toString(), petData.tier),
+ expData.expInCurrentLevel.toDouble(),
+ expData.expRequiredForNextLevel.toDouble(),
+ expData.expTotal.toDouble(),
+ expData.expTotal.toDouble() - expData.expInCurrentLevel.toDouble(),
+ expData.expRequiredForMaxLevel.toDouble(),
+ overflow,
+ petItem,
+ lazy { stack },
+ true
+ )
+ }
+}
+
+data class ParsedPet(
+ val name: String,
+ val rarity: Rarity,
+ var level: Int,
+ val maxLevel: Int,
+ val expLadder: ExpLadders.ExpLadder?,
+ var currentExp: Double,
+ var expForNextLevel: Double,
+ var totalExp: Double,
+ var totalExpBeforeLevel: Double,
+ val expForMax: Double,
+ var overflowExp: Double,
+ var petItem: String,
+ var petItemStack: Lazy<ItemStack>,
+ var isComplete: Boolean
+) {
+ fun update(other: ParsedPet) {
+ // Update the pet data to reflect another instance (of itself)
+ if (other.level > level) {
+ level = other.level
+ currentExp = other.currentExp
+ expForNextLevel = other.expForNextLevel
+ totalExp = other.totalExp
+ totalExpBeforeLevel = other.totalExpBeforeLevel
+ overflowExp = other.overflowExp
+ } else {
+ if (other.currentExp > currentExp) currentExp = other.currentExp
+ expForNextLevel = other.expForNextLevel
+ if (other.totalExp > totalExp) totalExp = other.totalExp
+ if (other.totalExpBeforeLevel > totalExpBeforeLevel) totalExpBeforeLevel = other.totalExpBeforeLevel
+ if (other.overflowExp > overflowExp) overflowExp = other.overflowExp
+ }
+ if (other.petItem != "Unknown") petItem = other.petItem
+ isComplete = false
+ }
}
diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt
index 4477203..54802db 100644
--- a/src/main/kotlin/features/inventory/PriceData.kt
+++ b/src/main/kotlin/features/inventory/PriceData.kt
@@ -1,51 +1,120 @@
-
-
package moe.nea.firmament.features.inventory
-import net.minecraft.text.Text
+import org.lwjgl.glfw.GLFW
+import net.minecraft.network.chat.Component
+import net.minecraft.util.StringRepresentable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ItemTooltipEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.HypixelStaticData
-import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.FirmFormatters.formatCommas
+import moe.nea.firmament.util.asBazaarStock
+import moe.nea.firmament.util.bold
+import moe.nea.firmament.util.darkGrey
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.getLogicalStackSize
+import moe.nea.firmament.util.gold
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.yellow
+
+object PriceData {
+ val identifier: String
+ get() = "price-data"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val tooltipEnabled by toggle("enable-always") { true }
+ val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
+ val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT }
+ val avgLowestBin by choice(
+ "avg-lowest-bin-days",
+ ) {
+ AvgLowestBin.THREEDAYAVGLOWESTBIN
+ }
+ }
-object PriceData : FirmamentFeature {
- override val identifier: String
- get() = "price-data"
+ enum class AvgLowestBin : StringRepresentable {
+ OFF,
+ ONEDAYAVGLOWESTBIN,
+ THREEDAYAVGLOWESTBIN,
+ SEVENDAYAVGLOWESTBIN;
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val tooltipEnabled by toggle("enable-always") { true }
- val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
- }
+ override fun getSerializedName(): String {
+ return name
+ }
+ }
- override val config get() = TConfig
+ fun formatPrice(label: Component, price: Double): Component {
+ return Component.literal("")
+ .yellow()
+ .bold()
+ .append(label)
+ .append(": ")
+ .append(
+ Component.literal(formatCommas(price, fractionalDigits = 1))
+ .append(if (price != 1.0) " coins" else " coin")
+ .gold()
+ .bold()
+ )
+ }
- @Subscribe
- fun onItemTooltip(it: ItemTooltipEvent) {
- if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) {
- return
- }
- val sbId = it.stack.skyBlockId
- val bazaarData = HypixelStaticData.bazaarData[sbId]
- val lowestBin = HypixelStaticData.lowestBin[sbId]
- if (bazaarData != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1))
- )
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1))
- )
- } else if (lowestBin != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin",
- FirmFormatters.formatCommas(lowestBin, 1))
- )
- }
- }
+ @Subscribe
+ fun onItemTooltip(it: ItemTooltipEvent) {
+ if (!TConfig.tooltipEnabled) return
+ if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return
+ val sbId = it.stack.skyBlockId
+ val stackSize = it.stack.getLogicalStackSize()
+ val isShowingStack = TConfig.stackSizeKey.isPressed()
+ val multiplier = if (isShowingStack) stackSize else 1
+ val multiplierText =
+ if (isShowingStack)
+ tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey()
+ else
+ tr(
+ "firmament.tooltip.multiply.hint",
+ "[${TConfig.stackSizeKey.format()}] to show x${stackSize}"
+ ).darkGrey()
+ val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock]
+ val lowestBin = HypixelStaticData.lowestBin[sbId]
+ val avgBinValue: Double? = when (TConfig.avgLowestBin) {
+ AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId]
+ AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId]
+ AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId]
+ AvgLowestBin.OFF -> null
+ }
+ if (bazaarData != null) {
+ it.lines.add(Component.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"),
+ bazaarData.quickStatus.sellPrice * multiplier
+ )
+ )
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"),
+ bazaarData.quickStatus.buyPrice * multiplier
+ )
+ )
+ } else if (lowestBin != null) {
+ it.lines.add(Component.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"),
+ lowestBin * multiplier
+ )
+ )
+ if (avgBinValue != null) {
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"),
+ avgBinValue * multiplier
+ )
+ )
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
index 1e9b1b8..e508016 100644
--- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt
+++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
@@ -1,12 +1,13 @@
package moe.nea.firmament.features.inventory
+import java.net.URI
import net.fabricmc.loader.api.FabricLoader
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
import net.minecraft.SharedConstants
-import net.minecraft.text.ClickEvent
-import net.minecraft.text.Text
+import net.minecraft.network.chat.ClickEvent
+import net.minecraft.network.chat.Component
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
@@ -30,27 +31,28 @@ object REIDependencyWarner {
var sentWarning = false
fun modrinthLink(slug: String) =
- "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric"
+ "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getCurrentVersion().name()}&l=fabric"
- fun downloadButton(modName: String, modId: String, slug: String): Text {
+ fun downloadButton(modName: String, modId: String, slug: String): Component {
val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId)
- return Text.literal(" - ")
+ return Component.literal(" - ")
.white()
- .append(Text.literal("[").aqua())
- .append(Text.translatable("firmament.download", modName)
- .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) }
+ .append(Component.literal("[").aqua())
+ .append(Component.translatable("firmament.download", modName)
+ .withStyle { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) }
.yellow()
.also {
if (alreadyDownloaded)
- it.append(Text.translatable("firmament.download.already", modName)
+ it.append(Component.translatable("firmament.download.already", modName)
.lime())
})
- .append(Text.literal("]").aqua())
+ .append(Component.literal("]").aqua())
}
@Subscribe
fun checkREIDependency(event: SkyblockServerUpdateEvent) {
if (!SBData.isOnSkyblock) return
+ if (!RepoManager.TConfig.warnForMissingItemListMod) return
if (hasREI) return
if (sentWarning) return
sentWarning = true
@@ -58,11 +60,11 @@ object REIDependencyWarner {
delay(2.seconds)
// TODO: should we offer an automatic install that actually downloads the JARs and places them into the mod folder?
MC.sendChat(
- Text.translatable("firmament.reiwarning").red().bold().append("\n")
+ Component.translatable("firmament.reiwarning").red().bold().append("\n")
.append(downloadButton("RoughlyEnoughItems", reiModId, "rei")).append("\n")
.append(downloadButton("Architectury API", "architectury", "architectury-api")).append("\n")
.append(downloadButton("Cloth Config API", "cloth-config", "cloth-config")).append("\n")
- .append(Text.translatable("firmament.reiwarning.disable")
+ .append(Component.translatable("firmament.reiwarning.disable")
.clickCommand("/firm disablereiwarning")
.grey())
)
@@ -74,9 +76,9 @@ object REIDependencyWarner {
if (hasREI) return
event.subcommand("disablereiwarning") {
thenExecute {
- RepoManager.Config.warnForMissingItemListMod = false
- RepoManager.Config.save()
- MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow())
+ RepoManager.TConfig.warnForMissingItemListMod = false
+ RepoManager.TConfig.markDirty()
+ MC.sendChat(Component.translatable("firmament.reiwarning.disabled").yellow())
}
}
}
diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt
index c47867b..c492a75 100644
--- a/src/main/kotlin/features/inventory/SaveCursorPosition.kt
+++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt
@@ -1,66 +1,63 @@
-
-
package moe.nea.firmament.features.inventory
+import org.lwjgl.glfw.GLFW
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.milliseconds
-import net.minecraft.client.util.InputUtil
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import com.mojang.blaze3d.platform.InputConstants
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.assertNotNullOr
-
-object SaveCursorPosition : FirmamentFeature {
- override val identifier: String
- get() = "save-cursor-position"
-
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val enable by toggle("enable") { true }
- val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
- }
-
- override val config: TConfig
- get() = TConfig
-
- var savedPositionedP1: Pair<Double, Double>? = null
- var savedPosition: SavedPosition? = null
-
- data class SavedPosition(
- val middle: Pair<Double, Double>,
- val cursor: Pair<Double, Double>,
- val savedAt: TimeMark = TimeMark.now()
- )
-
- @JvmStatic
- fun saveCursorOriginal(positionedX: Double, positionedY: Double) {
- savedPositionedP1 = Pair(positionedX, positionedY)
- }
-
- @JvmStatic
- fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? {
- if (!TConfig.enable) return null
- val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance }
- savedPosition = null
- if (lastPosition != null &&
- (lastPosition.middle.first - middleX).absoluteValue < 1 &&
- (lastPosition.middle.second - middleY).absoluteValue < 1
- ) {
- InputUtil.setCursorParameters(
- MC.window.handle,
- InputUtil.GLFW_CURSOR_NORMAL,
- lastPosition.cursor.first,
- lastPosition.cursor.second
- )
- return lastPosition.cursor
- }
- return null
- }
-
- @JvmStatic
- fun saveCursorMiddle(middleX: Double, middleY: Double) {
- if (!TConfig.enable) return
- val cursorPos = assertNotNullOr(savedPositionedP1) { return }
- savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
- }
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+
+object SaveCursorPosition {
+ val identifier: String
+ get() = "save-cursor-position"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val enable by toggle("enable") { true }
+ val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
+ }
+
+ var savedPositionedP1: Pair<Double, Double>? = null
+ var savedPosition: SavedPosition? = null
+
+ data class SavedPosition(
+ val middle: Pair<Double, Double>,
+ val cursor: Pair<Double, Double>,
+ val savedAt: TimeMark = TimeMark.now()
+ )
+
+ @JvmStatic
+ fun saveCursorOriginal(positionedX: Double, positionedY: Double) {
+ savedPositionedP1 = Pair(positionedX, positionedY)
+ }
+
+ @JvmStatic
+ fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? {
+ if (!TConfig.enable) return null
+ val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance }
+ savedPosition = null
+ if (lastPosition != null &&
+ (lastPosition.middle.first - middleX).absoluteValue < 1 &&
+ (lastPosition.middle.second - middleY).absoluteValue < 1
+ ) {
+ InputConstants.grabOrReleaseMouse(
+ MC.window,
+ InputConstants.CURSOR_NORMAL,
+ lastPosition.cursor.first,
+ lastPosition.cursor.second
+ )
+ return lastPosition.cursor
+ }
+ return null
+ }
+
+ @JvmStatic
+ fun saveCursorMiddle(middleX: Double, middleY: Double) {
+ if (!TConfig.enable) return
+ val cursorPos = assertNotNullOr(savedPositionedP1) { return }
+ savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
+ }
}
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index 99130d5..fca40c8 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -4,107 +4,197 @@ package moe.nea.firmament.features.inventory
import java.util.UUID
import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.int
import kotlinx.serialization.serializer
-import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.entity.player.PlayerInventory
-import net.minecraft.screen.GenericContainerScreenHandler
-import net.minecraft.screen.slot.Slot
-import net.minecraft.screen.slot.SlotActionType
-import net.minecraft.util.Identifier
-import net.minecraft.util.StringIdentifiable
+import net.minecraft.client.renderer.RenderPipelines
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.gui.screens.inventory.InventoryScreen
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.inventory.ChestMenu
+import net.minecraft.world.inventory.InventoryMenu
+import net.minecraft.world.inventory.Slot
+import net.minecraft.world.inventory.ClickType
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.util.StringRepresentable
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientInitEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.HandledScreenKeyReleasedEvent
import moe.nea.firmament.events.IsSlotProtectedEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.keybindings.InputModifiers
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.accessors.castAccessor
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.json.DashlessUUIDSerializer
+import moe.nea.firmament.util.lime
import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
-import moe.nea.firmament.util.render.GuiRenderLayers
+import moe.nea.firmament.util.red
import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.DungeonUtil
+import moe.nea.firmament.util.skyblock.SkyBlockItems
import moe.nea.firmament.util.skyblockUUID
+import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
-object SlotLocking : FirmamentFeature {
- override val identifier: String
+object SlotLocking {
+ val identifier: String
get() = "slot-locking"
@Serializable
- data class Data(
+ data class DimensionData(
val lockedSlots: MutableSet<Int> = mutableSetOf(),
- val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
+ val boundSlots: BoundSlots = BoundSlots(),
+ )
+
+ @Serializable
+ data class Data(
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
- val boundSlots: MutableMap<Int, Int> = mutableMapOf()
+ val rift: DimensionData = DimensionData(),
+ val overworld: DimensionData = DimensionData(),
+ )
+
+
+ val currentWorldData
+ get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT)
+ DConfig.data.rift
+ else
+ DConfig.data.overworld
+
+ @Serializable
+ data class BoundSlot(
+ val hotbar: Int,
+ val inventory: Int,
)
+ @Serializable(with = BoundSlots.Serializer::class)
+ data class BoundSlots(
+ val pairs: MutableSet<BoundSlot> = mutableSetOf()
+ ) {
+ fun findMatchingSlots(index: Int): List<BoundSlot> {
+ return pairs.filter { it.hotbar == index || it.inventory == index }
+ }
+
+ fun removeDuplicateForInventory(index: Int) {
+ pairs.removeIf { it.inventory == index }
+ }
+
+ fun removeAllInvolving(index: Int): Boolean {
+ return pairs.removeIf { it.inventory == index || it.hotbar == index }
+ }
+
+ fun insert(hotbar: Int, inventory: Int) {
+ if (!TConfig.allowMultiBinding) {
+ removeAllInvolving(hotbar)
+ removeAllInvolving(inventory)
+ }
+ pairs.add(BoundSlot(hotbar, inventory))
+ }
+
+ object Serializer : KSerializer<BoundSlots> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BoundSlots
+ ) {
+ serializer<MutableSet<BoundSlot>>()
+ .serialize(encoder, value.pairs)
+ }
+
+ override fun deserialize(decoder: Decoder): BoundSlots {
+ decoder as JsonDecoder
+ val json = decoder.decodeJsonElement()
+ if (json is JsonObject) {
+ return BoundSlots(json.entries.map {
+ BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int)
+ }.toMutableSet())
+ }
+ return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json))
+
+ }
+ }
+ }
+
+
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L }
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
- SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true)
+ SavedKeyBinding.keyWithMods(GLFW.GLFW_KEY_L, InputModifiers.of(shift = true))
}
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
+ val slotBindOnlyInInv by toggle("bind-only-in-inv") { false }
+ val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option
+ val protectAllHuntingBoxes by toggle("hunting-box") { false }
+ val allowDroppingInDungeons by toggle("drop-in-dungeons") { true }
}
- enum class SlotRenderLinesMode : StringIdentifiable {
+ enum class SlotRenderLinesMode : StringRepresentable {
EVERYTHING,
ONLY_BOXES,
NOTHING;
- override fun asString(): String {
+ override fun getSerializedName(): String {
return name
}
}
- override val config: TConfig
- get() = TConfig
-
+ @Config
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data)
val lockedUUIDs get() = DConfig.data?.lockedUUIDs
val lockedSlots
- get() = when (SBData.skyblockLocation) {
- SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift
- null -> null
- else -> DConfig.data?.lockedSlots
- }
+ get() = currentWorldData?.lockedSlots
- fun isSalvageScreen(screen: HandledScreen<*>?): Boolean {
+ fun isSalvageScreen(screen: AbstractContainerScreen<*>?): Boolean {
if (screen == null) return false
return screen.title.unformattedString.contains("Salvage Item")
}
- fun isTradeScreen(screen: HandledScreen<*>?): Boolean {
+ fun isTradeScreen(screen: AbstractContainerScreen<*>?): Boolean {
if (screen == null) return false
- val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false
- if (handler.inventory.size() < 9) return false
- val middlePane = handler.inventory.getStack(handler.inventory.size() - 5)
+ val handler = screen.menu as? ChestMenu ?: return false
+ if (handler.container.containerSize < 9) return false
+ val middlePane = handler.container.getItem(handler.container.containerSize - 5)
if (middlePane == null) return false
return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff"
}
- fun isNpcShop(screen: HandledScreen<*>?): Boolean {
+ fun isNpcShop(screen: AbstractContainerScreen<*>?): Boolean {
if (screen == null) return false
- val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false
- if (handler.inventory.size() < 9) return false
- val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
+ val handler = screen.menu as? ChestMenu ?: return false
+ if (handler.container.containerSize < 9) return false
+ val sellItem = handler.container.getItem(handler.container.containerSize - 5)
if (sellItem == null) return false
if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
@@ -114,13 +204,19 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
- if (!event.slot.hasStack()) return
- if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
- val inv = event.slot.inventory
+ if (!event.slot.hasItem()) return
+ if (event.slot.item.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
+ val inv = event.slot.container
var anyBlocked = false
- for (i in 0 until event.slot.index) {
- val stack = inv.getStack(i)
- if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack))
+ for (i in 0 until event.slot.containerSlot) {
+ val stack = inv.getItem(i)
+ if (IsSlotProtectedEvent.shouldBlockInteraction(
+ null,
+ ClickType.THROW,
+ IsSlotProtectedEvent.MoveOrigin.SALVAGE,
+ stack
+ )
+ )
anyBlocked = true
}
if (anyBlocked) {
@@ -130,46 +226,69 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onProtectUuidItems(event: IsSlotProtectedEvent) {
- val doesNotDeleteItem = event.actionType == SlotActionType.SWAP
- || event.actionType == SlotActionType.PICKUP
- || event.actionType == SlotActionType.QUICK_MOVE
- || event.actionType == SlotActionType.QUICK_CRAFT
- || event.actionType == SlotActionType.CLONE
- || event.actionType == SlotActionType.PICKUP_ALL
+ val doesNotDeleteItem = event.actionType == ClickType.SWAP
+ || event.actionType == ClickType.PICKUP
+ || event.actionType == ClickType.QUICK_MOVE
+ || event.actionType == ClickType.QUICK_CRAFT
+ || event.actionType == ClickType.CLONE
+ || event.actionType == ClickType.PICKUP_ALL
val isSellOrTradeScreen =
isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen)
- if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory)
+ if ((!isSellOrTradeScreen || event.slot?.container !is Inventory)
&& doesNotDeleteItem
) return
val stack = event.itemStack ?: return
+ if (TConfig.protectAllHuntingBoxes && (stack.isHuntingBox())) {
+ event.protect()
+ return
+ }
val uuid = stack.skyblockUUID ?: return
if (uuid in (lockedUUIDs ?: return)) {
event.protect()
}
}
+ fun ItemStack.isHuntingBox(): Boolean {
+ return skyBlockId == SkyBlockItems.HUNTING_TOOLKIT || extraAttributes.get("tool_kit") != null
+ }
+
@Subscribe
fun onProtectSlot(it: IsSlotProtectedEvent) {
- if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) {
+ if (it.slot != null && it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf())) {
it.protect()
}
}
@Subscribe
+ fun onEvent(event: ClientInitEvent) {
+ IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") {
+ if (it.isProtected
+ && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR
+ && DungeonUtil.isInActiveDungeon
+ && TConfig.allowDroppingInDungeons
+ ) {
+ it.isProtected = false
+ }
+ }
+ }
+
+ @Subscribe
fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) {
- val boundSlots = DConfig.data?.boundSlots ?: mapOf()
+ val boundSlots = currentWorldData?.boundSlots ?: BoundSlots()
val isValidAction =
- it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift)
+ it.actionType == ClickType.QUICK_MOVE || (it.actionType == ClickType.PICKUP && !TConfig.slotBindRequireShift)
if (!isValidAction) return
- val handler = MC.handledScreen?.screenHandler ?: return
+ val handler = MC.handledScreen?.menu ?: return
+ if (TConfig.slotBindOnlyInInv && handler !is InventoryMenu)
+ return
val slot = it.slot
- if (slot != null && it.slot.inventory is PlayerInventory) {
- val boundSlot = boundSlots.entries.find {
- it.value == slot.index || it.key == slot.index
- } ?: return
+ if (slot != null && it.slot.container is Inventory) {
+ val matchingSlots = boundSlots.findMatchingSlots(slot.containerSlot)
+ if (matchingSlots.isEmpty()) return
it.protectSilent()
- val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true)
- inventorySlot?.swapWithHotBar(handler, boundSlot.key)
+ val boundSlot = matchingSlots.singleOrNull() ?: return
+ val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true)
+ inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar)
}
}
@@ -177,10 +296,25 @@ object SlotLocking : FirmamentFeature {
fun onLockUUID(it: HandledScreenKeyPressedEvent) {
if (!it.matches(TConfig.lockUUID)) return
val inventory = MC.handledScreen ?: return
- inventory as AccessorHandledScreen
+ inventory.castAccessor()
val slot = inventory.focusedSlot_Firmament ?: return
- val stack = slot.stack ?: return
+ val stack = slot.item ?: return
+ if (stack.isHuntingBox()) {
+ MC.sendChat(
+ tr(
+ "firmament.slot-locking.hunting-box-unbindable-hint",
+ "The hunting box cannot be UUID bound reliably. It changes its own UUID frequently when switching tools. "
+ ).red().append(
+ tr(
+ "firmament.slot-locking.hunting-box-unbindable-hint.solution",
+ "Use the Firmament config option for locking all hunting boxes instead."
+ ).lime()
+ )
+ )
+ CommonSoundEffects.playFailure()
+ return
+ }
val uuid = stack.skyblockUUID ?: return
val lockedUUIDs = lockedUUIDs ?: return
if (uuid in lockedUUIDs) {
@@ -197,7 +331,7 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) {
val inventory = MC.handledScreen ?: return
- inventory as AccessorHandledScreen
+ inventory.castAccessor()
val slot = inventory.focusedSlot_Firmament
val storedSlot = storedLockingSlot ?: return
@@ -205,13 +339,11 @@ object SlotLocking : FirmamentFeature {
storedLockingSlot = null
val hotBarSlot = if (slot.isHotbar()) slot else storedSlot
val invSlot = if (slot.isHotbar()) storedSlot else slot
- val boundSlots = DConfig.data?.boundSlots ?: return
- lockedSlots?.remove(hotBarSlot.index)
- lockedSlots?.remove(invSlot.index)
- boundSlots.entries.removeIf {
- it.value == invSlot.index
- }
- boundSlots[hotBarSlot.index] = invSlot.index
+ val boundSlots = currentWorldData?.boundSlots ?: return
+ lockedSlots?.remove(hotBarSlot.containerSlot)
+ lockedSlots?.remove(invSlot.containerSlot)
+ boundSlots.removeDuplicateForInventory(invSlot.containerSlot)
+ boundSlots.insert(hotBarSlot.containerSlot, invSlot.containerSlot)
DConfig.markDirty()
CommonSoundEffects.playSuccess()
return
@@ -223,61 +355,75 @@ object SlotLocking : FirmamentFeature {
}
if (it.matches(TConfig.slotBind)) {
storedLockingSlot = null
- val boundSlots = DConfig.data?.boundSlots ?: return
+ val boundSlots = currentWorldData?.boundSlots ?: return
if (slot != null)
- boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }
+ boundSlots.removeAllInvolving(slot.containerSlot)
}
}
@Subscribe
fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) {
- val boundSlots = DConfig.data?.boundSlots ?: return
+ val boundSlots = currentWorldData?.boundSlots ?: return
fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true)
- val accScreen = event.screen as AccessorHandledScreen
+ val accScreen = event.screen.castAccessor()
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
- for (it in boundSlots.entries) {
- val hotbarSlot = findByIndex(it.key) ?: continue
- val inventorySlot = findByIndex(it.value) ?: continue
+ val highlitSlots = mutableSetOf<Slot>()
+ for (it in boundSlots.pairs) {
+ val hotbarSlot = findByIndex(it.hotbar) ?: continue
+ val inventorySlot = findByIndex(it.inventory) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
- || accScreen.focusedSlot_Firmament === inventorySlot
+ || accScreen.focusedSlot_Firmament === inventorySlot
if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
continue
- val color = if (anyHovered)
- me.shedaniel.math.Color.ofOpaque(0x00FF00)
- else
- me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
+ if (anyHovered) {
+ highlitSlots.add(hotbarSlot)
+ highlitSlots.add(inventorySlot)
+ }
+ fun color(highlit: Boolean) =
+ if (highlit)
+ me.shedaniel.math.Color.ofOpaque(0x00FF00)
+ else
+ me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
- color
+ color(anyHovered)
)
- event.context.drawBorder(hotbarSlot.x + sx,
- hotbarSlot.y + sy,
- 16, 16, color.color)
- event.context.drawBorder(inventorySlot.x + sx,
- inventorySlot.y + sy,
- 16, 16, color.color)
+ event.context.submitOutline(
+ hotbarSlot.x + sx,
+ hotbarSlot.y + sy,
+ 16, 16, color(hotbarSlot in highlitSlots).color
+ )
+ event.context.submitOutline( // TODO: 1.21.10
+ inventorySlot.x + sx,
+ inventorySlot.y + sy,
+ 16, 16, color(inventorySlot in highlitSlots).color
+ )
}
}
@Subscribe
fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) {
val draggingSlot = storedLockingSlot ?: return
- val accScreen = event.screen as AccessorHandledScreen
+ val accScreen = event.screen.castAccessor()
val hoveredSlot = accScreen.focusedSlot_Firmament
- ?.takeIf { it.inventory is PlayerInventory }
+ ?.takeIf { it.container is Inventory }
?.takeIf { it == draggingSlot || it.isHotbar() != draggingSlot.isHotbar() }
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
val (borderX, borderY) = draggingSlot.lineCenter()
- event.context.drawBorder(draggingSlot.x + sx, draggingSlot.y + sy, 16, 16, 0xFF00FF00u.toInt())
+ event.context.submitOutline(
+ draggingSlot.x + sx,
+ draggingSlot.y + sy,
+ 16,
+ 16,
+ 0xFF00FF00u.toInt()
+ ) // TODO: 1.21.10
if (hoveredSlot == null) {
event.context.drawLine(
borderX + sx, borderY + sy,
@@ -291,9 +437,11 @@ object SlotLocking : FirmamentFeature {
hovX + sx, hovY + sy,
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
- event.context.drawBorder(hoveredSlot.x + sx,
- hoveredSlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
+ event.context.submitOutline(
+ hoveredSlot.x + sx,
+ hoveredSlot.y + sy,
+ 16, 16, 0xFF00FF00u.toInt()
+ )
}
}
@@ -301,13 +449,13 @@ object SlotLocking : FirmamentFeature {
return if (isHotbar()) {
x + 9 to y
} else {
- x + 9 to y + 17
+ x + 9 to y + 16
}
}
fun Slot.isHotbar(): Boolean {
- return index < 9
+ return containerSlot < 9
}
@Subscribe
@@ -319,16 +467,14 @@ object SlotLocking : FirmamentFeature {
fun toggleSlotLock(slot: Slot) {
val lockedSlots = lockedSlots ?: return
- val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf()
- if (slot.inventory is PlayerInventory) {
- if (boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }) {
+ val boundSlots = currentWorldData?.boundSlots ?: BoundSlots()
+ if (slot.container is Inventory) {
+ if (boundSlots.removeAllInvolving(slot.containerSlot)) {
// intentionally do nothing
- } else if (slot.index in lockedSlots) {
- lockedSlots.remove(slot.index)
+ } else if (slot.containerSlot in lockedSlots) {
+ lockedSlots.remove(slot.containerSlot)
} else {
- lockedSlots.add(slot.index)
+ lockedSlots.add(slot.containerSlot)
}
DConfig.markDirty()
CommonSoundEffects.playSuccess()
@@ -338,10 +484,10 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onLockSlot(it: HandledScreenKeyPressedEvent) {
val inventory = MC.handledScreen ?: return
- inventory as AccessorHandledScreen
+ inventory.castAccessor()
val slot = inventory.focusedSlot_Firmament ?: return
- if (slot.inventory !is PlayerInventory) return
+ if (slot.container !is Inventory) return
if (it.matches(TConfig.slotBind)) {
storedLockingSlot = storedLockingSlot ?: slot
return
@@ -354,17 +500,17 @@ object SlotLocking : FirmamentFeature {
@Subscribe
fun onRenderSlotOverlay(it: SlotRenderEvents.After) {
- val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())
- val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf())
+ val isSlotLocked = it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf())
+ val isUUIDLocked = (it.slot.item?.skyblockUUID) in (lockedUUIDs ?: setOf())
if (isSlotLocked || isUUIDLocked) {
- it.context.drawGuiTexture(
- GuiRenderLayers.GUI_TEXTURED_NO_DEPTH,
+ it.context.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
when {
isSlotLocked ->
- (Identifier.of("firmament:slot_locked"))
+ (ResourceLocation.parse("firmament:slot_locked"))
isUUIDLocked ->
- (Identifier.of("firmament:uuid_locked"))
+ (ResourceLocation.parse("firmament:uuid_locked"))
else ->
error("unreachable")
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
index f1b77c6..9bb78c9 100644
--- a/src/main/kotlin/features/inventory/TimerInLore.kt
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -7,25 +7,29 @@ import java.time.format.DateTimeFormatterBuilder
import java.time.format.FormatStyle
import java.time.format.TextStyle
import java.time.temporal.ChronoField
-import net.minecraft.text.Text
-import net.minecraft.util.StringIdentifiable
+import net.minecraft.network.chat.Component
+import net.minecraft.util.StringRepresentable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ItemTooltipEvent
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.aqua
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.timestamp
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
object TimerInLore {
+ @Config
object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
val showTimers by toggle("show") { true }
+ val showCreationTimestamp by toggle("show-creation") { true }
val timerFormat by choice("format") { TimerFormat.SOCIALIST }
}
- enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable {
+ enum class TimerFormat(val formatter: DateTimeFormatter) : StringRepresentable {
RFC(DateTimeFormatter.RFC_1123_DATE_TIME),
LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)),
SOCIALIST(
@@ -45,6 +49,7 @@ object TimerInLore {
appendValue(ChronoField.SECOND_OF_MINUTE, 2)
}),
AMERICAN("EEEE, MMM d h:mm a yyyy"),
+ RFCPrecise(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss.SSS Z")),
;
constructor(block: DateTimeFormatterBuilder.() -> Unit)
@@ -52,7 +57,7 @@ object TimerInLore {
constructor(format: String) : this(DateTimeFormatter.ofPattern(format))
- override fun asString(): String {
+ override fun getSerializedName(): String {
return name
}
}
@@ -80,13 +85,25 @@ object TimerInLore {
COMMUNITYPROJECTS("Contribute again", "Come back at"),
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
- LIZSTONKREDEMPTION("Resets in:", "Resets at");
+ LIZSTONKREDEMPTION("Resets in:", "Resets at"),
+ TIMEREMAININGS("Time Remaining:", "Ends at"),
+ COOLDOWN("Cooldown:", "Come back at"),
+ ONCOOLDOWN("On cooldown:", "Available at"),
+ EVENTENDING("Event ends in:", "Ends at");
}
val regex =
"(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex()
@Subscribe
+ fun creationInLore(event: ItemTooltipEvent) {
+ if (!TConfig.showCreationTimestamp) return
+ val timestamp = event.stack.timestamp ?: return
+ val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault()))
+ event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey())
+ }
+
+ @Subscribe
fun modifyLore(event: ItemTooltipEvent) {
if (!TConfig.showTimers) return
var lastTimer: ZonedDateTime? = null
@@ -107,9 +124,13 @@ object TimerInLore {
var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone)
if (countdownType.isRelative) {
if (lastTimer == null) {
- event.lines.add(i + 1,
- tr("firmament.loretimer.missingrelative",
- "Found a relative countdown with no baseline (Firmament)").grey())
+ event.lines.add(
+ i + 1,
+ tr(
+ "firmament.loretimer.missingrelative",
+ "Found a relative countdown with no baseline (Firmament)"
+ ).grey()
+ )
continue
}
baseLine = lastTimer
@@ -119,10 +140,11 @@ object TimerInLore {
lastTimer = timer
val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault())
// TODO: install approximate time stabilization algorithm
- event.lines.add(i + 1,
- Text.literal("${countdownType.label}: ")
- .grey()
- .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
+ event.lines.add(
+ i + 1,
+ Component.literal("${countdownType.label}: ")
+ .grey()
+ .append(Component.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
)
}
}
diff --git a/src/main/kotlin/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
new file mode 100644
index 0000000..8d4760b
--- /dev/null
+++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
@@ -0,0 +1,80 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.world.item.Items
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton
+
+object WardrobeKeybinds {
+ @Config
+ object TConfig : ManagedConfig("wardrobe-keybinds", Category.INVENTORY) {
+ val wardrobeKeybinds by toggle("wardrobe-keybinds") { false }
+ val changePageKeybind by keyBinding("change-page") { GLFW.GLFW_KEY_ENTER }
+ val nextPage by keyBinding("next-page") { GLFW.GLFW_KEY_D }
+ val previousPage by keyBinding("previous-page") { GLFW.GLFW_KEY_A }
+ val slotKeybinds = (1..9).map {
+ keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it }
+ }
+ val allowUnequipping by toggle("allow-unequipping") { true }
+ }
+
+ val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) ->
+ index + 36 to keybinding
+ }
+
+ @Subscribe
+ fun switchSlot(event: HandledScreenKeyPressedEvent) {
+ if (MC.player == null || MC.world == null || MC.interactionManager == null) return
+ if (event.screen !is AbstractContainerScreen<*>) return
+
+ val regex = Regex("Wardrobe \\([12]/2\\)")
+ if (!regex.matches(event.screen.title.string)) return
+ if (!TConfig.wardrobeKeybinds) return
+
+ if (
+ event.matches(TConfig.changePageKeybind) ||
+ event.matches(TConfig.previousPage) ||
+ event.matches(TConfig.nextPage)
+ ) {
+ event.cancel()
+
+ val handler = event.screen.menu
+ val previousSlot = handler.getSlot(45)
+ val nextSlot = handler.getSlot(53)
+
+ val backPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.previousPage)
+ val nextPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.nextPage)
+
+ if (backPressed && previousSlot.item.item == Items.ARROW) {
+ previousSlot.clickLeftMouseButton(handler)
+ } else if (nextPressed && nextSlot.item.item == Items.ARROW) {
+ nextSlot.clickLeftMouseButton(handler)
+ }
+ }
+
+
+ val slot =
+ slotKeybindsWithSlot
+ .find { event.matches(it.second.get()) }
+ ?.first ?: return
+
+ event.cancel()
+
+ val handler = event.screen.menu
+ val invSlot = handler.getSlot(slot)
+
+ val itemStack = invSlot.item
+ val isSelected = itemStack.item == Items.LIME_DYE
+ val isSelectable = itemStack.item == Items.PINK_DYE
+ if (!isSelectable && !isSelected) return
+ if (!TConfig.allowUnequipping && isSelected) return
+
+ invSlot.clickLeftMouseButton(handler)
+ }
+
+}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
index a46bd76..0cb51ca 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.features.inventory.buttons
import com.mojang.brigadier.StringReader
@@ -7,80 +5,120 @@ import me.shedaniel.math.Dimension
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.command.CommandRegistryAccess
-import net.minecraft.command.argument.ItemStackArgumentType
-import net.minecraft.item.ItemStack
-import net.minecraft.resource.featuretoggle.FeatureFlags
-import net.minecraft.util.Identifier
+import net.minecraft.client.renderer.RenderPipelines
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.commands.CommandBuildContext
+import net.minecraft.commands.arguments.item.ItemArgument
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import net.minecraft.world.flag.FeatureFlags
+import net.minecraft.resources.ResourceLocation
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.ItemCache.isBroken
import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.collections.memoize
+import moe.nea.firmament.util.mc.arbitraryUUID
+import moe.nea.firmament.util.mc.createSkullItem
import moe.nea.firmament.util.render.drawGuiTexture
@Serializable
data class InventoryButton(
- var x: Int,
- var y: Int,
- var anchorRight: Boolean,
- var anchorBottom: Boolean,
- var icon: String? = "",
- var command: String? = "",
+ var x: Int,
+ var y: Int,
+ var anchorRight: Boolean,
+ var anchorBottom: Boolean,
+ var icon: String? = "",
+ var command: String? = "",
+ var isGigantic: Boolean = false,
) {
- companion object {
- val itemStackParser by lazy {
- ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries,
- FeatureFlags.VANILLA_FEATURES))
- }
- val dimensions = Dimension(18, 18)
- val getItemForName = ::getItemForName0.memoize(1024)
- fun getItemForName0(icon: String): ItemStack {
- val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
- var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
- if (repoItem == null) {
- val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
- icon.split(" ", limit = 3).getOrNull(2) ?: icon
- else icon
- val componentItem =
- runCatching {
- itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
- }.getOrNull()
- if (componentItem != null)
- itemStack = componentItem
- }
- return itemStack
- }
- }
- fun render(context: DrawContext) {
- context.drawGuiTexture(
- 0,
- 0,
- 0,
- dimensions.width,
- dimensions.height,
- Identifier.of("firmament:inventory_button_background")
- )
- context.drawItem(getItem(), 1, 1)
- }
+ val myDimension get() = if (isGigantic) bigDimension else dimensions
+
+ companion object {
+ val itemStackParser by lazy {
+ ItemArgument.item(
+ CommandBuildContext.simple(
+ MC.defaultRegistries,
+ FeatureFlags.VANILLA_SET
+ )
+ )
+ }
+ val dimensions = Dimension(18, 18)
+ val gap = 2
+ val bigDimension = Dimension(dimensions.width * 2 + gap, dimensions.height * 2 + gap)
+ val getItemForName = ::getItemForName0.memoize(1024)
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun getItemForName0(icon: String): ItemStack {
+ val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
+ var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
+ if (repoItem == null) {
+ when {
+ icon.startsWith("skull:") -> {
+ itemStack = createSkullItem(
+ arbitraryUUID,
+ "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}"
+ )
+ }
+
+ else -> {
+ val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
+ icon.split(" ", limit = 3).getOrNull(2) ?: icon
+ else icon
+ val componentItem =
+ runCatching {
+ itemStackParser.parse(StringReader(giveSyntaxItem)).createItemStack(1, false)
+ }.getOrNull()
+ if (componentItem != null)
+ itemStack = componentItem
+ }
+ }
+ }
+ if (itemStack.item == Items.PAINTING)
+ ErrorUtil.logError("created broken itemstack for inventory button $icon: $itemStack")
+ return itemStack
+ }
+ }
+
+ fun render(context: GuiGraphics) {
+ context.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ ResourceLocation.parse("firmament:inventory_button_background"),
+ 0,
+ 0,
+ myDimension.width,
+ myDimension.height,
+ )
+ if (isGigantic) {
+ context.pose().pushMatrix()
+ context.pose().translate(myDimension.width / 2F, myDimension.height / 2F)
+ context.pose().scale(2F)
+ context.renderItem(getItem(), -8, -8)
+ context.pose().popMatrix()
+ } else {
+ context.renderItem(getItem(), 1, 1)
+ }
+ }
- fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
+ fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
- fun getPosition(guiRect: Rectangle): Point {
- return Point(
- (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
- (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
- )
- }
+ fun getPosition(guiRect: Rectangle): Point {
+ return Point(
+ (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
+ (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
+ )
+ }
- fun getBounds(guiRect: Rectangle): Rectangle {
- return Rectangle(getPosition(guiRect), dimensions)
- }
+ fun getBounds(guiRect: Rectangle): Rectangle {
+ return Rectangle(getPosition(guiRect), myDimension)
+ }
- fun getItem(): ItemStack {
- return getItemForName(icon ?: "")
- }
+ fun getItem(): ItemStack {
+ return getItemForName(icon ?: "")
+ }
}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
index ee3ae8b..6b6a2d6 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
@@ -1,17 +1,24 @@
package moe.nea.firmament.features.inventory.buttons
import io.github.notenoughupdates.moulconfig.common.IItemStack
-import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext
import io.github.notenoughupdates.moulconfig.xml.Bind
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import org.lwjgl.glfw.GLFW
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.widget.ButtonWidget
-import net.minecraft.client.util.InputUtil
-import net.minecraft.text.Text
-import net.minecraft.util.math.MathHelper
-import net.minecraft.util.math.Vec2f
+import net.minecraft.client.Minecraft
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.components.Button
+import net.minecraft.client.gui.components.MultiLineTextWidget
+import net.minecraft.client.gui.components.StringWidget
+import net.minecraft.client.input.KeyEvent
+import com.mojang.blaze3d.platform.InputConstants
+import net.minecraft.network.chat.Component
+import net.minecraft.util.Mth
+import net.minecraft.world.phys.Vec2
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.FragmentGuiScreen
import moe.nea.firmament.util.MC
@@ -28,10 +35,13 @@ class InventoryButtonEditor(
@field:Bind
var icon: String = originalButton.icon ?: ""
+ @field:Bind
+ var isGigantic: Boolean = originalButton.isGigantic
+
@Bind
fun getItemIcon(): IItemStack {
save()
- return ModernItemStack.of(InventoryButton.getItemForName(icon))
+ return MoulConfigPlatform.wrap(InventoryButton.getItemForName(icon))
}
@Bind
@@ -42,6 +52,7 @@ class InventoryButtonEditor(
fun save() {
originalButton.icon = icon
+ originalButton.isGigantic = isGigantic
originalButton.command = command
}
}
@@ -49,30 +60,92 @@ class InventoryButtonEditor(
var buttons: MutableList<InventoryButton> =
InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList()
- override fun close() {
+ override fun onClose() {
InventoryButtons.DConfig.data.buttons = buttons
InventoryButtons.DConfig.markDirty()
- super.close()
+ super.onClose()
+ }
+
+ override fun resize(client: Minecraft, width: Int, height: Int) {
+ lastGuiRect.move(
+ MC.window.guiScaledWidth / 2 - lastGuiRect.width / 2,
+ MC.window.guiScaledHeight / 2 - lastGuiRect.height / 2
+ )
+ super.resize(client, width, height)
}
override fun init() {
super.init()
- addDrawableChild(
- ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) {
+ addRenderableWidget(
+ MultiLineTextWidget(
+ lastGuiRect.minX,
+ 25,
+ Component.translatable("firmament.inventory-buttons.delete"),
+ MC.font
+ ).setCentered(true).setMaxWidth(lastGuiRect.width)
+ )
+ addRenderableWidget(
+ MultiLineTextWidget(
+ lastGuiRect.minX,
+ 40,
+ Component.translatable("firmament.inventory-buttons.info"),
+ MC.font
+ ).setCentered(true).setMaxWidth(lastGuiRect.width)
+ )
+ addRenderableWidget(
+ Button.builder(Component.translatable("firmament.inventory-buttons.reset")) {
+ val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==")
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 10)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addRenderableWidget(
+ Button.builder(Component.translatable("firmament.inventory-buttons.load-preset")) {
val t = ClipboardUtils.getTextContents()
val newButtons = InventoryButtonTemplates.loadTemplate(t)
if (newButtons != null)
buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
}
- .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35)
+ .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 35)
.width(lastGuiRect.width - 20)
.build()
)
- addDrawableChild(
- ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) {
+ addRenderableWidget(
+ Button.builder(Component.translatable("firmament.inventory-buttons.save-preset")) {
ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons))
}
- .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60)
+ .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 60)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addRenderableWidget(
+ Button.builder(Component.translatable("firmament.inventory-buttons.simple-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348
+ val newButtons = InventoryButtonTemplates.loadTemplate(
+ "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0="
+ )
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 85)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addRenderableWidget(
+ Button.builder(Component.translatable("firmament.inventory-buttons.all-warps-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276
+ val newButtons = InventoryButtonTemplates.loadTemplate(
+ "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd"
+ )
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 110)
.width(lastGuiRect.width - 20)
.build()
)
@@ -83,14 +156,20 @@ class InventoryButtonEditor(
val movedButtons = mutableListOf<InventoryButton>()
for (button in buttons) {
if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) {
- MC.sendChat(tr("firmament.inventory-buttons.button-moved",
- "One of your imported buttons intersects with the inventory and has been moved to the top left."))
- movedButtons.add(button.copy(
- x = 0,
- y = -InventoryButton.dimensions.width,
- anchorRight = false,
- anchorBottom = false
- ))
+ MC.sendChat(
+ tr(
+ "firmament.inventory-buttons.button-moved",
+ "One of your imported buttons intersects with the inventory and has been moved to the top left."
+ )
+ )
+ movedButtons.add(
+ button.copy(
+ x = 0,
+ y = -InventoryButton.dimensions.width,
+ anchorRight = false,
+ anchorBottom = false
+ )
+ )
} else {
newButtons.add(button)
}
@@ -99,9 +178,11 @@ class InventoryButtonEditor(
val zeroRect = Rectangle(0, 0, 1, 1)
for (movedButton in movedButtons) {
fun getPosition(button: InventoryButton, index: Int) =
- button.copy(x = (index % 10) * InventoryButton.dimensions.width,
- y = (index / 10) * -InventoryButton.dimensions.height,
- anchorRight = false, anchorBottom = false)
+ button.copy(
+ x = (index % 10) * InventoryButton.dimensions.width,
+ y = (index / 10) * -InventoryButton.dimensions.height,
+ anchorRight = false, anchorBottom = false
+ )
while (true) {
val newPos = getPosition(movedButton, i++)
val newBounds = newPos.getBounds(zeroRect)
@@ -114,35 +195,48 @@ class InventoryButtonEditor(
return newButtons
}
- override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ context.pose().pushMatrix()
+ PanelComponent.DefaultBackgroundRenderer.VANILLA
+ .render(
+ MoulConfigRenderContext(context),
+ lastGuiRect.minX, lastGuiRect.minY,
+ lastGuiRect.width, lastGuiRect.height,
+ )
+ context.pose().popMatrix()
super.render(context, mouseX, mouseY, delta)
- context.matrices.push()
- context.matrices.translate(0f, 0f, -10f)
- context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1)
- context.matrices.pop()
for (button in buttons) {
val buttonPosition = button.getBounds(lastGuiRect)
- context.matrices.push()
- context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F)
+ context.pose().pushMatrix()
+ context.pose().translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat())
button.render(context)
- context.matrices.pop()
+ context.pose().popMatrix()
}
+ renderPopup(context, mouseX, mouseY, delta)
}
- override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- if (super.keyPressed(keyCode, scanCode, modifiers)) return true
- if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
- close()
+ override fun keyPressed(input: KeyEvent): Boolean {
+ if (super.keyPressed(input)) return true
+ if (input.input() == GLFW.GLFW_KEY_ESCAPE) {
+ onClose()
return true
}
return false
}
- override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
- if (super.mouseReleased(mouseX, mouseY, button)) return true
- val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
+ override fun mouseReleased(click: MouseButtonEvent): Boolean {
+ if (super.mouseReleased(click)) return true
+ val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(click.x, click.y)) }
if (clickedButton != null && !justPerformedAClickAction) {
- createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
+ if (InputConstants.isKeyDown(
+ MC.window,
+ InputConstants.KEY_LCONTROL
+ )
+ ) Editor(clickedButton).delete()
+ else createPopup(
+ MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)),
+ Point(click.x, click.y)
+ )
return true
}
justPerformedAClickAction = false
@@ -150,19 +244,27 @@ class InventoryButtonEditor(
return false
}
- override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
- if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true
+ override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean {
+ if (super.mouseDragged(click, offsetX, offsetY)) return true
- if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) {
- initialDragMousePosition = Vec2f(-10F, -10F)
+ if (initialDragMousePosition.distanceToSqr(Vec2(click.x.toFloat(), click.y.toFloat())) >= 4 * 4) {
+ initialDragMousePosition = Vec2(-10F, -10F)
lastDraggedButton?.let { dragging ->
justPerformedAClickAction = true
- val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt())
+ val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(click.x.toInt(), click.y.toInt())
?: return true
dragging.x = offsetX
dragging.y = offsetY
dragging.anchorRight = anchorRight
dragging.anchorBottom = anchorBottom
+ if (!anchorRight && offsetX > -dragging.myDimension.width
+ && dragging.getBounds(lastGuiRect).intersects(lastGuiRect)
+ )
+ dragging.x = -dragging.myDimension.width
+ if (!anchorRight && offsetY > -dragging.myDimension.height
+ && dragging.getBounds(lastGuiRect).intersects(lastGuiRect)
+ )
+ dragging.y = -dragging.myDimension.height
}
}
return false
@@ -170,7 +272,7 @@ class InventoryButtonEditor(
var lastDraggedButton: InventoryButton? = null
var justPerformedAClickAction = false
- var initialDragMousePosition = Vec2f(-10F, -10F)
+ var initialDragMousePosition = Vec2(-10F, -10F)
data class AnchoredCoords(
val anchorRight: Boolean,
@@ -180,35 +282,30 @@ class InventoryButtonEditor(
)
fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? {
- if (lastGuiRect.contains(mx, my) || lastGuiRect.contains(
- Point(
- mx + InventoryButton.dimensions.width,
- my + InventoryButton.dimensions.height,
- )
- )
- ) return null
-
val anchorRight = mx > lastGuiRect.maxX
val anchorBottom = my > lastGuiRect.maxY
var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX
var offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY
- if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_SHIFT)) {
- offsetX = MathHelper.floor(offsetX / 20F) * 20
- offsetY = MathHelper.floor(offsetY / 20F) * 20
+ if (InputConstants.isKeyDown(MC.window, InputConstants.KEY_LSHIFT)) {
+ offsetX = Mth.floor(offsetX / 20F) * 20
+ offsetY = Mth.floor(offsetY / 20F) * 20
}
- return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect)
+ if (rect.intersects(lastGuiRect)) return null
+ val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ return anchoredCoords
}
- override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
- if (super.mouseClicked(mouseX, mouseY, button)) return true
- val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
+ override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean {
+ if (super.mouseClicked(click, doubled)) return true
+ val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(click.x, click.y) }
if (clickedButton != null) {
lastDraggedButton = clickedButton
- initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat())
+ initialDragMousePosition = Vec2(click.y.toFloat(), click.y.toFloat())
return true
}
- val mx = mouseX.toInt()
- val my = mouseY.toInt()
+ val mx = click.x.toInt()
+ val my = click.y.toInt()
val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true
buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null))
justPerformedAClickAction = true
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
index d282157..c6ad14d 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
@@ -1,7 +1,6 @@
package moe.nea.firmament.features.inventory.buttons
-import kotlinx.serialization.encodeToString
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
@@ -18,7 +17,7 @@ object InventoryButtonTemplates {
ErrorUtil.catch<InventoryButton?>("Could not import button") {
Firmament.json.decodeFromString<InventoryButton>(it).also {
if (it.icon?.startsWith("extra:") == true) {
- MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed"))
+ MC.sendChat(Component.translatable("firmament.inventory-buttons.import-failed"))
}
}
}.or {
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
index d5b5417..eaa6138 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
@@ -1,88 +1,118 @@
-
-
package moe.nea.firmament.features.inventory.buttons
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.gui.screens.inventory.InventoryScreen
+import net.minecraft.network.chat.Component
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenClickEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenPushREIEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.impl.v1.FirmamentAPIImpl
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.accessors.getProperRectangle
+import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.DataHolder
-import moe.nea.firmament.util.accessors.getRectangle
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.gold
+
+object InventoryButtons {
+
+ @Config
+ object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) {
+ val _openEditor by button("open-editor") {
+ openEditor()
+ }
+ val hoverText by toggle("hover-text") { true }
+ val onlyInv by toggle("only-inv") { false }
+ }
-object InventoryButtons : FirmamentFeature {
- override val identifier: String
- get() = "inventory-buttons"
+ @Config
+ object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data)
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val _openEditor by button("open-editor") {
- openEditor()
- }
- }
+ @Serializable
+ data class Data(
+ var buttons: MutableList<InventoryButton> = mutableListOf()
+ )
- object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+ fun getValidButtons(screen: AbstractContainerScreen<*>): Sequence<InventoryButton> {
+ if (TConfig.onlyInv && screen !is InventoryScreen) return emptySequence()
+ if (FirmamentAPIImpl.extensions.any { it.shouldHideInventoryButtons(screen) }) {
+ return emptySequence()
+ }
+ return DConfig.data.buttons.asSequence().filter(InventoryButton::isValid)
+ }
- @Serializable
- data class Data(
- var buttons: MutableList<InventoryButton> = mutableListOf()
- )
+ @Subscribe
+ fun onRectangles(it: HandledScreenPushREIEvent) {
+ val bounds = it.screen.getProperRectangle()
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ it.block(buttonBounds)
+ }
+ }
- override val config: ManagedConfig
- get() = TConfig
+ @Subscribe
+ fun onClickScreen(it: HandledScreenClickEvent) {
+ val bounds = it.screen.getProperRectangle()
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ if (buttonBounds.contains(it.mouseX, it.mouseY)) {
+ MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
+ break
+ }
+ }
+ }
- fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
+ var lastHoveredComponent: InventoryButton? = null
+ var lastMouseMove = TimeMark.farPast()
- @Subscribe
- fun onRectangles(it: HandledScreenPushREIEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- it.block(buttonBounds)
- }
- }
+ @Subscribe
+ fun onRenderForeground(it: HandledScreenForegroundEvent) {
+ val bounds = it.screen.getProperRectangle()
- @Subscribe
- fun onClickScreen(it: HandledScreenClickEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- if (buttonBounds.contains(it.mouseX, it.mouseY)) {
- MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
- break
- }
- }
- }
+ var hoveredComponent: InventoryButton? = null
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ it.context.pose().pushMatrix()
+ it.context.pose().translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat())
+ button.render(it.context)
+ it.context.pose().popMatrix()
- @Subscribe
- fun onRenderForeground(it: HandledScreenForegroundEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- it.context.matrices.push()
- it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
- button.render(it.context)
- it.context.matrices.pop()
- }
- lastRectangle = bounds
- }
+ if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) {
+ hoveredComponent = button
+ if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) {
+ it.context.setComponentTooltipForNextFrame(
+ MC.font,
+ listOf(Component.literal(button.command).gold()),
+ buttonBounds.minX - 15,
+ buttonBounds.maxY + 20,
+ )
+ }
+ }
+ }
+ if (hoveredComponent !== lastHoveredComponent)
+ lastMouseMove = TimeMark.now()
+ lastHoveredComponent = hoveredComponent
+ lastRectangle = bounds
+ }
- var lastRectangle: Rectangle? = null
- fun openEditor() {
- ScreenUtil.setScreenLater(
- InventoryButtonEditor(
- lastRectangle ?: Rectangle(
- MC.window.scaledWidth / 2 - 100,
- MC.window.scaledHeight / 2 - 100,
- 200, 200,
- )
- )
- )
- }
+ var lastRectangle: Rectangle? = null
+ fun openEditor() {
+ ScreenUtil.setScreenLater(
+ InventoryButtonEditor(
+ lastRectangle ?: Rectangle(
+ MC.window.guiScaledWidth / 2 - 88,
+ MC.window.guiScaledHeight / 2 - 83,
+ 176, 166,
+ )
+ )
+ )
+ }
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
index 8fad4df..964f415 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
@@ -4,9 +4,9 @@ package moe.nea.firmament.features.inventory.storageoverlay
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
-import net.minecraft.screen.GenericContainerScreenHandler
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.gui.screens.inventory.ContainerScreen
+import net.minecraft.world.inventory.ChestMenu
import moe.nea.firmament.util.ifMatches
import moe.nea.firmament.util.unformattedString
@@ -16,24 +16,24 @@ import moe.nea.firmament.util.unformattedString
sealed interface StorageBackingHandle {
sealed interface HasBackingScreen {
- val handler: GenericContainerScreenHandler
+ val handler: ChestMenu
}
/**
* The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage`
*/
- data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen
+ data class Overview(override val handler: ChestMenu) : StorageBackingHandle, HasBackingScreen
/**
* An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via
* the [Overview] or via `/ec <index + 1>` for enderchest pages.
*/
- data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) :
+ data class Page(override val handler: ChestMenu, val storagePageSlot: StoragePageSlot) :
StorageBackingHandle, HasBackingScreen
companion object {
- private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex()
- private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex()
+ private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex()
+ private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex()
/**
* Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not
@@ -46,13 +46,13 @@ sealed interface StorageBackingHandle {
returnsNotNull() implies (screen != null)
}
if (screen == null) return null
- if (screen !is GenericContainerScreen) return null
+ if (screen !is ContainerScreen) return null
val title = screen.title.unformattedString
- if (title == "Storage") return Overview(screen.screenHandler)
+ if (title == "Storage") return Overview(screen.menu)
return title.ifMatches(enderChestName) {
- Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt()))
+ Page(screen.menu, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt()))
} ?: title.ifMatches(backPackName) {
- Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt()))
+ Page(screen.menu, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt()))
}
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
index 2e807de..7f96637 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
@@ -1,59 +1,106 @@
package moe.nea.firmament.features.inventory.storageoverlay
+import io.github.notenoughupdates.moulconfig.ChromaColour
import java.util.SortedMap
import kotlinx.serialization.serializer
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
-import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.entity.player.PlayerInventory
-import net.minecraft.item.Items
-import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
+import net.minecraft.client.gui.screens.inventory.ContainerScreen
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.item.Items
+import net.minecraft.network.protocol.game.ServerboundContainerClosePacket
+import net.minecraft.network.chat.Component
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ChestInventoryUpdateEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotClickEvent
+import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.async.discard
import moe.nea.firmament.util.customgui.customGui
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
-object StorageOverlay : FirmamentFeature {
-
+object StorageOverlay {
+ @Config
object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData)
- override val identifier: String
+ val identifier: String
get() = "storage-overlay"
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val alwaysReplace by toggle("always-replace") { true }
+ val outlineActiveStoragePage by toggle("outline-active-page") { false }
+ val outlineActiveStoragePageColour by colour("outline-active-page-colour") {
+ ChromaColour.fromRGB(
+ 255,
+ 255,
+ 0,
+ 0,
+ 255
+ )
+ }
+ val showInactivePageTooltips by toggle("inactive-page-tooltips") { false }
val columns by integer("rows", 1, 10) { 3 }
val height by integer("height", 80, 3000) { 3 * 18 * 6 }
+ val retainScroll by toggle("retain-scroll") { true }
val scrollSpeed by integer("scroll-speed", 1, 50) { 10 }
val inverseScroll by toggle("inverse-scroll") { false }
val padding by integer("padding", 1, 20) { 5 }
val margin by integer("margin", 1, 60) { 20 }
+ val itemsBlockScrolling by toggle("block-item-scrolling") { true }
+ val highlightSearchResults by toggle("highlight-search-results") { true }
+ val highlightSearchResultsColour by colour("highlight-search-results-colour") {
+ ChromaColour.fromRGB(
+ 0,
+ 176,
+ 0,
+ 0,
+ 255
+ )
+ }
+ }
+
+ @Subscribe
+ fun highlightSlots(event: SlotRenderEvents.Before) {
+ if (!TConfig.highlightSearchResults) return
+ val storageOverlayScreen =
+ (MC.screen as? StorageOverlayScreen)
+ ?: (MC.handledScreen?.customGui as? StorageOverlayCustom)?.overview
+ ?: return
+ val stack = event.slot.item ?: return
+ val search = storageOverlayScreen.searchText.get().takeIf { it.isNotBlank() } ?: return
+ if (storageOverlayScreen.matchesSearch(stack, search)) {
+ event.context.fill(
+ event.slot.x,
+ event.slot.y,
+ event.slot.x + 16,
+ event.slot.y + 16,
+ TConfig.highlightSearchResultsColour.getEffectiveColourRGB()
+ )
+ }
}
+
fun adjustScrollSpeed(amount: Double): Double {
return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1)
}
- override val config: TConfig
- get() = TConfig
-
var lastStorageOverlay: StorageOverviewScreen? = null
var skipNextStorageOverlayBackflip = false
var currentHandler: StorageBackingHandle? = null
@Subscribe
- fun onTick(event: TickEvent) {
- rememberContent(currentHandler ?: return)
+ fun onChestContentUpdate(event: ChestInventoryUpdateEvent) {
+ rememberContent(currentHandler)
}
@Subscribe
fun onClick(event: SlotClickEvent) {
- if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9
+ if (lastStorageOverlay != null && event.slot.container !is Inventory && event.slot.containerSlot < 9
&& event.stack.item != Items.BLACK_STAINED_GLASS_PANE
) {
skipNextStorageOverlayBackflip = true
@@ -64,18 +111,18 @@ object StorageOverlay : FirmamentFeature {
fun onScreenChange(it: ScreenChangeEvent) {
if (it.old == null && it.new == null) return
val storageOverlayScreen = it.old as? StorageOverlayScreen
- ?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview
+ ?: ((it.old as? AbstractContainerScreen<*>)?.customGui as? StorageOverlayCustom)?.overview
var storageOverviewScreen = it.old as? StorageOverviewScreen
- val screen = it.new as? GenericContainerScreen
+ val screen = it.new as? ContainerScreen
+ rememberContent(currentHandler)
val oldHandler = currentHandler
currentHandler = StorageBackingHandle.fromScreen(screen)
- rememberContent(currentHandler)
if (storageOverviewScreen != null && oldHandler is StorageBackingHandle.HasBackingScreen) {
val player = MC.player
assert(player != null)
- player?.networkHandler?.sendPacket(CloseHandledScreenC2SPacket(oldHandler.handler.syncId))
- if (player?.currentScreenHandler === oldHandler.handler) {
- player.currentScreenHandler = player.playerScreenHandler
+ player?.connection?.send(ServerboundContainerClosePacket(oldHandler.handler.containerId))
+ if (player?.containerMenu === oldHandler.handler) {
+ player.containerMenu = player.inventoryMenu
}
}
storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay
@@ -100,25 +147,24 @@ object StorageOverlay : FirmamentFeature {
screen.customGui = StorageOverlayCustom(
currentHandler ?: return,
screen,
- storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return))
+ storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)
+ )
}
fun rememberContent(handler: StorageBackingHandle?) {
handler ?: return
- // TODO: Make all of these functions work on deltas / updates instead of the entire contents
- val data = Data.data?.storageInventories ?: return
+ val data = Data.data.storageInventories
when (handler) {
is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data)
is StorageBackingHandle.Page -> rememberPage(handler, data)
}
- Data.markDirty()
}
private fun rememberStorageOverview(
handler: StorageBackingHandle.Overview,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
- for ((index, stack) in handler.handler.stacks.withIndex()) {
+ for ((index, stack) in handler.handler.items.withIndex()) { // TODO: replace with slot iteration
// Ignore unloaded item stacks
if (stack.isEmpty) continue
val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue
@@ -132,15 +178,15 @@ object StorageOverlay : FirmamentFeature {
data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null)
}
}
+ Data.markDirty()
}
private fun rememberPage(
handler: StorageBackingHandle.Page,
data: SortedMap<StoragePageSlot, StorageData.StorageInventory>
) {
- // TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence
val newStacks =
- VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() })
+ VirtualInventory(handler.handler.items.take(handler.handler.rowCount * 9).drop(9).map { it.copy() })
data.compute(handler.storagePageSlot) { slot, existingInventory ->
(existingInventory ?: StorageData.StorageInventory(
slot.defaultName(),
@@ -150,5 +196,6 @@ object StorageOverlay : FirmamentFeature {
it.inventory = newStacks
}
}
+ Data.markDirty(newStacks.serializationCache.discard())
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
index 6092e26..98e8085 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
@@ -2,21 +2,27 @@ package moe.nea.firmament.features.inventory.storageoverlay
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
-import net.minecraft.client.MinecraftClient
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
-import net.minecraft.entity.player.PlayerInventory
-import net.minecraft.screen.slot.Slot
+import net.minecraft.client.Minecraft
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.inventory.ContainerScreen
+import net.minecraft.client.input.CharacterEvent
+import net.minecraft.client.input.KeyEvent
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.inventory.Slot
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+import moe.nea.firmament.util.accessors.castAccessor
import moe.nea.firmament.util.customgui.CustomGui
+import moe.nea.firmament.util.focusedItemStack
class StorageOverlayCustom(
- val handler: StorageBackingHandle,
- val screen: GenericContainerScreen,
- val overview: StorageOverlayScreen,
+ val handler: StorageBackingHandle,
+ val screen: ContainerScreen,
+ val overview: StorageOverlayScreen,
) : CustomGui() {
override fun onVoluntaryExit(): Boolean {
overview.isExiting = true
+ StorageOverlayScreen.resetScroll()
return super.onVoluntaryExit()
}
@@ -24,20 +30,20 @@ class StorageOverlayCustom(
return overview.getBounds()
}
- override fun afterSlotRender(context: DrawContext, slot: Slot) {
- if (slot.inventory !is PlayerInventory)
+ override fun afterSlotRender(context: GuiGraphics, slot: Slot) {
+ if (slot.container !is Inventory)
context.disableScissor()
}
- override fun beforeSlotRender(context: DrawContext, slot: Slot) {
- if (slot.inventory !is PlayerInventory)
+ override fun beforeSlotRender(context: GuiGraphics, slot: Slot) {
+ if (slot.container !is Inventory)
overview.createScissors(context)
}
override fun onInit() {
- overview.init(MinecraftClient.getInstance(), screen.width, screen.height)
+ overview.init(Minecraft.getInstance(), screen.width, screen.height)
overview.init()
- screen as AccessorHandledScreen
+ screen.castAccessor()
screen.x_Firmament = overview.measurements.x
screen.y_Firmament = overview.measurements.y
screen.backgroundWidth_Firmament = overview.measurements.totalWidth
@@ -47,7 +53,7 @@ class StorageOverlayCustom(
override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY))
return false
- if (slot.inventory !is PlayerInventory) {
+ if (slot.container !is Inventory) {
if (!overview.getScrollPanelInner().contains(pointX, pointY))
return false
}
@@ -58,48 +64,50 @@ class StorageOverlayCustom(
return false
}
- override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
- return overview.mouseReleased(mouseX, mouseY, button)
+ override fun mouseReleased(click: MouseButtonEvent): Boolean {
+ return overview.mouseReleased(click)
}
- override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
- return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
+ override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean {
+ return overview.mouseDragged(click, offsetX, offsetY)
}
- override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- return overview.keyReleased(keyCode, scanCode, modifiers)
+ override fun keyReleased(input: KeyEvent): Boolean {
+ return overview.keyReleased(input)
}
- override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- return overview.keyPressed(keyCode, scanCode, modifiers)
+ override fun keyPressed(input: KeyEvent): Boolean {
+ return overview.keyPressed(input)
}
- override fun charTyped(chr: Char, modifiers: Int): Boolean {
- return overview.charTyped(chr, modifiers)
+ override fun charTyped(input: CharacterEvent): Boolean {
+ return overview.charTyped(input)
}
- override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
- return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot)
+ override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean {
+ return overview.mouseClicked(click, doubled, (handler as? StorageBackingHandle.Page)?.storagePageSlot)
}
- override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) {
+ override fun render(drawContext: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) {
overview.drawBackgrounds(drawContext)
- overview.drawPages(drawContext,
- mouseX,
- mouseY,
- delta,
- (handler as? StorageBackingHandle.Page)?.storagePageSlot,
- screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9),
- Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament))
+ overview.drawPages(
+ drawContext,
+ mouseX,
+ mouseY,
+ delta,
+ (handler as? StorageBackingHandle.Page)?.storagePageSlot,
+ screen.menu.slots.take(screen.menu.rowCount * 9).drop(9),
+ Point((screen.castAccessor()).x_Firmament, screen.y_Firmament)
+ )
overview.drawScrollBar(drawContext)
overview.drawControls(drawContext, mouseX, mouseY)
}
override fun moveSlot(slot: Slot) {
- val index = slot.index
+ val index = slot.containerSlot
if (index in 0..<36) {
val (x, y) = overview.getPlayerInventorySlotPosition(index)
- slot.x = x - (screen as AccessorHandledScreen).x_Firmament
+ slot.x = x - (screen.castAccessor()).x_Firmament
slot.y = y - screen.y_Firmament
} else {
slot.x = -100000
@@ -113,6 +121,8 @@ class StorageOverlayCustom(
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
+ if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling)
+ return false
return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
index cf1cf1d..d56e4d2 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -13,13 +13,17 @@ import io.github.notenoughupdates.moulconfig.observer.Property
import java.util.TreeSet
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.item.ItemStack
-import net.minecraft.screen.slot.Slot
-import net.minecraft.text.Text
-import net.minecraft.util.Identifier
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.input.CharacterEvent
+import net.minecraft.client.input.KeyEvent
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.inventory.Slot
+import net.minecraft.network.chat.Component
+import net.minecraft.ChatFormatting
+import net.minecraft.resources.ResourceLocation
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.gui.EmptyComponent
import moe.nea.firmament.gui.FirmButtonComponent
@@ -35,10 +39,11 @@ import moe.nea.firmament.util.mc.FakeSlot
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.render.enableScissorWithoutTranslation
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
-class StorageOverlayScreen : Screen(Text.literal("")) {
+class StorageOverlayScreen : Screen(Component.literal("")) {
companion object {
val PLAYER_WIDTH = 184
@@ -46,19 +51,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val PLAYER_Y_INSET = 3
val SLOT_SIZE = 18
val PADDING = 10
- val PAGE_WIDTH = SLOT_SIZE * 9
+ val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9
+ val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4
val HOTBAR_X = 12
val HOTBAR_Y = 67
val MAIN_INVENTORY_Y = 9
val SCROLL_BAR_WIDTH = 8
val SCROLL_BAR_HEIGHT = 16
+ val CONTROL_X_INSET = 3
+ val CONTROL_Y_INSET = 5
val CONTROL_WIDTH = 70
- val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + PLAYER_Y_INSET
- val CONTROL_HEIGHT = 100
+ val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1
+ val CONTROL_HEIGHT = 50
+
+ var scroll: Float = 0F
+ var lastRenderedInnerHeight = 0
+
+ fun resetScroll() {
+ if (!StorageOverlay.TConfig.retainScroll) scroll = 0F
+ }
}
var isExiting: Boolean = false
- var scroll: Float = 0F
var pageWidthCount = StorageOverlay.TConfig.columns
inner class Measurements {
@@ -67,20 +81,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val x = width / 2 - overviewWidth / 2
val overviewHeight = minOf(
height - PLAYER_HEIGHT - minOf(80, height / 10),
- StorageOverlay.TConfig.height)
+ StorageOverlay.TConfig.height
+ )
val innerScrollPanelHeight = overviewHeight - PADDING * 2
val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
val playerX = width / 2 - PLAYER_WIDTH / 2
val playerY = y + overviewHeight - PLAYER_Y_INSET
- val controlX = x - CONTROL_WIDTH
- val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2
+ val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET
+ val controlY = playerY - CONTROL_Y_INSET
val totalWidth = overviewWidth
val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT
}
var measurements = Measurements()
- var lastRenderedInnerHeight = 0
public override fun init() {
super.init()
pageWidthCount = StorageOverlay.TConfig.columns
@@ -99,6 +113,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat())
return true
}
+
fun coerceScroll(offset: Float) {
scroll = (scroll + offset)
.coerceAtMost(getMaxScroll())
@@ -107,19 +122,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height
- val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory")
- val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background")
- val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row")
- val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background")
- val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob")
- val controllerBackground = Identifier.of("firmament:storageoverlay/storage_controls")
+ val playerInventorySprite = ResourceLocation.parse("firmament:storageoverlay/player_inventory")
+ val upperBackgroundSprite = ResourceLocation.parse("firmament:storageoverlay/upper_background")
+ val slotRowSprite = ResourceLocation.parse("firmament:storageoverlay/storage_row")
+ val scrollbarBackground = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_background")
+ val scrollbarKnob = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_knob")
+ val controllerBackground = ResourceLocation.parse("firmament:storageoverlay/storage_controls")
- override fun close() {
+ override fun onClose() {
isExiting = true
- super.close()
+ resetScroll()
+ super.onClose()
}
- override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
drawBackgrounds(context)
drawPages(context, mouseX, mouseY, delta, null, null, Point())
@@ -132,7 +148,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
return scroll / getMaxScroll()
}
- fun drawScrollBar(context: DrawContext) {
+ fun drawScrollBar(context: GuiGraphics) {
val sbRect = getScrollBarRect()
context.drawGuiTexture(
scrollbarBackground,
@@ -148,21 +164,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
fun editPages() {
isExiting = true
- val hs = MC.screen as? HandledScreen<*>
- if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
- hs.customGui = null
- } else {
- MC.sendCommand("storage")
+ MC.instance.schedule {
+ val hs = MC.screen as? AbstractContainerScreen<*>
+ if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
+ hs.customGui = null
+ hs.init(MC.instance, width, height)
+ } else {
+ MC.sendCommand("storage")
+ }
}
}
val guiContext = GuiContext(EmptyComponent())
private val knobStub = EmptyComponent()
- val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages)
+ val editButton = FirmButtonComponent(
+ TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string),
+ action = ::editPages
+ )
val searchText = Property.of("") // TODO: sync with REI
- val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true),
- tr("firmament.storage-overlay.search.suggestion", "Search...").string,
- IMinecraft.instance.defaultFontRenderer)
+ val searchField = TextFieldComponent(
+ searchText, 100, GetSetter.constant(true),
+ tr("firmament.storage-overlay.search.suggestion", "Search...").string,
+ IMinecraft.INSTANCE.defaultFontRenderer
+ )
val controlComponent = PanelComponent(
ColumnComponent(
searchField,
@@ -180,30 +204,36 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
guiContext.adopt(controlComponent)
}
- fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) {
+ fun drawControls(context: GuiGraphics, mouseX: Int, mouseY: Int) {
context.drawGuiTexture(
controllerBackground,
measurements.controlX,
measurements.controlY,
- CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT)
+ CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT
+ )
context.drawMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX, mouseY)
+ mouseX, mouseY
+ )
}
- fun drawBackgrounds(context: DrawContext) {
- context.drawGuiTexture(upperBackgroundSprite,
- measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight)
- context.drawGuiTexture(playerInventorySprite,
- measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT)
+ fun drawBackgrounds(context: GuiGraphics) {
+ context.drawGuiTexture(
+ upperBackgroundSprite,
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ )
+ context.drawGuiTexture(
+ playerInventorySprite,
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ )
}
fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
@@ -216,55 +246,63 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
)
}
- fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
- val items = MC.player?.inventory?.main ?: return
+ fun drawPlayerInventory(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
+ val items = MC.player?.inventory?.nonEquipmentItems ?: return
items.withIndex().forEach { (index, item) ->
val (x, y) = getPlayerInventorySlotPosition(index)
- context.drawItem(item, x, y, 0)
- context.drawStackOverlay(textRenderer, item, x, y)
+ context.renderItem(item, x, y, 0)
+ context.renderItemDecorations(font, item, x, y)
}
}
fun getScrollBarRect(): Rectangle {
- return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
- measurements.y + PADDING,
- SCROLL_BAR_WIDTH,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
+ measurements.y + PADDING,
+ SCROLL_BAR_WIDTH,
+ measurements.innerScrollPanelHeight
+ )
}
fun getScrollPanelInner(): Rectangle {
- return Rectangle(measurements.x + PADDING,
- measurements.y + PADDING,
- measurements.innerScrollPanelWidth,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING,
+ measurements.y + PADDING,
+ measurements.innerScrollPanelWidth,
+ measurements.innerScrollPanelHeight
+ )
}
- fun createScissors(context: DrawContext) {
+ fun createScissors(context: GuiGraphics) {
val rect = getScrollPanelInner()
- context.enableScissor(
- rect.minX, rect.minY,
- rect.maxX, rect.maxY
+ context.enableScissorWithoutTranslation(
+ rect.minX.toFloat(), rect.minY.toFloat(),
+ rect.maxX.toFloat(), rect.maxY.toFloat(),
)
}
fun drawPages(
- context: DrawContext, mouseX: Int, mouseY: Int, delta: Float,
- excluding: StoragePageSlot?,
- slots: List<Slot>?,
- slotOffset: Point
+ context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float,
+ excluding: StoragePageSlot?,
+ slots: List<Slot>?,
+ slotOffset: Point
) {
createScissors(context)
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, inventory ->
- drawPage(context,
- rect.x,
- rect.y,
- page, inventory,
- if (excluding == page) slots else null,
- slotOffset
+ drawPage(
+ context,
+ rect.x,
+ rect.y,
+ page, inventory,
+ if (excluding == page) slots else null,
+ slotOffset,
+ mouseX,
+ mouseY
)
}
context.disableScissor()
+
}
@@ -272,40 +310,45 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
get() = guiContext.focusedElement == knobStub
set(value) = knobStub.setFocus(value)
- override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
- return mouseClicked(mouseX, mouseY, button, null)
+ override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean {
+ return mouseClicked(click, doubled, null)
}
- override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ override fun mouseReleased(click: MouseButtonEvent): Boolean {
if (knobGrabbed) {
knobGrabbed = false
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, false))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ click.x.toInt(), click.y.toInt(),
+ MouseEvent.Click(click.button(), false)
+ )
) return true
- return super.mouseReleased(mouseX, mouseY, button)
+ return super.mouseReleased(click)
}
- override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
+ override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean {
if (knobGrabbed) {
val sbRect = getScrollBarRect()
- val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight()
+ val percentage = (click.x - sbRect.getY()) / sbRect.getHeight()
scroll = (getMaxScroll() * percentage).toFloat()
mouseScrolled(0.0, 0.0, 0.0, 0.0)
return true
}
- return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
+ return super.mouseDragged(click, offsetX, offsetY)
}
- fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean {
+ fun mouseClicked(click: MouseButtonEvent, doubled: Boolean, activePage: StoragePageSlot?): Boolean {
+ guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components.
+ val mouseX = click.x
+ val mouseY = click.y
if (getScrollPanelInner().contains(mouseX, mouseY)) {
- val data = StorageOverlay.Data.data ?: StorageData()
+ val data = StorageOverlay.Data.data
layoutedForEach(data) { rect, page, _ ->
- if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) {
+ if (rect.contains(mouseX, mouseY) && activePage != page && click.button() == 0) {
page.navigateTo()
return true
}
@@ -320,52 +363,58 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
knobGrabbed = true
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, true))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ mouseX.toInt(), mouseY.toInt(),
+ MouseEvent.Click(click.button(), true)
+ )
) return true
return false
}
- override fun charTyped(chr: Char, modifiers: Int): Boolean {
+ override fun charTyped(input: CharacterEvent): Boolean {
if (typeMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- KeyboardEvent.CharTyped(chr)
+ KeyboardEvent.CharTyped(input.codepointAsString().first()) // TODO: i dont like this .first()
)
) {
return true
}
- return super.charTyped(chr, modifiers)
+ return super.charTyped(input)
}
- override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ override fun keyReleased(input: KeyEvent): Boolean {
if (typeMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- KeyboardEvent.KeyPressed(keyCode, false)
+ KeyboardEvent.KeyPressed(input.input(), input.scancode, false)
)
) {
return true
}
- return super.keyReleased(keyCode, scanCode, modifiers)
+ return super.keyReleased(input)
+ }
+
+ override fun shouldCloseOnEsc(): Boolean {
+ return this === MC.screen // Fixes this UI closing the handled screen on Escape press.
}
- override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ override fun keyPressed(input: KeyEvent): Boolean {
if (typeMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- KeyboardEvent.KeyPressed(keyCode, true)
+ KeyboardEvent.KeyPressed(input.input(), input.scancode, true)
)
) {
return true
}
- return super.keyPressed(keyCode, scanCode, modifiers)
+ return super.keyPressed(input)
}
@@ -414,7 +463,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val filter = getFilteredPages()
for ((page, inventory) in data.storageInventories.entries) {
if (page !in filter) continue
- val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
+ val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + font.lineHeight }
?: 18
maxHeight = maxOf(maxHeight, currentHeight)
val rect = Rectangle(
@@ -435,61 +484,102 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun drawPage(
- context: DrawContext,
- x: Int,
- y: Int,
- page: StoragePageSlot,
- inventory: StorageData.StorageInventory,
- slots: List<Slot>?,
- slotOffset: Point,
+ context: GuiGraphics,
+ x: Int,
+ y: Int,
+ page: StoragePageSlot,
+ inventory: StorageData.StorageInventory,
+ slots: List<Slot>?,
+ slotOffset: Point,
+ mouseX: Int,
+ mouseY: Int,
): Int {
val inv = inventory.inventory
if (inv == null) {
context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
- context.drawText(textRenderer,
- Text.literal("TODO: open this page"),
- x + 4,
- y + 4,
- -1,
- true)
+ context.drawString(
+ font,
+ Component.literal("TODO: open this page"),
+ x + 4,
+ y + 4,
+ -1,
+ true
+ )
return 18
}
assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
- val name = page.defaultName()
- context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
- if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
- context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
+ val name = inventory.title
+ val pageHeight = inv.rows * SLOT_SIZE + 8 + font.lineHeight
+ if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage)
+ context.submitOutline(
+ x,
+ y + 3 + font.lineHeight,
+ PAGE_WIDTH,
+ inv.rows * SLOT_SIZE + 4,
+ StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB()
+ )
+ context.drawString(
+ font, Component.literal(name), x + 6, y + 3,
+ if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true
+ )
+ context.drawGuiTexture(
+ slotRowSprite,
+ x + 2,
+ y + 5 + font.lineHeight,
+ PAGE_SLOTS_WIDTH,
+ inv.rows * SLOT_SIZE
+ )
+ val scrollPanel = getScrollPanelInner()
inv.stacks.forEachIndexed { index, stack ->
- val slotX = (index % 9) * SLOT_SIZE + x + 1
- val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
- val fakeSlot = FakeSlot(stack, slotX, slotY)
+ val slotX = (index % 9) * SLOT_SIZE + x + 3
+ val slotY = (index / 9) * SLOT_SIZE + y + 5 + font.lineHeight + 1
if (slots == null) {
+ val fakeSlot = FakeSlot(stack, slotX, slotY)
SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot))
- context.drawItem(stack, slotX, slotY)
- context.drawStackOverlay(textRenderer, stack, slotX, slotY)
+ context.renderItem(stack, slotX, slotY)
+ context.renderItemDecorations(font, stack, slotX, slotY)
SlotRenderEvents.After.publish(SlotRenderEvents.After(context, fakeSlot))
+ if (StorageOverlay.TConfig.showInactivePageTooltips && !stack.isEmpty &&
+ mouseX >= slotX && mouseY >= slotY &&
+ mouseX <= slotX + 16 && mouseY <= slotY + 16 &&
+ scrollPanel.contains(mouseX, mouseY)) {
+ try {
+ context.setTooltipForNextFrame(font, stack, mouseX, mouseY)
+ } catch (e: IllegalStateException) {
+ context.setComponentTooltipForNextFrame(font, listOf(Component.nullToEmpty(ChatFormatting.RED.toString() +
+ "Error Getting Tooltip!"), Component.nullToEmpty(ChatFormatting.YELLOW.toString() +
+ "Open page to fix" + ChatFormatting.RESET)), mouseX, mouseY)
+ }
+ }
} else {
val slot = slots[index]
slot.x = slotX - slotOffset.x
slot.y = slotY - slotOffset.y
}
}
- return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
+ return pageHeight + 6
}
fun getBounds(): List<Rectangle> {
return listOf(
- Rectangle(measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight),
- Rectangle(measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT),
- Rectangle(measurements.controlX,
- measurements.controlY,
- CONTROL_WIDTH,
- CONTROL_HEIGHT))
+ Rectangle(
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ ),
+ Rectangle(
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ ),
+ Rectangle(
+ measurements.controlX,
+ measurements.controlY,
+ CONTROL_WIDTH,
+ CONTROL_HEIGHT
+ )
+ )
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
index 9112fab..3c40fc6 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
@@ -4,17 +4,19 @@ package moe.nea.firmament.features.inventory.storageoverlay
import org.lwjgl.glfw.GLFW
import kotlin.math.max
-import net.minecraft.block.Blocks
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.item.Item
-import net.minecraft.item.Items
-import net.minecraft.text.Text
-import net.minecraft.util.DyeColor
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.input.KeyEvent
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.Items
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.DyeColor
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.toShedaniel
-class StorageOverviewScreen() : Screen(Text.empty()) {
+class StorageOverviewScreen() : Screen(Component.empty()) {
companion object {
val emptyStorageSlotItems = listOf<Item>(
Blocks.RED_STAINED_GLASS_PANE.asItem(),
@@ -22,39 +24,49 @@ class StorageOverviewScreen() : Screen(Text.empty()) {
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
+
+ var scroll = 0
+ var lastRenderedHeight = 0
}
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
- var scroll = 0
- var lastRenderedHeight = 0
+ override fun init() {
+ super.init()
+ scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0)
+ }
+
+ override fun onClose() {
+ if (!StorageOverlay.TConfig.retainScroll) scroll = 0
+ super.onClose()
+ }
- override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
- context.matrices.push()
- context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
+ context.pose().pushMatrix()
+ context.pose().translate(offsetX.toFloat(), offsetY.toFloat())
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
- context.matrices.pop()
+ context.pose().popMatrix()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
- var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
+ var currentMaxHeight = StorageOverlay.TConfig.margin - StorageOverlay.TConfig.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
- val pageX = (index % StorageOverlay.config.columns)
+ val pageX = (index % StorageOverlay.TConfig.columns)
if (pageX == 0) {
- currentMaxHeight += StorageOverlay.config.padding
+ currentMaxHeight += StorageOverlay.TConfig.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
- width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
+ width / 2 - (StorageOverlay.TConfig.columns * (pageWidth + StorageOverlay.TConfig.padding) - StorageOverlay.TConfig.padding) / 2 + pageX * (pageWidth + StorageOverlay.TConfig.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
@@ -62,22 +74,22 @@ class StorageOverviewScreen() : Screen(Text.empty()) {
lastRenderedHeight = totalHeight + currentMaxHeight
}
- override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean {
layoutedForEach { (k, p), x, y ->
- val rx = mouseX - x
- val ry = mouseY - y
+ val rx = click.x - x
+ val ry = click.y - y
if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
- close()
+ onClose()
StorageOverlay.lastStorageOverlay = this
k.navigateTo()
return true
}
}
- return super.mouseClicked(mouseX, mouseY, button)
+ return super.mouseClicked(click, doubled)
}
fun getStorePageHeight(page: StorageData.StorageInventory): Int {
- return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
+ return page.inventory?.rows?.let { it * 19 + MC.font.lineHeight + 2 } ?: 60
}
override fun mouseScrolled(
@@ -88,36 +100,38 @@ class StorageOverviewScreen() : Screen(Text.empty()) {
): Boolean {
scroll =
(scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt()
- .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
+ .coerceAtMost(getMaxScroll()).coerceAtLeast(0)
return true
}
- private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
- context.drawText(MC.font, page.title, 2, 2, -1, true)
+ private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.TConfig.margin
+
+ private fun renderStoragePage(context: GuiGraphics, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
+ context.drawString(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
if (inventory == null) {
// TODO: Missing texture
context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
- context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
+ context.drawCenteredString(MC.font, Component.literal("Not loaded yet"), pageWidth / 2, 30, -1)
return
}
for ((index, stack) in inventory.stacks.withIndex()) {
val x = (index % 9) * 19
- val y = (index / 9) * 19 + MC.font.fontHeight + 2
+ val y = (index / 9) * 19 + MC.font.lineHeight + 2
if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
} else {
context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
}
- context.drawItem(stack, x + 1, y + 1)
- context.drawStackOverlay(MC.font, stack, x + 1, y + 1)
+ context.renderItem(stack, x + 1, y + 1)
+ context.renderItemDecorations(MC.font, stack, x + 1, y + 1)
}
}
- override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- if (keyCode == GLFW.GLFW_KEY_ESCAPE)
+ override fun keyPressed(input: KeyEvent): Boolean {
+ if (input.input() == GLFW.GLFW_KEY_ESCAPE)
isClosing = true
- return super.keyPressed(keyCode, scanCode, modifiers)
+ return super.keyPressed(input)
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index 3b86184..69d686f 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
@@ -1,9 +1,10 @@
package moe.nea.firmament.features.inventory.storageoverlay
-import io.ktor.util.decodeBase64Bytes
-import io.ktor.util.encodeBase64
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
+import java.util.Base64
+import java.util.concurrent.CompletableFuture
+import kotlinx.coroutines.async
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
@@ -11,13 +12,16 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-import net.minecraft.item.ItemStack
-import net.minecraft.nbt.NbtCompound
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.world.item.ItemStack
+import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtIo
-import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.ListTag
import net.minecraft.nbt.NbtOps
-import net.minecraft.nbt.NbtSizeTracker
-import net.minecraft.registry.RegistryOps
+import net.minecraft.nbt.NbtAccounter
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.features.inventory.storageoverlay.VirtualInventory.Serializer.writeToByteArray
+import moe.nea.firmament.util.Base64Util
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.mc.TolerantRegistriesOps
@@ -28,6 +32,10 @@ data class VirtualInventory(
) {
val rows = stacks.size / 9
+ val serializationCache = CompletableFuture.supplyAsync {
+ writeToByteArray(this)
+ }
+
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
@@ -35,41 +43,47 @@ data class VirtualInventory(
object Serializer : KSerializer<VirtualInventory> {
+ fun writeToByteArray(value: VirtualInventory): ByteArray {
+ val list = ListTag()
+ val ops = getOps()
+ value.stacks.forEach {
+ if (it.isEmpty) list.add(CompoundTag())
+ else list.add(ErrorUtil.catch("Could not serialize item") {
+ ItemStack.CODEC.encode(
+ it,
+ ops,
+ CompoundTag()
+ ).orThrow
+ }
+ .or { CompoundTag() })
+ }
+ val baos = ByteArrayOutputStream()
+ NbtIo.writeCompressed(CompoundTag().also { it.put(INVENTORY, list) }, baos)
+ return baos.toByteArray()
+ }
+
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
- val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
- val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
+ val n = NbtIo.readCompressed(ByteArrayInputStream(Base64Util.decodeBytes(s)), NbtAccounter.create(100_000_000))
+ val items = n.getList(INVENTORY).getOrNull()
val ops = getOps()
- return VirtualInventory(items.map {
- it as NbtCompound
+ return VirtualInventory(items?.map {
+ it as CompoundTag
if (it.isEmpty) ItemStack.EMPTY
else ErrorUtil.catch("Could not deserialize item") {
ItemStack.CODEC.parse(ops, it).orThrow
}.or { ItemStack.EMPTY }
- })
+ } ?: listOf())
}
- fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
+ fun getOps() = MC.currentOrDefaultRegistryNbtOps
override fun serialize(encoder: Encoder, value: VirtualInventory) {
- val list = NbtList()
- val ops = getOps()
- value.stacks.forEach {
- if (it.isEmpty) list.add(NbtCompound())
- else list.add(ErrorUtil.catch("Could not serialize item") {
- ItemStack.CODEC.encode(it,
- ops,
- NbtCompound()).orThrow
- }
- .or { NbtCompound() })
- }
- val baos = ByteArrayOutputStream()
- NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
- encoder.encodeString(baos.toByteArray().encodeBase64())
+ encoder.encodeString(Base64Util.encodeToString(value.serializationCache.get()))
}
}
}
diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt
new file mode 100644
index 0000000..cc58f8a
--- /dev/null
+++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt
@@ -0,0 +1,139 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import java.util.LinkedList
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.HitResult
+import net.minecraft.core.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object BlockZapperOverlay {
+ val identifier: String
+ get() = "block-zapper-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var blockZapperOverlay by toggle("block-zapper-overlay") { false }
+ val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) }
+ var undoKey by keyBindingWithDefaultUnbound("undo-key")
+ }
+
+ val bannedZapper: List<Block> = listOf<Block>(
+ Blocks.WHEAT,
+ Blocks.CARROTS,
+ Blocks.POTATOES,
+ Blocks.PUMPKIN,
+ Blocks.PUMPKIN_STEM,
+ Blocks.MELON,
+ Blocks.MELON_STEM,
+ Blocks.CACTUS,
+ Blocks.SUGAR_CANE,
+ Blocks.NETHER_WART,
+ Blocks.TALL_GRASS,
+ Blocks.SUNFLOWER,
+ Blocks.FARMLAND,
+ Blocks.BREWING_STAND,
+ Blocks.SNOW,
+ Blocks.RED_MUSHROOM,
+ Blocks.BROWN_MUSHROOM,
+ )
+
+ private val zapperOffsets: List<BlockPos> = listOf(
+ BlockPos(0, 0, -1),
+ BlockPos(0, 0, 1),
+ BlockPos(-1, 0, 0),
+ BlockPos(1, 0, 0),
+ BlockPos(0, 1, 0),
+ BlockPos(0, -1, 0)
+ )
+
+ // Skidded from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified)
+ @Subscribe
+ fun renderBlockZapperOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.blockZapperOverlay) return
+ val player = MC.player ?: return
+ val world = player.level ?: return
+ val heldItem = MC.stackInHand
+ if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
+ val hitResult = MC.instance.hitResult ?: return
+
+ val zapperBlocks: HashSet<BlockPos> = HashSet()
+ val returnablePositions = LinkedList<BlockPos>()
+
+ if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) {
+ var pos: BlockPos = hitResult.blockPos
+ val firstBlockState: BlockState = world.getBlockState(pos)
+ val block = firstBlockState.block
+
+ val initialAboveBlock = world.getBlockState(pos.above()).block
+ if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) {
+ var i = 0
+ while (i < 164) {
+ zapperBlocks.add(pos)
+ returnablePositions.remove(pos)
+
+ val availableNeighbors: MutableList<BlockPos> = ArrayList()
+
+ for (offset in zapperOffsets) {
+ val newPos = pos.offset(offset)
+
+ if (zapperBlocks.contains(newPos)) continue
+
+ val state: BlockState? = world.getBlockState(newPos)
+ if (state != null && state.block === block) {
+ val above = newPos.above()
+ val aboveBlock = world.getBlockState(above).block
+ if (!bannedZapper.contains(aboveBlock)) {
+ availableNeighbors.add(newPos)
+ }
+ }
+ }
+
+ if (availableNeighbors.size >= 2) {
+ returnablePositions.add(pos)
+ pos = availableNeighbors[0]
+ } else if (availableNeighbors.size == 1) {
+ pos = availableNeighbors[0]
+ } else if (returnablePositions.isEmpty()) {
+ break
+ } else {
+ i--
+ pos = returnablePositions.last()
+ }
+
+ i++
+ }
+ }
+
+ RenderInWorldContext.renderInWorld(event) {
+ if (MC.player?.isShiftKeyDown ?: false) {
+ zapperBlocks.forEach {
+ block(it, TConfig.color.getEffectiveColourRGB())
+ }
+ } else {
+ sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB())
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldKeyboard(it: WorldKeyboardEvent) {
+ if (!TConfig.undoKey.isBound) return
+ if (!it.matches(TConfig.undoKey)) return
+ if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
+ MC.sendCommand("undozap")
+ }
+}
diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt
new file mode 100644
index 0000000..3f16922
--- /dev/null
+++ b/src/main/kotlin/features/items/BonemerangOverlay.kt
@@ -0,0 +1,94 @@
+package moe.nea.firmament.features.items
+
+import me.shedaniel.math.Color
+import org.joml.Vector2i
+import net.minecraft.world.entity.LivingEntity
+import net.minecraft.world.entity.decoration.ArmorStand
+import net.minecraft.world.entity.player.Player
+import net.minecraft.ChatFormatting
+import net.minecraft.world.phys.AABB
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object BonemerangOverlay {
+ val identifier: String
+ get() = "bonemerang-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var bonemerangOverlay by toggle("bonemerang-overlay") { false }
+ val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() }
+ var highlightHitEntities by toggle("highlight-hit-entities") { false }
+ }
+
+ fun getEntities(): MutableSet<LivingEntity> {
+ val entities = mutableSetOf<LivingEntity>()
+ val camera = MC.camera as? Player ?: return entities
+ val player = MC.player ?: return entities
+ val world = player.level ?: return entities
+
+ val cameraPos = camera.eyePosition
+ val rayDirection = camera.lookAngle.normalize()
+ val endPos = cameraPos.add(rayDirection.scale(15.0))
+ val foundEntities = world.getEntities(camera, AABB(cameraPos, endPos).inflate(1.0))
+
+ for (entity in foundEntities) {
+ if (entity !is LivingEntity || entity is ArmorStand || entity.isInvisible) continue
+ val hitResult = entity.boundingBox.inflate(0.35).clip(cameraPos, endPos).orElse(null)
+ if (hitResult != null) entities.add(entity)
+ }
+
+ return entities
+ }
+
+
+ val throwableWeapons = listOf(
+ SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG,
+ SkyBlockItems.TRIBAL_SPEAR,
+ )
+
+
+ @Subscribe
+ fun onEntityRender(event: EntityRenderTintEvent) {
+ if (!TConfig.highlightHitEntities) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+ if (entities.isEmpty()) return
+ if (event.entity !in entities) return
+
+ val tintOverlay by lazy {
+ TintedOverlayTexture().setColor(Color.ofOpaque(ChatFormatting.BLUE.color!!))
+ }
+
+ event.renderState.overlayTexture_firmament = tintOverlay
+ }
+
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.bonemerangOverlay) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+
+ it.context.pose().pushMatrix()
+ TConfig.bonemerangOverlayHud.applyTransformations(it.context.pose())
+ it.context.drawString(
+ MC.font, String.format(
+ tr(
+ "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s"
+ ).string, entities.size
+ ), 0, 0, -1, true
+ )
+ it.context.pose().popMatrix()
+ }
+}
diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt
new file mode 100644
index 0000000..a59fcbd
--- /dev/null
+++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt
@@ -0,0 +1,234 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.core.Holder
+import net.minecraft.tags.BlockTags
+import net.minecraft.tags.TagKey
+import net.minecraft.network.chat.Component
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.HitResult
+import net.minecraft.core.BlockPos
+import net.minecraft.world.phys.Vec3
+import net.minecraft.world.phys.shapes.Shapes
+import net.minecraft.world.level.BlockGetter
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object EtherwarpOverlay {
+ val identifier: String
+ get() = "etherwarp-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var etherwarpOverlay by toggle("etherwarp-overlay") { false }
+ var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true }
+ var cube by toggle("cube") { true }
+ val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) }
+ val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) }
+ val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) }
+ val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) }
+ var wireframe by toggle("wireframe") { false }
+ var failureText by toggle("failure-text") { false }
+ }
+
+ enum class EtherwarpResult(val label: Component?, val color: () -> ChromaColour) {
+ SUCCESS(null, TConfig::cubeColour),
+ INTERACTION_BLOCKED(
+ tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"),
+ TConfig::tooCloseCubeColour
+ ),
+ TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour),
+ OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour),
+ }
+
+ val interactionBlocked = Checker(
+ setOf(
+ Blocks.HOPPER,
+ Blocks.CHEST,
+ Blocks.ENDER_CHEST,
+ Blocks.FURNACE,
+ Blocks.CRAFTING_TABLE,
+ Blocks.CAULDRON,
+ Blocks.WATER_CAULDRON,
+ Blocks.ENCHANTING_TABLE,
+ Blocks.DISPENSER,
+ Blocks.DROPPER,
+ Blocks.BREWING_STAND,
+ Blocks.TRAPPED_CHEST,
+ Blocks.LEVER,
+ ),
+ setOf(
+ BlockTags.DOORS,
+ BlockTags.TRAPDOORS,
+ BlockTags.ANVIL,
+ BlockTags.FENCE_GATES,
+ )
+ )
+
+ data class Checker<T>(
+ val direct: Set<T>,
+ val byTag: Set<TagKey<T>>,
+ ) {
+ fun matches(entry: Holder<T>): Boolean {
+ return entry.value() in direct || checkTags(entry, byTag)
+ }
+ }
+
+ val etherwarpHallpasses = Checker(
+ setOf(
+ Blocks.CREEPER_HEAD,
+ Blocks.CREEPER_WALL_HEAD,
+ Blocks.DRAGON_HEAD,
+ Blocks.DRAGON_WALL_HEAD,
+ Blocks.SKELETON_SKULL,
+ Blocks.SKELETON_WALL_SKULL,
+ Blocks.WITHER_SKELETON_SKULL,
+ Blocks.WITHER_SKELETON_WALL_SKULL,
+ Blocks.PIGLIN_HEAD,
+ Blocks.PIGLIN_WALL_HEAD,
+ Blocks.ZOMBIE_HEAD,
+ Blocks.ZOMBIE_WALL_HEAD,
+ Blocks.PLAYER_HEAD,
+ Blocks.PLAYER_WALL_HEAD,
+ Blocks.REPEATER,
+ Blocks.COMPARATOR,
+ Blocks.BIG_DRIPLEAF_STEM,
+ Blocks.MOSS_CARPET,
+ Blocks.PALE_MOSS_CARPET,
+ Blocks.COCOA,
+ Blocks.LADDER,
+ Blocks.SEA_PICKLE,
+ ),
+ setOf(
+ BlockTags.FLOWER_POTS,
+ BlockTags.WOOL_CARPETS,
+ ),
+ )
+ val etherwarpConsidersFat = Checker(
+ setOf(), setOf(
+ // Wall signs have a hitbox
+ BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS,
+ BlockTags.BANNERS,
+ )
+ )
+
+
+ fun <T> checkTags(holder: Holder<out T>, set: Set<TagKey<out T>>) =
+ holder.tags()
+ .anyMatch(set::contains)
+
+
+ fun isEtherwarpTransparent(world: BlockGetter, blockPos: BlockPos): Boolean {
+ val blockState = world.getBlockState(blockPos)
+ val block = blockState.block
+ if (etherwarpConsidersFat.matches(blockState.blockHolder))
+ return false
+ if (block.defaultBlockState().getCollisionShape(world, blockPos).isEmpty)
+ return true
+ if (etherwarpHallpasses.matches(blockState.blockHolder))
+ return true
+ return false
+ }
+
+ sealed interface EtherwarpBlockHit {
+ data class BlockHit(val blockPos: BlockPos, val accuratePos: Vec3?) : EtherwarpBlockHit
+ data object Miss : EtherwarpBlockHit
+ }
+
+ fun raycastWithEtherwarpTransparency(world: BlockGetter, start: Vec3, end: Vec3): EtherwarpBlockHit {
+ return BlockGetter.traverseBlocks<EtherwarpBlockHit, Unit>(
+ start, end, Unit,
+ { _, blockPos ->
+ if (isEtherwarpTransparent(world, blockPos)) {
+ return@traverseBlocks null
+ }
+// val defaultedState = world.getBlockState(blockPos).block.defaultState
+// val hitShape = defaultedState.getCollisionShape(
+// world,
+// blockPos,
+// ShapeContext.absent()
+// )
+// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) {
+// return@raycast null
+// }
+ val partialResult = world.clipWithInteractionOverride(start, end, blockPos, Shapes.block(), world.getBlockState(blockPos).block.defaultBlockState())
+ return@traverseBlocks EtherwarpBlockHit.BlockHit(blockPos, partialResult?.location)
+ },
+ { EtherwarpBlockHit.Miss })
+ }
+
+ enum class EtherwarpItemKind {
+ MERGED,
+ RAW
+ }
+
+ @Subscribe
+ fun renderEtherwarpOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.etherwarpOverlay) return
+ val player = MC.player ?: return
+ if (TConfig.onlyShowWhileSneaking && !player.isShiftKeyDown) return
+ val world = player.level
+ val heldItem = MC.stackInHand
+ val etherwarpTyp = run {
+ if (heldItem.extraAttributes.contains("ethermerge"))
+ EtherwarpItemKind.MERGED
+ else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT)
+ EtherwarpItemKind.RAW
+ else
+ return
+ }
+ val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21)
+ if (player.isShiftKeyDown || etherwarpTyp == EtherwarpItemKind.MERGED)
+ (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54)
+ else 1.62
+ val playerEyePos = player.position.add(0.0, playerEyeHeight, 0.0)
+ val start = playerEyePos
+ val end = player.getViewVector(0F).scale(160.0).add(playerEyePos)
+ val hitResult = raycastWithEtherwarpTransparency(
+ world,
+ start,
+ end,
+ )
+ if (hitResult !is EtherwarpBlockHit.BlockHit) return
+ val blockPos = hitResult.blockPos
+ val success = run {
+ if (!isEtherwarpTransparent(world, blockPos.above()))
+ EtherwarpResult.OCCUPIED
+ else if (!isEtherwarpTransparent(world, blockPos.above(2)))
+ EtherwarpResult.OCCUPIED
+ else if (playerEyePos.distanceToSqr(hitResult.accuratePos ?: blockPos.center) > 61 * 61)
+ EtherwarpResult.TOO_DISTANT
+ else if ((MC.instance.hitResult as? BlockHitResult)
+ ?.takeIf { it.type == HitResult.Type.BLOCK }
+ ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).blockHolder) }
+ ?: false
+ )
+ EtherwarpResult.INTERACTION_BLOCKED
+ else
+ EtherwarpResult.SUCCESS
+ }
+ RenderInWorldContext.renderInWorld(event) {
+ if (TConfig.cube)
+ block(
+ blockPos,
+ success.color().getEffectiveColourRGB()
+ )
+ if (TConfig.wireframe) wireframeCube(blockPos, 10f)
+ if (TConfig.failureText && success.label != null) {
+ withFacingThePlayer(blockPos.center) {
+ text(success.label)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/ArrowWidget.kt b/src/main/kotlin/features/items/recipes/ArrowWidget.kt
new file mode 100644
index 0000000..db0cf60
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ArrowWidget.kt
@@ -0,0 +1,38 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.renderer.RenderPipelines
+import net.minecraft.resources.ResourceLocation
+
+class ArrowWidget(override var position: Point) : RecipeWidget() {
+ override val size: Dimension
+ get() = Dimension(14, 14)
+
+ companion object {
+ val arrowSprite = ResourceLocation.withDefaultNamespace("container/furnace/lit_progress")
+ }
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ arrowSprite,
+ 14,
+ 14,
+ 0,
+ 0,
+ position.x,
+ position.y,
+ 14,
+ 14
+ )
+ }
+
+}
diff --git a/src/main/kotlin/features/items/recipes/ComponentWidget.kt b/src/main/kotlin/features/items/recipes/ComponentWidget.kt
new file mode 100644
index 0000000..08a2aa2
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ComponentWidget.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+import moe.nea.firmament.util.MC
+
+class ComponentWidget(override var position: Point, var text: Component) : RecipeWidget(), RecipeLayouter.Updater<Component> {
+ override fun update(newValue: Component) {
+ this.text = newValue
+ }
+
+ override val size: Dimension
+ get() = Dimension(MC.font.width(text), MC.font.lineHeight)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.drawString(MC.font, text, position.x, position.y, -1)
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/EntityWidget.kt b/src/main/kotlin/features/items/recipes/EntityWidget.kt
new file mode 100644
index 0000000..4a087e5
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/EntityWidget.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.world.entity.LivingEntity
+import moe.nea.firmament.gui.entity.EntityRenderer
+
+class EntityWidget(
+ override var position: Point,
+ override val size: Dimension,
+ val entity: LivingEntity
+) : RecipeWidget() {
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ EntityRenderer.renderEntity(
+ entity, guiGraphics,
+ rect.x, rect.y,
+ rect.width.toDouble(), rect.height.toDouble(),
+ mouseX.toDouble(), mouseY.toDouble()
+ )
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/FireWidget.kt b/src/main/kotlin/features/items/recipes/FireWidget.kt
new file mode 100644
index 0000000..565152b
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/FireWidget.kt
@@ -0,0 +1,19 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+
+class FireWidget(override var position: Point, val animationTicks: Int) : RecipeWidget() {
+ override val size: Dimension
+ get() = Dimension(10, 10)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/ItemList.kt b/src/main/kotlin/features/items/recipes/ItemList.kt
new file mode 100644
index 0000000..340e1c3
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ItemList.kt
@@ -0,0 +1,308 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.observer.Property
+import java.util.Optional
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.components.Renderable
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.client.gui.navigation.ScreenAxis
+import net.minecraft.client.gui.navigation.ScreenRectangle
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.input.MouseButtonInfo
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.api.v1.FirmamentAPI
+import moe.nea.firmament.events.HandledScreenClickEvent
+import moe.nea.firmament.events.HandledScreenForegroundEvent
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.accessors.castAccessor
+import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.skyblockId
+
+object ItemList {
+ // TODO: add a global toggle for this and RecipeRegistry
+
+ fun collectExclusions(screen: Screen): Set<ScreenRectangle> {
+ val exclusions = mutableSetOf<ScreenRectangle>()
+ if (screen is AbstractContainerScreen<*>) {
+ val screenHandler = screen.castAccessor()
+ exclusions.add(
+ ScreenRectangle(
+ screenHandler.x_Firmament,
+ screenHandler.y_Firmament,
+ screenHandler.backgroundWidth_Firmament,
+ screenHandler.backgroundHeight_Firmament
+ )
+ )
+ }
+ FirmamentAPI.getInstance().extensions
+ .forEach { extension ->
+ for (rectangle in extension.getExclusionZones(screen)) {
+ if (exclusions.any { it.encompasses(rectangle) })
+ continue
+ exclusions.add(rectangle)
+ }
+ }
+
+ return exclusions
+ }
+
+ var reachableItems = listOf<SBItemStack>()
+ var pageOffset = 0
+ fun recalculateVisibleItems() {
+ reachableItems = RepoManager.neuRepo.items
+ .items.values.map { SBItemStack(it.skyblockId) }
+ }
+
+ @Subscribe
+ fun onReload(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener { recalculateVisibleItems() }
+ }
+
+ fun coordinates(outer: ScreenRectangle, exclusions: Collection<ScreenRectangle>): Sequence<ScreenRectangle> {
+ val entryWidth = 18
+ val columns = outer.width / entryWidth
+ val rows = outer.height / entryWidth
+ val lowX = outer.right() - columns * entryWidth
+ val lowY = outer.top()
+ return generateSequence(0) { it + 1 }
+ .map {
+ val xIndex = it % columns
+ val yIndex = it / columns
+ ScreenRectangle(
+ lowX + xIndex * entryWidth, lowY + yIndex * entryWidth,
+ entryWidth, entryWidth
+ )
+ }
+ .take(rows * columns)
+ .filter { candidate -> exclusions.none { it.intersects(candidate) } }
+ }
+
+ var lastRenderPositions: List<Pair<ScreenRectangle, SBItemStack>> = listOf()
+ var lastHoveredItemStack: Pair<ScreenRectangle, SBItemStack>? = null
+
+ abstract class ItemListElement(
+ ) : Renderable, GuiEventListener {
+ abstract val rectangle: Rectangle
+ override fun setFocused(focused: Boolean) {
+ }
+
+ override fun isFocused(): Boolean {
+ return false
+ }
+
+ override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
+ return rectangle.contains(mouseX, mouseY)
+ }
+ }
+
+ interface HasLabel {
+ fun component(): Component
+ }
+
+
+ class PopupSettingsElement<T : HasLabel>(
+ x: Int,
+ y: Int,
+ width: Int,
+ val selected: GetSetter<T>,
+ val options: List<T>,
+ ) : ItemListElement() {
+ override val rectangle: Rectangle = Rectangle(x, y, width, 4 + (MC.font.lineHeight + 2) * options.size)
+ fun bb(i: Int) =
+ Rectangle(
+ rectangle.minX, rectangle.minY + (2 + MC.font.lineHeight) * i + 2,
+ rectangle.width, MC.font.lineHeight
+ )
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.fill(rectangle.minX, rectangle.minY, rectangle.maxX, rectangle.maxY, 0xFF000000.toInt())
+ guiGraphics.submitOutline(rectangle.x, rectangle.y, rectangle.width, rectangle.height, -1)
+ val sel = selected.get()
+ for ((index, element) in options.withIndex()) {
+ val b = bb(index)
+ val tw = MC.font.width(element.component())
+ guiGraphics.drawString(
+ MC.font, element.component(), b.centerX - tw / 2,
+ b.y + 1,
+ if (element == sel) 0xFFA0B000.toInt() else -1
+ )
+ if (b.contains(mouseX, mouseY))
+ guiGraphics.hLine(b.centerX - tw / 2, b.centerX + tw / 2 - 1, b.maxY + 1, -1)
+ }
+ }
+
+ override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean {
+ popupElement = null
+ for ((index, element) in options.withIndex()) {
+ val b = bb(index)
+ if (b.contains(event.x, event.y)) {
+ selected.set(element)
+ break
+ }
+ }
+ return true
+ }
+ }
+
+ class SettingElement<T : HasLabel>(
+ x: Int,
+ y: Int,
+ val selected: GetSetter<T>,
+ val options: List<T>
+ ) : ItemListElement() {
+ val height = MC.font.lineHeight + 4
+ val width = options.maxOf { MC.font.width(it.component()) } + 4
+ override val rectangle: Rectangle = Rectangle(x, y, width, height)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.drawCenteredString(MC.font, selected.get().component(), rectangle.centerX, rectangle.y + 2, -1)
+ if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) {
+ guiGraphics.hLine(rectangle.minX, rectangle.maxX - 1, rectangle.maxY - 2, -1)
+ }
+ }
+
+ override fun mouseClicked(
+ event: MouseButtonEvent,
+ isDoubleClick: Boolean
+ ): Boolean {
+ popupElement = PopupSettingsElement(
+ rectangle.x,
+ rectangle.y - options.size * (MC.font.lineHeight + 2) - 2,
+ width,
+ selected,
+ options
+ )
+ return true
+ }
+ }
+
+ var popupElement: ItemListElement? = null
+
+
+ fun findStackUnder(mouseX: Int, mouseY: Int): Pair<ScreenRectangle, SBItemStack>? {
+ val lhis = lastHoveredItemStack
+ if (lhis != null && lhis.first.containsPoint(mouseX, mouseY))
+ return lhis
+ return lastRenderPositions.firstOrNull { it.first.containsPoint(mouseX, mouseY) }
+ }
+
+ val isItemListEnabled get() = false
+
+ @Subscribe
+ fun onClick(event: HandledScreenClickEvent) {
+ if(!isItemListEnabled)return
+ val pe = popupElement
+ val me = MouseButtonEvent(
+ event.mouseX, event.mouseY,
+ MouseButtonInfo(event.button, 0) // TODO: missing modifiers
+ )
+ if (pe != null) {
+ event.cancel()
+ if (!pe.isMouseOver(event.mouseX, event.mouseY)) {
+ popupElement = null
+ return
+ }
+ pe.mouseClicked(
+ me,
+ false
+ )
+ return
+ }
+ listElements.forEach {
+ if (it.isMouseOver(event.mouseX, event.mouseY))
+ it.mouseClicked(me, false)
+ }
+ }
+
+ var listElements = listOf<ItemListElement>()
+
+ @Subscribe
+ fun onRender(event: HandledScreenForegroundEvent) {
+ if(!isItemListEnabled) return
+ lastHoveredItemStack = null
+ lastRenderPositions = listOf()
+ val exclusions = collectExclusions(event.screen)
+ val potentiallyVisible = reachableItems.subList(pageOffset, reachableItems.size)
+ val screenWidth = event.screen.width
+ val rightThird = ScreenRectangle(
+ screenWidth - screenWidth / 3, 0,
+ screenWidth / 3, event.screen.height - MC.font.lineHeight - 4
+ )
+ val coords = coordinates(rightThird, exclusions)
+
+ lastRenderPositions = coords.zip(potentiallyVisible.asSequence()).toList()
+ val isPopupHovered = popupElement?.isMouseOver(event.mouseX.toDouble(),event.mouseY.toDouble())
+ ?: false
+ lastRenderPositions.forEach { (pos, stack) ->
+ val realStack = stack.asLazyImmutableItemStack()
+ val toRender = realStack ?: ItemStack(Items.PAINTING)
+ event.context.renderItem(toRender, pos.left() + 1, pos.top() + 1)
+ if (!isPopupHovered && pos.containsPoint(event.mouseX, event.mouseY)) {
+ lastHoveredItemStack = pos to stack
+ event.context.setTooltipForNextFrame(
+ MC.font,
+ if (realStack != null)
+ ItemSlotWidget.getTooltip(realStack)
+ else
+ stack.estimateLore(),
+ Optional.empty(),
+ event.mouseX, event.mouseY
+ )
+ }
+ }
+ event.context.fill(
+ rightThird.left(),
+ rightThird.bottom(),
+ rightThird.right(),
+ event.screen.height,
+ 0xFF000000.toInt()
+ )
+ val le = mutableListOf<ItemListElement>()
+ le.add(
+ SettingElement(
+ 0,
+ rightThird.bottom(),
+ sortOrder,
+ SortOrder.entries
+ )
+ )
+ val bottomWidth = le.sumOf { it.rectangle.width + 2 } - 2
+ var startX = rightThird.getCenterInAxis(ScreenAxis.HORIZONTAL) - bottomWidth / 2
+ le.forEach {
+ it.rectangle.translate(startX, 0)
+ startX += it.rectangle.width + 2
+ }
+ le.forEach { it.render(event.context, event.mouseX, event.mouseY, event.delta) }
+ listElements = le
+ popupElement?.render(event.context, event.mouseX, event.mouseY, event.delta)
+ }
+
+ enum class SortOrder(val component: Component) : HasLabel {
+ NAME(Component.literal("Name")),
+ RARITY(Component.literal("Rarity"));
+
+ override fun component(): Component = component
+ }
+
+ val sortOrder = Property.of(SortOrder.NAME)
+}
diff --git a/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt
new file mode 100644
index 0000000..b659643
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt
@@ -0,0 +1,141 @@
+package moe.nea.firmament.features.items.recipes
+
+import java.util.Optional
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.TooltipFlag
+import moe.nea.firmament.api.v1.FirmamentItemWidget
+import moe.nea.firmament.events.ItemTooltipEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.FirmFormatters.shortFormat
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.darkGrey
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+
+class ItemSlotWidget(
+ point: Point,
+ var content: List<SBItemStack>,
+ val slotKind: RecipeLayouter.SlotKind
+) : RecipeWidget(),
+ RecipeLayouter.CyclingItemSlot,
+ FirmamentItemWidget {
+ override var position = point
+ override val size get() = Dimension(16, 16)
+ val itemRect get() = Rectangle(position, Dimension(16, 16))
+
+ val backgroundTopLeft
+ get() =
+ if (slotKind.isBig) Point(position.x - 4, position.y - 4)
+ else Point(position.x - 1, position.y - 1)
+ val backgroundSize =
+ if (slotKind.isBig) Dimension(16 + 8, 16 + 8)
+ else Dimension(18, 18)
+ override val rect: Rectangle
+ get() = Rectangle(backgroundTopLeft, backgroundSize)
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ val stack = current().asImmutableItemStack()
+ // TODO: draw slot background
+ if (stack.isEmpty) return
+ guiGraphics.renderItem(stack, position.x, position.y)
+ guiGraphics.renderItemDecorations(
+ MC.font, stack, position.x, position.y,
+ if (stack.count >= SHORT_NUM_CUTOFF) shortFormat(stack.count.toDouble())
+ else null
+ )
+ if (itemRect.contains(mouseX, mouseY)
+ && guiGraphics.containsPointInScissor(mouseX, mouseY)
+ ) guiGraphics.setTooltipForNextFrame(
+ MC.font, getTooltip(stack), Optional.empty(),
+ mouseX, mouseY
+ )
+ }
+
+ companion object {
+ val SHORT_NUM_CUTOFF = 1000
+ var canUseTooltipEvent = true
+
+ fun getTooltip(itemStack: ItemStack): List<Component> {
+ val lore = mutableListOf(itemStack.displayNameAccordingToNbt)
+ lore.addAll(itemStack.loreAccordingToNbt)
+ if (canUseTooltipEvent) {
+ try {
+ ItemTooltipCallback.EVENT.invoker().getTooltip(
+ itemStack, Item.TooltipContext.EMPTY,
+ TooltipFlag.NORMAL, lore
+ )
+ } catch (ex: Exception) {
+ canUseTooltipEvent = false
+ ErrorUtil.softError("Failed to use vanilla tooltips", ex)
+ }
+ } else {
+ ItemTooltipEvent.publish(
+ ItemTooltipEvent(
+ itemStack,
+ Item.TooltipContext.EMPTY,
+ TooltipFlag.NORMAL,
+ lore
+ )
+ )
+ }
+ if (itemStack.count >= SHORT_NUM_CUTOFF && lore.isNotEmpty())
+ lore.add(1, Component.literal("${itemStack.count}x").darkGrey())
+ return lore
+ }
+ }
+
+
+ override fun tick() {
+ if (SavedKeyBinding.isShiftDown()) return
+ if (content.size <= 1) return
+ if (MC.currentTick % 5 != 0) return
+ index = (index + 1) % content.size
+ }
+
+ var index = 0
+ var onUpdate: () -> Unit = {}
+
+ override fun onUpdate(action: () -> Unit) {
+ this.onUpdate = action
+ }
+
+ override fun current(): SBItemStack {
+ return content.getOrElse(index) { SBItemStack.EMPTY }
+ }
+
+ override fun update(newValue: SBItemStack) {
+ content = listOf(newValue)
+ // SAFE: content was just assigned to a non-empty list
+ index = index.coerceIn(content.indices)
+ }
+
+ override fun getPlacement(): FirmamentItemWidget.Placement {
+ return FirmamentItemWidget.Placement.RECIPE_SCREEN
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun getItemStack(): ItemStack {
+ return current().asImmutableItemStack()
+ }
+
+ override fun getSkyBlockId(): String {
+ return current().skyblockId.neuItem
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt
new file mode 100644
index 0000000..aad3bda
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt
@@ -0,0 +1,46 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.input.MouseButtonEvent
+import moe.nea.firmament.util.MoulConfigUtils.createAndTranslateFullContext
+
+class MoulConfigWidget(
+ val component: GuiComponent,
+ override var position: Point,
+ override val size: Dimension,
+) : RecipeWidget() {
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ createAndTranslateFullContext(
+ guiGraphics, mouseX, mouseY, rect,
+ component::render
+ )
+ }
+
+ override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean {
+ return createAndTranslateFullContext(null, event.x.toInt(), event.y.toInt(), rect) {
+ component.mouseEvent(MouseEvent.Click(event.button(), true), it)
+ }
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ createAndTranslateFullContext(null, mouseX, mouseY, rect) {
+ component.mouseEvent(MouseEvent.Move(0F, 0F), it)
+ }
+ }
+
+ override fun mouseReleased(event: MouseButtonEvent): Boolean {
+ return createAndTranslateFullContext(null, event.x, event.y, rect) {
+ component.mouseEvent(MouseEvent.Click(event.button(), false), it)
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeRegistry.kt b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt
new file mode 100644
index 0000000..c2df46f
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt
@@ -0,0 +1,116 @@
+package moe.nea.firmament.features.items.recipes
+
+import com.mojang.blaze3d.platform.InputConstants
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.GenericRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBReforgeRecipeRenderer
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.focusedItemStack
+
+object RecipeRegistry {
+ val recipeTypes: List<GenericRecipeRenderer<*>> = listOf(
+ SBCraftingRecipeRenderer,
+ SBForgeRecipeRenderer,
+ SBReforgeRecipeRenderer,
+ SBEssenceUpgradeRecipeRenderer,
+ )
+
+
+ @Subscribe
+ fun showUsages(event: HandledScreenKeyPressedEvent) {
+ val provider =
+ if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_R))) {
+ ::getRecipesFor
+ } else if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_U))) {
+ ::getUsagesFor
+ } else {
+ return
+ }
+ val stack = event.screen.focusedItemStack ?: return
+ val recipes = provider(SBItemStack(stack))
+ if (recipes.isEmpty()) return
+ MC.screen = RecipeScreen(recipes.toList())
+ }
+
+
+ object RecipeIndexes : IReloadable {
+
+ private fun <T : Any> createIndexFor(
+ neuRepository: NEURepository,
+ recipeRenderer: GenericRecipeRenderer<T>,
+ outputs: Boolean,
+ ): List<Pair<SkyblockId, RenderableRecipe<T>>> {
+ val indexer: (T) -> Collection<SBItemStack> =
+ if (outputs) recipeRenderer::getOutputs
+ else recipeRenderer::getInputs
+ return recipeRenderer.findAllRecipes(neuRepository)
+ .flatMap {
+ val wrappedRecipe = RenderableRecipe(it, recipeRenderer, null)
+ indexer(it).map { it.skyblockId to wrappedRecipe }
+ }
+ }
+
+ fun createIndex(outputs: Boolean): MutableMap<SkyblockId, List<RenderableRecipe<*>>> {
+ val m: MutableMap<SkyblockId, List<RenderableRecipe<*>>> = mutableMapOf()
+ recipeTypes.forEach { renderer ->
+ createIndexFor(RepoManager.neuRepo, renderer, outputs)
+ .forEach { (stack, recipe) ->
+ m.merge(stack, listOf(recipe)) { a, b -> a + b }
+ }
+ }
+ return m
+ }
+
+ lateinit var recipesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>>
+ lateinit var usagesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>>
+ override fun reload(recipe: NEURepository) {
+ recipesForIndex = createIndex(true)
+ usagesForIndex = createIndex(false)
+ }
+ }
+
+ @Subscribe
+ fun onRepoBuild(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener(RecipeIndexes)
+ }
+
+
+ fun getRecipesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> {
+ val recipes = LinkedHashSet<RenderableRecipe<*>>()
+ recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, true) }
+ recipes.addAll(RecipeIndexes.recipesForIndex[itemStack.skyblockId] ?: emptyList())
+ return recipes
+ }
+
+ fun getUsagesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> {
+ val recipes = LinkedHashSet<RenderableRecipe<*>>()
+ recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, false) }
+ recipes.addAll(RecipeIndexes.usagesForIndex[itemStack.skyblockId] ?: emptyList())
+ return recipes
+ }
+
+ private fun <T : Any> injectRecipesFor(
+ recipeRenderer: GenericRecipeRenderer<T>,
+ collector: MutableCollection<RenderableRecipe<*>>,
+ relevantItem: SBItemStack,
+ mustBeInOutputs: Boolean
+ ) {
+ collector.addAll(
+ recipeRenderer.discoverExtraRecipes(RepoManager.neuRepo, relevantItem, mustBeInOutputs)
+ .map { RenderableRecipe(it, recipeRenderer, relevantItem) }
+ )
+ }
+
+
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeScreen.kt b/src/main/kotlin/features/items/recipes/RecipeScreen.kt
new file mode 100644
index 0000000..9a746f3
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeScreen.kt
@@ -0,0 +1,129 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.renderer.RenderPipelines
+import moe.nea.firmament.util.mc.CommonTextures
+import moe.nea.firmament.util.render.enableScissorWithTranslation
+import moe.nea.firmament.util.tr
+
+class RecipeScreen(
+ val recipes: List<RenderableRecipe<*>>,
+) : Screen(tr("firmament.recipe.screen", "SkyBlock Recipe")) {
+
+ data class PlacedRecipe(
+ val bounds: Rectangle,
+ val layoutedRecipe: StandaloneRecipeRenderer,
+ ) {
+ fun moveTo(position: Point) {
+ val Δx = position.x - bounds.x
+ val Δy = position.y - bounds.y
+ bounds.translate(Δx, Δy)
+ layoutedRecipe.widgets.forEach { widget ->
+ widget.position = widget.position.clone().also {
+ it.translate(Δx, Δy)
+ }
+ }
+ }
+ }
+
+ lateinit var placedRecipes: List<PlacedRecipe>
+ var scrollViewport: Int = 0
+ var scrollOffset: Int = 0
+ var scrollPortWidth: Int = 0
+ var heightEstimate: Int = 0
+ val gutter = 10
+ override fun init() {
+ super.init()
+ scrollViewport = minOf(height - 20, 250)
+ scrollPortWidth = 0
+ heightEstimate = 0
+ var offset = height / 2 - scrollViewport / 2
+ placedRecipes = recipes.map {
+ val effectiveWidth = minOf(it.renderer.displayWidth, width - 20)
+ val bounds = Rectangle(
+ width / 2 - effectiveWidth / 2,
+ offset,
+ effectiveWidth,
+ it.renderer.displayHeight
+ )
+ if (heightEstimate > 0)
+ heightEstimate += gutter
+ heightEstimate += bounds.height
+ scrollPortWidth = maxOf(effectiveWidth, scrollPortWidth)
+ offset += bounds.height + gutter
+ val layoutedRecipe = it.render(bounds)
+ layoutedRecipe.widgets.forEach(this::addRenderableWidget)
+ PlacedRecipe(bounds, layoutedRecipe)
+ }
+ }
+
+ fun scrollRect() =
+ Rectangle(
+ width / 2 - scrollPortWidth / 2, height / 2 - scrollViewport / 2,
+ scrollPortWidth, scrollViewport
+ )
+
+ fun scissorScrollPort(guiGraphics: GuiGraphics) {
+ guiGraphics.enableScissorWithTranslation(scrollRect())
+ }
+
+ override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
+ if (!scrollRect().contains(mouseX, mouseY))
+ return false
+ scrollOffset = (scrollOffset + scrollY * -4)
+ .coerceAtMost(heightEstimate - scrollViewport.toDouble())
+ .coerceAtLeast(.0)
+ .toInt()
+ var offset = height / 2 - scrollViewport / 2 - scrollOffset
+ placedRecipes.forEach {
+ it.moveTo(Point(it.bounds.x, offset))
+ offset += it.bounds.height + gutter
+ }
+ return true
+ }
+
+ override fun renderBackground(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ super.renderBackground(guiGraphics, mouseX, mouseY, partialTick)
+
+ val srect = scrollRect()
+ srect.grow(8, 8)
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ CommonTextures.genericWidget(),
+ srect.x, srect.y,
+ srect.width, srect.height
+ )
+
+ scissorScrollPort(guiGraphics)
+ placedRecipes.forEach {
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ CommonTextures.genericWidget(),
+ it.bounds.x, it.bounds.y,
+ it.bounds.width, it.bounds.height
+ )
+ }
+ guiGraphics.disableScissor()
+ }
+
+ override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
+ scissorScrollPort(guiGraphics)
+ super.render(guiGraphics, mouseX, mouseY, partialTick)
+ guiGraphics.disableScissor()
+ }
+
+ override fun tick() {
+ super.tick()
+ placedRecipes.forEach {
+ it.layoutedRecipe.tick()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeWidget.kt b/src/main/kotlin/features/items/recipes/RecipeWidget.kt
new file mode 100644
index 0000000..f13707c
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeWidget.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.components.Renderable
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.client.gui.narration.NarratableEntry
+import net.minecraft.client.gui.narration.NarrationElementOutput
+import net.minecraft.client.gui.navigation.ScreenRectangle
+import moe.nea.firmament.util.mc.asScreenRectangle
+
+abstract class RecipeWidget : GuiEventListener, Renderable, NarratableEntry {
+ override fun narrationPriority(): NarratableEntry.NarrationPriority? {
+ return NarratableEntry.NarrationPriority.NONE// I am so sorry
+ }
+
+ override fun updateNarration(narrationElementOutput: NarrationElementOutput) {
+ }
+
+ open fun tick() {}
+ private var _focused = false
+ abstract var position: Point
+ abstract val size: Dimension
+ open val rect: Rectangle get() = Rectangle(position, size)
+ override fun setFocused(focused: Boolean) {
+ this._focused = focused
+ }
+
+ override fun isFocused(): Boolean {
+ return this._focused
+ }
+
+ override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
+ return rect.contains(mouseX, mouseY)
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/RenderableRecipe.kt b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt
new file mode 100644
index 0000000..20ca17e
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import java.util.Objects
+import me.shedaniel.math.Rectangle
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.GenericRecipeRenderer
+
+class RenderableRecipe<T : Any>(
+ val recipe: T,
+ val renderer: GenericRecipeRenderer<T>,
+ val mainItemStack: SBItemStack?,
+) {
+ fun render(bounds: Rectangle): StandaloneRecipeRenderer {
+ val layouter = StandaloneRecipeRenderer(bounds)
+ renderer.render(recipe, bounds, layouter, mainItemStack)
+ return layouter
+ }
+
+// override fun equals(other: Any?): Boolean {
+// if (other !is RenderableRecipe<*>) return false
+// return renderer == other.renderer && recipe == other.recipe
+// }
+//
+// override fun hashCode(): Int {
+// return Objects.hash(recipe, renderer)
+// }
+}
diff --git a/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt
new file mode 100644
index 0000000..5a834eb
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.components.events.AbstractContainerEventHandler
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.network.chat.Component
+import net.minecraft.world.entity.LivingEntity
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+
+class StandaloneRecipeRenderer(val bounds: Rectangle) : AbstractContainerEventHandler(), RecipeLayouter {
+
+ fun tick() {
+ widgets.forEach { it.tick() }
+ }
+
+ fun <T : RecipeWidget> addWidget(widget: T): T {
+ this.widgets.add(widget)
+ return widget
+ }
+
+ override fun createCyclingItemSlot(
+ x: Int,
+ y: Int,
+ content: List<SBItemStack>,
+ slotKind: RecipeLayouter.SlotKind
+ ): RecipeLayouter.CyclingItemSlot {
+ return addWidget(ItemSlotWidget(Point(x, y), content, slotKind))
+ }
+
+ val Rectangle.topLeft get() = Point(x, y)
+
+ override fun createTooltip(
+ rectangle: Rectangle,
+ label: List<Component>
+ ) {
+ addWidget(TooltipWidget(rectangle.topLeft, rectangle.size, label))
+ }
+
+ override fun createLabel(
+ x: Int,
+ y: Int,
+ text: Component
+ ): RecipeLayouter.Updater<Component> {
+ return addWidget(ComponentWidget(Point(x, y), text))
+ }
+
+ override fun createArrow(x: Int, y: Int): Rectangle {
+ return addWidget(ArrowWidget(Point(x, y))).rect
+ }
+
+ override fun createMoulConfig(
+ x: Int,
+ y: Int,
+ w: Int,
+ h: Int,
+ component: GuiComponent
+ ) {
+ addWidget(MoulConfigWidget(component, Point(x, y), Dimension(w, h)))
+ }
+
+ override fun createFire(point: Point, animationTicks: Int) {
+ addWidget(FireWidget(point, animationTicks))
+ }
+
+ override fun createEntity(rectangle: Rectangle, entity: LivingEntity) {
+ addWidget(EntityWidget(rectangle.topLeft, rectangle.size, entity))
+ }
+
+ val widgets: MutableList<RecipeWidget> = mutableListOf()
+ override fun children(): List<GuiEventListener> {
+ return widgets
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/TooltipWidget.kt b/src/main/kotlin/features/items/recipes/TooltipWidget.kt
new file mode 100644
index 0000000..87feb61
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/TooltipWidget.kt
@@ -0,0 +1,30 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+
+class TooltipWidget(
+ override var position: Point,
+ override val size: Dimension,
+ label: List<Component>
+) : RecipeWidget(), RecipeLayouter.Updater<List<Component>> {
+ override fun update(newValue: List<Component>) {
+ this.formattedComponent = newValue.map { it.visualOrderText }
+ }
+
+ var formattedComponent = label.map { it.visualOrderText }
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ if (rect.contains(mouseX, mouseY))
+ guiGraphics.setTooltipForNextFrame(formattedComponent, mouseX, mouseY)
+ }
+
+}
diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt
new file mode 100644
index 0000000..9dadb80
--- /dev/null
+++ b/src/main/kotlin/features/macros/ComboProcessor.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.features.macros
+
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.tr
+
+object ComboProcessor {
+
+ var rootTrie: Branch = Branch(mapOf())
+ private set
+
+ var activeTrie: Branch = rootTrie
+ private set
+
+ var isInputting = false
+ var lastInput = TimeMark.farPast()
+ val breadCrumbs = mutableListOf<SavedKeyBinding>()
+
+ fun setActions(actions: List<ComboKeyAction>) {
+ rootTrie = KeyComboTrie.fromComboList(actions)
+ reset()
+ }
+
+ fun reset() {
+ activeTrie = rootTrie
+ lastInput = TimeMark.now()
+ isInputting = false
+ breadCrumbs.clear()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ if (isInputting && lastInput.passedTime() > 3.seconds)
+ reset()
+ }
+
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ if (!isInputting) return
+ if (!event.isRenderingHud) return
+ event.context.pose().pushMatrix()
+ val width = 120
+ event.context.pose().translate(
+ (MC.window.guiScaledWidth - width) / 2F,
+ (MC.window.guiScaledHeight) / 2F + 8
+ )
+ val breadCrumbText = breadCrumbs.joinToString(" > ")
+ event.context.drawString(
+ MC.font,
+ tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.pose().translate(0F, MC.font.lineHeight + 2F)
+ for ((key, value) in activeTrie.nodes) {
+ event.context.drawString(
+ MC.font,
+ Component.literal("$breadCrumbText > $key: ").append(value.label),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.pose().translate(0F, MC.font.lineHeight + 1F)
+ }
+ event.context.pose().popMatrix()
+ }
+
+ @Subscribe
+ fun onKeyBinding(event: WorldKeyboardEvent) {
+ val nextEntry = activeTrie.nodes.entries
+ .find { event.matches(it.key) }
+ if (nextEntry == null) {
+ reset()
+ return
+ }
+ event.cancel()
+ breadCrumbs.add(nextEntry.key)
+ lastInput = TimeMark.now()
+ isInputting = true
+ val value = nextEntry.value
+ when (value) {
+ is Branch -> {
+ activeTrie = value
+ }
+
+ is Leaf -> {
+ value.execute()
+ reset()
+ }
+ }.let { }
+ }
+}
diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt
new file mode 100644
index 0000000..18c95bc
--- /dev/null
+++ b/src/main/kotlin/features/macros/HotkeyAction.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.util.MC
+
+@Serializable
+sealed interface HotkeyAction {
+ // TODO: execute
+ val label: Component
+ fun execute()
+}
+
+@Serializable
+@SerialName("command")
+data class CommandAction(val command: String) : HotkeyAction {
+ override val label: Component
+ get() = Component.literal("/$command")
+
+ override fun execute() {
+ MC.sendCommand(command)
+ }
+}
+
+// Mit onscreen anzeige:
+// F -> 1 /equipment
+// F -> 2 /wardrobe
+// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße)
+
+// Radial menu
+// Hold F
+// Weight (mach eins doppelt so groß)
+// /equipment
+// /wardrobe
+
+// Bei allen: Filter!
+// - Nur in Dungeons / andere Insel
+// - Nur wenn ich Item X im inventar habe (fishing rod)
+
diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt
new file mode 100644
index 0000000..2701ac1
--- /dev/null
+++ b/src/main/kotlin/features/macros/KeyComboTrie.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.ErrorUtil
+
+sealed interface KeyComboTrie {
+ val label: Component
+
+ companion object {
+ fun fromComboList(
+ combos: List<ComboKeyAction>,
+ ): Branch {
+ val root = Branch(mutableMapOf())
+ for (combo in combos) {
+ var p = root
+ if (combo.keySequence.isEmpty()) {
+ ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty")
+ continue
+ }
+ for ((index, key) in combo.keySequence.withIndex()) {
+ val m = (p.nodes as MutableMap)
+ if (index == combo.keySequence.lastIndex) {
+ if (key in m) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence.joinToString(" > ")} (another action ${m[key]} already exists).")
+ break
+ }
+
+ m[key] = Leaf(combo.action)
+ } else {
+ val c = m.getOrPut(key) { Branch(mutableMapOf()) }
+ if (c !is Branch) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence} (final node exists at index $index) through another action already")
+ break
+ } else {
+ p = c
+ }
+ }
+ }
+ }
+ return root
+ }
+ }
+}
+
+@Serializable
+data class MacroWheel(
+ val keyBinding: SavedKeyBinding = SavedKeyBinding.unbound(),
+ val options: List<HotkeyAction>
+)
+
+@Serializable
+data class ComboKeyAction(
+ val action: HotkeyAction,
+ val keySequence: List<SavedKeyBinding> = listOf(),
+)
+
+data class Leaf(val action: HotkeyAction) : KeyComboTrie {
+ override val label: Component
+ get() = action.label
+
+ fun execute() {
+ action.execute()
+ }
+}
+
+data class Branch(
+ val nodes: Map<SavedKeyBinding, KeyComboTrie>
+) : KeyComboTrie {
+ override val label: Component
+ get() = Component.literal("...") // TODO: better labels
+}
diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt
new file mode 100644
index 0000000..af1b0e8
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroData.kt
@@ -0,0 +1,19 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.DataHolder
+
+@Serializable
+data class MacroData(
+ var comboActions: List<ComboKeyAction> = listOf(),
+ var wheels: List<MacroWheel> = listOf(),
+) {
+ @Config
+ object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) {
+ override fun onLoad() {
+ ComboProcessor.setActions(data.comboActions)
+ RadialMacros.setWheels(data.wheels)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt
new file mode 100644
index 0000000..e73f076
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroUI.kt
@@ -0,0 +1,293 @@
+package moe.nea.firmament.features.macros
+
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList
+import moe.nea.firmament.gui.config.KeyBindingStateManager
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+
+class MacroUI {
+
+
+ companion object {
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ // TODO: add button in config
+ event.subcommand("macros") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null))
+ }
+ }
+ }
+
+ }
+
+ @field:Bind("combos")
+ val combos = Combos()
+
+ @field:Bind("wheels")
+ val wheels = Wheels()
+ var dontSave = false
+
+ @Bind
+ fun beforeClose(): CloseEventListener.CloseAction {
+ if (!dontSave)
+ save()
+ return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE
+ }
+
+ fun save() {
+ MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() }
+ MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() }
+ MacroData.DConfig.markDirty()
+ RadialMacros.setWheels(MacroData.DConfig.data.wheels)
+ ComboProcessor.setActions(MacroData.DConfig.data.comboActions)
+ }
+
+ fun discard() {
+ dontSave = true
+ MC.screen?.onClose()
+ }
+
+ class Command(
+ @field:Bind("text")
+ var text: String,
+ val parent: Wheel,
+ ) {
+ @Bind
+ fun textR() = StructuredText.of(text)
+
+ @Bind
+ fun delete() {
+ parent.editableCommands.removeIf { it === this }
+ parent.editableCommands.update()
+ parent.commands.update()
+ }
+
+ fun asCommandAction() = CommandAction(text)
+ }
+
+ inner class Wheel(
+ val parent: Wheels,
+ var binding: SavedKeyBinding,
+ commands: List<CommandAction>,
+ ) {
+
+ fun asSaveable(): MacroWheel {
+ return MacroWheel(binding, commands.map { it.asCommandAction() })
+ }
+
+ @Bind("keyCombo")
+ fun text() = MoulConfigPlatform.wrap(binding.format())
+
+ @field:Bind("commands")
+ val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) }
+
+ @field:Bind("editableCommands")
+ val editableCommands = this.commands.toObservableList()
+
+ @Bind
+ fun addOption() {
+ editableCommands.add(Command("", this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.onClose()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen)
+ }
+
+ @Bind
+ fun delete() {
+ parent.wheels.removeIf { it === this }
+ parent.wheels.update()
+ }
+
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+ }
+
+ inner class Wheels {
+ @field:Bind("wheels")
+ val wheels: ObservableList<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) {
+ Wheel(this, it.keyBinding, it.options.map { CommandAction((it as CommandAction).command) })
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+
+ @Bind
+ fun addWheel() {
+ wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf()))
+ }
+ }
+
+ fun saveAndClose() {
+ save()
+ MC.screen?.onClose()
+ }
+
+ inner class Combos {
+ @field:Bind("actions")
+ val actions: ObservableList<ActionEditor> = ObservableList(
+ MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) {
+ ActionEditor(it, this)
+ }
+ )
+
+ @Bind
+ fun addCommand() {
+ actions.add(
+ ActionEditor(
+ ComboKeyAction(
+ CommandAction("ac Hello from a Firmament Hotkey"),
+ listOf()
+ ),
+ this
+ )
+ )
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+ }
+
+ class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) {
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+
+ @Bind
+ fun delete() {
+ parent.combo.removeIf { it === this }
+ parent.combo.update()
+ }
+ }
+
+ class ActionEditor(val action: ComboKeyAction, val parent: Combos) {
+ fun asSaveable(): ComboKeyAction {
+ return ComboKeyAction(
+ CommandAction(command),
+ combo.map { it.binding }
+ )
+ }
+
+ @field:Bind("command")
+ var command: String = (action.action as CommandAction).command
+
+ @Bind
+ fun commandR() = StructuredText.of(command)
+
+ @field:Bind("combo")
+ val combo = action.keySequence.map { KeyBindingEditor(it, this) }.toObservableList()
+
+ @Bind
+ fun formattedCombo() =
+ StructuredText.of(combo.joinToString(" > ") { it.binding.toString() }) // TODO: this can be joined without .toString()
+
+ @Bind
+ fun addStep() {
+ combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.onClose()
+ }
+
+ @Bind
+ fun delete() {
+ parent.actions.removeIf { it === this }
+ parent.actions.update()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen)
+ }
+ }
+}
+
+private fun <T> ObservableList<T>.setAll(ts: Collection<T>) {
+ val observer = this.observer
+ this.clear()
+ this.addAll(ts)
+ this.observer = observer
+ this.update()
+}
diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt
new file mode 100644
index 0000000..2519123
--- /dev/null
+++ b/src/main/kotlin/features/macros/RadialMenu.kt
@@ -0,0 +1,153 @@
+package moe.nea.firmament.features.macros
+
+import me.shedaniel.math.Color
+import org.joml.Vector2f
+import util.render.CustomRenderLayers
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+import net.minecraft.client.gui.GuiGraphics
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldMouseMoveEvent
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.render.RenderCircleProgress
+import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.render.lerpAngle
+import moe.nea.firmament.util.render.wrapAngle
+import moe.nea.firmament.util.render.τ
+
+object RadialMenuViewer {
+ interface RadialMenu {
+ val key: SavedKeyBinding
+ val options: List<RadialMenuOption>
+ }
+
+ interface RadialMenuOption {
+ val isEnabled: Boolean
+ fun resolve()
+ fun renderSlice(drawContext: GuiGraphics)
+ }
+
+ var activeMenu: RadialMenu? = null
+ set(value) {
+ if (value?.options.isNullOrEmpty()) {
+ field = null
+ } else {
+ field = value
+ }
+ delta = Vector2f(0F, 0F)
+ }
+ var delta = Vector2f(0F, 0F)
+ val maxSelectionSize = 100F
+
+ @Subscribe
+ fun onMouseMotion(event: WorldMouseMoveEvent) {
+ val menu = activeMenu ?: return
+ event.cancel()
+ delta.add(event.deltaX.toFloat(), event.deltaY.toFloat())
+ val m = delta.lengthSquared()
+ if (m > maxSelectionSize * maxSelectionSize) {
+ delta.mul(maxSelectionSize / sqrt(m))
+ }
+ }
+
+ val INNER_CIRCLE_RADIUS = 16
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ val menu = activeMenu ?: return
+ val mat = event.context.pose()
+ mat.pushMatrix()
+ mat.translate(
+ (MC.window.guiScaledWidth) / 2F,
+ (MC.window.guiScaledHeight) / 2F,
+ )
+ val sliceWidth = (τ / menu.options.size).toFloat()
+ var selectedAngle = wrapAngle(atan2(delta.y, delta.x))
+ if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS)
+ selectedAngle = Float.NaN
+ for ((idx, option) in menu.options.withIndex()) {
+ val range = (sliceWidth * idx)..(sliceWidth * (idx + 1))
+ mat.pushMatrix()
+ mat.scale(64F, 64F)
+ val cutout = INNER_CIRCLE_RADIUS / 64F / 2
+ RenderCircleProgress.renderCircularSlice(
+ event.context,
+ CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI,
+ 0F, 1F, 0F, 1F,
+ range,
+ color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF,
+ innerCutoutRadius = cutout
+ )
+ mat.popMatrix()
+ mat.pushMatrix()
+ val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F)
+ val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F)
+ mat.translate(vec.x, vec.y)
+ option.renderSlice(event.context)
+ mat.popMatrix()
+ }
+ event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), Color.ofOpaque(0x00FF00))
+ mat.popMatrix()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ val menu = activeMenu ?: return
+ if (!menu.key.isPressed(true)) {
+ val angle = atan2(delta.y, delta.x)
+
+ val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt()
+ val choice = menu.options[choiceIndex]
+ val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS
+ activeMenu = null
+ if (selectedAny)
+ choice.resolve()
+ }
+ }
+
+}
+
+object RadialMacros {
+ lateinit var wheels: List<MacroWheel>
+ private set
+
+ fun setWheels(wheels: List<MacroWheel>) {
+ this.wheels = wheels
+ RadialMenuViewer.activeMenu = null
+ }
+
+ @Subscribe
+ fun onOpen(event: WorldKeyboardEvent) {
+ if (RadialMenuViewer.activeMenu != null) return
+ wheels.forEach { wheel ->
+ if (event.matches(wheel.keyBinding, atLeast = true)) {
+ class R(val action: HotkeyAction) : RadialMenuOption {
+ override val isEnabled: Boolean
+ get() = true
+
+ override fun resolve() {
+ action.execute()
+ }
+
+ override fun renderSlice(drawContext: GuiGraphics) {
+ drawContext.drawCenteredString(MC.font, action.label, 0, 0, -1)
+ }
+ }
+ RadialMenuViewer.activeMenu = object : RadialMenu {
+ override val key: SavedKeyBinding
+ get() = wheel.keyBinding
+ override val options: List<RadialMenuOption> =
+ wheel.options.map { R(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/mining/CommissionFeatures.kt b/src/main/kotlin/features/mining/CommissionFeatures.kt
index faba253..bfc635a 100644
--- a/src/main/kotlin/features/mining/CommissionFeatures.kt
+++ b/src/main/kotlin/features/mining/CommissionFeatures.kt
@@ -3,22 +3,24 @@ package moe.nea.firmament.features.mining
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.unformattedString
object CommissionFeatures {
- object Config : ManagedConfig("commissions", Category.MINING) {
+ @Config
+ object TConfig : ManagedConfig("commissions", Category.MINING) {
val highlightCompletedCommissions by toggle("highlight-completed") { true }
}
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
- if (!Config.highlightCompletedCommissions) return
+ if (!TConfig.highlightCompletedCommissions) return
if (MC.screenName != "Commissions") return
- val stack = event.slot.stack
+ val stack = event.slot.item
if (stack.loreAccordingToNbt.any { it.unformattedString == "COMPLETED" }) {
event.highlight(Firmament.identifier("completed_commission_background"))
}
diff --git a/src/main/kotlin/features/mining/Histogram.kt b/src/main/kotlin/features/mining/Histogram.kt
index ed48437..08ee893 100644
--- a/src/main/kotlin/features/mining/Histogram.kt
+++ b/src/main/kotlin/features/mining/Histogram.kt
@@ -1,7 +1,8 @@
package moe.nea.firmament.features.mining
-import java.util.*
+import java.util.NavigableMap
+import java.util.TreeMap
import kotlin.time.Duration
import moe.nea.firmament.util.TimeMark
diff --git a/src/main/kotlin/features/mining/HotmPresets.kt b/src/main/kotlin/features/mining/HotmPresets.kt
index 2241fee..4930f90 100644
--- a/src/main/kotlin/features/mining/HotmPresets.kt
+++ b/src/main/kotlin/features/mining/HotmPresets.kt
@@ -3,14 +3,15 @@ package moe.nea.firmament.features.mining
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.block.Blocks
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.entity.player.PlayerInventory
-import net.minecraft.item.Items
-import net.minecraft.screen.GenericContainerScreenHandler
-import net.minecraft.screen.slot.Slot
-import net.minecraft.text.Text
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.world.entity.player.Inventory
+import net.minecraft.world.item.Items
+import net.minecraft.world.inventory.ChestMenu
+import net.minecraft.world.inventory.Slot
+import net.minecraft.network.chat.Component
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.thenExecute
@@ -18,12 +19,12 @@ import moe.nea.firmament.events.ChestInventoryUpdateEvent
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-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.TemplateUtil
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.accessors.castAccessor
import moe.nea.firmament.util.customgui.CustomGui
import moe.nea.firmament.util.customgui.customGui
import moe.nea.firmament.util.mc.CommonTextures
@@ -51,8 +52,8 @@ object HotmPresets {
fun onScreenOpen(event: ScreenChangeEvent) {
val title = event.new?.title?.unformattedString
if (title != hotmInventoryName) return
- val screen = event.new as? HandledScreen<*> ?: return
- val oldHandler = (event.old as? HandledScreen<*>)?.customGui
+ val screen = event.new as? AbstractContainerScreen<*> ?: return
+ val oldHandler = (event.old as? AbstractContainerScreen<*>)?.customGui
if (oldHandler is HotmScrollPrompt) {
event.new.customGui = oldHandler
oldHandler.setNewScreen(screen)
@@ -63,32 +64,32 @@ object HotmPresets {
screen.customGui = HotmScrollPrompt(screen)
}
- class HotmScrollPrompt(var screen: HandledScreen<*>) : CustomGui() {
+ class HotmScrollPrompt(var screen: AbstractContainerScreen<*>) : CustomGui() {
var bounds = Rectangle(
0, 0, 0, 0
)
- fun setNewScreen(screen: HandledScreen<*>) {
+ fun setNewScreen(screen: AbstractContainerScreen<*>) {
this.screen = screen
onInit()
hasScrolled = false
}
- override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) {
+ override fun render(drawContext: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) {
drawContext.drawGuiTexture(
CommonTextures.genericWidget(),
bounds.x, bounds.y,
bounds.width,
bounds.height,
)
- drawContext.drawCenteredTextWithShadow(
+ drawContext.drawCenteredString(
MC.font,
if (hasAll) {
- Text.translatable("firmament.hotmpreset.copied")
+ Component.translatable("firmament.hotmpreset.copied")
} else if (!hasScrolled) {
- Text.translatable("firmament.hotmpreset.scrollprompt")
+ Component.translatable("firmament.hotmpreset.scrollprompt")
} else {
- Text.translatable("firmament.hotmpreset.scrolled")
+ Component.translatable("firmament.hotmpreset.scrolled")
},
bounds.centerX,
bounds.centerY - 5,
@@ -100,14 +101,14 @@ object HotmPresets {
var hasScrolled = false
var hasAll = false
- override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean {
if (!hasScrolled) {
- val slot = screen.screenHandler.getSlot(8)
- println("Clicking ${slot.stack}")
- slot.clickRightMouseButton(screen.screenHandler)
+ val slot = screen.menu.getSlot(8)
+ println("Clicking ${slot.item}")
+ slot.clickRightMouseButton(screen.menu)
}
hasScrolled = true
- return super.mouseClick(mouseX, mouseY, button)
+ return super.mouseClick(click, doubled)
}
override fun shouldDrawForeground(): Boolean {
@@ -124,7 +125,7 @@ object HotmPresets {
screen.height / 2 - 100,
300, 200
)
- val screen = screen as AccessorHandledScreen
+ val screen = screen.castAccessor()
screen.x_Firmament = bounds.x
screen.y_Firmament = bounds.y
screen.backgroundWidth_Firmament = bounds.width
@@ -140,10 +141,10 @@ object HotmPresets {
val allRows = (1..10).toSet()
fun onNewItems(event: ChestInventoryUpdateEvent) {
- val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return
+ val handler = screen.menu as? ChestMenu ?: return
for (it in handler.slots) {
- if (it.inventory is PlayerInventory) continue
- val stack = it.stack
+ if (it.container is Inventory) continue
+ val stack = it.item
val name = stack.displayNameAccordingToNbt.unformattedString
tierRegex.useMatch(name) {
coveredRows.add(group("tier").toInt())
@@ -156,7 +157,9 @@ object HotmPresets {
}
}
if (allRows == coveredRows) {
- ClipboardUtils.setTextContent(TemplateUtil.encodeTemplate(SHARE_PREFIX, HotmPreset(
+ ClipboardUtils.setTextContent(
+ TemplateUtil.encodeTemplate(
+ SHARE_PREFIX, HotmPreset(
unlockedPerks.map { PerkPreset(it) }
)))
hasAll = true
@@ -169,7 +172,7 @@ object HotmPresets {
@Subscribe
fun onSlotUpdates(event: ChestInventoryUpdateEvent) {
- val customGui = (event.inventory as? HandledScreen<*>)?.customGui
+ val customGui = (event.inventory as? AbstractContainerScreen<*>)?.customGui
if (customGui is HotmScrollPrompt) {
customGui.onNewItems(event)
}
@@ -185,7 +188,7 @@ object HotmPresets {
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
if (hotmInventoryName == MC.screenName
- && event.slot.stack.displayNameAccordingToNbt.unformattedString in highlightedPerks
+ && event.slot.item.displayNameAccordingToNbt.unformattedString in highlightedPerks
) {
event.highlight((Firmament.identifier("hotm_perk_preset")))
}
@@ -197,7 +200,7 @@ object HotmPresets {
thenExecute {
hotmCommandSent = TimeMark.now()
MC.sendCommand("hotm")
- source.sendFeedback(Text.translatable("firmament.hotmpreset.openinghotm"))
+ source.sendFeedback(Component.translatable("firmament.hotmpreset.openinghotm"))
}
}
event.subcommand("importhotm") {
@@ -205,10 +208,10 @@ object HotmPresets {
val template =
TemplateUtil.maybeDecodeTemplate<HotmPreset>(SHARE_PREFIX, ClipboardUtils.getTextContents())
if (template == null) {
- source.sendFeedback(Text.translatable("firmament.hotmpreset.failedimport"))
+ source.sendFeedback(Component.translatable("firmament.hotmpreset.failedimport"))
} else {
highlightedPerks = template.perks.mapTo(mutableSetOf()) { it.perkName }
- source.sendFeedback(Text.translatable("firmament.hotmpreset.okayimport"))
+ source.sendFeedback(Component.translatable("firmament.hotmpreset.okayimport"))
MC.sendCommand("hotm")
}
}
diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
new file mode 100644
index 0000000..6c50665
--- /dev/null
+++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
@@ -0,0 +1,53 @@
+package moe.nea.firmament.features.mining
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.world.item.ItemStack
+import moe.nea.firmament.repo.MiningRepoData
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.SkyBlockIsland
+
+object MiningBlockInfoUi {
+ class MiningInfo(miningData: MiningRepoData) {
+ @field:Bind("search")
+ @JvmField
+ var search = ""
+
+ @get:Bind("ores")
+ val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) }
+ }
+
+ class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) {
+ @get:Bind("oreName")
+ val oreName = block.name ?: "No Name"
+
+ @get:Bind("blocks")
+ val res = ObservableList(block.blocks189.map { BlockInfo(it, info) })
+ }
+
+ class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) {
+ @get:Bind("item")
+ val item = MoulConfigPlatform.wrap(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY)
+
+ @get:Bind("isSelected")
+ val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) }
+
+ @get:Bind("itemName")
+ val itemName get() = item.getDisplayName()
+
+ @get:Bind("restrictions")
+ val res = ObservableList(
+ if (block.onlyIn != null)
+ block.onlyIn.map { " §r- §a${it.userFriendlyName}" }
+ else
+ listOf("Everywhere")
+ )
+ }
+
+ fun makeScreen(): Screen {
+ return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null)
+ }
+}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 94b49f9..23b55e5 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -1,13 +1,17 @@
package moe.nea.firmament.features.mining
+import io.github.notenoughupdates.moulconfig.ChromaColour
import java.util.regex.Pattern
+import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.item.ItemStack
-import net.minecraft.util.DyeColor
-import net.minecraft.util.Hand
-import net.minecraft.util.Identifier
-import net.minecraft.util.StringIdentifiable
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.components.toasts.SystemToast
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.DyeColor
+import net.minecraft.world.InteractionHand
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.util.StringRepresentable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.ProcessChatEvent
@@ -15,8 +19,6 @@ import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.SlotClickEvent
import moe.nea.firmament.events.UseItemEvent
import moe.nea.firmament.events.WorldReadyEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.DurabilityBarEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
@@ -24,6 +26,8 @@ import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.TIME_PATTERN
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
@@ -33,20 +37,40 @@ import moe.nea.firmament.util.red
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.skyblock.AbilityUtils
+import moe.nea.firmament.util.skyblock.DungeonUtil
import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.toShedaniel
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
-object PickaxeAbility : FirmamentFeature {
- override val identifier: String
+object PickaxeAbility {
+ val identifier: String
get() = "pickaxe-info"
+ enum class ShowOnTools(val label: String, val items: Set<ItemType>) : StringRepresentable {
+ ALL("all", ItemType.DRILL, ItemType.PICKAXE, ItemType.SHOVEL, ItemType.AXE),
+ PICKAXES_AND_DRILLS("pick-and-drill", ItemType.PICKAXE, ItemType.DRILL),
+ DRILLS("drills", ItemType.DRILL),
+ ;
+ override fun getSerializedName(): String? {
+ return label
+ }
+
+ constructor(label: String, vararg items: ItemType) : this(label, items.toSet())
+
+ fun matches(type: ItemType) = items.contains(type)
+ }
+
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) {
val cooldownEnabled by toggle("ability-cooldown") { false }
+ val disableInDungeons by toggle("disable-in-dungeons") { true }
+ val showOnTools by choice("show-on-tools") { ShowOnTools.PICKAXES_AND_DRILLS }
val cooldownScale by integer("ability-scale", 16, 64) { 16 }
+ val cooldownColour by colour("ability-colour") { ChromaColour.fromStaticRGB(187, 54, 44, 128) }
+ val cooldownReadyToast by toggle("ability-cooldown-toast") { false }
val drillFuelBar by toggle("fuel-bar") { true }
val blockOnPrivateIsland by choice(
"block-on-dynamic",
@@ -55,12 +79,12 @@ object PickaxeAbility : FirmamentFeature {
}
}
- enum class BlockPickaxeAbility : StringIdentifiable {
+ enum class BlockPickaxeAbility : StringRepresentable {
NEVER,
ALWAYS,
ONLY_DESTRUCTIVE;
- override fun asString(): String {
+ override fun getSerializedName(): String {
return name
}
}
@@ -79,9 +103,6 @@ object PickaxeAbility : FirmamentFeature {
val destructiveAbilities = setOf("Pickobulus")
val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET)
- override val config: ManagedConfig
- get() = TConfig
-
fun getCooldownPercentage(name: String, cooldown: Duration): Double {
val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE
val sinceLobbyJoin = lobbyJoinTime.passedTime()
@@ -108,9 +129,12 @@ object PickaxeAbility : FirmamentFeature {
BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities }
}
if (shouldBlock) {
- MC.sendChat(tr("firmament.pickaxe.blocked",
- "Firmament blocked a pickaxe ability from being used on a private island.")
- .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic")
+ MC.sendChat(
+ tr(
+ "firmament.pickaxe.blocked",
+ "Firmament blocked a pickaxe ability from being used on a private island."
+ )
+ .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic")
)
event.cancel()
}
@@ -140,9 +164,9 @@ object PickaxeAbility : FirmamentFeature {
}
} ?: return
val extra = it.item.extraAttributes
- if (!extra.contains("drill_fuel")) return
- val fuel = extra.getInt("drill_fuel")
- val percentage = fuel / maxFuel.toFloat()
+ val fuel = extra.getInt("drill_fuel").getOrNull() ?: return
+ var percentage = fuel / maxFuel.toFloat()
+ if (percentage > 1f) percentage = 1f
it.barOverride = DurabilityBarEvent.DurabilityBar(
lerp(
DyeColor.RED.toShedaniel(),
@@ -156,10 +180,31 @@ object PickaxeAbility : FirmamentFeature {
fun onChatMessage(it: ProcessChatEvent) {
abilityUsePattern.useMatch(it.unformattedString) {
lastUsage[group("name")] = TimeMark.now()
+ abilityOverride = group("name")
}
abilitySwitchPattern.useMatch(it.unformattedString) {
abilityOverride = group("ability")
}
+ pickaxeAbilityCooldownPattern.useMatch(it.unformattedString) {
+ val ability = abilityOverride ?: return@useMatch
+ val remainingCooldown = parseTimePattern(group("remainingCooldown"))
+ val length = defaultAbilityDurations[ability] ?: return@useMatch
+ lastUsage[ability] = TimeMark.ago(length - remainingCooldown)
+ }
+ nowAvailable.useMatch(it.unformattedString) {
+ val ability = group("name")
+ lastUsage[ability] = TimeMark.farPast()
+ if (!TConfig.cooldownReadyToast) return
+ val mc: Minecraft = Minecraft.getInstance()
+ mc.toastManager.addToast(
+ SystemToast.multiline(
+ mc,
+ SystemToast.SystemToastId.NARRATOR_TOGGLE,
+ tr("firmament.pickaxe.ability-ready", "Pickaxe Cooldown"),
+ tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!")
+ )
+ )
+ }
}
@Subscribe
@@ -179,6 +224,7 @@ object PickaxeAbility : FirmamentFeature {
val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
val pickaxeAbilityCooldownPattern =
Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.")
+ val nowAvailable = Pattern.compile("(?<name>[a-zA-Z0-9 ]+) is now available!")
data class PickaxeAbilityData(
val name: String,
@@ -200,21 +246,27 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun renderHud(event: HudRenderEvent) {
if (!TConfig.cooldownEnabled) return
+ if (TConfig.disableInDungeons && DungeonUtil.isInDungeonIsland) return
if (!event.isRenderingCursor) return
- var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
- defaultAbilityDurations[ability.name] = ability.cooldown
+ val stack = MC.player?.getItemInHand(InteractionHand.MAIN_HAND) ?: return
+ if (!TConfig.showOnTools.matches(ItemType.fromItemStack(stack) ?: ItemType.NIL))
+ return
+ var ability = getCooldownFromLore(stack)?.also { ability ->
+ defaultAbilityDurations[ability.name] = ability.cooldown
+ }
val ao = abilityOverride
- if (ao != ability.name && ao != null) {
- ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds)
+ if (ability == null || (ao != ability.name && ao != null)) {
+ ability = PickaxeAbilityData(ao ?: return, defaultAbilityDurations[ao] ?: 120.seconds)
}
- event.context.matrices.push()
- event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F)
- event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F)
+ event.context.pose().pushMatrix()
+ event.context.pose().translate(MC.window.guiScaledWidth / 2F, MC.window.guiScaledHeight / 2F)
+ event.context.pose().scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat())
RenderCircleProgress.renderCircle(
- event.context, Identifier.of("firmament", "textures/gui/circle.png"),
+ event.context, ResourceLocation.fromNamespaceAndPath("firmament", "textures/gui/circle.png"),
getCooldownPercentage(ability.name, ability.cooldown).toFloat(),
- 0f, 1f, 0f, 1f
+ 0f, 1f, 0f, 1f,
+ color = TConfig.cooldownColour.getEffectiveColourRGB()
)
- event.context.matrices.pop()
+ event.context.pose().popMatrix()
}
}
diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt
index 377a470..0470702 100644
--- a/src/main/kotlin/features/mining/PristineProfitTracker.kt
+++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt
@@ -1,26 +1,26 @@
package moe.nea.firmament.features.mining
import io.github.notenoughupdates.moulconfig.xml.Bind
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.util.BazaarPriceStrategy
import moe.nea.firmament.util.FirmFormatters.formatCommas
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.StringUtil.parseIntWithComma
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.formattedString
import moe.nea.firmament.util.useMatch
-object PristineProfitTracker : FirmamentFeature {
- override val identifier: String
+object PristineProfitTracker {
+ val identifier: String
get() = "pristine-profit"
enum class GemstoneKind(
@@ -50,14 +50,13 @@ object PristineProfitTracker : FirmamentFeature {
var maxCollectionPerSecond: Double = 1.0,
)
+ @Config
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data)
- override val config: ManagedConfig?
- get() = TConfig
-
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) {
val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds }
- val gui by position("position", 100, 30) { Point(0.05, 0.2) }
+ val gui by position("position", 100, 30) { Vector2i() }
val useFineGemstones by toggle("fine-gemstones") { false }
}
@@ -106,28 +105,26 @@ object PristineProfitTracker : FirmamentFeature {
val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds)
if (collectionPerSecond == null || moneyPerSecond == null) return
ProfitHud.collectionCurrent = collectionPerSecond
- ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection",
+ ProfitHud.collectionText = Component.translatableEscape("firmament.pristine-profit.collection",
formatCommas(collectionPerSecond * SECONDS_PER_HOUR,
1)).formattedString()
ProfitHud.moneyCurrent = moneyPerSecond
- ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money",
+ ProfitHud.moneyText = Component.translatableEscape("firmament.pristine-profit.money",
formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1))
.formattedString()
val data = DConfig.data
- if (data != null) {
- if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
- .passedTime() > 30.seconds
- ) {
- data.maxCollectionPerSecond = collectionPerSecond
- DConfig.markDirty()
- }
- if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
- data.maxMoneyPerSecond = moneyPerSecond
- DConfig.markDirty()
- }
- ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
- ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
+ if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
+ .passedTime() > 30.seconds
+ ) {
+ data.maxCollectionPerSecond = collectionPerSecond
+ DConfig.markDirty()
+ }
+ if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
+ data.maxMoneyPerSecond = moneyPerSecond
+ DConfig.markDirty()
}
+ ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
+ ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
}
diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt
new file mode 100644
index 0000000..fe54e6b
--- /dev/null
+++ b/src/main/kotlin/features/misc/CustomCapes.kt
@@ -0,0 +1,172 @@
+package moe.nea.firmament.features.misc
+
+import util.render.CustomRenderPipelines
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.player.AbstractClientPlayer
+import net.minecraft.client.renderer.RenderType
+import com.mojang.blaze3d.vertex.VertexConsumer
+import net.minecraft.client.renderer.MultiBufferSource
+import net.minecraft.client.renderer.entity.state.AvatarRenderState
+import com.mojang.blaze3d.vertex.PoseStack
+import net.minecraft.world.entity.player.PlayerSkin
+import net.minecraft.core.ClientAsset
+import net.minecraft.resources.ResourceLocation
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.CustomRenderPassHelper
+
+object CustomCapes {
+ val identifier: String
+ get() = "developer-capes"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.DEV) {
+ val showCapes by toggle("show-cape") { true }
+ }
+
+ interface CustomCapeRenderer {
+ fun replaceRender(
+ renderLayer: RenderType,
+ vertexConsumerProvider: MultiBufferSource,
+ matrixStack: PoseStack,
+ model: (VertexConsumer) -> Unit
+ )
+ }
+
+ data class TexturedCapeRenderer(
+ val location: ResourceLocation
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderType,
+ vertexConsumerProvider: MultiBufferSource,
+ matrixStack: PoseStack,
+ model: (VertexConsumer) -> Unit
+ ) {
+ model(vertexConsumerProvider.getBuffer(RenderType.entitySolid(location)))
+ }
+ }
+
+ data class ParallaxedHighlightCapeRenderer(
+ val template: ResourceLocation,
+ val background: ResourceLocation,
+ val overlay: ResourceLocation,
+ val animationSpeed: Duration,
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderType,
+ vertexConsumerProvider: MultiBufferSource,
+ matrixStack: PoseStack,
+ model: (VertexConsumer) -> Unit
+ ) {
+ val animationValue = (startTime.passedTime() / animationSpeed).mod(1F)
+ CustomRenderPassHelper(
+ { "Firmament Cape Renderer" },
+ renderLayer.mode(),
+ renderLayer.format(),
+ MC.instance.mainRenderTarget,
+ true,
+ ).use { renderPass ->
+ renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER)
+ renderPass.setAllDefaultUniforms()
+ renderPass.setUniform("Animation", 4) {
+ it.putFloat(animationValue.toFloat())
+ }
+ renderPass.bindSampler("Sampler0", template)
+ renderPass.bindSampler("Sampler1", background)
+ renderPass.bindSampler("Sampler3", overlay)
+ renderPass.uploadVertices(2048, model)
+ renderPass.draw()
+ }
+ }
+ }
+
+ interface CapeStorage {
+ companion object {
+ @JvmStatic
+ fun cast(playerEntityRenderState: AvatarRenderState) =
+ playerEntityRenderState as CapeStorage
+
+ }
+
+ var cape_firmament: CustomCape?
+ }
+
+ data class CustomCape(
+ val id: String,
+ val label: String,
+ val render: CustomCapeRenderer,
+ )
+
+ enum class AllCapes(val label: String, val render: CustomCapeRenderer) {
+ FIRMAMENT_ANIMATED(
+ "Animated Firmament", ParallaxedHighlightCapeRenderer(
+ Firmament.identifier("textures/cape/parallax_template.png"),
+ Firmament.identifier("textures/cape/parallax_background.png"),
+ Firmament.identifier("textures/cape/firmament_star.png"),
+ 110.seconds
+ )
+ ),
+ UNPLEASANT_GRADIENT(
+ "unpleasant_gradient",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/unpleasant_gradient.png"))
+ ),
+ FURFSKY_STATIC(
+ "FurfSky",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png"))
+ ),
+
+ FIRMAMENT_STATIC(
+ "Firmament",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png"))
+ ),
+ HYPIXEL_PLUS(
+ "Hypixel+",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/h_plus.png"))
+ ),
+ ;
+
+ val cape = CustomCape(name, label, render)
+ }
+
+ val byId = AllCapes.entries.associateBy { it.cape.id }
+ val byUuid =
+ listOf(
+ listOf(
+ Devs.nea to AllCapes.UNPLEASANT_GRADIENT,
+ Devs.kath to AllCapes.FIRMAMENT_STATIC,
+ Devs.jani to AllCapes.FIRMAMENT_ANIMATED,
+ Devs.nat to AllCapes.FIRMAMENT_ANIMATED,
+ Devs.HPlus.ic22487 to AllCapes.HYPIXEL_PLUS,
+ ),
+ Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC },
+ ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap()
+
+ @JvmStatic
+ fun addCapeData(
+ player: AbstractClientPlayer,
+ playerEntityRenderState: AvatarRenderState
+ ) {
+ if (true) return // TODO: see capefeaturerenderer mixin
+ val cape = if (TConfig.showCapes) byUuid[player.uuid] else null
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ if (cape == null) {
+ capeStorage.cape_firmament = null
+ } else {
+ capeStorage.cape_firmament = cape
+ playerEntityRenderState.skin = PlayerSkin(
+ playerEntityRenderState.skin.body,
+ ClientAsset.ResourceTexture(Firmament.identifier("placeholder/fake_cape"), Firmament.identifier("placeholder/fake_cape")),
+ playerEntityRenderState.skin.elytra,
+ playerEntityRenderState.skin.model,
+ playerEntityRenderState.skin.secure,
+ )
+ playerEntityRenderState.showCape = true
+ }
+ }
+
+ val startTime = TimeMark.now()
+}
diff --git a/src/main/kotlin/features/misc/Devs.kt b/src/main/kotlin/features/misc/Devs.kt
new file mode 100644
index 0000000..b9f7e03
--- /dev/null
+++ b/src/main/kotlin/features/misc/Devs.kt
@@ -0,0 +1,42 @@
+package moe.nea.firmament.features.misc
+
+import java.util.UUID
+
+object Devs {
+ data class Dev(
+ val uuids: List<UUID>,
+ ) {
+ constructor(vararg uuid: UUID) : this(uuid.toList())
+ constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) })
+ }
+
+ val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948")
+ val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf")
+ val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df")
+ val nat = Dev("168300e6-4e74-4a6d-89a0-7b7cf8ad6a7d", "06914e9d-7bc2-4cb7-b112-62c4cc958d96")
+
+ object FurfSky {
+ val smolegit = Dev("02b38b96-eb19-405a-b319-d6bc00b26ab3")
+ val itsCen = Dev("ada70b5a-ac37-49d2-b18c-1351672f8051")
+ val webster = Dev("02166f1b-9e8d-4e48-9e18-ea7a4499492d")
+ val vrachel = Dev("22e98637-ba97-4b6b-a84f-fb57a461ce43")
+ val cunuduh = Dev("2a15e3b3-c46e-4718-b907-166e1ab2efdc")
+ val eiiies = Dev("2ae162f2-81a7-4f91-a4b2-104e78a0a7e1")
+ val june = Dev("2584a4e3-f917-4493-8ced-618391f3b44f")
+ val denasu = Dev("313cbd25-8ade-4e41-845c-5cab555a30c9")
+ val libyKiwii = Dev("4265c52e-bd6f-4d3c-9cf6-bdfc8fb58023")
+ val madeleaan = Dev("bcb119a3-6000-4324-bda1-744f00c44b31")
+ val turtleSP = Dev("f1ca1934-a582-4723-8283-89921d008657")
+ val papayamm = Dev("ae0eea30-f6a2-40fe-ac17-9c80b3423409")
+ val persuasiveViksy = Dev("ba7ac144-28e0-4f55-a108-1a72fe744c9e")
+ val all = listOf(
+ smolegit, itsCen, webster, vrachel, cunuduh, eiiies,
+ june, denasu, libyKiwii, madeleaan, turtleSP, papayamm,
+ persuasiveViksy
+ )
+ }
+ object HPlus {
+ val ic22487 = Dev("ab2be3b2-bb75-4aaa-892d-9fff5a7e3953")
+ }
+
+}
diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt
new file mode 100644
index 0000000..bbe8044
--- /dev/null
+++ b/src/main/kotlin/features/misc/Hud.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.features.misc
+
+import org.joml.Vector2i
+import net.minecraft.client.multiplayer.PlayerInfo
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
+
+object Hud {
+ val identifier: String
+ get() = "hud"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.MISC) {
+ var dayCount by toggle("day-count") { false }
+ val dayCountHud by position("day-count-hud", 80, 10) { Vector2i() }
+ var fpsCount by toggle("fps-count") { false }
+ val fpsCountHud by position("fps-count-hud", 80, 10) { Vector2i() }
+ var pingCount by toggle("ping-count") { false }
+ val pingCountHud by position("ping-count-hud", 80, 10) { Vector2i() }
+ }
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (TConfig.dayCount) {
+ it.context.pose().pushMatrix()
+ TConfig.dayCountHud.applyTransformations(it.context.pose())
+ val day = (MC.world?.dayTime ?: 0L) / 24000
+ it.context.drawString(
+ MC.font,
+ Component.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)),
+ 36,
+ MC.font.lineHeight,
+ -1,
+ true
+ )
+ it.context.pose().popMatrix()
+ }
+
+ if (TConfig.fpsCount) {
+ it.context.pose().pushMatrix()
+ TConfig.fpsCountHud.applyTransformations(it.context.pose())
+ it.context.drawString(
+ MC.font, Component.literal(
+ String.format(
+ tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.fps
+ )
+ ), 36, MC.font.lineHeight, -1, true
+ )
+ it.context.pose().popMatrix()
+ }
+
+ if (TConfig.pingCount) {
+ it.context.pose().pushMatrix()
+ TConfig.pingCountHud.applyTransformations(it.context.pose())
+ val ping = MC.player?.let {
+ val entry: PlayerInfo? = MC.networkHandler?.getPlayerInfo(it.uuid)
+ entry?.latency ?: -1
+ } ?: -1
+ it.context.drawString(
+ MC.font, Component.literal(
+ String.format(
+ tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping
+ )
+ ), 36, MC.font.lineHeight, -1, true
+ )
+
+ it.context.pose().popMatrix()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt
new file mode 100644
index 0000000..26bec80
--- /dev/null
+++ b/src/main/kotlin/features/misc/LicenseViewer.kt
@@ -0,0 +1,136 @@
+package moe.nea.firmament.features.misc
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.json.decodeFromStream
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.tr
+
+object LicenseViewer {
+ @Serializable
+ data class Software(
+ val licenses: List<License> = listOf(),
+ val webPresence: String? = null,
+ val projectName: String,
+ val projectDescription: String? = null,
+ val developers: List<Developer> = listOf(),
+ ) {
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = Component.literal(webPresence ?: "<no web presence>")
+
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun projectName() = Component.literal(projectName)
+
+ @Bind
+ fun projectDescription() = Component.literal(projectDescription ?: "<no project description>")
+
+ @get:Bind("developers")
+ @Transient
+ val developersO = ObservableList(developers)
+
+ @get:Bind("licenses")
+ @Transient
+ val licenses0 = ObservableList(licenses)
+ }
+
+ @Serializable
+ data class Developer(
+ val name: String,
+ val webPresence: String? = null
+ ) {
+
+ @Bind("name")
+ fun nameT() = Component.literal(name)
+
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = Component.literal(webPresence ?: "<no web presence>")
+ }
+
+ @Serializable
+ data class License(
+ val licenseName: String,
+ val licenseUrl: String? = null
+ ) {
+ @Bind("name")
+ fun nameG() = Component.literal(licenseName)
+
+ @Bind
+ fun open() {
+ MC.openUrl(licenseUrl ?: return)
+ }
+
+ @Bind
+ fun hasUrl() = licenseUrl != null
+
+ @Bind
+ fun url() = Component.literal(licenseUrl ?: "<no link to license text>")
+ }
+
+ data class LicenseList(
+ val softwares: List<Software>
+ ) {
+ @get:Bind("softwares")
+ val obs = ObservableList(softwares)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") {
+ Firmament.json.decodeFromStream<List<Software>?>(
+ javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json")
+ )?.let { LicenseList(it) }
+ }.orNull()
+
+ fun showLicenses() {
+ ErrorUtil.catch("Could not display licenses") {
+ ScreenUtil.setScreenLater(
+ MoulConfigUtils.loadScreen(
+ "license_viewer/index", licenses!!, null
+ )
+ )
+ }.or {
+ MC.sendChat(
+ tr(
+ "firmament.licenses.notfound",
+ "Could not load licenses. Please check the Firmament source code for information directly."
+ )
+ )
+ }
+ }
+
+ @Subscribe
+ fun onSubcommand(event: CommandEvent.SubCommand) {
+ event.subcommand("licenses") {
+ thenExecute {
+ showLicenses()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/misc/ModAnnouncer.kt b/src/main/kotlin/features/misc/ModAnnouncer.kt
new file mode 100644
index 0000000..c2aa013
--- /dev/null
+++ b/src/main/kotlin/features/misc/ModAnnouncer.kt
@@ -0,0 +1,80 @@
+package moe.nea.firmament.features.misc
+
+import io.netty.buffer.ByteBuf
+import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
+import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry
+import net.fabricmc.loader.api.FabricLoader
+import net.minecraft.network.codec.StreamCodec
+import net.minecraft.network.codec.ByteBufCodecs
+import net.minecraft.network.protocol.common.custom.CustomPacketPayload
+import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.JoinServerEvent
+
+/**
+ * This is a class that announces all mods to hypixel (or any other server you connect to).
+ *
+ * I don't mind cheat mods, i just think they should be honest about themselves.
+ *
+ * If you are a cheat mod (or think you could possibly be seen as one), you have two options:
+ *
+ * - risk getting your users banned from hypixel by exposing your modid + version number in a packet sent on first connect
+ * - hide yourself from this list by either a mixin, or (more easily) by specifying a tag in your fabric.mod.json:
+ *
+ * ```json
+ * {
+ * "schemaVersion": 1,
+ * "id": "my-cheat-mod",
+ * "custom": { "firmament:hide_from_modlist": true }
+ * }
+ * ```
+ */
+object ModAnnouncer {
+
+ data class ModEntry(
+ val modid: String,
+ val modVersion: String,
+ ) {
+ companion object {
+ val CODEC: StreamCodec<ByteBuf, ModEntry> = StreamCodec.composite(
+ ByteBufCodecs.STRING_UTF8, ModEntry::modid,
+ ByteBufCodecs.STRING_UTF8, ModEntry::modVersion,
+ ::ModEntry
+ )
+ }
+ }
+
+ data class ModPacket(
+ val mods: List<ModEntry>,
+ ) : CustomPacketPayload {
+ override fun type(): CustomPacketPayload.Type<out ModPacket> {
+ return ID
+ }
+
+ companion object {
+ val ID = CustomPacketPayload.Type<ModPacket>(Firmament.identifier("mod_list"))
+ val CODEC: StreamCodec<ByteBuf, ModPacket> = ModEntry.CODEC.apply(ByteBufCodecs.list())
+ .map(::ModPacket, ModPacket::mods)
+ }
+ }
+
+ @Subscribe
+ fun onServerJoin(event: JoinServerEvent) {
+ val packet = ModPacket(
+ FabricLoader.getInstance()
+ .allMods
+ .filter { !it.metadata.containsCustomValue("firmament:hide_from_modlist") }
+ .map { ModEntry(it.metadata.id, it.metadata.version.friendlyString) })
+ val pbb = PacketByteBufs.create()
+ ModPacket.CODEC.encode(pbb, packet)
+ if (pbb.writerIndex() > ServerboundCustomPayloadPacket.MAX_PAYLOAD_SIZE)
+ return
+
+ event.networkHandler.send(event.packetSender.createPacket(packet))
+ }
+
+ init {
+ PayloadTypeRegistry.playC2S().register(ModPacket.ID, ModPacket.CODEC)
+ }
+}
diff --git a/src/main/kotlin/features/notifications/Notifications.kt b/src/main/kotlin/features/notifications/Notifications.kt
deleted file mode 100644
index 8d912d1..0000000
--- a/src/main/kotlin/features/notifications/Notifications.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-
-package moe.nea.firmament.features.notifications
-
-import moe.nea.firmament.features.FirmamentFeature
-
-object Notifications {
-}
diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt
new file mode 100644
index 0000000..3597d6d
--- /dev/null
+++ b/src/main/kotlin/features/world/ColeWeightCompat.kt
@@ -0,0 +1,125 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import net.minecraft.network.chat.Component
+import net.minecraft.core.BlockPos
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+
+object ColeWeightCompat {
+ @Serializable
+ data class ColeWeightWaypoint(
+ val x: Int?,
+ val y: Int?,
+ val z: Int?,
+ val r: Int = 0,
+ val g: Int = 0,
+ val b: Int = 0,
+ )
+
+ fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> {
+ return waypoints.waypoints.map {
+ ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z)
+ }
+ }
+
+ fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints {
+ val w = waypoints
+ .filter { it.x != null && it.y != null && it.z != null }
+ .map { FirmWaypoints.Waypoint(it.x!! + relativeTo.x, it.y!! + relativeTo.y, it.z!! + relativeTo.z) }
+ return FirmWaypoints(
+ "Imported Waypoints",
+ "imported",
+ null,
+ w.toMutableList(),
+ false
+ )
+ }
+
+ fun copyAndInform(
+ source: DefaultSource,
+ origin: BlockPos,
+ positiveFeedback: (Int) -> Component,
+ ) {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ ?.let { fromFirm(it, origin) }
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return
+ }
+ val data =
+ Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ fun importAndInform(
+ source: DefaultSource,
+ pos: BlockPos?,
+ positiveFeedback: (Int) -> Component
+ ) {
+ val text = ClipboardUtils.getTextContents()
+ val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ZERO) }
+ val waypoints = wr.getOrElse {
+ source.sendError(
+ tr("firmament.command.waypoint.import.cw.error",
+ "Could not import ColeWeight waypoints."))
+ Firmament.logger.error(it)
+ return
+ }
+ waypoints.lastRelativeImport = pos
+ Waypoints.waypoints = waypoints
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ @Subscribe
+ fun onEvent(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("exportcw") {
+ thenExecute {
+ copyAndInform(source, BlockPos.ZERO) {
+ tr("firmament.command.waypoint.export.cw",
+ "Copied $it waypoints to clipboard in ColeWeight format.")
+ }
+ }
+ }
+ thenLiteral("exportrelativecw") {
+ thenExecute {
+ copyAndInform(source, MC.player?.blockPosition() ?: BlockPos.ZERO) {
+ tr("firmament.command.waypoint.export.cw.relative",
+ "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.")
+ }
+ }
+ }
+ thenLiteral("importcw") {
+ thenExecute {
+ importAndInform(source, null) {
+ tr("firmament.command.waypoint.import.cw.success",
+ "Imported $it waypoints from ColeWeight.")
+ }
+ }
+ }
+ thenLiteral("importrelativecw") {
+ thenExecute {
+ importAndInform(source, MC.player!!.blockPosition()) {
+ tr("firmament.command.waypoint.import.cw.relative",
+ "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.")
+ }
+ }
+ }
+ }
+ }
+
+ fun tryParse(string: String): Result<List<ColeWeightWaypoint>> {
+ return runCatching {
+ Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt
index 1263074..b073726 100644
--- a/src/main/kotlin/features/world/FairySouls.kt
+++ b/src/main/kotlin/features/world/FairySouls.kt
@@ -1,131 +1,123 @@
-
-
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.Coordinate
+import me.shedaniel.math.Color
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
-import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldRenderLastEvent
-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
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.blockPos
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
-import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
import moe.nea.firmament.util.unformattedString
-object FairySouls : FirmamentFeature {
-
-
- @Serializable
- data class Data(
- val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
- )
-
- override val config: ManagedConfig
- get() = TConfig
-
- object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
-
-
- object TConfig : ManagedConfig("fairy-souls", Category.MISC) {
- val displaySouls by toggle("show") { false }
- val resetSouls by button("reset") {
- DConfig.data?.foundSouls?.clear() != null
- updateMissingSouls()
- }
- }
-
-
- override val identifier: String get() = "fairy-souls"
-
- val playerReach = 5
- val playerReachSquared = playerReach * playerReach
-
- var currentLocationName: SkyBlockIsland? = null
- var currentLocationSouls: List<Coordinate> = emptyList()
- var currentMissingSouls: List<Coordinate> = emptyList()
-
- fun updateMissingSouls() {
- currentMissingSouls = emptyList()
- val c = DConfig.data ?: return
- val fi = c.foundSouls[currentLocationName] ?: setOf()
- val cms = currentLocationSouls.toMutableList()
- fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
- currentMissingSouls = cms
- }
-
- fun updateWorldSouls() {
- currentLocationSouls = emptyList()
- val loc = currentLocationName ?: return
- currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
- }
-
- fun findNearestClickableSoul(): Coordinate? {
- val player = MC.player ?: return null
- val pos = player.pos
- val location = SBData.skyblockLocation ?: return null
- val soulLocations: List<Coordinate> =
- RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
- return soulLocations
- .map { it to it.blockPos.getSquaredDistance(pos) }
- .filter { it.second < playerReachSquared }
- .minByOrNull { it.second }
- ?.first
- }
-
- private fun markNearestSoul() {
- val nearestSoul = findNearestClickableSoul() ?: return
- val c = DConfig.data ?: return
- val loc = currentLocationName ?: return
- val idx = currentLocationSouls.indexOf(nearestSoul)
- c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
- DConfig.markDirty()
- updateMissingSouls()
- }
-
- @Subscribe
- fun onWorldRender(it: WorldRenderLastEvent) {
- if (!TConfig.displaySouls) return
- renderInWorld(it) {
- currentMissingSouls.forEach {
- block(it.blockPos, 0x80FFFF00.toInt())
- }
- color(1f, 0f, 1f, 1f)
- currentLocationSouls.forEach {
- wireframeCube(it.blockPos)
- }
- }
- }
-
- @Subscribe
- fun onProcessChat(it: ProcessChatEvent) {
- when (it.text.unformattedString) {
- "You have already found that Fairy Soul!" -> {
- markNearestSoul()
- }
-
- "SOUL! You found a Fairy Soul!" -> {
- markNearestSoul()
- }
- }
- }
-
- @Subscribe
- fun onLocationChange(it: SkyblockServerUpdateEvent) {
- currentLocationName = it.newLocraw?.skyblockLocation
- updateWorldSouls()
- updateMissingSouls()
- }
+object FairySouls {
+
+
+ @Serializable
+ data class Data(
+ val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
+ )
+
+ @Config
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
+
+ @Config
+ object TConfig : ManagedConfig("fairy-souls", Category.MISC) {
+ val displaySouls by toggle("show") { false }
+ val resetSouls by button("reset") {
+ DConfig.data?.foundSouls?.clear() != null
+ updateMissingSouls()
+ }
+ }
+
+
+ val identifier: String get() = "fairy-souls"
+
+ val playerReach = 5
+ val playerReachSquared = playerReach * playerReach
+
+ var currentLocationName: SkyBlockIsland? = null
+ var currentLocationSouls: List<Coordinate> = emptyList()
+ var currentMissingSouls: List<Coordinate> = emptyList()
+
+ fun updateMissingSouls() {
+ currentMissingSouls = emptyList()
+ val c = DConfig.data ?: return
+ val fi = c.foundSouls[currentLocationName] ?: setOf()
+ val cms = currentLocationSouls.toMutableList()
+ fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
+ currentMissingSouls = cms
+ }
+
+ fun updateWorldSouls() {
+ currentLocationSouls = emptyList()
+ val loc = currentLocationName ?: return
+ currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
+ }
+
+ fun findNearestClickableSoul(): Coordinate? {
+ val player = MC.player ?: return null
+ val pos = player.position
+ val location = SBData.skyblockLocation ?: return null
+ val soulLocations: List<Coordinate> =
+ RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
+ return soulLocations
+ .map { it to it.blockPos.distToCenterSqr(pos) }
+ .filter { it.second < playerReachSquared }
+ .minByOrNull { it.second }
+ ?.first
+ }
+
+ private fun markNearestSoul() {
+ val nearestSoul = findNearestClickableSoul() ?: return
+ val c = DConfig.data ?: return
+ val loc = currentLocationName ?: return
+ val idx = currentLocationSouls.indexOf(nearestSoul)
+ c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
+ DConfig.markDirty()
+ updateMissingSouls()
+ }
+
+ @Subscribe
+ fun onWorldRender(it: WorldRenderLastEvent) {
+ if (!TConfig.displaySouls) return
+ renderInWorld(it) {
+ currentMissingSouls.forEach {
+ block(it.blockPos, Color.ofRGBA(176, 0, 255, 128).color)
+ }
+ currentLocationSouls.forEach {
+ wireframeCube(it.blockPos)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ when (it.text.unformattedString) {
+ "You have already found that Fairy Soul!" -> {
+ markNearestSoul()
+ }
+
+ "SOUL! You found a Fairy Soul!" -> {
+ markNearestSoul()
+ }
+ }
+ }
+
+ @Subscribe
+ fun onLocationChange(it: SkyblockServerUpdateEvent) {
+ currentLocationName = it.newLocraw?.skyblockLocation
+ updateWorldSouls()
+ updateMissingSouls()
+ }
}
diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt
new file mode 100644
index 0000000..c6e2610
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypointManager.kt
@@ -0,0 +1,168 @@
+package moe.nea.firmament.features.world
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import kotlinx.serialization.serializer
+import net.minecraft.network.chat.Component
+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.suggestsList
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TemplateUtil
+import moe.nea.firmament.util.data.DataHolder
+import moe.nea.firmament.util.tr
+
+object FirmWaypointManager {
+ object DConfig : DataHolder<MutableMap<String, FirmWaypoints>>(serializer(), "waypoints", ::mutableMapOf)
+
+ val SHARE_PREFIX = "FIRM_WAYPOINTS/"
+ val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX)
+
+ fun createExportableCopy(
+ waypoints: FirmWaypoints,
+ ): FirmWaypoints {
+ val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList())
+ if (waypoints.isRelativeTo != null) {
+ val origin = waypoints.lastRelativeImport
+ if (origin != null) {
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x - origin.x,
+ y = it.y - origin.y,
+ z = it.z - origin.z,
+ )
+ }
+ } else {
+ TODO("Add warning!")
+ }
+ }
+ return copy
+ }
+
+ fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Component) -> Unit) {
+ val copy = waypoints.deepCopy()
+ if (copy.isRelativeTo != null) {
+ val origin = MC.player!!.blockPosition()
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x + origin.x,
+ y = it.y + origin.y,
+ z = it.z + origin.z,
+ )
+ }
+ copy.lastRelativeImport = origin.immutable()
+ sendFeedback(tr("firmament.command.waypoint.import.ordered.success",
+ "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}."))
+ } else {
+ sendFeedback(tr("firmament.command.waypoint.import.success",
+ "Imported ${copy.size} waypoints."))
+ }
+ Waypoints.waypoints = copy
+ }
+
+ fun setOrigin(source: DefaultSource, text: String?) {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: ""
+ val pos = MC.player!!.blockPosition()
+ waypoints.lastRelativeImport = pos
+ source.sendFeedback(tr("firmament.command.waypoint.originset",
+ "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position."))
+ }
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("setorigin") {
+ thenExecute {
+ setOrigin(source, null)
+ }
+ thenArgument("hint", RestArgumentType) { text ->
+ thenExecute {
+ setOrigin(source, this[text])
+ }
+ }
+ }
+ thenLiteral("clearorigin") {
+ thenExecute {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.lastRelativeImport = null
+ waypoints.isRelativeTo = null
+ source.sendFeedback(tr("firmament.command.waypoint.originunset",
+ "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates."))
+ }
+ }
+ thenLiteral("save") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DConfig.data.keys }
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ waypoints.id = get(name)
+ val exportableWaypoints = createExportableCopy(waypoints)
+ DConfig.data[get(name)] = exportableWaypoints
+ DConfig.markDirty()
+ source.sendFeedback(tr("firmament.command.waypoint.saved",
+ "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again."))
+ }
+ }
+ }
+ thenLiteral("load") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DConfig.data.keys }
+ thenExecute {
+ val name = get(name)
+ val waypoints = DConfig.data[name]
+ if (waypoints == null) {
+ source.sendError(
+ tr("firmament.command.waypoint.nosaved",
+ "No saved waypoint for ${name}. Use tab completion to see available names."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ thenLiteral("export") {
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ val exportableWaypoints = createExportableCopy(waypoints)
+ val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(tr("firmament.command.waypoint.export",
+ "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format."))
+ }
+ }
+ thenLiteral("import") {
+ thenExecute {
+ val text = ClipboardUtils.getTextContents()
+ if (text.startsWith("[")) {
+ source.sendError(tr("firmament.command.waypoint.import.lookslikecw",
+ "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw."))
+ return@thenExecute
+ }
+ val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text)
+ if (waypoints == null) {
+ source.sendError(tr("firmament.command.waypoint.import.error",
+ "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt
new file mode 100644
index 0000000..0b018cb
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypoints.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.minecraft.core.BlockPos
+
+@Serializable
+data class FirmWaypoints(
+ var label: String,
+ var id: String,
+ /**
+ * A hint to indicate where to stand while loading the waypoints.
+ */
+ var isRelativeTo: String?,
+ var waypoints: MutableList<Waypoint>,
+ var isOrdered: Boolean,
+ // TODO: val resetOnSwap: Boolean,
+) {
+
+ fun deepCopy() = copy(waypoints = waypoints.toMutableList())
+ @Transient
+ var lastRelativeImport: BlockPos? = null
+
+ val size get() = waypoints.size
+ @Serializable
+ data class Waypoint(
+ val x: Int,
+ val y: Int,
+ val z: Int,
+ ) {
+ val blockPos get() = BlockPos(x, y, z)
+
+ companion object {
+ fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/NavigableWaypoint.kt b/src/main/kotlin/features/world/NavigableWaypoint.kt
index 28a517f..653fd87 100644
--- a/src/main/kotlin/features/world/NavigableWaypoint.kt
+++ b/src/main/kotlin/features/world/NavigableWaypoint.kt
@@ -1,7 +1,7 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.NEUItem
-import net.minecraft.util.math.BlockPos
+import net.minecraft.core.BlockPos
import moe.nea.firmament.util.SkyBlockIsland
abstract class NavigableWaypoint {
diff --git a/src/main/kotlin/features/world/NavigationHelper.kt b/src/main/kotlin/features/world/NavigationHelper.kt
index acdfb86..ea886bf 100644
--- a/src/main/kotlin/features/world/NavigationHelper.kt
+++ b/src/main/kotlin/features/world/NavigationHelper.kt
@@ -1,10 +1,10 @@
package moe.nea.firmament.features.world
import io.github.moulberry.repo.constants.Islands
-import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Position
-import net.minecraft.util.math.Vec3i
+import net.minecraft.network.chat.Component
+import net.minecraft.core.BlockPos
+import net.minecraft.core.Position
+import net.minecraft.core.Vec3i
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.TickEvent
@@ -72,7 +72,7 @@ object NavigationHelper {
fun onMovement(event: TickEvent) { // TODO: add a movement tick event maybe?
val tp = targetWaypoint ?: return
val p = MC.player ?: return
- if (p.squaredDistanceTo(tp.position.toCenterPos()) < 5 * 5) {
+ if (p.distanceToSqr(tp.position.center) < 5 * 5) {
targetWaypoint = null
}
}
@@ -84,11 +84,11 @@ object NavigationHelper {
RenderInWorldContext.renderInWorld(event) {
if (nt != null) {
waypoint(nt.blockPos,
- Text.literal("Teleporter to " + nt.toIsland.userFriendlyName),
- Text.literal("(towards " + tp.name + "§f)"))
+ Component.literal("Teleporter to " + nt.toIsland.userFriendlyName),
+ Component.literal("(towards " + tp.name + "§f)"))
} else if (tp.island == SBData.skyblockLocation) {
waypoint(tp.position,
- Text.literal(tp.name))
+ Component.literal(tp.name))
}
}
}
@@ -96,7 +96,7 @@ object NavigationHelper {
fun tryWarpNear() {
val tp = targetWaypoint
if (tp == null) {
- MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first."))
+ MC.sendChat(Component.literal("Could not find a waypoint to warp you to. Select one first."))
return
}
WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView())
@@ -106,15 +106,15 @@ object NavigationHelper {
fun Vec3i.asPositionView(): Position {
return object : Position {
- override fun getX(): Double {
+ override fun x(): Double {
return this@asPositionView.x.toDouble()
}
- override fun getY(): Double {
+ override fun y(): Double {
return this@asPositionView.y.toDouble()
}
- override fun getZ(): Double {
+ override fun z(): Double {
return this@asPositionView.z.toDouble()
}
}
diff --git a/src/main/kotlin/features/world/NpcWaypointGui.kt b/src/main/kotlin/features/world/NpcWaypointGui.kt
index 6146e50..3bae3e8 100644
--- a/src/main/kotlin/features/world/NpcWaypointGui.kt
+++ b/src/main/kotlin/features/world/NpcWaypointGui.kt
@@ -2,6 +2,7 @@ package moe.nea.firmament.features.world
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
+import net.minecraft.network.chat.Component
import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce
import moe.nea.firmament.keybindings.SavedKeyBinding
@@ -11,7 +12,7 @@ class NpcWaypointGui(
data class NavigableWaypointW(val waypoint: NavigableWaypoint) {
@Bind
- fun name() = waypoint.name
+ fun name() = Component.literal(waypoint.name)
@Bind
fun isSelected() = NavigationHelper.targetWaypoint == waypoint
diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt
new file mode 100644
index 0000000..ac1600e
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,72 @@
+package moe.nea.firmament.features.world
+
+import me.shedaniel.math.Color
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.network.chat.Component
+import net.minecraft.core.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.world.Waypoints.TConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object TemporaryWaypoints {
+ data class TemporaryWaypoint(
+ val pos: BlockPos,
+ val postedAt: TimeMark,
+ )
+ val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
+ val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
+ if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
+ temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos(
+ matcher.group(1).toInt(),
+ matcher.group(2).toInt(),
+ matcher.group(3).toInt(),
+ ), TimeMark.now())
+ }
+ }
+ @Subscribe
+ fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
+ temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
+ if (temporaryPlayerWaypointList.isEmpty()) return
+ RenderInWorldContext.renderInWorld(event) {
+ temporaryPlayerWaypointList.forEach { (_, waypoint) ->
+ block(waypoint.pos, Color.ofRGBA(255, 255, 0, 128).color)
+ }
+ temporaryPlayerWaypointList.forEach { (player, waypoint) ->
+ val skin =
+ MC.networkHandler?.listedOnlinePlayers?.find { it.profile.name == player }?.skin?.body
+ withFacingThePlayer(waypoint.pos.center) {
+ waypoint(waypoint.pos, Component.translatableEscape("firmament.waypoint.temporary", player))
+ if (skin != null) {
+ matrixStack.translate(0F, -20F, 0F)
+ // Head front
+ texture(
+ skin.id(), 16, 16, // TODO: 1.21.10 test
+ 1 / 8f, 1 / 8f,
+ 2 / 8f, 2 / 8f,
+ )
+ // Head overlay
+ texture(
+ skin.id(), 16, 16, // TODO: 1.21.10
+ 5 / 8f, 1 / 8f,
+ 6 / 8f, 2 / 8f,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldReady(event: WorldReadyEvent) {
+ temporaryPlayerWaypointList.clear()
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
index 16db059..9097d3a 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,150 +2,129 @@ package moe.nea.firmament.features.world
import com.mojang.brigadier.arguments.IntegerArgumentType
import me.shedaniel.math.Color
-import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
-import kotlinx.serialization.Serializable
-import kotlin.collections.component1
-import kotlin.collections.component2
-import kotlin.collections.set
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
-import net.minecraft.command.argument.BlockPosArgumentType
-import net.minecraft.server.command.CommandOutput
-import net.minecraft.server.command.ServerCommandSource
-import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Vec3d
-import moe.nea.firmament.Firmament
+import net.minecraft.commands.arguments.coordinates.BlockPosArgument
+import net.minecraft.network.chat.Component
+import net.minecraft.world.phys.Vec3
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
-import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.TickEvent
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
-import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.asFakeServer
import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.tr
-object Waypoints : FirmamentFeature {
- override val identifier: String
+object Waypoints {
+ val identifier: String
get() = "waypoints"
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) { // TODO: add to misc
val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds }
val showIndex by toggle("show-index") { true }
val skipToNearest by toggle("skip-to-nearest") { false }
+ val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true }
// TODO: look ahead size
}
- data class TemporaryWaypoint(
- val pos: BlockPos,
- val postedAt: TimeMark,
- )
-
- override val config get() = TConfig
-
- val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
- val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
-
- val waypoints = mutableListOf<BlockPos>()
- var ordered = false
+ var waypoints: FirmWaypoints? = null
var orderedIndex = 0
- @Serializable
- data class ColeWeightWaypoint(
- val x: Int,
- val y: Int,
- val z: Int,
- val r: Int = 0,
- val g: Int = 0,
- val b: Int = 0,
- )
-
@Subscribe
fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) {
- if (waypoints.isEmpty()) return
+ val w = useNonEmptyWaypoints() ?: return
RenderInWorldContext.renderInWorld(event) {
- if (!ordered) {
- waypoints.withIndex().forEach {
- block(it.value, 0x800050A0.toInt())
- if (TConfig.showIndex)
- withFacingThePlayer(it.value.toCenterPos()) {
- text(Text.literal(it.index.toString()))
- }
+ if (!w.isOrdered) {
+ w.waypoints.withIndex().forEach {
+ block(it.value.blockPos, Color.ofRGBA(0, 80, 160, 128).color)
+ if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.center) {
+ text(Component.literal(it.index.toString()))
+ }
}
} else {
- orderedIndex %= waypoints.size
+ orderedIndex %= w.waypoints.size
val firstColor = Color.ofRGBA(0, 200, 40, 180)
- color(firstColor)
- tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f)
- waypoints.withIndex().toList()
- .wrappingWindow(orderedIndex, 3)
- .zip(
- listOf(
- firstColor,
- Color.ofRGBA(180, 200, 40, 150),
- Color.ofRGBA(180, 80, 20, 140),
- )
+ tracer(w.waypoints[orderedIndex].blockPos.center, color = firstColor.color, lineWidth = 3f)
+ w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(
+ listOf(
+ firstColor,
+ Color.ofRGBA(180, 200, 40, 150),
+ Color.ofRGBA(180, 80, 20, 140),
)
- .reversed()
- .forEach { (waypoint, col) ->
- val (index, pos) = waypoint
- block(pos, col.color)
- if (TConfig.showIndex)
- withFacingThePlayer(pos.toCenterPos()) {
- text(Text.literal(index.toString()))
- }
+ ).reversed().forEach { (waypoint, col) ->
+ val (index, pos) = waypoint
+ block(pos.blockPos, col.color)
+ if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.center) {
+ text(Component.literal(index.toString()))
}
+ }
}
}
}
@Subscribe
fun onTick(event: TickEvent) {
- if (waypoints.isEmpty() || !ordered) return
- orderedIndex %= waypoints.size
- val p = MC.player?.pos ?: return
+ val w = useNonEmptyWaypoints() ?: return
+ if (!w.isOrdered) return
+ orderedIndex %= w.waypoints.size
+ val p = MC.player?.position ?: return
if (TConfig.skipToNearest) {
orderedIndex =
- (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size
+ (w.waypoints.withIndex().minBy { it.value.blockPos.distToCenterSqr(p) }.index + 1) % w.waypoints.size
+
} else {
- if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ if (w.waypoints[orderedIndex].blockPos.closerToCenterThan(p, 3.0)) {
+ orderedIndex = (orderedIndex + 1) % w.waypoints.size
}
}
}
+
+ fun useEditableWaypoints(): FirmWaypoints {
+ var w = waypoints
+ if (w == null) {
+ w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false)
+ waypoints = w
+ }
+ return w
+ }
+
+ fun useNonEmptyWaypoints(): FirmWaypoints? {
+ val w = waypoints
+ if (w == null) return null
+ if (w.waypoints.isEmpty()) return null
+ return w
+ }
+
+ val WAYPOINTS_SUBCOMMAND = "waypoints"
+
@Subscribe
- fun onProcessChat(it: ProcessChatEvent) {
- val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
- if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
- temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(
- BlockPos(
- matcher.group(1).toInt(),
- matcher.group(2).toInt(),
- matcher.group(3).toInt(),
- ),
- TimeMark.now()
- )
+ fun onWorldSwap(event: WorldReadyEvent) {
+ if (TConfig.resetWaypointOrderOnWorldSwap) {
+ orderedIndex = 0
}
}
@Subscribe
fun onCommand(event: CommandEvent.SubCommand) {
event.subcommand("waypoint") {
- thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
+ thenArgument("pos", BlockPosArgument.blockPos()) { pos ->
thenExecute {
- val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
- waypoints.add(position)
+ source
+ val position = pos.get(this).getBlockPos(source.asFakeServer())
+ val w = useEditableWaypoints()
+ w.waypoints.add(FirmWaypoints.Waypoint.from(position))
source.sendFeedback(
- Text.stringifiedTranslatable(
+ Component.translatableEscape(
"firmament.command.waypoint.added",
position.x,
position.y,
@@ -155,31 +134,75 @@ object Waypoints : FirmamentFeature {
}
}
}
- event.subcommand("waypoints") {
+ event.subcommand(WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("reset") {
+ thenExecute {
+ orderedIndex = 0
+ source.sendFeedback(
+ tr(
+ "firmament.command.waypoint.reset",
+ "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead."
+ )
+ )
+ }
+ }
+ thenLiteral("changeindex") {
+ thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex ->
+ thenArgument("to", IntegerArgumentType.integer(0)) { toIndex ->
+ thenExecute {
+ val w = useEditableWaypoints()
+ val toIndex = toIndex.get(this)
+ val fromIndex = fromIndex.get(this)
+ if (fromIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(fromIndex))
+ return@thenExecute
+ }
+ if (toIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(toIndex))
+ return@thenExecute
+ }
+ val waypoint = w.waypoints.removeAt(fromIndex)
+ w.waypoints.add(
+ if (toIndex > fromIndex) toIndex - 1
+ else toIndex,
+ waypoint
+ )
+ source.sendFeedback(
+ tr(
+ "firmament.command.waypoint.indexchange",
+ "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints."
+ )
+ )
+ }
+ }
+ }
+ }
thenLiteral("clear") {
thenExecute {
- waypoints.clear()
- source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
+ waypoints = null
+ source.sendFeedback(Component.translatable("firmament.command.waypoint.clear"))
}
}
thenLiteral("toggleordered") {
thenExecute {
- ordered = !ordered
- if (ordered) {
- val p = MC.player?.pos ?: Vec3d.ZERO
- orderedIndex =
- waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
+ val w = useEditableWaypoints()
+ w.isOrdered = !w.isOrdered
+ if (w.isOrdered) {
+ val p = MC.player?.position ?: Vec3.ZERO
+ orderedIndex = // TODO: this should be extracted to a utility method
+ w.waypoints.withIndex().minByOrNull { it.value.blockPos.distToCenterSqr(p) }?.index ?: 0
}
- source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
+ source.sendFeedback(Component.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}"))
}
}
thenLiteral("skip") {
thenExecute {
- if (ordered && waypoints.isNotEmpty()) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
- source.sendFeedback(Text.translatable("firmament.command.waypoint.skip"))
+ val w = useNonEmptyWaypoints()
+ if (w != null && w.isOrdered) {
+ orderedIndex = (orderedIndex + 1) % w.size
+ source.sendFeedback(Component.translatable("firmament.command.waypoint.skip"))
} else {
- source.sendError(Text.translatable("firmament.command.waypoint.skip.error"))
+ source.sendError(Component.translatable("firmament.command.waypoint.skip.error"))
}
}
}
@@ -187,79 +210,35 @@ object Waypoints : FirmamentFeature {
thenArgument("index", IntegerArgumentType.integer(0)) { indexArg ->
thenExecute {
val index = get(indexArg)
- if (index in waypoints.indices) {
- waypoints.removeAt(index)
- source.sendFeedback(Text.stringifiedTranslatable(
- "firmament.command.waypoint.remove",
- index))
+ val w = useNonEmptyWaypoints()
+ if (w != null && index in w.waypoints.indices) {
+ w.waypoints.removeAt(index)
+ source.sendFeedback(
+ Component.translatableEscape(
+ "firmament.command.waypoint.remove",
+ index
+ )
+ )
} else {
- source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error"))
+ source.sendError(Component.translatableEscape("firmament.command.waypoint.remove.error"))
}
}
}
}
- thenLiteral("import") {
- thenExecute {
- val contents = ClipboardUtils.getTextContents()
- val data = try {
- Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents)
- } catch (ex: Exception) {
- Firmament.logger.error("Could not load waypoints from clipboard", ex)
- source.sendError(Text.translatable("firmament.command.waypoint.import.error"))
- return@thenExecute
- }
- waypoints.clear()
- data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) }
- source.sendFeedback(
- Text.stringifiedTranslatable(
- "firmament.command.waypoint.import",
- data.size
- )
- )
- }
- }
}
}
- @Subscribe
- fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
- temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
- if (temporaryPlayerWaypointList.isEmpty()) return
- RenderInWorldContext.renderInWorld(event) {
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- block(waypoint.pos, 0xFFFFFF00.toInt())
- }
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- val skin =
- MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
- ?.skinTextures
- ?.texture
- withFacingThePlayer(waypoint.pos.toCenterPos()) {
- waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
- if (skin != null) {
- matrixStack.translate(0F, -20F, 0F)
- // Head front
- texture(
- skin, 16, 16,
- 1 / 8f, 1 / 8f,
- 2 / 8f, 2 / 8f,
- )
- // Head overlay
- texture(
- skin, 16, 16,
- 5 / 8f, 1 / 8f,
- 6 / 8f, 2 / 8f,
- )
- }
- }
- }
- }
- }
-
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- temporaryPlayerWaypointList.clear()
- }
+ fun textInvalidIndex(index: Int) =
+ tr(
+ "firmament.command.waypoint.invalid-index",
+ "Invalid index $index provided."
+ )
+
+ fun textNothingToExport(): Component =
+ tr(
+ "firmament.command.waypoint.export.nowaypoints",
+ "No waypoints to export found. Add some with /firm waypoint ~ ~ ~."
+ )
}
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
@@ -272,35 +251,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
}
return result
}
-
-
-fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
- val source = this
- return ServerCommandSource(
- object : CommandOutput {
- override fun sendMessage(message: Text?) {
- source.player.sendMessage(message, false)
- }
-
- override fun shouldReceiveFeedback(): Boolean {
- return true
- }
-
- override fun shouldTrackOutput(): Boolean {
- return true
- }
-
- override fun shouldBroadcastConsoleToOps(): Boolean {
- return true
- }
- },
- source.position,
- source.rotation,
- null,
- 0,
- "FakeServerCommandSource",
- Text.literal("FakeServerCommandSource"),
- null,
- source.player
- )
-}