diff options
author | Linnea Gräf <nea@nea.moe> | 2024-12-31 16:52:29 +0100 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-12-31 16:52:29 +0100 |
commit | a892a5f90b87b530331a7652dd4eb5bc07bf1c03 (patch) | |
tree | 808ac4b59557b2b0a6c8459a2dac13ce272937a6 /src | |
parent | 533fd68e2be8236c842f53fd0cafa52341226226 (diff) | |
parent | 620f1a45ac02b078e95c68a59a45bd4d24ff176b (diff) | |
download | Firmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.tar.gz Firmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.tar.bz2 Firmament-a892a5f90b87b530331a7652dd4eb5bc07bf1c03.zip |
Merge branch 'mc-1.21.3'
Diffstat (limited to 'src')
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" + } |