aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-12-31 16:52:29 +0100
committerLinnea Gräf <nea@nea.moe>2024-12-31 16:52:29 +0100
commita892a5f90b87b530331a7652dd4eb5bc07bf1c03 (patch)
tree808ac4b59557b2b0a6c8459a2dac13ce272937a6 /src
parent533fd68e2be8236c842f53fd0cafa52341226226 (diff)
parent620f1a45ac02b078e95c68a59a45bd4d24ff176b (diff)
downloadFirmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.tar.gz
Firmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.tar.bz2
Firmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.zip
Merge branch 'mc-1.21.3'
Diffstat (limited to 'src')
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt34
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt6
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt210
-rw-r--r--src/compat/yacl/java/YaclIntegration.kt29
-rw-r--r--src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java18
-rw-r--r--src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java29
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java17
-rw-r--r--src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java18
-rw-r--r--src/main/kotlin/commands/Duration.kt75
-rw-r--r--src/main/kotlin/events/PartyMessageReceivedEvent.kt9
-rw-r--r--src/main/kotlin/features/chat/PartyCommands.kt134
-rw-r--r--src/main/kotlin/features/chat/QuickCommands.kt212
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt94
-rw-r--r--src/main/kotlin/features/inventory/ItemRarityCosmetics.kt13
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt44
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt130
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt82
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt2
-rw-r--r--src/main/kotlin/features/misc/TimerFeature.kt124
-rw-r--r--src/main/kotlin/gui/config/ChoiceHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt19
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt9
-rw-r--r--src/main/kotlin/gui/entity/EntityRenderer.kt13
-rw-r--r--src/main/kotlin/repo/BetterRepoRecipeCache.kt36
-rw-r--r--src/main/kotlin/repo/EssenceRecipeProvider.kt75
-rw-r--r--src/main/kotlin/repo/ExtraRecipeProvider.kt7
-rw-r--r--src/main/kotlin/repo/ItemCache.kt48
-rw-r--r--src/main/kotlin/repo/Reforge.kt160
-rw-r--r--src/main/kotlin/repo/ReforgeStore.kt124
-rw-r--r--src/main/kotlin/repo/RepoItemTypeCache.kt15
-rw-r--r--src/main/kotlin/repo/RepoManager.kt6
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt186
-rw-r--r--src/main/kotlin/util/AprilFoolsUtil.kt10
-rw-r--r--src/main/kotlin/util/FirmFormatters.kt14
-rw-r--r--src/main/kotlin/util/LegacyFormattingCode.kt54
-rw-r--r--src/main/kotlin/util/MC.kt5
-rw-r--r--src/main/kotlin/util/SBData.kt116
-rw-r--r--src/main/kotlin/util/SkyBlockIsland.kt1
-rw-r--r--src/main/kotlin/util/SkyblockId.kt23
-rw-r--r--src/main/kotlin/util/mc/NbtItemData.kt4
-rw-r--r--src/main/kotlin/util/mc/TolerantRegistriesOps.kt29
-rw-r--r--src/main/kotlin/util/skyblock/ItemType.kt27
-rw-r--r--src/main/kotlin/util/skyblock/Rarity.kt38
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt1
-rw-r--r--src/main/kotlin/util/textutil.kt9
-rw-r--r--src/main/resources/firmament.accesswidener3
-rw-r--r--src/test/resources/testdata/items/hyperion.snbt96
-rw-r--r--src/test/resources/testdata/items/implosion-belt.snbt105
49 files changed, 2147 insertions, 369 deletions
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt
index d8238be..2b9e4bf 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt
@@ -1,19 +1,22 @@
package moe.nea.firmament.compat.rei
import me.shedaniel.math.Dimension
+import me.shedaniel.math.FloatingDimension
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.Drawable
import net.minecraft.client.gui.Element
-import net.minecraft.client.gui.ParentElement
import net.minecraft.entity.LivingEntity
import moe.nea.firmament.gui.entity.EntityRenderer
import moe.nea.firmament.util.ErrorUtil
-class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBounds() {
+class EntityWidget(
+ val entity: LivingEntity?,
+ val point: Point,
+ val size: FloatingDimension = FloatingDimension(defaultSize)
+) : WidgetWithBounds() {
override fun children(): List<Element> {
return emptyList()
}
@@ -22,18 +25,35 @@ class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBoun
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
try {
- if (!hasErrored)
- EntityRenderer.renderEntity(entity!!, context, point.x, point.y, mouseX.toFloat(), mouseY.toFloat())
+ context.matrices.push()
+ if (!hasErrored) {
+ context.matrices.translate(point.x.toDouble(), point.y.toDouble(), 0.0)
+ val xScale = size.width / defaultSize.width.toDouble()
+ val yScale = size.height / defaultSize.height.toDouble()
+ context.matrices.scale(xScale.toFloat(), yScale.toFloat(), 1.0F)
+ EntityRenderer.renderEntity(
+ entity!!,
+ context,
+ 0, 0,
+ (mouseX - point.x) * xScale,
+ (mouseY - point.y) * yScale)
+ }
} catch (ex: Exception) {
ErrorUtil.softError("Failed to render constructed entity: $entity", ex)
hasErrored = true
+ } finally {
+ context.matrices.pop()
}
if (hasErrored) {
- context.fill(point.x, point.y, point.x + 50, point.y + 80, 0xFFAA2222.toInt())
+ context.fill(point.x, point.y, point.x + size.width.toInt(), point.y + size.height.toInt(), 0xFFAA2222.toInt())
}
}
+ companion object {
+ val defaultSize = Dimension(50, 80)
+ }
+
override fun getBounds(): Rectangle {
- return Rectangle(point, Dimension(50, 80))
+ return Rectangle(point, size)
}
}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
index f576eda..92f2cfc 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
@@ -24,6 +24,7 @@ import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe
import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe
import moe.nea.firmament.compat.rei.recipes.SBKatRecipe
import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe
+import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
@@ -78,6 +79,7 @@ class FirmamentReiPlugin : REIClientPlugin {
registry.add(SBForgeRecipe.Category)
registry.add(SBMobDropRecipe.Category)
registry.add(SBKatRecipe.Category)
+ registry.add(SBReforgeRecipe.Category)
registry.add(SBEssenceUpgradeRecipe.Category)
}
@@ -91,6 +93,10 @@ class FirmamentReiPlugin : REIClientPlugin {
SBCraftingRecipe.Category.catIdentifier,
SkyblockCraftingRecipeDynamicGenerator)
registry.registerDisplayGenerator(
+ SBReforgeRecipe.catIdentifier,
+ SBReforgeRecipe.DynamicGenerator
+ )
+ registry.registerDisplayGenerator(
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator)
registry.registerDisplayGenerator(
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
new file mode 100644
index 0000000..9c8d8f4
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
@@ -0,0 +1,210 @@
+package moe.nea.firmament.compat.rei.recipes
+
+import java.util.Optional
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.FloatingDimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.Renderer
+import me.shedaniel.rei.api.client.gui.widgets.Label
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory
+import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator
+import me.shedaniel.rei.api.client.view.ViewSearchBuilder
+import me.shedaniel.rei.api.common.category.CategoryIdentifier
+import me.shedaniel.rei.api.common.display.Display
+import me.shedaniel.rei.api.common.display.DisplaySerializer
+import me.shedaniel.rei.api.common.entry.EntryIngredient
+import me.shedaniel.rei.api.common.entry.EntryStack
+import net.minecraft.entity.EntityType
+import net.minecraft.entity.SpawnReason
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.village.VillagerProfession
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.compat.rei.EntityWidget
+import moe.nea.firmament.compat.rei.SBItemEntryDefinition
+import moe.nea.firmament.gui.entity.EntityRenderer
+import moe.nea.firmament.repo.Reforge
+import moe.nea.firmament.repo.ReforgeStore
+import moe.nea.firmament.repo.RepoItemTypeCache
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.AprilFoolsUtil
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.gold
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.skyblockId
+import moe.nea.firmament.util.tr
+
+class SBReforgeRecipe(
+ val reforge: Reforge,
+ val limitToItem: SkyblockId?,
+) : Display {
+ companion object {
+ val catIdentifier = CategoryIdentifier.of<SBReforgeRecipe>(Firmament.MOD_ID, "reforge_recipe")
+ }
+
+ object Category : DisplayCategory<SBReforgeRecipe> {
+ override fun getCategoryIdentifier(): CategoryIdentifier<out SBReforgeRecipe> {
+ return catIdentifier
+ }
+
+ override fun getTitle(): Text {
+ return tr("firmament.recipecategory.reforge", "Reforge")
+ }
+
+ override fun getIcon(): Renderer {
+ return SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)
+ }
+
+ override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList<Widget> {
+ val list = mutableListOf<Widget>()
+ list.add(Widgets.createRecipeBase(bounds))
+ val inputSlot = Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY - 9))
+ .markInput().entries(display.inputItems)
+ list.add(inputSlot)
+ if (display.reforgeStone != null) {
+ list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24, bounds.centerY - 9 - 10))
+ .markInput().entry(display.reforgeStone))
+ list.add(Widgets.withTooltip(
+ Widgets.withTranslate(Widgets.wrapRenderer(
+ Rectangle(Point(bounds.minX + 10 + 24, bounds.centerY - 9 + 10), Dimension(16, 16)),
+ SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)), 0.0, 0.0, 150.0),
+ Rarity.entries.mapNotNull { rarity ->
+ display.reforge.reforgeCosts?.get(rarity)?.let { rarity to it }
+ }.map { (rarity, cost) ->
+ Text.literal("")
+ .append(rarity.text)
+ .append(": ")
+ .append(Text.literal("${FirmFormatters.formatCommas(cost, 0)} Coins").gold())
+ }
+ ))
+ } else {
+ val size = if (AprilFoolsUtil.isAprilFoolsDay) 1.2 else 0.6
+ val dimension =
+ FloatingDimension(EntityWidget.defaultSize.width * size, EntityWidget.defaultSize.height * size)
+ list.add(Widgets.withTooltip(
+ EntityWidget(
+ EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND)
+ ?.also { it.villagerData = it.villagerData.withProfession(VillagerProfession.WEAPONSMITH) },
+ Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2),
+ dimension
+ ),
+ tr("firmament.recipecategory.reforge.basic",
+ "This is a basic reforge, available at the Blacksmith.").grey()
+ ))
+ }
+ list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24 + 24, bounds.centerY - 9))
+ .markInput().entries(display.outputItems))
+ val statToLineMappings = mutableListOf<Pair<String, Label>>()
+ for ((i, statId) in display.reforge.statUniverse.withIndex()) {
+ val label = Widgets.createLabel(
+ Point(bounds.minX + 10 + 24 + 24 + 20, bounds.minY + 8 + i * 11),
+ SBItemStack.Companion.StatLine(SBItemStack.statIdToName(statId), null).reconstitute(7))
+ .horizontalAlignment(Label.LEFT_ALIGNED)
+ statToLineMappings.add(statId to label)
+ list.add(label)
+ }
+ fun updateStatLines() {
+ val entry = inputSlot.currentEntry?.castValue<SBItemStack>() ?: return
+ val stats = display.reforge.reforgeStats?.get(entry.rarity) ?: mapOf()
+ for ((stat, label) in statToLineMappings) {
+ label.message =
+ SBItemStack.Companion.StatLine(
+ SBItemStack.statIdToName(stat), null,
+ valueNum = stats[stat]
+ ).reconstitute(7)
+ }
+ }
+ updateStatLines()
+ inputSlot.withEntriesListener { updateStatLines() }
+ return list
+ }
+ }
+
+ object DynamicGenerator : DynamicDisplayGenerator<SBReforgeRecipe> {
+ fun getRecipesForSBItemStack(item: SBItemStack): Optional<List<SBReforgeRecipe>> {
+ val reforgeRecipes = mutableListOf<SBReforgeRecipe>()
+ for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) {
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ }
+ for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) {
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ }
+ if (reforgeRecipes.isEmpty()) return Optional.empty()
+ return Optional.of(reforgeRecipes)
+ }
+
+ override fun getRecipeFor(entry: EntryStack<*>): Optional<List<SBReforgeRecipe>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ return getRecipesForSBItemStack(item)
+ }
+
+ override fun getUsageFor(entry: EntryStack<*>): Optional<List<SBReforgeRecipe>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ ReforgeStore.byReforgeStone[item.skyblockId]?.let { stoneReforge ->
+ return Optional.of(listOf(SBReforgeRecipe(stoneReforge, null)))
+ }
+ return getRecipesForSBItemStack(item)
+ }
+
+ override fun generate(builder: ViewSearchBuilder): Optional<List<SBReforgeRecipe>> {
+ // TODO: check builder.recipesFor and such and optionally return all reforge recipes
+ return Optional.empty()
+ }
+ }
+
+ private val eligibleItems =
+ if (limitToItem != null) listOfNotNull(RepoManager.getNEUItem(limitToItem))
+ else reforge.eligibleItems.flatMap {
+ when (it) {
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName ->
+ listOfNotNull(RepoManager.getNEUItem(it.internalName))
+
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType ->
+ ReforgeStore.resolveItemType(it.itemType)
+ .flatMap {
+ RepoItemTypeCache.byItemType[it] ?: listOf()
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type
+ }
+ }
+ }
+ private val inputItems = eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) }
+ private val outputItems =
+ inputItems.map { SBItemEntryDefinition.getEntry(it.value.copy(reforge = reforge.reforgeId)) }
+ private val reforgeStone = reforge.reforgeStone?.let(SBItemEntryDefinition::getEntry)
+ private val inputEntries =
+ listOf(EntryIngredient.of(inputItems)) + listOfNotNull(reforgeStone?.let(EntryIngredient::of))
+ private val outputEntries = listOf(EntryIngredient.of(outputItems))
+
+ override fun getInputEntries(): List<EntryIngredient> {
+ return inputEntries
+ }
+
+ override fun getOutputEntries(): List<EntryIngredient> {
+ return outputEntries
+ }
+
+ override fun getCategoryIdentifier(): CategoryIdentifier<*> {
+ return catIdentifier
+ }
+
+ override fun getDisplayLocation(): Optional<Identifier> {
+ return Optional.empty()
+ }
+
+ override fun getSerializer(): DisplaySerializer<out Display>? {
+ return null
+ }
+}
diff --git a/src/compat/yacl/java/YaclIntegration.kt b/src/compat/yacl/java/YaclIntegration.kt
index 9aec501..45a0d02 100644
--- a/src/compat/yacl/java/YaclIntegration.kt
+++ b/src/compat/yacl/java/YaclIntegration.kt
@@ -11,9 +11,11 @@ import dev.isxander.yacl3.api.OptionGroup
import dev.isxander.yacl3.api.YetAnotherConfigLib
import dev.isxander.yacl3.api.controller.ControllerBuilder
import dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder
+import dev.isxander.yacl3.api.controller.EnumControllerBuilder
import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder
import dev.isxander.yacl3.api.controller.StringControllerBuilder
import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder
+import dev.isxander.yacl3.api.controller.ValueFormatter
import dev.isxander.yacl3.gui.YACLScreen
import dev.isxander.yacl3.gui.tab.ListHolderWidget
import kotlin.time.Duration
@@ -23,8 +25,10 @@ import net.minecraft.client.gui.Element
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
import moe.nea.firmament.gui.config.BooleanHandler
+import moe.nea.firmament.gui.config.ChoiceHandler
import moe.nea.firmament.gui.config.ClickHandler
import moe.nea.firmament.gui.config.DurationHandler
+import moe.nea.firmament.gui.config.EnumRenderer
import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider
import moe.nea.firmament.gui.config.HudMeta
import moe.nea.firmament.gui.config.HudMetaHandler
@@ -89,6 +93,10 @@ class YaclIntegration : FirmamentConfigScreenProvider {
}
.build()
+ is ChoiceHandler<*> -> return createDefaultBinding {
+ createChoiceBinding(handler as ChoiceHandler<*>, managedOption as ManagedOption<*>, it as Option<*>)
+ }.build()
+
is BooleanHandler -> return createDefaultBinding(TickBoxControllerBuilder::create).build()
is StringHandler -> return createDefaultBinding(StringControllerBuilder::create).build()
is IntegerHandler -> return createDefaultBinding {
@@ -114,6 +122,27 @@ class YaclIntegration : FirmamentConfigScreenProvider {
}
}
+ private enum class Sacrifice {}
+
+ private fun createChoiceBinding(
+ handler: ChoiceHandler<*>,
+ managedOption: ManagedOption<*>,
+ option: Option<*>
+ ): ControllerBuilder<Any> {
+ val b = EnumControllerBuilder.create(option as Option<Sacrifice>)
+ b.enumClass(handler.enumClass as Class<Sacrifice>)
+ /**
+ * This is a function with E to avoid realizing the Sacrifice outside of a `X<E>` wrapper.
+ */
+ fun <E : Enum<*>> makeValueFormatter(): ValueFormatter<E> {
+ return ValueFormatter<E> {
+ (handler.renderer as EnumRenderer<E>).getName(managedOption as ManagedOption<E>, it)
+ }
+ }
+ b.formatValue(makeValueFormatter())
+ return b as ControllerBuilder<Any>
+ }
+
fun buildConfig(): YetAnotherConfigLib {
return YetAnotherConfigLib.createBuilder()
diff --git a/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java
new file mode 100644
index 0000000..59769c6
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java
@@ -0,0 +1,18 @@
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(ClientCommandInternals.class)
+public class AlwaysDisplayFirmamentClientCommandErrors {
+ @ModifyExpressionValue(method = "executeCommand", at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/command/client/ClientCommandInternals;isIgnoredException(Lcom/mojang/brigadier/exceptions/CommandExceptionType;)Z"))
+ private static boolean markFirmamentExceptionsAsNotIgnores(boolean original, @Local(argsOnly = true) String command) {
+ if (command.startsWith("firm ") || command.equals("firm") || command.startsWith("firmament ") || command.equals("firmament")) {
+ return false;
+ }
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java
index b386604..699d5b7 100644
--- a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java
@@ -51,7 +51,7 @@ public class FirmKeybindsInVanillaControlsPatch {
var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding);
if (config == null) return;
resetButton.active = false;
- editButton.setMessage(Text.translatable("firmament.keybinding.external", config.value.format()));
+ editButton.setMessage(Text.translatable("firmament.keybinding.external", config.getValue().format()));
ci.cancel();
}
diff --git a/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java
new file mode 100644
index 0000000..c5af8b6
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java
@@ -0,0 +1,29 @@
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.client.gui.screen.ingame.StatusEffectsDisplay;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(InventoryScreen.class)
+public abstract class HideStatusEffectsPatch {
+ @Shadow
+ public abstract boolean shouldHideStatusEffectHud();
+
+ @Inject(method = "shouldHideStatusEffectHud", at = @At("HEAD"), cancellable = true)
+ private void hideStatusEffects(CallbackInfoReturnable<Boolean> cir) {
+ cir.setReturnValue(!Fixes.TConfig.INSTANCE.getHidePotionEffects());
+ }
+
+ @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/StatusEffectsDisplay;drawStatusEffects(Lnet/minecraft/client/gui/DrawContext;IIF)V"))
+ private boolean conditionalRenderStatuses(StatusEffectsDisplay instance, DrawContext context, int mouseX, int mouseY, float tickDelta) {
+ return shouldHideStatusEffectHud() || !Fixes.TConfig.INSTANCE.getHidePotionEffects();
+ }
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java
new file mode 100644
index 0000000..2f2f188
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java
@@ -0,0 +1,17 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.features.chat.QuickCommands;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public class SaveOriginalCommandTreePacket {
+ @Inject(method = "onCommandTree", at = @At(value = "RETURN"))
+ private void saveUnmodifiedCommandTree(CommandTreeS2CPacket packet, CallbackInfo ci) {
+ QuickCommands.INSTANCE.setLastReceivedTreePacket(packet);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java
new file mode 100644
index 0000000..ac6f614
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java
@@ -0,0 +1,18 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.util.mc.TolerantRegistriesOps;
+import net.minecraft.registry.entry.RegistryEntryOwner;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(RegistryEntryOwner.class)
+public interface TolerateFirmamentTolerateRegistryOwners<T> {
+ @Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true)
+ private void equalTolerantRegistryOwners(RegistryEntryOwner<T> other, CallbackInfoReturnable<Boolean> cir) {
+ if (other instanceof TolerantRegistriesOps.TolerantOwner<?>) {
+ cir.setReturnValue(true);
+ }
+ }
+}
diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt
new file mode 100644
index 0000000..42f143d
--- /dev/null
+++ b/src/main/kotlin/commands/Duration.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.commands
+
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.exceptions.DynamicCommandExceptionType
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import java.util.concurrent.CompletableFuture
+import java.util.function.Function
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+import moe.nea.firmament.util.tr
+
+object DurationArgumentType : ArgumentType<Duration> {
+ val unknownTimeCode = DynamicCommandExceptionType { timeCode ->
+ tr("firmament.command-argument.duration.error",
+ "Unknown time code '$timeCode'")
+ }
+
+ override fun parse(reader: StringReader): Duration {
+ val start = reader.cursor
+ val string = reader.readUnquotedString()
+ val matcher = regex.matcher(string)
+ var s = 0
+ var time = 0.seconds
+ fun createError(till: Int) {
+ throw unknownTimeCode.createWithContext(
+ reader.also { it.cursor = start + s },
+ string.substring(s, till))
+ }
+
+ while (matcher.find()) {
+ if (matcher.start() != s) {
+ createError(matcher.start())
+ }
+ s = matcher.end()
+ val amount = matcher.group("count").toDouble()
+ val what = timeSuffixes[matcher.group("what").single()]!!
+ time += amount.toDuration(what)
+ }
+ if (string.length != s) {
+ createError(string.length)
+ }
+ return time
+ }
+
+
+ override fun <S : Any?> listSuggestions(
+ context: CommandContext<S>,
+ builder: SuggestionsBuilder
+ ): CompletableFuture<Suggestions> {
+ val remaining = builder.remainingLowerCase.substringBefore(' ')
+ if (remaining.isEmpty()) return super.listSuggestions(context, builder)
+ if (remaining.last().isDigit()) {
+ for (timeSuffix in timeSuffixes.keys) {
+ builder.suggest(remaining + timeSuffix)
+ }
+ }
+ return builder.buildFuture()
+ }
+
+ val timeSuffixes = mapOf(
+ 'm' to DurationUnit.MINUTES,
+ 's' to DurationUnit.SECONDS,
+ 'h' to DurationUnit.HOURS,
+ )
+ val regex = "(?<count>[0-9]+)(?<what>[${timeSuffixes.keys.joinToString("")}])".toPattern()
+
+ override fun getExamples(): Collection<String> {
+ return listOf("3m", "20s", "1h45m")
+ }
+}
diff --git a/src/main/kotlin/events/PartyMessageReceivedEvent.kt b/src/main/kotlin/events/PartyMessageReceivedEvent.kt
new file mode 100644
index 0000000..4688dfe
--- /dev/null
+++ b/src/main/kotlin/events/PartyMessageReceivedEvent.kt
@@ -0,0 +1,9 @@
+package moe.nea.firmament.events
+
+data class PartyMessageReceivedEvent(
+ val from: ProcessChatEvent,
+ val message: String,
+ val name: String,
+) : FirmamentEvent() {
+ companion object : FirmamentEventBus<PartyMessageReceivedEvent>()
+}
diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt
new file mode 100644
index 0000000..de3a0d9
--- /dev/null
+++ b/src/main/kotlin/features/chat/PartyCommands.kt
@@ -0,0 +1,134 @@
+package moe.nea.firmament.features.chat
+
+import com.mojang.brigadier.CommandDispatcher
+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 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.tr
+import moe.nea.firmament.util.useMatch
+
+object PartyCommands {
+
+ val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern()
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ messageInChannel.useMatch(event.unformattedString) {
+ val channel = group("channel")
+ val message = group("message")
+ val name = group("name")
+ if (channel == "Party") {
+ PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent(
+ event, message, name
+ ))
+ }
+ }
+ }
+
+ val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet()
+
+ data class PartyCommandContext(
+ val name: String
+ )
+
+ val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch ->
+ fun register(
+ name: String,
+ vararg alias: String,
+ block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {},
+ ): LiteralCommandNode<PartyCommandContext> {
+ val node =
+ dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block))
+ alias.forEach { register(it) { redirect(node) } }
+ return node
+ }
+
+ register("warp", "pw", "pwarp", "partywarp") {
+ executes {
+ // TODO: add check if you are the party leader
+ MC.sendCommand("p warp")
+ 0
+ }
+ }
+
+ register("transfer", "pt", "ptme") {
+ executes {
+ MC.sendCommand("p transfer ${it.source.name}")
+ 0
+ }
+ }
+
+ register("allinvite", "allinv") {
+ executes {
+ MC.sendCommand("p settings allinvite")
+ 0
+ }
+ }
+
+ register("coords") {
+ executes {
+ val p = MC.player?.blockPos ?: BlockPos.ORIGIN
+ MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
+ 0
+ }
+ }
+ // TODO: downtime tracker (display message again at end of dungeon)
+ // instance ends: kuudra, dungeons, bacte
+ // TODO: at TPS command
+ }
+
+ object TConfig : ManagedConfig("party-commands", Category.CHAT) {
+ val enable by toggle("enable") { false }
+ val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds }
+ val ignoreOwnCommands by toggle("ignore-own") { false }
+ }
+
+ var lastCommand = TimeMark.farPast()
+
+ @Subscribe
+ fun listPartyCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("partycommands") {
+ thenExecute {
+ // TODO: Better help, including descriptions and redirect detection
+ MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes"))
+ }
+ }
+ }
+
+ @Subscribe
+ fun onPartyMessage(event: PartyMessageReceivedEvent) {
+ if (!TConfig.enable) return
+ if (event.message.firstOrNull() !in commandPrefixes) return
+ if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return
+ if (lastCommand.passedTime() < TConfig.cooldown) {
+ MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed."))
+ return
+ }
+ // TODO: add trust levels
+ val commandLine = event.message.substring(1)
+ try {
+ dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name))
+ } catch (ex: Exception) {
+ if (ex is CommandSyntaxException) {
+ MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command."))
+ return
+ } else {
+ MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution."))
+ ErrorUtil.softError("Unknown error during command execution.", ex)
+ }
+ }
+ lastCommand = TimeMark.now()
+ }
+}
diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt
index 5944b92..7963171 100644
--- a/src/main/kotlin/features/chat/QuickCommands.kt
+++ b/src/main/kotlin/features/chat/QuickCommands.kt
@@ -1,8 +1,12 @@
-
-
package moe.nea.firmament.features.chat
+import com.mojang.brigadier.CommandDispatcher
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 moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
@@ -12,89 +16,139 @@ 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.grey
+import moe.nea.firmament.util.tr
object QuickCommands : FirmamentFeature {
- override val identifier: String
- get() = "quick-commands"
+ override val identifier: String
+ get() = "quick-commands"
+
+ object TConfig : ManagedConfig("quick-commands", Category.CHAT) {
+ val enableJoin by toggle("join") { true }
+ val enableDh by toggle("dh") { true }
+ override fun onChange(option: ManagedOption<*>) {
+ reloadCommands()
+ }
+ }
+
+ fun reloadCommands() {
+ val lastPacket = lastReceivedTreePacket ?: return
+ val network = MC.networkHandler ?: return
+ val fallback = ClientCommandInternals.getActiveDispatcher()
+ try {
+ val dispatcher = CommandDispatcher<FabricClientCommandSource>()
+ ClientCommandInternals.setActiveDispatcher(dispatcher)
+ ClientCommandRegistrationCallback.EVENT.invoker()
+ .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries,
+ network.enabledFeatures))
+ ClientCommandInternals.finalizeInit()
+ network.onCommandTree(lastPacket)
+ } catch (ex: Exception) {
+ ClientCommandInternals.setActiveDispatcher(fallback)
+ throw ex
+ }
+ }
+
+
+ fun removePartialPrefix(text: String, prefix: String): String? {
+ var lf: String? = null
+ for (i in 1..prefix.length) {
+ if (text.startsWith(prefix.substring(0, i))) {
+ lf = text.substring(i)
+ }
+ }
+ return lf
+ }
+
+ var lastReceivedTreePacket: CommandTreeS2CPacket? = null
- fun removePartialPrefix(text: String, prefix: String): String? {
- var lf: String? = null
- for (i in 1..prefix.length) {
- if (text.startsWith(prefix.substring(0, i))) {
- lf = text.substring(i)
- }
- }
- return lf
- }
+ val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
+ val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
- val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
- val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
+ @Subscribe
+ fun registerDh(event: CommandEvent) {
+ if (!TConfig.enableDh) return
+ event.register("dh") {
+ thenExecute {
+ MC.sendCommand("warp dhub")
+ }
+ }
+ event.register("dn") {
+ thenExecute {
+ MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey())
+ MC.sendCommand("warp dhub")
+ }
+ }
+ }
- @Subscribe
- fun onCommands(it: CommandEvent) {
- it.register("join") {
- thenArgument("what", RestArgumentType) { what ->
- thenExecute {
- val what = this[what]
- if (!SBData.isOnSkyblock) {
- MC.sendCommand("join $what")
- return@thenExecute
- }
- val joinName = getNameForFloor(what.replace(" ", "").lowercase())
- if (joinName == null) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
- } else {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
- joinName))
- MC.sendCommand("joininstance $joinName")
- }
- }
- }
- thenExecute {
- source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
- }
- }
- }
+ @Subscribe
+ fun registerJoin(it: CommandEvent) {
+ if (!TConfig.enableJoin) return
+ it.register("join") {
+ thenArgument("what", RestArgumentType) { what ->
+ thenExecute {
+ val what = this[what]
+ if (!SBData.isOnSkyblock) {
+ MC.sendCommand("join $what")
+ return@thenExecute
+ }
+ val joinName = getNameForFloor(what.replace(" ", "").lowercase())
+ if (joinName == null) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
+ } else {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
+ joinName))
+ MC.sendCommand("joininstance $joinName")
+ }
+ }
+ }
+ thenExecute {
+ source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
+ }
+ }
+ }
- fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
- val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
- if (kuudraLevel != null) {
- val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
- it.startsWith(
- kuudraLevel,
- true
- )
- }
- if (l !in kuudraLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
- kuudraLevel))
- return null
- }
- return "KUUDRA_${kuudraLevelNames[l]}"
- }
- val masterLevel = removePartialPrefix(w, "master")
- val normalLevel =
- removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
- val dungeonLevel = masterLevel ?: normalLevel
- if (dungeonLevel != null) {
- val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
- it.startsWith(
- dungeonLevel,
- true
- )
- }
- if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
- return "CATACOMBS_ENTRANCE"
- }
- if (l !in dungeonLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
- kuudraLevel))
- return null
- }
- return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
- }
- return null
- }
+ fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
+ val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
+ if (kuudraLevel != null) {
+ val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
+ it.startsWith(
+ kuudraLevel,
+ true
+ )
+ }
+ if (l !in kuudraLevelNames.indices) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
+ kuudraLevel))
+ return null
+ }
+ return "KUUDRA_${kuudraLevelNames[l]}"
+ }
+ val masterLevel = removePartialPrefix(w, "master")
+ val normalLevel =
+ removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
+ val dungeonLevel = masterLevel ?: normalLevel
+ if (dungeonLevel != null) {
+ val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
+ it.startsWith(
+ dungeonLevel,
+ true
+ )
+ }
+ if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
+ return "CATACOMBS_ENTRANCE"
+ }
+ if (l !in dungeonLevelNames.indices) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
+ kuudraLevel))
+ return null
+ }
+ return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
+ }
+ return null
+ }
}
diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 5d70b1a..7030319 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -1,71 +1,67 @@
-
-
package moe.nea.firmament.features.fixes
import moe.nea.jarvis.api.Point
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
-import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
-import net.minecraft.util.Arm
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.errorBoundary
object Fixes : FirmamentFeature {
- override val identifier: String
- get() = "fixes"
+ override val identifier: String
+ get() = "fixes"
- 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 peekChat by keyBindingWithDefaultUnbound("peek-chat")
- }
+ 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 peekChat by keyBindingWithDefaultUnbound("peek-chat")
+ val hidePotionEffects by toggle("hide-mob-effects") { false }
+ }
- override val config: ManagedConfig
- get() = TConfig
+ override val config: ManagedConfig
+ get() = TConfig
- fun handleIsPressed(
- keyBinding: KeyBinding,
- cir: CallbackInfoReturnable<Boolean>
- ) {
- if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
- cir.returnValue = true
- }
+ fun handleIsPressed(
+ keyBinding: KeyBinding,
+ cir: CallbackInfoReturnable<Boolean>
+ ) {
+ if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
+ 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
- )
- it.context.matrices.pop()
- }
+ @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
+ )
+ it.context.matrices.pop()
+ }
- @Subscribe
- fun onWorldKeyboard(it: WorldKeyboardEvent) {
- if (it.matches(TConfig.autoSprintKeyBinding)) {
- TConfig.autoSprint = !TConfig.autoSprint
- }
- }
+ @Subscribe
+ fun onWorldKeyboard(it: WorldKeyboardEvent) {
+ if (it.matches(TConfig.autoSprintKeyBinding)) {
+ TConfig.autoSprint = !TConfig.autoSprint
+ }
+ }
- fun shouldPeekChat(): Boolean {
- return TConfig.peekChat.isPressed(atLeast = true)
- }
+ fun shouldPeekChat(): Boolean {
+ return TConfig.peekChat.isPressed(atLeast = true)
+ }
}
diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
index d2c555b..fdc378a 100644
--- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
+++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
@@ -29,18 +29,7 @@ object ItemRarityCosmetics : FirmamentFeature {
override val config: ManagedConfig
get() = TConfig
- private val rarityToColor = mapOf(
- Rarity.COMMON to Formatting.WHITE,
- Rarity.UNCOMMON to Formatting.GREEN,
- Rarity.RARE to Formatting.BLUE,
- Rarity.EPIC to Formatting.DARK_PURPLE,
- Rarity.LEGENDARY to Formatting.GOLD,
- Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
- Rarity.DIVINE to Formatting.AQUA,
- Rarity.SPECIAL to Formatting.RED,
- Rarity.VERY_SPECIAL to Formatting.RED,
- Rarity.SUPREME to Formatting.DARK_RED,
- ).mapValues {
+ private val rarityToColor = Rarity.colourMap.mapValues {
val c = Color(it.value.colorValue!!)
c.rgb
}
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index fc09476..99130d5 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -2,7 +2,6 @@
package moe.nea.firmament.features.inventory
-import com.mojang.blaze3d.systems.RenderSystem
import java.util.UUID
import org.lwjgl.glfw.GLFW
import kotlinx.serialization.Serializable
@@ -14,6 +13,7 @@ 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 moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
@@ -59,6 +59,17 @@ object SlotLocking : FirmamentFeature {
}
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 }
+ }
+
+ enum class SlotRenderLinesMode : StringIdentifiable {
+ EVERYTHING,
+ ONLY_BOXES,
+ NOTHING;
+
+ override fun asString(): String {
+ return name
+ }
}
override val config: TConfig
@@ -95,7 +106,7 @@ object SlotLocking : FirmamentFeature {
if (handler.inventory.size() < 9) return false
val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
if (sellItem == null) return false
- if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true
+ if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!"
}
@@ -104,7 +115,7 @@ object SlotLocking : FirmamentFeature {
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
if (!event.slot.hasStack()) return
- if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return
+ if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
val inv = event.slot.inventory
var anyBlocked = false
for (i in 0 until event.slot.index) {
@@ -227,23 +238,32 @@ object SlotLocking : FirmamentFeature {
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
- boundSlots.entries.forEach {
- val hotbarSlot = findByIndex(it.key) ?: return@forEach
- val inventorySlot = findByIndex(it.value) ?: return@forEach
+ for (it in boundSlots.entries) {
+ val hotbarSlot = findByIndex(it.key) ?: continue
+ val inventorySlot = findByIndex(it.value) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
- event.context.drawLine(
- invX + sx, invY + sy,
- hotX + sx, hotY + sy,
+ val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
+ || 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 (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
+ event.context.drawLine(
+ invX + sx, invY + sy,
+ hotX + sx, hotY + sy,
+ color
+ )
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
+ 16, 16, color.color)
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
+ 16, 16, color.color)
}
}
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
new file mode 100644
index 0000000..f1b77c6
--- /dev/null
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -0,0 +1,130 @@
+package moe.nea.firmament.features.inventory
+
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+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 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.grey
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.unformattedString
+
+object TimerInLore {
+ object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
+ val showTimers by toggle("show") { true }
+ val timerFormat by choice("format") { TimerFormat.SOCIALIST }
+ }
+
+ enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable {
+ RFC(DateTimeFormatter.RFC_1123_DATE_TIME),
+ LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)),
+ SOCIALIST(
+ {
+ appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT)
+ appendLiteral(" ")
+ appendValue(ChronoField.DAY_OF_MONTH, 2)
+ appendLiteral(".")
+ appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ appendLiteral(".")
+ appendValue(ChronoField.YEAR, 4)
+ appendLiteral(" ")
+ appendValue(ChronoField.HOUR_OF_DAY, 2)
+ appendLiteral(":")
+ appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ appendLiteral(":")
+ appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+ }),
+ AMERICAN("EEEE, MMM d h:mm a yyyy"),
+ ;
+
+ constructor(block: DateTimeFormatterBuilder.() -> Unit)
+ : this(DateTimeFormatterBuilder().also(block).toFormatter())
+
+ constructor(format: String) : this(DateTimeFormatter.ofPattern(format))
+
+ override fun asString(): String {
+ return name
+ }
+ }
+
+ enum class CountdownTypes(
+ val match: String,
+ val label: String, // TODO: convert to a string
+ val isRelative: Boolean = false,
+ ) {
+ STARTING("Starting in:", "Starts at"),
+ STARTS("Starts in:", "Starts at"),
+ INTEREST("Interest in:", "Interest at"),
+ UNTILINTEREST("Until interest:", "Interest at"),
+ ENDS("Ends in:", "Ends at"),
+ REMAINING("Remaining:", "Ends at"),
+ DURATION("Duration:", "Finishes at"),
+ TIMELEFT("Time left:", "Ends at"),
+ EVENTTIMELEFT("Event lasts for", "Ends at", isRelative = true),
+ SHENSUCKS("Auction ends in:", "Auction ends at"),
+ ENDS_PET_LEVELING(
+ "Ends:",
+ "Finishes at"
+ ),
+ CALENDARDETAILS(" (§e", "Starts at"),
+ COMMUNITYPROJECTS("Contribute again", "Come back at"),
+ CHOCOLATEFACTORY("Next Charge", "Available at"),
+ STONKSAUCTION("Auction ends in", "Ends at"),
+ LIZSTONKREDEMPTION("Resets in:", "Resets 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 modifyLore(event: ItemTooltipEvent) {
+ if (!TConfig.showTimers) return
+ var lastTimer: ZonedDateTime? = null
+ for (i in event.lines.indices) {
+ val line = event.lines[i].unformattedString
+ val countdownType = CountdownTypes.entries.find { it.match in line } ?: continue
+ if (countdownType == CountdownTypes.CALENDARDETAILS
+ && !event.stack.displayNameAccordingToNbt.unformattedString.startsWith("Day ")
+ ) continue
+
+ val countdownMatch = regex.findAll(line).filter { it.value.isNotBlank() }.lastOrNull() ?: continue
+ val (years, days, hours, minutes, seconds) =
+ listOf("years", "days", "hours", "minutes", "seconds")
+ .map {
+ countdownMatch.groups[it]?.value?.toLong() ?: 0L
+ }
+ if (years + days + hours + minutes + seconds == 0L) continue
+ 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())
+ continue
+ }
+ baseLine = lastTimer
+ }
+ val timer =
+ baseLine.plusYears(years).plusDays(days).plusHours(hours).plusMinutes(minutes).plusSeconds(seconds)
+ 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())
+ )
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index e07df8a..3b86184 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.features.inventory.storageoverlay
import io.ktor.util.decodeBase64Bytes
@@ -19,47 +17,59 @@ import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
+import net.minecraft.registry.RegistryOps
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.TolerantRegistriesOps
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
- val stacks: List<ItemStack>
+ val stacks: List<ItemStack>
) {
- val rows = stacks.size / 9
+ val rows = stacks.size / 9
+
+ init {
+ assert(stacks.size % 9 == 0)
+ assert(stacks.size / 9 in 1..5)
+ }
- init {
- assert(stacks.size % 9 == 0)
- assert(stacks.size / 9 in 1..5)
- }
+ object Serializer : KSerializer<VirtualInventory> {
+ const val INVENTORY = "INVENTORY"
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
- object Serializer : KSerializer<VirtualInventory> {
- 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 ops = getOps()
+ return VirtualInventory(items.map {
+ it as NbtCompound
+ if (it.isEmpty) ItemStack.EMPTY
+ else ErrorUtil.catch("Could not deserialize item") {
+ ItemStack.CODEC.parse(ops, it).orThrow
+ }.or { ItemStack.EMPTY }
+ })
+ }
- 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())
- return VirtualInventory(items.map {
- it as NbtCompound
- if (it.isEmpty) ItemStack.EMPTY
- else runCatching {
- ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow
- }.getOrElse { ItemStack.EMPTY }
- })
- }
+ fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
- override fun serialize(encoder: Encoder, value: VirtualInventory) {
- val list = NbtList()
- value.stacks.forEach {
- if (it.isEmpty) list.add(NbtCompound())
- else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow }
- .getOrElse { NbtCompound() })
- }
- val baos = ByteArrayOutputStream()
- NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
- encoder.encodeString(baos.toByteArray().encodeBase64())
- }
- }
+ 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())
+ }
+ }
}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 2d6c3ee..94b49f9 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -50,7 +50,6 @@ object PickaxeAbility : FirmamentFeature {
val drillFuelBar by toggle("fuel-bar") { true }
val blockOnPrivateIsland by choice(
"block-on-dynamic",
- BlockPickaxeAbility.entries,
) {
BlockPickaxeAbility.ONLY_DESTRUCTIVE
}
@@ -99,6 +98,7 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun onPickaxeRightClick(event: UseItemEvent) {
if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return
+ if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return
val itemType = ItemType.fromItemStack(event.item)
if (itemType !in pickaxeTypes) return
val ability = AbilityUtils.getAbilities(event.item)
diff --git a/src/main/kotlin/features/misc/TimerFeature.kt b/src/main/kotlin/features/misc/TimerFeature.kt
new file mode 100644
index 0000000..7c4833d
--- /dev/null
+++ b/src/main/kotlin/features/misc/TimerFeature.kt
@@ -0,0 +1,124 @@
+package moe.nea.firmament.features.misc
+
+import com.mojang.brigadier.arguments.IntegerArgumentType
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DurationArgumentType
+import moe.nea.firmament.commands.RestArgumentType
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.util.CommonSoundEffects
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MinecraftDispatcher
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.clickCommand
+import moe.nea.firmament.util.lime
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.yellow
+
+object TimerFeature {
+ data class Timer(
+ val start: TimeMark,
+ val duration: Duration,
+ val message: String,
+ val timerId: Int,
+ ) {
+ fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds)
+ fun isDone() = start.passedTime() >= duration
+ }
+
+ // Theoretically for optimal performance this could be a treeset keyed to the end time
+ val timers = mutableListOf<Timer>()
+
+ @Subscribe
+ fun tick(event: TickEvent) {
+ timers.removeAll {
+ if (it.isDone()) {
+ MC.sendChat(tr("firmament.timer.finished",
+ "The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}")
+ .yellow())
+ Firmament.coroutineScope.launch {
+ withContext(MinecraftDispatcher) {
+ repeat(5) {
+ CommonSoundEffects.playSuccess()
+ delay(0.2.seconds)
+ }
+ }
+ }
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ fun startTimer(duration: Duration, message: String) {
+ val timerId = createTimerId++
+ timers.add(Timer(TimeMark.now(), duration, message, timerId))
+ MC.sendChat(
+ tr("firmament.timer.start",
+ "Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime()
+ .append(" ")
+ .append(
+ tr("firmament.timer.cancelbutton",
+ "Click here to cancel the timer."
+ ).clickCommand("/firm timer clear $timerId").red()
+ )
+ )
+ }
+
+ fun clearTimer(timerId: Int) {
+ val timer = timers.indexOfFirst { it.timerId == timerId }
+ if (timer < 0) {
+ MC.sendChat(tr("firmament.timer.cancel.fail",
+ "Could not cancel that timer. Maybe it was already cancelled?").red())
+ } else {
+ val timerData = timers[timer]
+ timers.removeAt(timer)
+ MC.sendChat(tr("firmament.timer.cancel.done",
+ "Cancelled timer ${timerData.message}. It would have been done in ${
+ FirmFormatters.formatTimespan(timerData.timeLeft())
+ }.").lime())
+ }
+ }
+
+ var createTimerId = 0
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("cleartimer") {
+ thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId ->
+ thenExecute {
+ clearTimer(this[timerId])
+ }
+ }
+ thenExecute {
+ timers.map { it.timerId }.forEach {
+ clearTimer(it)
+ }
+ }
+ }
+ event.subcommand("timer") {
+ thenArgument("time", DurationArgumentType) { duration ->
+ thenExecute {
+ startTimer(this[duration], "no message")
+ }
+ thenArgument("message", RestArgumentType) { message ->
+ thenExecute {
+ startTimer(this[duration], this[message])
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt
index 25e885a..2ea3efc 100644
--- a/src/main/kotlin/gui/config/ChoiceHandler.kt
+++ b/src/main/kotlin/gui/config/ChoiceHandler.kt
@@ -13,6 +13,7 @@ import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.json.KJsonOps
class ChoiceHandler<E>(
+ val enumClass: Class<E>,
val universe: List<E>,
) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable {
val codec = StringIdentifiable.createCodec {
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
index 47a9c92..7ddda9e 100644
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -117,13 +117,24 @@ abstract class ManagedConfig(
protected fun <E> choice(
propertyName: String,
- universe: List<E>,
+ enumClass: Class<E>,
default: () -> E
): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
- return option(propertyName, default, ChoiceHandler(universe))
+ return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList()))
}
-// TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434
+ protected inline fun <reified E> choice(
+ propertyName: String,
+ noinline default: () -> E
+ ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
+ return choice(propertyName, E::class.java, default)
+ }
+
+ private fun <E> createStringIdentifiable(x: () -> Array<out E>): Codec<E> where E : Enum<E>, E : StringIdentifiable {
+ return StringIdentifiable.createCodec { x() }
+ }
+
+ // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434
// protected inline fun <reified E> choice(
// propertyName: String,
// noinline default: () -> E
@@ -136,6 +147,8 @@ abstract class ManagedConfig(
// default
// )
// }
+ open fun onChange(option: ManagedOption<*>) {
+ }
protected fun duration(
propertyName: String,
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
index d1aba83..383f392 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -6,7 +6,6 @@ import kotlinx.serialization.json.JsonObject
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ErrorUtil
class ManagedOption<T : Any>(
@@ -28,7 +27,13 @@ class ManagedOption<T : Any>(
val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description"
val labelDescription: Text = Text.translatable(descriptionTranslationKey)
- lateinit var value: T
+ private var actualValue: T? = null
+ var value: T
+ get() = actualValue ?: error("Lateinit variable not initialized")
+ set(value) {
+ actualValue = value
+ element.onChange(this)
+ }
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt
index 022b9a3..9a09fc6 100644
--- a/src/main/kotlin/gui/entity/EntityRenderer.kt
+++ b/src/main/kotlin/gui/entity/EntityRenderer.kt
@@ -111,8 +111,9 @@ object EntityRenderer {
renderContext: DrawContext,
posX: Int,
posY: Int,
- mouseX: Float,
- mouseY: Float
+ // TODO: Add width, height properties here
+ mouseX: Double,
+ mouseY: Double
) {
var bottomOffset = 0.0F
var currentEntity = entity
@@ -148,15 +149,15 @@ object EntityRenderer {
y2: Int,
size: Float,
bottomOffset: Float,
- mouseX: Float,
- mouseY: Float,
+ mouseX: Double,
+ mouseY: Double,
entity: LivingEntity
) {
context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat())
val centerX = (x1 + x2) / 2f
val centerY = (y1 + y2) / 2f
- val targetYaw = atan(((centerX - mouseX) / 40.0f).toDouble()).toFloat()
- val targetPitch = atan(((centerY - mouseY) / 40.0f).toDouble()).toFloat()
+ val targetYaw = atan(((centerX - mouseX) / 40.0f)).toFloat()
+ val targetPitch = atan(((centerY - mouseY) / 40.0f)).toFloat()
val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat())
val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180))
rotateToFaceTheFront.mul(rotateToFaceTheCamera)
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
index 91a6b50..4b32e57 100644
--- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt
+++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -6,23 +5,22 @@ import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
-class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) : IReloadable {
- var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
+ var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+ var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- override fun reload(repository: NEURepository) {
- val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val baseRecipes = repository.items.items.values
- .asSequence()
- .flatMap { it.recipes }
- val extraRecipes = essenceRecipeProvider.recipes
- (baseRecipes + extraRecipes)
- .forEach { recipe ->
- recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- }
- this.usages = usages
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val baseRecipes = repository.items.items.values
+ .asSequence()
+ .flatMap { it.recipes }
+ (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() })
+ .forEach { recipe ->
+ recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ }
+ this.usages = usages
+ this.recipes = recipes
+ }
}
diff --git a/src/main/kotlin/repo/EssenceRecipeProvider.kt b/src/main/kotlin/repo/EssenceRecipeProvider.kt
index 1833258..38559d5 100644
--- a/src/main/kotlin/repo/EssenceRecipeProvider.kt
+++ b/src/main/kotlin/repo/EssenceRecipeProvider.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -7,44 +6,46 @@ import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
-class EssenceRecipeProvider : IReloadable {
- data class EssenceUpgradeRecipe(
- val itemId: SkyblockId,
- val starCountAfter: Int,
- val essenceCost: Int,
- val essenceType: String, // TODO: replace with proper type
- val extraItems: List<NEUIngredient>,
- ) : NEURecipe {
- val essenceIngredient= NEUIngredient.fromString("${essenceType}:$essenceCost")
- val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider {
+ data class EssenceUpgradeRecipe(
+ val itemId: SkyblockId,
+ val starCountAfter: Int,
+ val essenceCost: Int,
+ val essenceType: String, // TODO: replace with proper type
+ val extraItems: List<NEUIngredient>,
+ ) : NEURecipe {
+ val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost")
+ val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+
+ override fun getAllInputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
+ }
- override fun getAllInputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
- }
+ override fun getAllOutputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
+ }
+ }
- override fun getAllOutputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
- }
- }
+ var recipes = listOf<EssenceUpgradeRecipe>()
+ private set
- var recipes = listOf<EssenceUpgradeRecipe>()
- private set
+ override fun provideExtraRecipes(): Iterable<NEURecipe> = recipes
- override fun reload(repository: NEURepository) {
- val recipes = mutableListOf<EssenceUpgradeRecipe>()
- for ((neuId, costs) in repository.constants.essenceCost.costs) {
- // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
- for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
- val items = costs.itemCosts[starCountAfter] ?: emptyList()
- recipes.add(
- EssenceUpgradeRecipe(
- SkyblockId(neuId),
- starCountAfter,
- essenceCost,
- "ESSENCE_" + costs.type.uppercase(), // how flimsy
- items.map { NEUIngredient.fromString(it) }))
- }
- }
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val recipes = mutableListOf<EssenceUpgradeRecipe>()
+ for ((neuId, costs) in repository.constants.essenceCost.costs) {
+ // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
+ for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
+ val items = costs.itemCosts[starCountAfter] ?: emptyList()
+ recipes.add(
+ EssenceUpgradeRecipe(
+ SkyblockId(neuId),
+ starCountAfter,
+ essenceCost,
+ "ESSENCE_" + costs.type.uppercase(), // how flimsy
+ items.map { NEUIngredient.fromString(it) }))
+ }
+ }
+ this.recipes = recipes
+ }
}
diff --git a/src/main/kotlin/repo/ExtraRecipeProvider.kt b/src/main/kotlin/repo/ExtraRecipeProvider.kt
new file mode 100644
index 0000000..9d3b5a0
--- /dev/null
+++ b/src/main/kotlin/repo/ExtraRecipeProvider.kt
@@ -0,0 +1,7 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.data.NEURecipe
+
+interface ExtraRecipeProvider {
+ fun provideExtraRecipes(): Iterable<NEURecipe>
+}
diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt
index 23c5ffb..e140dd8 100644
--- a/src/main/kotlin/repo/ItemCache.kt
+++ b/src/main/kotlin/repo/ItemCache.kt
@@ -24,21 +24,27 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtString
+import net.minecraft.text.Style
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.config.HudMeta
import moe.nea.firmament.gui.config.HudPosition
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.repo.RepoManager.initialize
+import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
+import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
import moe.nea.firmament.util.mc.appendLore
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.mc.modifyLore
import moe.nea.firmament.util.mc.setCustomName
import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.transformEachRecursively
object ItemCache : IReloadable {
private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
@@ -94,6 +100,35 @@ object ItemCache : IReloadable {
}
}
+ fun un189Lore(lore: String): Text {
+ val base = Text.literal("")
+ base.setStyle(Style.EMPTY.withItalic(false))
+ var lastColorCode = Style.EMPTY
+ var readOffset = 0
+ while (readOffset < lore.length) {
+ var nextCode = lore.indexOf('§', readOffset)
+ if (nextCode < 0) {
+ nextCode = lore.length
+ }
+ val text = lore.substring(readOffset, nextCode)
+ if (text.isNotEmpty()) {
+ base.append(Text.literal(text).setStyle(lastColorCode))
+ }
+ readOffset = nextCode + 2
+ if (nextCode + 1 < lore.length) {
+ val colorCode = lore[nextCode + 1]
+ val formatting = LegacyFormattingCode.byCode[colorCode.lowercaseChar()] ?: LegacyFormattingCode.RESET
+ val modernFormatting = formatting.modern
+ if (modernFormatting.isColor) {
+ lastColorCode = Style.EMPTY.withColor(modernFormatting)
+ } else {
+ lastColorCode = lastColorCode.withFormatting(modernFormatting)
+ }
+ }
+ }
+ return base
+ }
+
private fun NEUItem.asItemStackNow(): ItemStack {
try {
val oldItemTag = get10809CompoundTag()
@@ -101,6 +136,8 @@ object ItemCache : IReloadable {
?: return brokenItemStack(this)
val itemInstance =
ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
+ itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) }
+ itemInstance.displayNameAccordingToNbt = un189Lore(displayName)
val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
if (extraAttributes != null)
itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
@@ -135,12 +172,13 @@ object ItemCache : IReloadable {
}
fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text {
- assert(this.siblings.isEmpty())
- var string = this.string
- loreReplacements.forEach { (find, replace) ->
- string = string.replace("{$find}", replace)
+ return this.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ loreReplacements.forEach { (find, replace) ->
+ string = string.replace("{$find}", replace)
+ }
+ Text.literal(string).setStyle(it.style)
}
- return Text.literal(string).styled { this.style }
}
var job: Job? = null
diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt
new file mode 100644
index 0000000..dc0d93d
--- /dev/null
+++ b/src/main/kotlin/repo/Reforge.kt
@@ -0,0 +1,160 @@
+package moe.nea.firmament.repo
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblock.Rarity
+
+@Serializable
+data class Reforge(
+ val reforgeName: String,
+ @SerialName("internalName") val reforgeStone: SkyblockId? = null,
+ val nbtModifier: ReforgeId? = null,
+ val requiredRarities: List<Rarity>? = null,
+ val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List<ReforgeEligibilityFilter>? = null,
+ val allowOn: List<ReforgeEligibilityFilter>? = null,
+ val reforgeCosts: RarityMapped<Double>? = null,
+ val reforgeAbility: RarityMapped<String>? = null,
+ val reforgeStats: RarityMapped<Map<String, Double>>? = null,
+) {
+ val eligibleItems get() = allowOn ?: itemTypes ?: listOf()
+
+ val statUniverse: Set<String> = Rarity.entries.flatMapTo(mutableSetOf()) {
+ reforgeStats?.get(it)?.keys ?: emptySet()
+ }
+
+ @Serializable(with = ReforgeEligibilityFilter.Serializer::class)
+ sealed interface ReforgeEligibilityFilter {
+ object ItemTypesSerializer : KSerializer<List<ReforgeEligibilityFilter>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ override fun deserialize(decoder: Decoder): List<ReforgeEligibilityFilter> {
+ decoder as JsonDecoder
+ val jsonElement = decoder.decodeJsonElement()
+ if (jsonElement is JsonPrimitive && jsonElement.isString) {
+ return jsonElement.content.split("/").map { AllowsItemType(ItemType.ofName(it)) }
+ }
+ if (jsonElement is JsonArray) {
+ return decoder.json.decodeFromJsonElement(serializer<List<ReforgeEligibilityFilter>>(), jsonElement)
+ }
+ jsonElement as JsonObject
+ val filters = mutableListOf<ReforgeEligibilityFilter>()
+ jsonElement["internalName"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<SkyblockId>>(), it).forEach {
+ filters.add(AllowsInternalName(it))
+ }
+ }
+ jsonElement["itemId"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach {
+ val ident = Identifier.tryParse(it)
+ if (ident != null)
+ filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident)))
+ }
+ }
+ return filters
+ }
+
+ override fun serialize(encoder: Encoder, value: List<ReforgeEligibilityFilter>) {
+ TODO("Not yet implemented")
+ }
+ }
+
+ object Serializer : KSerializer<ReforgeEligibilityFilter> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter {
+ val jsonObject = serializer<JsonObject>().deserialize(decoder)
+ jsonObject["internalName"]?.let {
+ return AllowsInternalName(SkyblockId((it as JsonPrimitive).content))
+ }
+ jsonObject["itemType"]?.let {
+ return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content))
+ }
+ jsonObject["minecraftId"]?.let {
+ return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM,
+ Identifier.of((it as JsonPrimitive).content)))
+ }
+ error("Unknown item type")
+ }
+
+ override fun serialize(encoder: Encoder, value: ReforgeEligibilityFilter) {
+ TODO("Not yet implemented")
+ }
+
+ }
+
+ data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter
+ data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter
+ data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter
+ }
+
+
+ val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase())
+
+ @Serializable(with = RarityMapped.Serializer::class)
+ sealed interface RarityMapped<T> {
+ fun get(rarity: Rarity?): T?
+
+ class Serializer<T>(
+ val values: KSerializer<T>
+ ) : KSerializer<RarityMapped<T>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ val indirect = MapSerializer(Rarity.serializer(), values)
+ override fun deserialize(decoder: Decoder): RarityMapped<T> {
+ decoder as JsonDecoder
+ val element = decoder.decodeJsonElement()
+ if (element is JsonObject) {
+ return PerRarity(decoder.json.decodeFromJsonElement(indirect, element))
+ } else {
+ return Direct(decoder.json.decodeFromJsonElement(values, element))
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: RarityMapped<T>) {
+ when (value) {
+ is Direct<T> ->
+ values.serialize(encoder, value.value)
+
+ is PerRarity<T> ->
+ indirect.serialize(encoder, value.values)
+ }
+ }
+ }
+
+ @Serializable
+ data class Direct<T>(val value: T) : RarityMapped<T> {
+ override fun get(rarity: Rarity?): T {
+ return value
+ }
+ }
+
+ @Serializable
+ data class PerRarity<T>(val values: Map<Rarity, T>) : RarityMapped<T> {
+ override fun get(rarity: Rarity?): T? {
+ return values[rarity]
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt
new file mode 100644
index 0000000..f03903b
--- /dev/null
+++ b/src/main/kotlin/repo/ReforgeStore.kt
@@ -0,0 +1,124 @@
+package moe.nea.firmament.repo
+
+import com.google.gson.JsonElement
+import com.mojang.serialization.JsonOps
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepoFile
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.NEURepositoryException
+import io.github.moulberry.repo.data.NEURecipe
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.json.KJsonOps
+import moe.nea.firmament.util.skyblock.ItemType
+
+object ReforgeStore : ExtraRecipeProvider, IReloadable {
+ override fun provideExtraRecipes(): Iterable<NEURecipe> {
+ return emptyList()
+ }
+
+ var byType: Map<ItemType, List<Reforge>> = mapOf()
+ var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf()
+ var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf()
+ var modifierLut = mapOf<ReforgeId, Reforge>()
+ var byReforgeStone = mapOf<SkyblockId, Reforge>()
+ var allReforges = listOf<Reforge>()
+
+ fun findEligibleForItem(itemType: ItemType): List<Reforge> {
+ return byType[itemType] ?: listOf()
+ }
+
+ fun findEligibleForInternalName(internalName: SkyblockId): List<Reforge> {
+ return byInternalName[internalName] ?: listOf()
+ }
+
+ //TODO: return byVanillla
+ override fun reload(repo: NEURepository) {
+ val basicReforges =
+ repo.file("constants/reforges.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val advancedReforges =
+ repo.file("constants/reforgestones.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val allReforges = (basicReforges + advancedReforges)
+ modifierLut = allReforges.associateBy { it.reforgeId }
+ byReforgeStone = allReforges.filter { it.reforgeStone != null }
+ .associateBy { it.reforgeStone!! }
+ val byType = mutableMapOf<ItemType, MutableList<Reforge>>()
+ val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>()
+ val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>()
+ this.byType = byType
+ this.byVanilla = byVanilla
+ this.byInternalName = byInternalName
+ for (reforge in allReforges) {
+ for (eligibleItem in reforge.eligibleItems) {
+ when (eligibleItem) {
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> {
+ byInternalName.getOrPut(eligibleItem.internalName, ::mutableListOf).add(reforge)
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType -> {
+ val actualItemTypes = resolveItemType(eligibleItem.itemType)
+ for (itemType in actualItemTypes) {
+ byType.getOrPut(itemType, ::mutableListOf).add(reforge)
+ }
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ byVanilla.getOrPut(eligibleItem.minecraftId, ::mutableListOf).add(reforge)
+ }
+ }
+ }
+ }
+ this.allReforges = allReforges
+ }
+
+ fun resolveItemType(itemType: ItemType): List<ItemType> {
+ if (ItemType.SWORD == itemType) {
+ return listOf(
+ ItemType.SWORD,
+ ItemType.GAUNTLET,
+ ItemType.LONGSWORD,// TODO: check name
+ ItemType.FISHING_WEAPON,// TODO: check name
+ )
+ }
+ if (itemType == ItemType.ofName("ARMOR")) {
+ return listOf(
+ ItemType.CHESTPLATE,
+ ItemType.LEGGINGS,
+ ItemType.HELMET,
+ ItemType.BOOTS,
+ )
+ }
+ if (itemType == ItemType.EQUIPMENT) {
+ return listOf(
+ ItemType.CLOAK,
+ ItemType.BRACELET,
+ ItemType.NECKLACE,
+ ItemType.BELT,
+ ItemType.GLOVES,
+ )
+ }
+ if (itemType == ItemType.ROD) {
+ return listOf(ItemType.FISHING_ROD, ItemType.FISHING_WEAPON)
+ }
+ return listOf(itemType)
+ }
+
+ fun <T> NEURepoFile.kJson(serializer: KSerializer<T>): T {
+ val rawJson = json(JsonElement::class.java)
+ try {
+ val kJsonElement = JsonOps.INSTANCE.convertTo(KJsonOps.INSTANCE, rawJson)
+ return Firmament.json.decodeFromJsonElement(serializer, kJsonElement)
+ } catch (ex: Exception) {
+ throw NEURepositoryException(path, "Could not decode kotlin JSON element", ex)
+ }
+ }
+}
diff --git a/src/main/kotlin/repo/RepoItemTypeCache.kt b/src/main/kotlin/repo/RepoItemTypeCache.kt
new file mode 100644
index 0000000..414ec09
--- /dev/null
+++ b/src/main/kotlin/repo/RepoItemTypeCache.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUItem
+import moe.nea.firmament.util.skyblock.ItemType
+
+object RepoItemTypeCache : IReloadable {
+
+ var byItemType: Map<ItemType?, List<NEUItem>> = mapOf()
+
+ override fun reload(repository: NEURepository) {
+ byItemType = repository.items.items.values.groupBy { ItemType.fromEscapeCodeLore(it.lore.lastOrNull() ?: "") }
+ }
+}
diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt
index 667ab73..6d9ba14 100644
--- a/src/main/kotlin/repo/RepoManager.kt
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -53,13 +53,16 @@ object RepoManager {
var recentlyFailedToUpdateItemList = false
val essenceRecipeProvider = EssenceRecipeProvider()
- val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider)
+ val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore)
fun makeNEURepository(path: Path): NEURepository {
return NEURepository.of(path).apply {
registerReloadListener(ItemCache)
+ registerReloadListener(RepoItemTypeCache)
registerReloadListener(ExpLadders)
registerReloadListener(ItemNameLookup)
+ registerReloadListener(ReforgeStore)
+ registerReloadListener(essenceRecipeProvider)
ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this))
registerReloadListener {
if (TestUtil.isInTest) return@registerReloadListener
@@ -70,7 +73,6 @@ object RepoManager {
}
}
}
- registerReloadListener(essenceRecipeProvider)
registerReloadListener(recipeCache)
}
}
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 75245d1..4d07801 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -9,18 +9,31 @@ import net.minecraft.item.ItemStack
import net.minecraft.network.RegistryByteBuf
import net.minecraft.network.codec.PacketCodec
import net.minecraft.network.codec.PacketCodecs
+import net.minecraft.text.Style
import net.minecraft.text.Text
+import net.minecraft.text.TextColor
import net.minecraft.util.Formatting
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.withFallback
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.ReforgeId
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.directLiteralStringContent
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.getReforgeId
+import moe.nea.firmament.util.getUpgradeStars
+import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.petData
+import moe.nea.firmament.util.prepend
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblock.Rarity
import moe.nea.firmament.util.skyblockId
+import moe.nea.firmament.util.useMatch
import moe.nea.firmament.util.withColor
data class SBItemStack constructor(
@@ -29,9 +42,9 @@ data class SBItemStack constructor(
private var stackSize: Int,
private var petData: PetData?,
val extraLore: List<Text> = emptyList(),
- // TODO: grab this star data from nbt if possible
val stars: Int = 0,
val fallback: ItemStack? = null,
+ val reforge: ReforgeId? = null,
) {
fun getStackSize() = stackSize
@@ -68,7 +81,9 @@ data class SBItemStack constructor(
skyblockId,
RepoManager.getNEUItem(skyblockId),
itemStack.count,
- petData = itemStack.petData?.let { PetData.fromHypixel(it) }
+ petData = itemStack.petData?.let { PetData.fromHypixel(it) },
+ stars = itemStack.getUpgradeStars(),
+ reforge = itemStack.getReforgeId()
)
}
@@ -83,6 +98,153 @@ data class SBItemStack constructor(
fun passthrough(itemStack: ItemStack): SBItemStack {
return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack)
}
+
+ fun appendEnhancedStats(
+ itemStack: ItemStack,
+ reforgeStats: Map<String, Double>,
+ buffKind: BuffKind,
+ ) {
+ val namedReforgeStats = reforgeStats
+ .mapKeysTo(mutableMapOf()) { statIdToName(it.key) }
+ val loreMut = itemStack.loreAccordingToNbt.toMutableList()
+ var statBlockLastIndex = -1
+ for (i in loreMut.indices) {
+ val statLine = parseStatLine(loreMut[i])
+ if (statLine == null && statBlockLastIndex >= 0) {
+ break
+ }
+ if (statLine == null) {
+ continue
+ }
+ statBlockLastIndex = i
+ val statBuff = namedReforgeStats.remove(statLine.statName) ?: continue
+ loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute()
+ }
+ if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) {
+ loreMut.add(0, Text.literal(""))
+ }
+ // If there is no stat block the statBlockLastIndex falls through to -1
+ // TODO: this is good enough for some items. some other items might have their stats at a different place.
+ for ((statName, statBuff) in namedReforgeStats) {
+ val statLine = StatLine(statName, null).addStat(statBuff, buffKind)
+ loreMut.add(statBlockLastIndex + 1, statLine.reconstitute())
+ }
+ itemStack.loreAccordingToNbt = loreMut
+ }
+
+ data class StatFormatting(
+ val postFix: String,
+ val color: Formatting,
+ )
+
+ val formattingOverrides = mapOf(
+ "Sea Creature Chance" to StatFormatting("%", Formatting.RED),
+ "Strength" to StatFormatting("", Formatting.RED),
+ "Damage" to StatFormatting("", Formatting.RED),
+ "Bonus Attack Speed" to StatFormatting("%", Formatting.RED),
+ "Shot Cooldown" to StatFormatting("s", Formatting.RED),
+ "Ability Damage" to StatFormatting("%", Formatting.RED),
+ "Crit Damage" to StatFormatting("%", Formatting.RED),
+ "Crit Chance" to StatFormatting("%", Formatting.RED),
+ "Ability Damage" to StatFormatting("%", Formatting.RED),
+ "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN),
+ "Health" to StatFormatting("", Formatting.GREEN),
+ "Defense" to StatFormatting("", Formatting.GREEN),
+ "Fishing Speed" to StatFormatting("", Formatting.GREEN),
+ "Double Hook Chance" to StatFormatting("%", Formatting.GREEN),
+ "Mining Speed" to StatFormatting("", Formatting.GREEN),
+ "Mining Fortune" to StatFormatting("", Formatting.GREEN),
+ "Heat Resistance" to StatFormatting("", Formatting.GREEN),
+ "Swing Range" to StatFormatting("", Formatting.GREEN),
+ "Rift Time" to StatFormatting("", Formatting.GREEN),
+ "Speed" to StatFormatting("", Formatting.GREEN),
+ "Farming Fortune" to StatFormatting("", Formatting.GREEN),
+ "True Defense" to StatFormatting("", Formatting.GREEN),
+ "Mending" to StatFormatting("", Formatting.GREEN),
+ "Foraging Wisdom" to StatFormatting("", Formatting.GREEN),
+ "Farming Wisdom" to StatFormatting("", Formatting.GREEN),
+ "Foraging Fortune" to StatFormatting("", Formatting.GREEN),
+ "Magic Find" to StatFormatting("", Formatting.GREEN),
+ "Ferocity" to StatFormatting("", Formatting.GREEN),
+ "Bonus Pest Chance" to StatFormatting("%", Formatting.GREEN),
+ "Cold Resistance" to StatFormatting("", Formatting.GREEN),
+ "Pet Luck" to StatFormatting("", Formatting.GREEN),
+ "Fear" to StatFormatting("", Formatting.GREEN),
+ "Mana Regen" to StatFormatting("%", Formatting.GREEN),
+ "Rift Damage" to StatFormatting("", Formatting.GREEN),
+ "Hearts" to StatFormatting("", Formatting.GREEN),
+ "Vitality" to StatFormatting("", Formatting.GREEN),
+ // TODO: make this a repo json
+ )
+
+
+ private val statLabelRegex = "(?<statName>.*): ".toPattern()
+
+ enum class BuffKind(
+ val color: Formatting,
+ val prefix: String,
+ val postFix: String,
+ ) {
+ REFORGE(Formatting.BLUE, "(", ")"),
+
+ ;
+ }
+
+ data class StatLine(
+ val statName: String,
+ val value: Text?,
+ val rest: List<Text> = listOf(),
+ val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull()
+ ) {
+ fun addStat(amount: Double, buffKind: BuffKind): StatLine {
+ val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true)
+ return copy(
+ valueNum = (valueNum ?: 0.0) + amount,
+ value = null,
+ rest = rest +
+ listOf(
+ Text.literal(
+ buffKind.prefix + formattedAmount +
+ statFormatting.postFix +
+ buffKind.postFix + " ")
+ .withColor(buffKind.color)))
+ }
+
+ fun formatValue() =
+ Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0,
+ 1,
+ includeSign = true) + statFormatting.postFix + " ")
+ .setStyle(Style.EMPTY.withColor(statFormatting.color))
+
+ val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN)
+ private fun abbreviate(abbreviateTo: Int): String {
+ if (abbreviateTo >= statName.length) return statName
+ val segments = statName.split(" ")
+ return segments.joinToString(" ") {
+ it.substring(0, maxOf(1, abbreviateTo / segments.size))
+ }
+ }
+
+ fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text =
+ Text.literal("").setStyle(Style.EMPTY.withItalic(false))
+ .append(Text.literal("${abbreviate(abbreviateTo)}: ").grey())
+ .append(value ?: formatValue())
+ .also { rest.forEach(it::append) }
+ }
+
+ fun statIdToName(statId: String): String {
+ val segments = statId.split("_")
+ return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } }
+ }
+
+ private fun parseStatLine(line: Text): StatLine? {
+ val sibs = line.siblings
+ val stat = sibs.firstOrNull() ?: return null
+ if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null
+ val statLabel = stat.directLiteralStringContent ?: return null
+ val statName = statLabelRegex.useMatch(statLabel) { group("statName") } ?: return null
+ return StatLine(statName, sibs[1], sibs.subList(2, sibs.size))
+ }
}
constructor(skyblockId: SkyblockId, petData: PetData) : this(
@@ -133,6 +295,25 @@ data class SBItemStack constructor(
}
+ private fun appendReforgeInfo(
+ itemStack: ItemStack,
+ ) {
+ val rarity = Rarity.fromItem(itemStack) ?: return
+ val reforgeId = this.reforge ?: return
+ val reforge = ReforgeStore.modifierLut[reforgeId] ?: return
+ val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf()
+ itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy()
+ .prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE))
+ val data = itemStack.extraAttributes.copy()
+ data.putString("modifier", reforgeId.id)
+ itemStack.extraAttributes = data
+ appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE)
+ }
+
+ // TODO: avoid instantiating the item stack here
+ val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack())
+ val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack())
+
private var itemStack_: ItemStack? = null
private val itemStack: ItemStack
@@ -147,6 +328,7 @@ data class SBItemStack constructor(
return@run neuItem.asItemStack(idHint = skyblockId, replacementData)
.withFallback(fallback)
.copyWithCount(stackSize)
+ .also { appendReforgeInfo(it) }
.also { it.appendLore(extraLore) }
.also { enhanceStatsByStars(it, stars) }
}
diff --git a/src/main/kotlin/util/AprilFoolsUtil.kt b/src/main/kotlin/util/AprilFoolsUtil.kt
new file mode 100644
index 0000000..a940fa1
--- /dev/null
+++ b/src/main/kotlin/util/AprilFoolsUtil.kt
@@ -0,0 +1,10 @@
+package moe.nea.firmament.util
+
+import java.time.LocalDateTime
+import java.time.Month
+
+object AprilFoolsUtil {
+ val isAprilFoolsDay = LocalDateTime.now().let {
+ it.dayOfMonth == 1 && it.month == Month.APRIL
+ }
+}
diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt
index 92fb9e5..4b32c2a 100644
--- a/src/main/kotlin/util/FirmFormatters.kt
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -15,21 +15,25 @@ import net.minecraft.text.Text
object FirmFormatters {
fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments)
- fun formatCommas(long: Long, segments: Int = 3): String {
+ fun formatCommas(long: Long, segments: Int = 3, includeSign: Boolean = false): String {
+ if (long < 0 && long != Long.MIN_VALUE) {
+ return "-" + formatCommas(-long, segments, false)
+ }
+ val prefix = if (includeSign) "+" else ""
val α = long / 1000
if (α != 0L) {
- return formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0')
+ return prefix + formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0')
}
- return long.toString()
+ return prefix + long.toString()
}
fun formatCommas(float: Float, fractionalDigits: Int): String = formatCommas(float.toDouble(), fractionalDigits)
- fun formatCommas(double: Double, fractionalDigits: Int): String {
+ fun formatCommas(double: Double, fractionalDigits: Int, includeSign: Boolean = false): String {
val long = double.toLong()
val δ = (double - long).absoluteValue
val μ = pow(10, fractionalDigits)
val digits = (μ * δ).toInt().toString().padStart(fractionalDigits, '0').trimEnd('0')
- return formatCommas(long) + (if (digits.isEmpty()) "" else ".$digits")
+ return formatCommas(long, includeSign = includeSign) + (if (digits.isEmpty()) "" else ".$digits")
}
fun formatDistance(distance: Double): String {
diff --git a/src/main/kotlin/util/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt
index 44bacfc..1a5d1dd 100644
--- a/src/main/kotlin/util/LegacyFormattingCode.kt
+++ b/src/main/kotlin/util/LegacyFormattingCode.kt
@@ -1,35 +1,37 @@
-
-
package moe.nea.firmament.util
import net.minecraft.util.Formatting
enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) {
- BLACK("BLACK", '0', 0),
- DARK_BLUE("DARK_BLUE", '1', 1),
- DARK_GREEN("DARK_GREEN", '2', 2),
- DARK_AQUA("DARK_AQUA", '3', 3),
- DARK_RED("DARK_RED", '4', 4),
- DARK_PURPLE("DARK_PURPLE", '5', 5),
- GOLD("GOLD", '6', 6),
- GRAY("GRAY", '7', 7),
- DARK_GRAY("DARK_GRAY", '8', 8),
- BLUE("BLUE", '9', 9),
- GREEN("GREEN", 'a', 10),
- AQUA("AQUA", 'b', 11),
- RED("RED", 'c', 12),
- LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
- YELLOW("YELLOW", 'e', 14),
- WHITE("WHITE", 'f', 15),
- OBFUSCATED("OBFUSCATED", 'k', -1),
- BOLD("BOLD", 'l', -1),
- STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
- UNDERLINE("UNDERLINE", 'n', -1),
- ITALIC("ITALIC", 'o', -1),
- RESET("RESET", 'r', -1);
+ BLACK("BLACK", '0', 0),
+ DARK_BLUE("DARK_BLUE", '1', 1),
+ DARK_GREEN("DARK_GREEN", '2', 2),
+ DARK_AQUA("DARK_AQUA", '3', 3),
+ DARK_RED("DARK_RED", '4', 4),
+ DARK_PURPLE("DARK_PURPLE", '5', 5),
+ GOLD("GOLD", '6', 6),
+ GRAY("GRAY", '7', 7),
+ DARK_GRAY("DARK_GRAY", '8', 8),
+ BLUE("BLUE", '9', 9),
+ GREEN("GREEN", 'a', 10),
+ AQUA("AQUA", 'b', 11),
+ RED("RED", 'c', 12),
+ LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
+ YELLOW("YELLOW", 'e', 14),
+ WHITE("WHITE", 'f', 15),
+ OBFUSCATED("OBFUSCATED", 'k', -1),
+ BOLD("BOLD", 'l', -1),
+ STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
+ UNDERLINE("UNDERLINE", 'n', -1),
+ ITALIC("ITALIC", 'o', -1),
+ RESET("RESET", 'r', -1);
+
+ companion object {
+ val byCode = entries.associateBy { it.char }
+ }
- val modern = Formatting.byCode(char)!!
+ val modern = Formatting.byCode(char)!!
- val formattingCode = "§$char"
+ val formattingCode = "§$char"
}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index 294334a..215d2a8 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -64,6 +64,8 @@ object MC {
}
fun sendCommand(command: String) {
+ // TODO: add a queue to this and sendServerChat
+ ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/"))
player?.networkHandler?.sendCommand(command)
}
@@ -96,8 +98,9 @@ object MC {
inline val camera: Entity? get() = instance.cameraEntity
inline val guiAtlasManager get() = instance.guiAtlasManager
inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
+ inline val playerName: String? get() = player?.name?.unformattedString
inline var screen: Screen?
- get() = TestUtil.unlessTesting{ instance.currentScreen }
+ get() = TestUtil.unlessTesting { instance.currentScreen }
set(value) = instance.setScreen(value)
val screenName get() = screen?.title?.unformattedString?.trim()
inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt
index 051d070..e785ff6 100644
--- a/src/main/kotlin/util/SBData.kt
+++ b/src/main/kotlin/util/SBData.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util
+import java.time.ZoneId
import java.util.UUID
import net.hypixel.modapi.HypixelModAPI
import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket
@@ -10,63 +11,66 @@ import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.ServerConnectedEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
-import moe.nea.firmament.events.WorldReadyEvent
object SBData {
- private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
- val profileSuggestTexts = listOf(
- "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
- "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
- )
- var profileId: UUID? = null
+ private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
+ val profileSuggestTexts = listOf(
+ "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
+ "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
+ )
+ var profileId: UUID? = null
- private var hasReceivedProfile = false
- var locraw: Locraw? = null
- val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
- val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
- val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
- var profileIdCommandDebounce = TimeMark.farPast()
- fun init() {
- ServerConnectedEvent.subscribe("SBData:onServerConnected") {
- HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
- }
- HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
- MC.onMainThread {
- val lastLocraw = locraw
- locraw = Locraw(it.serverName,
- it.serverType.getOrNull()?.name?.uppercase(),
- it.mode.getOrNull(),
- it.map.getOrNull())
- SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
- profileIdCommandDebounce = TimeMark.now()
- }
- }
- SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
- if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
- profileIdCommandDebounce = TimeMark.now()
- MC.sendServerCommand("profileid")
- }
- }
- AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
- if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
- event.cancel()
- }
- }
- ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
- val profileMatch = profileRegex.matchEntire(event.unformattedString)
- if (profileMatch != null) {
- val oldProfile = profileId
- try {
- profileId = UUID.fromString(profileMatch.groupValues[1])
- hasReceivedProfile = true
- } catch (e: IllegalArgumentException) {
- profileId = null
- e.printStackTrace()
- }
- if (oldProfile != profileId) {
- ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
- }
- }
- }
- }
+ /**
+ * Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems
+ */
+ val hypixelTimeZone = ZoneId.of("US/Eastern")
+ private var hasReceivedProfile = false
+ var locraw: Locraw? = null
+ val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
+ val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
+ val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
+ var profileIdCommandDebounce = TimeMark.farPast()
+ fun init() {
+ ServerConnectedEvent.subscribe("SBData:onServerConnected") {
+ HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
+ }
+ HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
+ MC.onMainThread {
+ val lastLocraw = locraw
+ locraw = Locraw(it.serverName,
+ it.serverType.getOrNull()?.name?.uppercase(),
+ it.mode.getOrNull(),
+ it.map.getOrNull())
+ SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
+ profileIdCommandDebounce = TimeMark.now()
+ }
+ }
+ SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
+ if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
+ profileIdCommandDebounce = TimeMark.now()
+ MC.sendServerCommand("profileid")
+ }
+ }
+ AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
+ if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
+ event.cancel()
+ }
+ }
+ ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
+ val profileMatch = profileRegex.matchEntire(event.unformattedString)
+ if (profileMatch != null) {
+ val oldProfile = profileId
+ try {
+ profileId = UUID.fromString(profileMatch.groupValues[1])
+ hasReceivedProfile = true
+ } catch (e: IllegalArgumentException) {
+ profileId = null
+ e.printStackTrace()
+ }
+ if (oldProfile != profileId) {
+ ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
+ }
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt
index c42a55c..f15cadb 100644
--- a/src/main/kotlin/util/SkyBlockIsland.kt
+++ b/src/main/kotlin/util/SkyBlockIsland.kt
@@ -35,6 +35,7 @@ private constructor(
val PRIVATE_ISLAND = forMode("dynamic")
val RIFT = forMode("rift")
val MINESHAFT = forMode("mineshaft")
+ val GARDEN = forMode("garden")
}
val userFriendlyName
diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
index 1c1aa77..631b444 100644
--- a/src/main/kotlin/util/SkyblockId.kt
+++ b/src/main/kotlin/util/SkyblockId.kt
@@ -106,7 +106,10 @@ data class HypixelPetInfo(
private val jsonparser = Json { ignoreUnknownKeys = true }
-val ItemStack.extraAttributes: NbtCompound
+var ItemStack.extraAttributes: NbtCompound
+ set(value) {
+ set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(value))
+ }
get() {
val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run {
val component = NbtComponent.of(NbtCompound())
@@ -125,10 +128,26 @@ val ItemStack.skyblockUUID: UUID?
private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") {
val jsonString = it.extraAttributes.getString("petInfo")
if (jsonString.isNullOrBlank()) return@memoize Optional.empty()
- ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
+ ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") {
+ jsonparser.decodeFromString<HypixelPetInfo>(jsonString)
+ }
.or { null }.intoOptional()
}
+fun ItemStack.getUpgradeStars(): Int {
+ return extraAttributes.getInt("upgrade_level").takeIf { it > 0 }
+ ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 }
+ ?: 0
+}
+
+@Serializable
+@JvmInline
+value class ReforgeId(val id: String)
+
+fun ItemStack.getReforgeId(): ReforgeId? {
+ return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId)
+}
+
val ItemStack.petData: HypixelPetInfo?
get() = petDataCache(this).getOrNull()
diff --git a/src/main/kotlin/util/mc/NbtItemData.kt b/src/main/kotlin/util/mc/NbtItemData.kt
index e8a908f..0c49862 100644
--- a/src/main/kotlin/util/mc/NbtItemData.kt
+++ b/src/main/kotlin/util/mc/NbtItemData.kt
@@ -5,8 +5,8 @@ import net.minecraft.component.type.LoreComponent
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
-var ItemStack.loreAccordingToNbt
- get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
+var ItemStack.loreAccordingToNbt: List<Text>
+ get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
set(value) {
set(DataComponentTypes.LORE, LoreComponent(value))
}
diff --git a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt
new file mode 100644
index 0000000..ce596a0
--- /dev/null
+++ b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt
@@ -0,0 +1,29 @@
+package moe.nea.firmament.util.mc
+
+import com.mojang.serialization.DynamicOps
+import java.util.Optional
+import net.minecraft.registry.Registry
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryOps
+import net.minecraft.registry.RegistryWrapper
+import net.minecraft.registry.entry.RegistryEntryOwner
+
+class TolerantRegistriesOps<T>(
+ delegate: DynamicOps<T>,
+ registryInfoGetter: RegistryInfoGetter
+) : RegistryOps<T>(delegate, registryInfoGetter) {
+ constructor(delegate: DynamicOps<T>, registry: RegistryWrapper.WrapperLookup) :
+ this(delegate, CachedRegistryInfoGetter(registry))
+
+ class TolerantOwner<E> : RegistryEntryOwner<E> {
+ override fun ownerEquals(other: RegistryEntryOwner<E>?): Boolean {
+ return true
+ }
+ }
+
+ override fun <E : Any?> getOwner(registryRef: RegistryKey<out Registry<out E>>?): Optional<RegistryEntryOwner<E>> {
+ return super.getOwner(registryRef).map {
+ TolerantOwner()
+ }
+ }
+}
diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt
index 6ddb077..7149379 100644
--- a/src/main/kotlin/util/skyblock/ItemType.kt
+++ b/src/main/kotlin/util/skyblock/ItemType.kt
@@ -13,6 +13,13 @@ value class ItemType private constructor(val name: String) {
return ItemType(name)
}
+ private val obfuscatedRegex = "§[kK].*?(§[0-9a-fA-FrR]|$)".toRegex()
+ fun fromEscapeCodeLore(lore: String): ItemType? {
+ return lore.replace(obfuscatedRegex, "").trim().substringAfter(" ", "")
+ .takeIf { it.isNotEmpty() }
+ ?.let(::ofName)
+ }
+
fun fromItemStack(itemStack: ItemStack): ItemType? {
if (itemStack.petData != null)
return PET
@@ -26,13 +33,31 @@ value class ItemType private constructor(val name: String) {
if (type.isEmpty()) return null
return ofName(type)
}
- return null
+ return itemStack.loreAccordingToNbt.lastOrNull()?.directLiteralStringContent?.let(::fromEscapeCodeLore)
}
+ // TODO: some of those are not actual in game item types, but rather ones included in the repository to splat to multiple in game types. codify those somehow
+
val SWORD = ofName("SWORD")
val DRILL = ofName("DRILL")
val PICKAXE = ofName("PICKAXE")
val GAUNTLET = ofName("GAUNTLET")
+ val LONGSWORD = ofName("LONG SWORD")
+ val EQUIPMENT = ofName("EQUIPMENT")
+ val FISHING_WEAPON = ofName("FISHING WEAPON")
+ val CLOAK = ofName("CLOAK")
+ val BELT = ofName("BELT")
+ val NECKLACE = ofName("NECKLACE")
+ val BRACELET = ofName("BRACELET")
+ val GLOVES = ofName("GLOVES")
+ val ROD = ofName("ROD")
+ val FISHING_ROD = ofName("FISHING ROD")
+ val VACUUM = ofName("VACUUM")
+ val CHESTPLATE = ofName("CHESTPLATE")
+ val LEGGINGS = ofName("LEGGINGS")
+ val HELMET = ofName("HELMET")
+ val BOOTS = ofName("BOOTS")
+ val NIL = ofName("__NIL")
/**
* This one is not really official (it never shows up in game).
diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt
index f26cefe..b19f371 100644
--- a/src/main/kotlin/util/skyblock/Rarity.kt
+++ b/src/main/kotlin/util/skyblock/Rarity.kt
@@ -1,7 +1,16 @@
package moe.nea.firmament.util.skyblock
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+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.text.Style
import net.minecraft.text.Text
+import net.minecraft.util.Formatting
import moe.nea.firmament.util.StringUtil.words
import moe.nea.firmament.util.collections.lastNotNullOfOrNull
import moe.nea.firmament.util.mc.loreAccordingToNbt
@@ -10,6 +19,7 @@ import moe.nea.firmament.util.unformattedString
typealias RepoRarity = io.github.moulberry.repo.data.Rarity
+@Serializable(with = Rarity.Serializer::class)
enum class Rarity(vararg altNames: String) {
COMMON,
UNCOMMON,
@@ -24,11 +34,37 @@ enum class Rarity(vararg altNames: String) {
UNKNOWN
;
- val names = setOf(name) + altNames
+ object Serializer : KSerializer<Rarity> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor(Rarity::class.java.name, PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): Rarity {
+ return valueOf(decoder.decodeString().replace(" ", "_"))
+ }
+ override fun serialize(encoder: Encoder, value: Rarity) {
+ encoder.encodeString(value.name)
+ }
+ }
+
+ val names = setOf(name) + altNames
+ val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this]))
val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name }
companion object {
+ // TODO: inline those formattings as fields
+ val colourMap = mapOf(
+ Rarity.COMMON to Formatting.WHITE,
+ Rarity.UNCOMMON to Formatting.GREEN,
+ Rarity.RARE to Formatting.BLUE,
+ Rarity.EPIC to Formatting.DARK_PURPLE,
+ Rarity.LEGENDARY to Formatting.GOLD,
+ Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
+ Rarity.DIVINE to Formatting.AQUA,
+ Rarity.SPECIAL to Formatting.RED,
+ Rarity.VERY_SPECIAL to Formatting.RED,
+ Rarity.SUPREME to Formatting.DARK_RED,
+ )
val byName = entries.flatMap { en -> en.names.map { it to en } }.toMap()
val fromNeuRepo = entries.associateBy { it.neuRepoRarity }
diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
index c94ebfe..cfd8429 100644
--- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt
+++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
@@ -7,4 +7,5 @@ object SkyBlockItems {
val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND")
val DIAMOND = SkyblockId("DIAMOND")
val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE")
+ val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL")
}
diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt
index 5d95d7a..ab3de43 100644
--- a/src/main/kotlin/util/textutil.kt
+++ b/src/main/kotlin/util/textutil.kt
@@ -133,6 +133,7 @@ fun MutableText.darkGreen() = withColor(Formatting.DARK_GREEN)
fun MutableText.purple() = withColor(Formatting.DARK_PURPLE)
fun MutableText.pink() = withColor(Formatting.LIGHT_PURPLE)
fun MutableText.yellow() = withColor(Formatting.YELLOW)
+fun MutableText.gold() = withColor(Formatting.GOLD)
fun MutableText.grey() = withColor(Formatting.GRAY)
fun MutableText.red() = withColor(Formatting.RED)
fun MutableText.white() = withColor(Formatting.WHITE)
@@ -142,11 +143,15 @@ fun MutableText.bold(): MutableText = styled { it.withBold(true) }
fun MutableText.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND,
- "/firm disablereiwarning"))
+ it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
}
}
+fun MutableText.prepend(text: Text): MutableText {
+ siblings.addFirst(text)
+ return this
+}
+
fun Text.transformEachRecursively(function: (Text) -> Text): Text {
val c = this.content
if (c is TranslatableTextContent) {
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index b045bea..7418529 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -3,6 +3,9 @@ accessible class net/minecraft/client/render/RenderLayer$MultiPhase
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/font/TextRenderer$Drawer
accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator;
+accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable;
+accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V
+accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter
accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V
diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt
new file mode 100644
index 0000000..c57d457
--- /dev/null
+++ b/src/test/resources/testdata/items/hyperion.snbt
@@ -0,0 +1,96 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ ability_scroll: [
+ "IMPLOSION_SCROLL",
+ "WITHER_SHIELD_SCROLL",
+ "SHADOW_WARP_SCROLL"
+ ],
+ art_of_war_count: 1,
+ champion_combat_xp: 1.3556020889209766E7d,
+ donated_museum: 1b,
+ enchantments: {
+ champion: 10,
+ cleave: 5,
+ critical: 6,
+ cubism: 5,
+ ender_slayer: 6,
+ execute: 5,
+ experience: 3,
+ fire_aspect: 2,
+ first_strike: 4,
+ giant_killer: 6,
+ impaling: 3,
+ lethality: 5,
+ looting: 4,
+ luck: 6,
+ scavenger: 4,
+ smite: 7,
+ syphon: 4,
+ thunderlord: 6,
+ ultimate_wise: 5,
+ vampirism: 5,
+ venomous: 5
+ },
+ hot_potato_count: 15,
+ id: "HYPERION",
+ modifier: "heroic",
+ rarity_upgrades: 1,
+ stats_book: 65934,
+ timestamp: 1658091600000L,
+ upgrade_level: 5,
+ uuid: "a45337aa-9eaa-4e6f-aa27-26a42f8eca95"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Heroic Hyperion "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}',
+ "minecraft:enchantment_glint_override": 1b,
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"1145 "},{"color":"dark_gray","text":"(4271)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+355 "},{"color":"yellow","text":"(+30) "},{"color":"dark_gray","text":"(+1,490.37)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+250 "},{"color":"yellow","text":"(+30) "},{"color":"gold","text":"[+5] "},{"color":"blue","text":"(+50) "},{"color":"dark_gray","text":"(+1,064.55)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+70% "},{"color":"dark_gray","text":"(+317.1%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+7% "},{"color":"blue","text":"(+7%) "},{"color":"dark_gray","text":"(+10.5%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+588 "},{"color":"blue","text":"(+125) "},{"color":"dark_gray","text":"(+2,505.09)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+33 "},{"color":"dark_gray","text":"(+45)"}],"italic":false,"text":""}',
+ '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"✎"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"},{"color":"blue","text":", "},{"color":"blue","text":"Champion X"},{"color":"blue","text":", "},{"color":"blue","text":"Cleave V"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Critical VI"},{"color":"blue","text":", "},{"color":"blue","text":"Cubism V"},{"color":"blue","text":", "},{"color":"blue","text":"Ender Slayer VI"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Execute V"},{"color":"blue","text":", "},{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"Fire Aspect II"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Giant Killer VI"},{"color":"blue","text":", "},{"color":"blue","text":"Impaling III"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Lethality V"},{"color":"blue","text":", "},{"color":"blue","text":"Looting IV"},{"color":"blue","text":", "},{"color":"blue","text":"Luck VI"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Scavenger IV"},{"color":"blue","text":", "},{"color":"blue","text":"Smite VII"},{"color":"blue","text":", "},{"color":"blue","text":"Syphon IV"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Thunderlord VI"},{"color":"blue","text":", "},{"color":"blue","text":"Vampirism V"},{"color":"blue","text":", "},{"color":"blue","text":"Venomous V"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Deals "},{"color":"red","text":"+50% "},{"color":"gray","text":"damage to Withers."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Grants "},{"color":"red","text":"+1 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"green","text":"+2 "},{"color":"aqua","text":"✎"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Intelligence "},{"color":"gray","text":"per "},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"green","text":"Scroll Abilities:"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Wither Impact "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"10 blocks"},{"color":"gray","text":" ahead of you."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Then implode dealing "},{"color":"red","text":"21,658 "},{"color":"gray","text":"damage"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"to nearby enemies. Also applies the"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"wither shield scroll ability reducing"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"damage taken and granting an"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"absorption shield for "},{"color":"yellow","text":"5 "},{"color":"gray","text":"seconds."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"150"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"white","text":"Kills: "},{"color":"gold","text":"65,934"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON SWORD "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}'
+ ],
+ "minecraft:unbreakable": {
+ show_in_tooltip: 0b
+ }
+ },
+ count: 1,
+ id: "minecraft:iron_sword"
+}
diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt
new file mode 100644
index 0000000..b73542d
--- /dev/null
+++ b/src/test/resources/testdata/items/implosion-belt.snbt
@@ -0,0 +1,105 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ attributes: {
+ dominance: 1,
+ experience: 1
+ },
+ id: "IMPLOSION_BELT",
+ timestamp: "12/5/22 5:17 PM",
+ uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}',
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}'
+ ],
+ "minecraft:profile": {
+ id: [I;
+ -896440193,
+ -59755884,
+ -1280665573,
+ -1297214643
+ ],
+ properties: [
+ {
+ name: "textures",
+ signature: "",
+ value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9"
+ }
+ ]
+ }
+ },
+ count: 1,
+ id: "minecraft:player_head"
+}{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ attributes: {
+ dominance: 1,
+ experience: 1
+ },
+ id: "IMPLOSION_BELT",
+ timestamp: "12/5/22 5:17 PM",
+ uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}',
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}'
+ ],
+ "minecraft:profile": {
+ id: [I;
+ -896440193,
+ -59755884,
+ -1280665573,
+ -1297214643
+ ],
+ properties: [
+ {
+ name: "textures",
+ signature: "",
+ value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9"
+ }
+ ]
+ }
+ },
+ count: 1,
+ id: "minecraft:player_head"
+ }