diff options
Diffstat (limited to 'src')
190 files changed, 5453 insertions, 2525 deletions
diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt index 20a79a8..dec2559 100644 --- a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt +++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt @@ -20,6 +20,7 @@ import io.github.notenoughupdates.moulconfig.gui.editors.ComponentEditor import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorButton +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorDropdown import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorText import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory @@ -31,9 +32,11 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import net.minecraft.client.gui.screen.Screen import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable import net.minecraft.util.Util import moe.nea.firmament.Firmament 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.FirmamentConfigScreenProvider @@ -115,7 +118,33 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { } } + fun <T> helpRegisterChoice() where T : Enum<T>, T : StringIdentifiable { + register(ChoiceHandler::class.java as Class<ChoiceHandler<T>>) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm<T>(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorDropdown( + this, + handler.universe.map { handler.renderer.getName(option, it).string }.toTypedArray() + ) + } + + override fun toT(any: Any?): T? { + return handler.universe[any as Int] + } + + override fun getType(): Type { + return Int::class.java + } + + override fun fromT(t: T): Any { + return t.ordinal + } + } + } + } + init { + helpRegisterChoice<Nothing>() register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject -> object : ProcessedEditableOptionFirm<Boolean>(option, categoryAccordionId, configObject) { override fun createEditor(): GuiOptionEditor { 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..1097654 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,30 @@ 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()) + if (!hasErrored) { + EntityRenderer.renderEntity( + entity!!, + context, + point.x, point.y, + size.width, size.height, + mouseX.toDouble(), + mouseY.toDouble()) + } } catch (ex: Exception) { ErrorUtil.softError("Failed to render constructed entity: $entity", ex) hasErrored = true + } finally { } 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..aade59e 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,8 @@ 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.compat.rei.recipes.SBShopRecipe import moe.nea.firmament.events.HandledScreenPushREIEvent import moe.nea.firmament.features.inventory.CraftingOverlay import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen @@ -78,7 +80,9 @@ class FirmamentReiPlugin : REIClientPlugin { registry.add(SBForgeRecipe.Category) registry.add(SBMobDropRecipe.Category) registry.add(SBKatRecipe.Category) + registry.add(SBReforgeRecipe.Category) registry.add(SBEssenceUpgradeRecipe.Category) + registry.add(SBShopRecipe.Category) } override fun registerExclusionZones(zones: ExclusionZones) { @@ -91,12 +95,19 @@ class FirmamentReiPlugin : REIClientPlugin { SBCraftingRecipe.Category.catIdentifier, SkyblockCraftingRecipeDynamicGenerator) registry.registerDisplayGenerator( + SBReforgeRecipe.catIdentifier, + SBReforgeRecipe.DynamicGenerator + ) + registry.registerDisplayGenerator( SBForgeRecipe.Category.categoryIdentifier, SkyblockForgeRecipeDynamicGenerator) registry.registerDisplayGenerator( SBMobDropRecipe.Category.categoryIdentifier, SkyblockMobDropRecipeDynamicGenerator) registry.registerDisplayGenerator( + SBShopRecipe.Category.categoryIdentifier, + SkyblockShopRecipeDynamicGenerator) + registry.registerDisplayGenerator( SBKatRecipe.Category.categoryIdentifier, SkyblockKatRecipeDynamicGenerator) registry.registerDisplayGenerator( diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt index 336c103..35a1e1b 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt @@ -9,11 +9,7 @@ package moe.nea.firmament.compat.rei -import com.mojang.blaze3d.platform.GlStateManager.DstFactor -import com.mojang.blaze3d.platform.GlStateManager.SrcFactor -import com.mojang.blaze3d.systems.RenderSystem import me.shedaniel.math.Rectangle -import me.shedaniel.rei.api.client.entry.renderer.BatchedEntryRenderer import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer import me.shedaniel.rei.api.client.gui.widgets.Tooltip import me.shedaniel.rei.api.client.gui.widgets.TooltipContext @@ -21,23 +17,20 @@ import me.shedaniel.rei.api.common.entry.EntryStack import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.DiffuseLighting -import net.minecraft.client.render.LightmapTextureManager -import net.minecraft.client.render.OverlayTexture -import net.minecraft.client.render.VertexConsumerProvider -import net.minecraft.client.render.model.BakedModel -import net.minecraft.client.texture.SpriteAtlasTexture -import net.minecraft.item.ModelTransformationMode import net.minecraft.item.tooltip.TooltipType +import net.minecraft.text.Text import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.ErrorUtil -import moe.nea.firmament.util.MC +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.darkGrey import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt -object NEUItemEntryRenderer : EntryRenderer<SBItemStack>, BatchedEntryRenderer<SBItemStack, BakedModel> { +// TODO: make this re implement BatchedEntryRenderer, if possible (likely not, due to no-alloc rendering) +// Also it is probably not even that much faster now, with render layers. +object NEUItemEntryRenderer : EntryRenderer<SBItemStack> { override fun render( entry: EntryStack<SBItemStack>, context: DrawContext, @@ -46,11 +39,21 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack>, BatchedEntryRenderer<S mouseY: Int, delta: Float ) { - entry.asItemEntry().render(context, bounds, mouseX, mouseY, delta) + context.matrices.push() + context.matrices.translate(bounds.centerX.toFloat(), bounds.centerY.toFloat(), 0F) + context.matrices.scale(bounds.width.toFloat() / 16F, bounds.height.toFloat() / 16F, 1f) + val item = entry.asItemEntry().value + context.drawItemWithoutEntity(item, -8, -8) + context.drawStackOverlay(minecraft.textRenderer, item, -8, -8, + if (entry.value.getStackSize() > 1000) FirmFormatters.shortFormat(entry.value.getStackSize() + .toDouble()) + else null + ) + context.matrices.pop() } val minecraft = MinecraftClient.getInstance() - var canUseVanillaTooltipEvents = false + var canUseVanillaTooltipEvents = true override fun getTooltip(entry: EntryStack<SBItemStack>, tooltipContext: TooltipContext): Tooltip? { val stack = entry.value.asImmutableItemStack() @@ -63,6 +66,7 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack>, BatchedEntryRenderer<S stack, tooltipContext.vanillaContext(), TooltipType.BASIC, lore ) } catch (ex: Exception) { + canUseVanillaTooltipEvents = false ErrorUtil.softError("Failed to use vanilla tooltips", ex) } } else { @@ -73,6 +77,8 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack>, BatchedEntryRenderer<S lore )) } + if (entry.value.getStackSize() > 1000 && lore.isNotEmpty()) + lore.add(1, Text.literal("${entry.value.getStackSize()}x").darkGrey()) // TODO: tags aren't sent as early now so some tooltip components that use tags will crash the game // stack.getTooltip( // Item.TooltipContext.create( @@ -85,88 +91,5 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack>, BatchedEntryRenderer<S return Tooltip.create(lore) } - override fun getExtraData(entry: EntryStack<SBItemStack>): BakedModel { - return MC.itemRenderer.getModel(entry.asItemEntry().value, - MC.world, - MC.player, 0) - - } - - override fun getBatchIdentifier(entry: EntryStack<SBItemStack>, bounds: Rectangle?, extraData: BakedModel): Int { - return 1738923 + if (extraData.isSideLit) 1 else 0 - } - - - override fun startBatch(entryStack: EntryStack<SBItemStack>, e: BakedModel, drawContext: DrawContext, v: Float) { - MC.textureManager.getTexture(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE) - .setFilter(false, false) - RenderSystem.setShaderTexture(0, SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE) - RenderSystem.enableBlend() - RenderSystem.blendFunc(SrcFactor.SRC_ALPHA, DstFactor.ONE_MINUS_SRC_ALPHA) - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) - if (!e.isSideLit) { - DiffuseLighting.disableGuiDepthLighting() - } - } - - override fun renderBase( - entryStack: EntryStack<SBItemStack>, - model: BakedModel, - drawContext: DrawContext, - immediate: VertexConsumerProvider.Immediate, - bounds: Rectangle, - i: Int, - i1: Int, - v: Float - ) { - if (entryStack.isEmpty) return - drawContext.matrices.push() - drawContext.matrices.translate(bounds.centerX.toDouble(), bounds.centerY.toDouble(), 0.0) - // TODO: check the scaling here again - drawContext.matrices.scale( - bounds.width.toFloat(), - (bounds.height + bounds.height) / -2F, - (bounds.width + bounds.height) / 2f) - MC.itemRenderer.renderItem( - entryStack.value.asImmutableItemStack(), - ModelTransformationMode.GUI, - false, drawContext.matrices, - immediate, LightmapTextureManager.MAX_LIGHT_COORDINATE, - OverlayTexture.DEFAULT_UV, - model - ) - drawContext.matrices.pop() - } - - override fun afterBase(entryStack: EntryStack<SBItemStack>?, e: BakedModel, drawContext: DrawContext?, v: Float) { - RenderSystem.enableDepthTest() - if (!e.isSideLit) - DiffuseLighting.enableGuiDepthLighting() - } - - override fun renderOverlay( - entryStack: EntryStack<SBItemStack>, - e: BakedModel, - drawContext: DrawContext, - immediate: VertexConsumerProvider.Immediate, - bounds: Rectangle, - i: Int, - i1: Int, - v: Float - ) { - if (entryStack.isEmpty) return - val modelViewStack = RenderSystem.getModelViewStack() - modelViewStack.pushMatrix() - modelViewStack.mul(drawContext.matrices.peek().positionMatrix) - modelViewStack.translate(bounds.x.toFloat(), bounds.y.toFloat(), 0F) - modelViewStack.scale(bounds.width / 16.0f, - (bounds.width + bounds.height) / 2.0f / 16.0f, - 1.0f) // TODO: weird scale again - drawContext.drawStackOverlay(MC.font, entryStack.value.asImmutableItemStack(), 0, 0, null) - modelViewStack.popMatrix() - } - - override fun endBatch(entryStack: EntryStack<SBItemStack>?, e: BakedModel?, drawContext: DrawContext?, v: Float) { - } } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt index a242c1b..2b1700d 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt @@ -9,17 +9,15 @@ import me.shedaniel.rei.api.common.entry.comparison.ComparisonContext import me.shedaniel.rei.api.common.entry.type.EntryDefinition import me.shedaniel.rei.api.common.entry.type.EntryType import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes +import net.minecraft.item.ItemConvertible import net.minecraft.item.ItemStack import net.minecraft.registry.tag.TagKey import net.minecraft.text.Text import net.minecraft.util.Identifier import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry -import moe.nea.firmament.repo.PetData import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.SkyblockId -import moe.nea.firmament.util.petData -import moe.nea.firmament.util.skyBlockId object SBItemEntryDefinition : EntryDefinition<SBItemStack> { override fun equals(o1: SBItemStack, o2: SBItemStack, context: ComparisonContext): Boolean { @@ -54,7 +52,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { override fun wildcard(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack { return value.copy(stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId), - stars = 0, extraLore = listOf()) + stars = 0, extraLore = listOf(), reforge = null) } override fun normalize(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack { @@ -82,13 +80,8 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> { fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> = getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt()) + fun getPassthrough(item: ItemConvertible) = getEntry(SBItemStack.passthrough(ItemStack(item.asItem()))) + fun getEntry(stack: ItemStack): EntryStack<SBItemStack> = - getEntry( - SBItemStack( - stack.skyBlockId ?: SkyblockId.NULL, - RepoManager.getNEUItem(stack.skyBlockId ?: SkyblockId.NULL), - stack.count, - petData = stack.petData?.let { PetData.fromHypixel(it) } - ) - ) + getEntry(SBItemStack(stack)) } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt index f52f418..e80840f 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt @@ -1,11 +1,10 @@ - - package moe.nea.firmament.compat.rei import io.github.moulberry.repo.data.NEUCraftingRecipe import io.github.moulberry.repo.data.NEUForgeRecipe import io.github.moulberry.repo.data.NEUKatUpgradeRecipe import io.github.moulberry.repo.data.NEUMobDropRecipe +import io.github.moulberry.repo.data.NEUNpcShopRecipe import io.github.moulberry.repo.data.NEURecipe import java.util.Optional import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator @@ -17,49 +16,51 @@ 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.SBShopRecipe import moe.nea.firmament.repo.EssenceRecipeProvider import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack val SkyblockCraftingRecipeDynamicGenerator = - neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> { SBCraftingRecipe(it) } + neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> { SBCraftingRecipe(it) } val SkyblockForgeRecipeDynamicGenerator = - neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> { SBForgeRecipe(it) } + neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> { SBForgeRecipe(it) } val SkyblockMobDropRecipeDynamicGenerator = - neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) } - + neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) } +val SkyblockShopRecipeDynamicGenerator = + neuDisplayGenerator<SBShopRecipe, NEUNpcShopRecipe> { SBShopRecipe(it) } val SkyblockKatRecipeDynamicGenerator = - neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) } + neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) } val SkyblockEssenceRecipeDynamicGenerator = - neuDisplayGenerator<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { SBEssenceUpgradeRecipe(it) } + neuDisplayGeneratorWithItem<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { item, recipe -> + SBEssenceUpgradeRecipe(recipe, item) + } inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(crossinline mapper: (T) -> D) = - object : DynamicDisplayGenerator<D> { - override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> { - if (entry.type != SBItemEntryDefinition.type) return Optional.empty() - val item = entry.castValue<SBItemStack>() - val recipes = RepoManager.getRecipesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() - return Optional.of(craftingRecipes.map(mapper)) - } + neuDisplayGeneratorWithItem<D, T> { _, it -> mapper(it) } - override fun generate(builder: ViewSearchBuilder): Optional<List<D>> { - if (SBCraftingRecipe.Category.catIdentifier !in builder.categories) return Optional.empty() - return Optional.of( - RepoManager.getAllRecipes().filterIsInstance<T>().map { mapper(it) } - .toList() - ) - } +inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(crossinline mapper: (SBItemStack, T) -> D) = + object : DynamicDisplayGenerator<D> { + override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue<SBItemStack>() + val recipes = RepoManager.getRecipesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance<T>() + return Optional.of(craftingRecipes.map { mapper(item, it) }) + } - override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> { - if (entry.type != SBItemEntryDefinition.type) return Optional.empty() - val item = entry.castValue<SBItemStack>() - val recipes = RepoManager.getUsagesFor(item.skyblockId) - val craftingRecipes = recipes.filterIsInstance<T>() - return Optional.of(craftingRecipes.map(mapper)) + override fun generate(builder: ViewSearchBuilder): Optional<List<D>> { + return Optional.empty() // TODO: allows searching without blocking getRecipeFor + } - } - } + override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue<SBItemStack>() + val recipes = RepoManager.getUsagesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance<T>() + return Optional.of(craftingRecipes.map { mapper(item, it) }) + } + } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt index cfb6f74..518f7b4 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt @@ -17,8 +17,7 @@ object SkyblockItemIdFocusedStackProvider : FocusedStackProvider { screen as AccessorHandledScreen val focusedSlot = screen.focusedSlot_Firmament ?: return CompoundEventResult.pass() val item = focusedSlot.stack ?: return CompoundEventResult.pass() - val skyblockId = item.skyBlockId ?: return CompoundEventResult.pass() - return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(skyblockId)) + return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(item)) } override fun getPriority(): Double = 1_000_000.0 diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt index fd04abc..c02e078 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt @@ -16,17 +16,18 @@ import net.minecraft.block.Blocks import net.minecraft.text.Text import moe.nea.firmament.Firmament import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.repo.SBItemStack class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() { override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier object Category : DisplayCategory<SBCraftingRecipe> { - val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafing_recipe") + val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafting_recipe") override fun getCategoryIdentifier(): CategoryIdentifier<out SBCraftingRecipe> = catIdentifier override fun getTitle(): Text = Text.literal("SkyBlock Crafting") - override fun getIcon(): Renderer = EntryStacks.of(Blocks.CRAFTING_TABLE) + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.CRAFTING_TABLE) override fun setupDisplay(display: SBCraftingRecipe, bounds: Rectangle): List<Widget> { val point = Point(bounds.centerX - 58, bounds.centerY - 27) return buildList { @@ -39,7 +40,7 @@ class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() { add(slot) val item = display.neuRecipe.inputs[i + j * 3] if (item == NEUIngredient.SENTINEL_EMPTY) continue - slot.entry(SBItemEntryDefinition.getEntry(item)) // TODO: make use of stackable item entries + slot.entry(SBItemEntryDefinition.getEntry(item)) } } add( diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt index ec71ec8..e0a3784 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt @@ -14,7 +14,8 @@ import moe.nea.firmament.repo.EssenceRecipeProvider import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.SkyblockId -class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.EssenceUpgradeRecipe) : SBRecipe() { +class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.EssenceUpgradeRecipe, + val sourceItem: SBItemStack) : SBRecipe() { object Category : DisplayCategory<SBEssenceUpgradeRecipe> { override fun getCategoryIdentifier(): CategoryIdentifier<SBEssenceUpgradeRecipe> = CategoryIdentifier.of(Firmament.MOD_ID, "essence_upgrade") @@ -33,13 +34,13 @@ class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.Essen list.add(Widgets.createRecipeBase(bounds)) list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 - 18 / 2)) .markInput() - .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter - 1)))) + .entry(SBItemEntryDefinition.getEntry(display.sourceItem.copy(stars = recipe.starCountAfter - 1)))) list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 + 18 / 2)) .markInput() .entry(SBItemEntryDefinition.getEntry(recipe.essenceIngredient))) list.add(Widgets.createSlot(Point(bounds.maxX - 12 - 16, bounds.centerY - 8)) .markOutput() - .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter)))) + .entry(SBItemEntryDefinition.getEntry(display.sourceItem.copy(stars = recipe.starCountAfter)))) val extraItems = recipe.extraItems list.add(Widgets.createArrow(Point(bounds.centerX - 24 / 2, if (extraItems.isEmpty()) bounds.centerY - 17 / 2 diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt index 96af3fd..7a0ec78 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt @@ -8,7 +8,6 @@ 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.common.category.CategoryIdentifier -import me.shedaniel.rei.api.common.util.EntryStacks import kotlin.math.cos import kotlin.math.sin import kotlin.time.Duration.Companion.seconds @@ -30,7 +29,7 @@ class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() { return 104 } - override fun getIcon(): Renderer = EntryStacks.of(Blocks.ANVIL) + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.ANVIL) override fun setupDisplay(display: SBForgeRecipe, bounds: Rectangle): List<Widget> { return buildList { add(Widgets.createRecipeBase(bounds)) @@ -41,6 +40,7 @@ class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() { Text.stringifiedTranslatable("firmament.recipe.forge.time", display.neuRecipe.duration.seconds))) val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) + add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(25.0)) val count = display.neuRecipe.inputs.size if (count == 1) { add( diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt index bafbdcc..cce1465 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt @@ -43,7 +43,7 @@ class SBKatRecipe(override val neuRecipe: NEUKatUpgradeRecipe) : SBRecipe() { return 100 } - override fun getIcon(): Renderer = EntryStacks.of(Items.BONE) + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.BONE) override fun setupDisplay(display: SBKatRecipe, bounds: Rectangle): List<Widget> { return buildList { val arrowWidth = 24 diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt index b05c3c7..b595c23 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt @@ -29,7 +29,7 @@ class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { return 100 } - override fun getIcon(): Renderer = EntryStacks.of(Items.DIAMOND_SWORD) + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.DIAMOND_SWORD) override fun setupDisplay(display: SBMobDropRecipe, bounds: Rectangle): List<Widget> { return buildList { add(Widgets.createRecipeBase(bounds)) 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..b8313a6 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -0,0 +1,212 @@ +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: SBItemStack?, +) : 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)) + } + for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) { + reforgeRecipes.add(SBReforgeRecipe(reforge, item)) + } + 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 inputItems = run { + if (limitToItem != null) return@run listOf(SBItemEntryDefinition.getEntry(limitToItem)) + val eligibleItems = reforge.eligibleItems.flatMap { + when (it) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> + listOfNotNull(RepoManager.getNEUItem(it.internalName)) + + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> + ReforgeStore.resolveItemType(it.itemType) + .flatMapTo(mutableSetOf()) { + (RepoItemTypeCache.byItemType[it] ?: listOf()) + + (RepoItemTypeCache.byItemType[it.dungeonVariant] ?: listOf()) + }.toList() + + 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 + } + } + } + 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/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt new file mode 100644 index 0000000..a252802 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt @@ -0,0 +1,61 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEUNpcShopRecipe +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.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.entry.EntryIngredient +import net.minecraft.item.Items +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.util.skyblockId + +class SBShopRecipe(override val neuRecipe: NEUNpcShopRecipe) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier + val merchant = SBItemEntryDefinition.getEntry(neuRecipe.isSoldBy.skyblockId) + override fun getInputEntries(): List<EntryIngredient> { + return listOf(EntryIngredient.of(merchant)) + super.getInputEntries() + } + + object Category : DisplayCategory<SBShopRecipe> { + val catIdentifier = CategoryIdentifier.of<SBShopRecipe>(Firmament.MOD_ID, "npc_shopping") + override fun getCategoryIdentifier(): CategoryIdentifier<SBShopRecipe> = catIdentifier + + override fun getTitle(): Text = Text.literal("SkyBlock NPC Shopping") + + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.EMERALD) + override fun setupDisplay(display: SBShopRecipe, bounds: Rectangle): List<Widget> { + val point = Point(bounds.centerX, bounds.centerY) + return buildList { + add(Widgets.createRecipeBase(bounds)) + add(Widgets.createSlot(Point(point.x - 2 - 18 / 2, point.y - 18 - 6)) + .unmarkInputOrOutput() + .entry(display.merchant) + .disableBackground()) + add(Widgets.createArrow(Point(point.x - 2 - 24 / 2, point.y - 6))) + val cost = display.neuRecipe.cost + for ((i, item) in cost.withIndex()) { + add(Widgets.createSlot(Point( + point.x - 14 - 18, + point.y + i * 18 - 18 * cost.size / 2)) + .entry(SBItemEntryDefinition.getEntry(item)) + .markInput()) + // TODO: fix frame clipping + } + add(Widgets.createResultSlotBackground(Point(point.x + 18, point.y - 18 / 2))) + add( + Widgets.createSlot(Point(point.x + 18, point.y - 18 / 2)) + .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.result)) + .disableBackground().markOutput() + ) + } + } + + } + +} diff --git a/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java b/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java new file mode 100644 index 0000000..14eeaf2 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java @@ -0,0 +1,20 @@ +package moe.nea.firmament.mixins.compat; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +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.CallbackInfo; + +@Mixin(targets = "me.shedaniel.rei.impl.client.gui.hints.ImportantWarningsWidget") +@Pseudo +public class HideREIRecipeWarning { + @Shadow + private boolean visible; + + @Inject(method = "<init>", at = @At("TAIL")) + private void onCreateImportantWidget(CallbackInfo ci) { + visible = false; + } +} diff --git a/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java b/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java index 723af59..c3e8950 100644 --- a/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java +++ b/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java @@ -1,14 +1,12 @@ package moe.nea.firmament.mixins.compat.wildfiregender; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Local; import com.wildfire.render.GenderArmorLayer; import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; -import net.minecraft.item.ArmorItem; -import net.minecraft.item.ArmorMaterial; +import net.minecraft.component.type.EquippableComponent; +import net.minecraft.entity.EquipmentSlot; import net.minecraft.item.ItemStack; -import net.minecraft.registry.entry.RegistryEntry; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.injection.At; @@ -16,22 +14,10 @@ import org.spongepowered.asm.mixin.injection.At; @Mixin(GenderArmorLayer.class) @Pseudo public class PatchArmorTexturesInGenderMod { - @WrapOperation(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/entity/LivingEntity;FFFFFF)V", - at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ArmorItem;getMaterial()Lnet/minecraft/registry/entry/RegistryEntry;")) - private RegistryEntry<ArmorMaterial> replaceArmorMaterial(ArmorItem instance, Operation<RegistryEntry<ArmorMaterial>> original, @Local ItemStack chestplate) { - var entry = original.call(instance); - var overrides = CustomGlobalArmorOverrides.overrideArmor(chestplate); - if (overrides == null) - return entry; - var material = entry.value(); - return RegistryEntry.of(new ArmorMaterial( - material.defense(), - material.enchantability(), - material.equipSound(), - material.repairIngredient(), - overrides, - material.toughness(), - material.knockbackResistance() - )); - } + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/BipedEntityRenderState;FF)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object replaceArmorMaterial(Object original, @Local ItemStack chestplate) { + var overrides = CustomGlobalArmorOverrides.overrideArmor(chestplate, EquipmentSlot.CHEST); + return overrides.orElse((EquippableComponent) original); + } } 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/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java index 9b891a4..0713068 100644 --- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java +++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java @@ -16,160 +16,168 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class AutoDiscoveryPlugin { - private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>(); - - public static List<AutoDiscoveryPlugin> getMixinPlugins() { - return mixinPlugins; - } - - private String mixinPackage; - - public void setMixinPackage(String mixinPackage) { - this.mixinPackage = mixinPackage; - mixinPlugins.add(this); - } - - /** - * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. - * In either case the return value of this + the class name will resolve back to the original class url, or to other - * class urls for other classes. - */ - public URL getBaseUrlForClassUrl(URL classUrl) { - String string = classUrl.toString(); - if (classUrl.getProtocol().equals("jar")) { - try { - return new URL(string.substring(4).split("!")[0]); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - if (string.endsWith(".class")) { - try { - return new URL(string.replace("\\", "/") - .replace(getClass().getCanonicalName() - .replace(".", "/") + ".class", "")); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - return classUrl; - } - - /** - * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}. - */ - public String getMixinPackage() { - return mixinPackage; - } - - /** - * Get the path inside the class root to the mixin package - */ - public String getMixinBaseDir() { - return mixinPackage.replace(".", "/"); - } - - /** - * A list of all discovered mixins. - */ - private List<String> mixins = null; - - /** - * Try to add mixin class ot the mixins based on the filepath inside of the class root. - * Removes the {@code .class} file suffix, as well as the base mixin package. - * <p><b>This method cannot be called after mixin initialization.</p> - * - * @param className the name or path of a class to be registered as a mixin. - */ - public void tryAddMixinClass(String className) { - if (!className.endsWith(".class")) return; - String norm = (className.substring(0, className.length() - ".class".length())) - .replace("\\", "/") - .replace("/", "."); - if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { - mixins.add(norm.substring(getMixinPackage().length() + 1)); - } - } - - private void tryDiscoverFromContentFile(URL url) { - Path file; - try { - file = Paths.get(getBaseUrlForClassUrl(url).toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - System.out.println("Base directory found at " + file); - if (!Files.exists(file)) { - System.out.println("Skipping non-existing mixin root: " + file); - return; - } - if (Files.isDirectory(file)) { - walkDir(file); - } else { - walkJar(file); - } - System.out.println("Found mixins: " + mixins); - - } - - /** - * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()} - */ - public List<String> getMixins() { - if (mixins != null) return mixins; - System.out.println("Trying to discover mixins"); - mixins = new ArrayList<>(); - URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); - System.out.println("Found classes at " + classUrl); - tryDiscoverFromContentFile(classUrl); - var classRoots = System.getProperty("firmament.classroots"); - if (classRoots != null && !classRoots.isBlank()) { - System.out.println("Found firmament class roots: " + classRoots); - for (String s : classRoots.split(File.pathSeparator)) { - if (s.isBlank()) { - continue; - } - try { - tryDiscoverFromContentFile(new File(s).toURI().toURL()); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - } - return mixins; - } - - /** - * Search through directory for mixin classes based on {@link #getMixinBaseDir}. - * - * @param classRoot The root directory in which classes are stored for the default package. - */ - private void walkDir(Path classRoot) { - System.out.println("Trying to find mixins from directory"); - var path = classRoot.resolve(getMixinBaseDir()); - if (!Files.exists(path)) return; - try (Stream<Path> classes = Files.walk(path)) { - classes.map(it -> classRoot.relativize(it).toString()) - .forEach(this::tryAddMixinClass); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Read through a JAR file, trying to find all mixins inside. - */ - private void walkJar(Path file) { - System.out.println("Trying to find mixins from jar file"); - try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { - ZipEntry next; - while ((next = zis.getNextEntry()) != null) { - tryAddMixinClass(next.getName()); - zis.closeEntry(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } + public static List<String> getDefaultAllMixinClassesFQNs() { + var defaultName = "moe.nea.firmament.mixins"; + var plugin = new AutoDiscoveryPlugin(); + plugin.setMixinPackage(defaultName); + var mixins = plugin.getMixins(); + return mixins.stream().map(it -> defaultName + "." + it).toList(); + } + + private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>(); + + public static List<AutoDiscoveryPlugin> getMixinPlugins() { + return mixinPlugins; + } + + private String mixinPackage; + + public void setMixinPackage(String mixinPackage) { + this.mixinPackage = mixinPackage; + mixinPlugins.add(this); + } + + /** + * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. + * In either case the return value of this + the class name will resolve back to the original class url, or to other + * class urls for other classes. + */ + public URL getBaseUrlForClassUrl(URL classUrl) { + String string = classUrl.toString(); + if (classUrl.getProtocol().equals("jar")) { + try { + return new URL(string.substring(4).split("!")[0]); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + if (string.endsWith(".class")) { + try { + return new URL(string.replace("\\", "/") + .replace(getClass().getCanonicalName() + .replace(".", "/") + ".class", "")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + return classUrl; + } + + /** + * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}. + */ + public String getMixinPackage() { + return mixinPackage; + } + + /** + * Get the path inside the class root to the mixin package + */ + public String getMixinBaseDir() { + return mixinPackage.replace(".", "/"); + } + + /** + * A list of all discovered mixins. + */ + private List<String> mixins = null; + + /** + * Try to add mixin class ot the mixins based on the filepath inside of the class root. + * Removes the {@code .class} file suffix, as well as the base mixin package. + * <p><b>This method cannot be called after mixin initialization.</p> + * + * @param className the name or path of a class to be registered as a mixin. + */ + public void tryAddMixinClass(String className) { + if (!className.endsWith(".class")) return; + String norm = (className.substring(0, className.length() - ".class".length())) + .replace("\\", "/") + .replace("/", "."); + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { + mixins.add(norm.substring(getMixinPackage().length() + 1)); + } + } + + private void tryDiscoverFromContentFile(URL url) { + Path file; + try { + file = Paths.get(getBaseUrlForClassUrl(url).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + System.out.println("Base directory found at " + file); + if (!Files.exists(file)) { + System.out.println("Skipping non-existing mixin root: " + file); + return; + } + if (Files.isDirectory(file)) { + walkDir(file); + } else { + walkJar(file); + } + System.out.println("Found mixins: " + mixins); + + } + + /** + * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()} + */ + public List<String> getMixins() { + if (mixins != null) return mixins; + System.out.println("Trying to discover mixins"); + mixins = new ArrayList<>(); + URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + System.out.println("Found classes at " + classUrl); + tryDiscoverFromContentFile(classUrl); + var classRoots = System.getProperty("firmament.classroots"); + if (classRoots != null && !classRoots.isBlank()) { + System.out.println("Found firmament class roots: " + classRoots); + for (String s : classRoots.split(File.pathSeparator)) { + if (s.isBlank()) { + continue; + } + try { + tryDiscoverFromContentFile(new File(s).toURI().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + } + return mixins; + } + + /** + * Search through directory for mixin classes based on {@link #getMixinBaseDir}. + * + * @param classRoot The root directory in which classes are stored for the default package. + */ + private void walkDir(Path classRoot) { + System.out.println("Trying to find mixins from directory"); + var path = classRoot.resolve(getMixinBaseDir()); + if (!Files.exists(path)) return; + try (Stream<Path> classes = Files.walk(path)) { + classes.map(it -> classRoot.relativize(it).toString()) + .forEach(this::tryAddMixinClass); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Read through a JAR file, trying to find all mixins inside. + */ + private void walkJar(Path file) { + System.out.println("Trying to find mixins from jar file"); + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { + ZipEntry next; + while ((next = zis.getNextEntry()) != null) { + tryAddMixinClass(next.getName()); + zis.closeEntry(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/moe/nea/firmament/init/EarlyRiser.java b/src/main/java/moe/nea/firmament/init/EarlyRiser.java index 9734e94..5441255 100644 --- a/src/main/java/moe/nea/firmament/init/EarlyRiser.java +++ b/src/main/java/moe/nea/firmament/init/EarlyRiser.java @@ -7,6 +7,6 @@ public class EarlyRiser implements Runnable { new ClientPlayerRiser().addTinkerers(); new HandledScreenRiser().addTinkerers(); new SectionBuilderRiser().addTinkerers(); - new ItemColorsSodiumRiser().addTinkerers(); +// TODO: new ItemColorsSodiumRiser().addTinkerers(); } } diff --git a/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java index 1fbbe45..f7db18c 100644 --- a/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java +++ b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java @@ -3,7 +3,6 @@ package moe.nea.firmament.init; import me.shedaniel.mm.api.ClassTinkerers; import net.minecraft.client.gui.Element; -import net.minecraft.client.gui.ParentElement; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; @@ -32,12 +31,18 @@ public class HandledScreenRiser extends RiserUtils { String keyReleased = remapper.mapMethodName("intermediary", Intermediary.<Element>className(), Intermediary.methodName(Element::keyReleased), keyReleasedDesc.getDescriptor()); + // public boolean charTyped(char chr, int modifiers) + Type charTypedDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.INT_TYPE); + String charTyped = remapper.mapMethodName("intermediary", Intermediary.<Element>className(), + Intermediary.methodName(Element::charTyped), + charTypedDesc.getDescriptor()); @Override public void addTinkerers() { ClassTinkerers.addTransformation(HandledScreen, this::addMouseScroll, true); ClassTinkerers.addTransformation(HandledScreen, this::addKeyReleased, true); + ClassTinkerers.addTransformation(HandledScreen, this::addCharTyped, true); } /** @@ -70,86 +75,77 @@ public class HandledScreenRiser extends RiserUtils { } void addKeyReleased(ClassNode classNode) { - var keyReleasedNode = findMethod(classNode, keyReleased, keyReleasedDesc); + addSuperInjector( + classNode, keyReleased, keyReleasedDesc, "keyReleased_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // ILOAD 1-3, load args + insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 3)); + }); + } + + void addCharTyped(ClassNode classNode) { + addSuperInjector( + classNode, charTyped, charTypedDesc, "charTyped_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // ILOAD 1-2, load args. chars = ints + insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); + }); + } + + void addSuperInjector( + ClassNode classNode, + String name, + Type desc, + String firmamentName, + Consumer<InsnList> loadArgs + ) { + var keyReleasedNode = findMethod(classNode, name, desc); if (keyReleasedNode == null) { keyReleasedNode = new MethodNode( Modifier.PUBLIC, - keyReleased, - keyReleasedDesc.getDescriptor(), + name, + desc.getDescriptor(), null, new String[0] ); var insns = keyReleasedNode.instructions; - // ALOAD 0, load this - insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - // ILOAD 1-3, load args - insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); - insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); - insns.add(new VarInsnNode(Opcodes.ILOAD, 3)); + loadArgs.accept(insns); // INVOKESPECIAL call super method insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(), - keyReleased, keyReleasedDesc.getDescriptor())); + name, desc.getDescriptor())); // IRETURN return int on stack (booleans are int at runtime) insns.add(new InsnNode(Opcodes.IRETURN)); classNode.methods.add(keyReleasedNode); } - insertTrueHandler(keyReleasedNode, insns -> { - // ALOAD 0, load this - insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - // ILOAD 1-3, load args - insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); - insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); - insns.add(new VarInsnNode(Opcodes.ILOAD, 3)); - }, insns -> { + insertTrueHandler(keyReleasedNode, loadArgs, insns -> { // INVOKEVIRTUAL call custom handler insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, getTypeForClassName(HandledScreen).getInternalName(), - "keyReleased_firmament", - keyReleasedDesc.getDescriptor())); + firmamentName, + desc.getDescriptor())); }); + } void addMouseScroll(ClassNode classNode) { - MethodNode mouseScrolledNode = findMethod(classNode, mouseScrolled, mouseScrolledDesc); - if (mouseScrolledNode == null) { - mouseScrolledNode = new MethodNode( - Modifier.PUBLIC, - mouseScrolled, - mouseScrolledDesc.getDescriptor(), - null, - new String[0] - ); - var insns = mouseScrolledNode.instructions; - // ALOAD 0, load this - insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time. - insns.add(new VarInsnNode(Opcodes.DLOAD, 1)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 3)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 5)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 7)); - // INVOKESPECIAL call super method - insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(), mouseScrolled, mouseScrolledDesc.getDescriptor())); - // IRETURN return int on stack (booleans are int at runtime) - insns.add(new InsnNode(Opcodes.IRETURN)); - classNode.methods.add(mouseScrolledNode); - } - - insertTrueHandler(mouseScrolledNode, insns -> { - // ALOAD 0, load this - insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); - // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time. - insns.add(new VarInsnNode(Opcodes.DLOAD, 1)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 3)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 5)); - insns.add(new VarInsnNode(Opcodes.DLOAD, 7)); - }, insns -> { - // INVOKEVIRTUAL call custom handler - insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, - getTypeForClassName(HandledScreen).getInternalName(), - "mouseScrolled_firmament", - mouseScrolledDesc.getDescriptor())); - - }); + addSuperInjector( + classNode, mouseScrolled, mouseScrolledDesc, "mouseScrolled_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time. + insns.add(new VarInsnNode(Opcodes.DLOAD, 1)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 3)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 5)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 7)); + }); } } diff --git a/src/main/java/moe/nea/firmament/init/ItemColorsSodiumRiser.java b/src/main/java/moe/nea/firmament/init/ItemColorsSodiumRiser.java deleted file mode 100644 index 80ee9aa..0000000 --- a/src/main/java/moe/nea/firmament/init/ItemColorsSodiumRiser.java +++ /dev/null @@ -1,64 +0,0 @@ -package moe.nea.firmament.init; - -import me.shedaniel.mm.api.ClassTinkerers; -import moe.nea.firmament.util.ErrorUtil; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.color.item.ItemColorProvider; -import net.minecraft.client.color.item.ItemColors; -import net.minecraft.item.ItemStack; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.InsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.VarInsnNode; - -public class ItemColorsSodiumRiser extends RiserUtils { - @IntermediaryName(ItemColors.class) - String ItemColors; - @IntermediaryName(ItemColorProvider.class) - String ItemColorProvider; - @IntermediaryName(ItemStack.class) - String ItemStack; - String getColorProvider = "sodium$getColorProvider"; - Type getColorProviderDesc = Type.getMethodType(getTypeForClassName(ItemColorProvider), - getTypeForClassName(ItemStack)); - - @Override - public void addTinkerers() { - ClassTinkerers.addTransformation(ItemColors, this::addSodiumOverride, true); - } - - private void addSodiumOverride(ClassNode classNode) { - var node = findMethod(classNode, getColorProvider, getColorProviderDesc); - if (node == null) { - if (!FabricLoader.getInstance().isModLoaded("sodium")) - ErrorUtil.INSTANCE.softError("Sodium is present, but sodium color override could not be injected."); - return; - } - var p = node.instructions.getFirst(); - while (p != null) { - if (p.getOpcode() == Opcodes.ARETURN) { - node.instructions.insertBefore( - p, - mkOverrideSodiumCall() - ); - } - p = p.getNext(); - } - } - - private InsnList mkOverrideSodiumCall() { - var insnList = new InsnList(); - insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); - insnList.add(new InsnNode(Opcodes.SWAP)); - insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, - getTypeForClassName(ItemColors).getInternalName(), - "overrideSodium_firmament", - Type.getMethodType(getTypeForClassName(ItemColorProvider), - getTypeForClassName(ItemColorProvider)).getDescriptor(), - false)); - return insnList; - } -} 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/CustomModelEventPatch.java b/src/main/java/moe/nea/firmament/mixins/CustomModelEventPatch.java deleted file mode 100644 index e0a7544..0000000 --- a/src/main/java/moe/nea/firmament/mixins/CustomModelEventPatch.java +++ /dev/null @@ -1,36 +0,0 @@ - - -package moe.nea.firmament.mixins; - -import moe.nea.firmament.events.CustomItemModelEvent; -import moe.nea.firmament.features.texturepack.CustomGlobalTextures; -import net.minecraft.client.render.item.ItemModels; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.BakedModelManager; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Final; -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; - -import java.util.Map; - -@Mixin(ItemModels.class) -public class CustomModelEventPatch { - - @Inject(method = "getModel(Lnet/minecraft/item/ItemStack;)Lnet/minecraft/client/render/model/BakedModel;", at = @At("HEAD"), cancellable = true) - public void onGetModel(ItemStack stack, CallbackInfoReturnable<BakedModel> cir) { - var $this = (ItemModels) (Object) this; - var model = CustomItemModelEvent.getModel(stack, $this); - if (model == null) { - model = CustomGlobalTextures.replaceGlobalModel($this, stack); - } - if (model != null) { - cir.setReturnValue(model); - } - } -} 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/IncomingPacketListenerPatches.java b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java index 80a9fd5..a7c3875 100644 --- a/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java +++ b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java @@ -18,7 +18,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public abstract class IncomingPacketListenerPatches { - @ModifyExpressionValue(method = "onCommandTree", at = @At(value = "NEW", target = "(Lcom/mojang/brigadier/tree/RootCommandNode;)Lcom/mojang/brigadier/CommandDispatcher;", remap = false)) public CommandDispatcher onOnCommandTree(CommandDispatcher dispatcher) { MaskCommands.Companion.publish(new MaskCommands(dispatcher)); @@ -31,7 +30,7 @@ public abstract class IncomingPacketListenerPatches { packet.getParameters(), new Vec3d(packet.getX(), packet.getY(), packet.getZ()), new Vector3f(packet.getOffsetX(), packet.getOffsetY(), packet.getOffsetZ()), - packet.isLongDistance(), + packet.isImportant(), packet.getCount(), packet.getSpeed() ); diff --git a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java b/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java deleted file mode 100644 index b1f9ee4..0000000 --- a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java +++ /dev/null @@ -1,21 +0,0 @@ - -package moe.nea.firmament.mixins; - -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.mojang.serialization.Codec; -import net.minecraft.component.type.ProfileComponent; -import net.minecraft.util.Uuids; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -import java.util.UUID; - -@Mixin(ProfileComponent.class) -public class LenientProfileComponentPatch { - // lambda in RecordCodecBuilder.create for BASE_CODEC - @ModifyExpressionValue(method = "method_57508", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/util/Uuids;INT_STREAM_CODEC:Lcom/mojang/serialization/Codec;")) - private static Codec<UUID> onStaticInit(Codec<UUID> original) { - return Uuids.CODEC; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java index 82f8f5d..43aec40 100644 --- a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java +++ b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java @@ -4,8 +4,11 @@ package moe.nea.firmament.mixins; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.events.*; +import moe.nea.firmament.events.HandledScreenClickEvent; +import moe.nea.firmament.events.HandledScreenForegroundEvent; +import moe.nea.firmament.events.HandledScreenKeyPressedEvent; +import moe.nea.firmament.events.IsSlotProtectedEvent; +import moe.nea.firmament.events.SlotRenderEvents; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.entity.player.PlayerInventory; @@ -22,9 +25,6 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import java.util.Iterator; @Mixin(value = HandledScreen.class, priority = 990) public abstract class MixinHandledScreen<T extends ScreenHandler> { @@ -62,10 +62,6 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> { } } - boolean keyReleased_firmament(int keyCode, int scanCode, int modifiers) { - return HandledScreenKeyReleasedEvent.Companion.publish(new HandledScreenKeyReleasedEvent((HandledScreen<?>) (Object) this, keyCode, scanCode, modifiers)).getCancelled(); - } - @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawForeground(Lnet/minecraft/client/gui/DrawContext;II)V", shift = At.Shift.AFTER)) public void onAfterRenderForeground(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { context.getMatrices().push(); @@ -78,17 +74,17 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> { public void onMouseClickedSlot(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) { if (slotId == -999 && getScreenHandler() != null && actionType == SlotActionType.PICKUP) { // -999 is code for "clicked outside the main window" ItemStack cursorStack = getScreenHandler().getCursorStack(); - if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, SlotActionType.THROW, cursorStack)) { + if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE, cursorStack)) { ci.cancel(); return; } } - if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType)) { + if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) { ci.cancel(); return; } if (actionType == SlotActionType.SWAP && 0 <= button && button < 9) { - if (IsSlotProtectedEvent.shouldBlockInteraction(new Slot(playerInventory, button, 0, 0), actionType)) { + if (IsSlotProtectedEvent.shouldBlockInteraction(new Slot(playerInventory, button, 0, 0), actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) { ci.cancel(); } } diff --git a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java index 9a4626f..b20c223 100644 --- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java @@ -14,15 +14,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(ClientPlayerEntity.class) public abstract class PlayerDropEventPatch extends PlayerEntity { - public PlayerDropEventPatch() { - super(null, null, 0, null); - } + public PlayerDropEventPatch() { + super(null, null, 0, null); + } - @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true) - public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) { - Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0); - if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW)) { - cir.setReturnValue(false); - } - } + @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true) + public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) { + Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0); + if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) { + cir.setReturnValue(false); + } + } } diff --git a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java deleted file mode 100644 index e7331c5..0000000 --- a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java +++ /dev/null @@ -1,36 +0,0 @@ - - -package moe.nea.firmament.mixins; - -import com.mojang.authlib.properties.Property; -import moe.nea.firmament.features.fixes.Fixes; -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; - -import java.security.PublicKey; - -@Mixin(value = Property.class, remap = false) -public class PropertySignatureIgnorePatch { - @Inject(method = "isSignatureValid", cancellable = true, at = @At("HEAD"), remap = false) - public void onValidateSignature(PublicKey publicKey, CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } - - @Inject(method = "signature", cancellable = true, at = @At("HEAD"), remap = false) - public void returnEmptySignatureInsteadOfNull(CallbackInfoReturnable<String> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(""); - } - } - - @Inject(method = "hasSignature", cancellable = true, at = @At("HEAD"), remap = false) - public void onHasSignature(CallbackInfoReturnable<Boolean> cir) { - if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { - cir.setReturnValue(true); - } - } -} 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/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java index 814f172..6e1090a 100644 --- a/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java +++ b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java @@ -5,6 +5,7 @@ import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.HandledScreenKeyReleasedEvent; import moe.nea.firmament.util.customgui.CoordRememberingSlot; import moe.nea.firmament.util.customgui.CustomGui; import moe.nea.firmament.util.customgui.HasCustomGui; @@ -73,6 +74,16 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen return override != null && override.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); } + public boolean keyReleased_firmament(int keyCode, int scanCode, int modifiers) { + if (HandledScreenKeyReleasedEvent.Companion.publish(new HandledScreenKeyReleasedEvent((HandledScreen<?>) (Object) this, keyCode, scanCode, modifiers)).getCancelled()) + return true; + return override != null && override.keyReleased(keyCode, scanCode, modifiers); + } + + public boolean charTyped_firmament(char chr, int modifiers) { + return override != null && override.charTyped(chr, modifiers); + } + @Inject(method = "init", at = @At("TAIL")) private void onInit(CallbackInfo ci) { if (override != null) { @@ -179,6 +190,16 @@ public class PatchHandledScreen<T extends ScreenHandler> extends Screen implemen } } + @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) + private void overrideKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) { + if (override != null) { + if (override.keyPressed(keyCode, scanCode, modifiers)) { + cir.setReturnValue(true); + } + } + } + + @Inject( method = "mouseReleased", at = @At("HEAD"), cancellable = true) diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java deleted file mode 100644 index dac65fe..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java +++ /dev/null @@ -1,35 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import net.minecraft.client.render.VertexConsumerProvider; -import net.minecraft.client.render.item.ItemRenderer; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ModelTransformationMode; -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(ItemRenderer.class) -public class ApplyHeadModelInItemRenderer { - @Inject(method = "renderItem(Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IILnet/minecraft/client/render/model/BakedModel;ZF)V", - at = @At("HEAD")) - private void applyHeadModel(ItemStack stack, ModelTransformationMode transformationMode, boolean leftHanded, - MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay, - BakedModel model, boolean useInventoryModel, float z, CallbackInfo ci, - @Local(argsOnly = true) LocalRef<BakedModel> modelMut - ) { - var extra = BakedModelExtra.cast(model); - if (transformationMode == ModelTransformationMode.HEAD && extra != null) { - var headModel = extra.getHeadModel_firmament(); - if (headModel != null) { - modelMut.set(headModel); - } - } - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBasic.java b/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBasic.java deleted file mode 100644 index 3ed2177..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBasic.java +++ /dev/null @@ -1,42 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import moe.nea.firmament.features.texturepack.TintOverrides; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.BasicBakedModel; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(BasicBakedModel.class) -public class BakedModelDataHolderBasic implements BakedModelExtra { - - @Unique - private BakedModel headModel; - - @Unique - @Nullable - private TintOverrides tintOverrides; - - @Nullable - @Override - public BakedModel getHeadModel_firmament() { - return headModel; - } - - @Override - public void setHeadModel_firmament(@Nullable BakedModel headModel) { - this.headModel = headModel; - } - - @Override - public @Nullable TintOverrides getTintOverrides_firmament() { - return tintOverrides; - } - - @Override - public void setTintOverrides_firmament(@Nullable TintOverrides tintOverrides) { - this.tintOverrides = tintOverrides; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBuiltin.java b/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBuiltin.java deleted file mode 100644 index 87aecb1..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedModelDataHolderBuiltin.java +++ /dev/null @@ -1,43 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import moe.nea.firmament.features.texturepack.TintOverrides; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.BuiltinBakedModel; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(BuiltinBakedModel.class) -public class BakedModelDataHolderBuiltin implements BakedModelExtra { - - @Unique - @Nullable - private BakedModel headModel; - - @Unique - @Nullable - private TintOverrides tintOverrides; - - @Override - public @Nullable TintOverrides getTintOverrides_firmament() { - return tintOverrides; - } - - @Override - public void setTintOverrides_firmament(@Nullable TintOverrides tintOverrides) { - this.tintOverrides = tintOverrides; - } - - @Nullable - @Override - public BakedModel getHeadModel_firmament() { - return headModel; - } - - @Override - public void setHeadModel_firmament(@Nullable BakedModel headModel) { - this.headModel = headModel; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedOverrideDataHolder.java b/src/main/java/moe/nea/firmament/mixins/custommodels/BakedOverrideDataHolder.java deleted file mode 100644 index 26972b1..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/BakedOverrideDataHolder.java +++ /dev/null @@ -1,28 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.BakedOverrideData; -import moe.nea.firmament.features.texturepack.FirmamentModelPredicate; -import net.minecraft.client.render.model.json.ModelOverrideList; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(ModelOverrideList.BakedOverride.class) -public class BakedOverrideDataHolder implements BakedOverrideData { - - @Unique - private FirmamentModelPredicate[] firmamentOverrides; - - @Nullable - @Override - public FirmamentModelPredicate[] getFirmamentOverrides() { - return firmamentOverrides; - } - - @Override - public void setFirmamentOverrides(@NotNull FirmamentModelPredicate[] overrides) { - this.firmamentOverrides = overrides; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/HeadModelReplacerPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/HeadModelReplacerPatch.java deleted file mode 100644 index 26c331e..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/HeadModelReplacerPatch.java +++ /dev/null @@ -1,57 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import net.minecraft.block.AbstractSkullBlock; -import net.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.client.render.VertexConsumerProvider; -import net.minecraft.client.render.entity.LivingEntityRenderer; -import net.minecraft.client.render.entity.feature.HeadFeatureRenderer; -import net.minecraft.client.render.entity.model.EntityModel; -import net.minecraft.client.render.entity.model.ModelWithHead; -import net.minecraft.client.render.entity.state.LivingEntityRenderState; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.entity.EquipmentSlot; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(HeadFeatureRenderer.class) -public class HeadModelReplacerPatch<S extends LivingEntityRenderState, M extends EntityModel<S> & ModelWithHead> { - /** - * This class serves to disable the replacing of head models with the vanilla block model. Vanilla first selects loads - * the model containing the head model regularly in {@link LivingEntityRenderer#updateRenderState}, but then discards - * the model in {@link HeadFeatureRenderer#render(MatrixStack, VertexConsumerProvider, int, LivingEntityRenderState, float, float)} - * if it detects a skull block. This serves to disable that functionality if a head model override is present. - */ - @WrapOperation(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V", - at = @At(value = "INVOKE", target = "Lnet/minecraft/item/BlockItem;getBlock()Lnet/minecraft/block/Block;")) - private Block replaceSkull(BlockItem instance, Operation<Block> original, @Local BakedModel bakedModel) { - var oldBlock = original.call(instance); - if (oldBlock instanceof AbstractSkullBlock) { - var extra = BakedModelExtra.cast(bakedModel); - if (extra != null && extra.getHeadModel_firmament() != null) - return Blocks.ENCHANTING_TABLE; // Any non skull block. Let's choose the enchanting table because it is very distinct. - } - return oldBlock; - } - - /** - * We disable the has model override, since texture packs get precedent to server data. - */ - @WrapOperation(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/feature/ArmorFeatureRenderer;hasModel(Lnet/minecraft/item/ItemStack;Lnet/minecraft/entity/EquipmentSlot;)Z")) - private boolean replaceHasModel(ItemStack stack, EquipmentSlot slot, Operation<Boolean> original, - @Local BakedModel bakedModel) { - var extra = BakedModelExtra.cast(bakedModel); - if (extra != null && extra.getHeadModel_firmament() != null) - return false; - return original.call(stack, slot); - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemColorRemovalPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ItemColorRemovalPatch.java deleted file mode 100644 index 8c76c60..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemColorRemovalPatch.java +++ /dev/null @@ -1,39 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.TintOverrides; -import moe.nea.firmament.init.ItemColorsSodiumRiser; -import net.minecraft.client.color.item.ItemColorProvider; -import net.minecraft.client.color.item.ItemColors; -import net.minecraft.item.ItemStack; -import org.jetbrains.annotations.Nullable; -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(ItemColors.class) -public class ItemColorRemovalPatch { - - /** - * @see ItemColorsSodiumRiser - */ - private @Nullable ItemColorProvider overrideSodium_firmament(@Nullable ItemColorProvider original) { - var tintOverrides = TintOverrides.Companion.getCurrentOverrides(); - if (!tintOverrides.hasOverrides()) return original; - return (stack, tintIndex) -> { - var override = tintOverrides.getOverride(tintIndex); - if (override != null) return override; - if (original != null) return original.getColor(stack, tintIndex); - return -1; - }; - } - - - @Inject(method = "getColor", at = @At("HEAD"), cancellable = true) - private void overrideGetColorCall(ItemStack item, int tintIndex, CallbackInfoReturnable<Integer> cir) { - var tintOverrides = TintOverrides.Companion.getCurrentOverrides(); - var override = tintOverrides.getOverride(tintIndex); - if (override != null) - cir.setReturnValue(override); - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemModelGeneratorJsonUnbakedModelCopy.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ItemModelGeneratorJsonUnbakedModelCopy.java deleted file mode 100644 index 89d0411..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemModelGeneratorJsonUnbakedModelCopy.java +++ /dev/null @@ -1,22 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.features.texturepack.JsonUnbakedModelFirmExtra; -import net.minecraft.client.render.model.json.ItemModelGenerator; -import net.minecraft.client.render.model.json.JsonUnbakedModel; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(ItemModelGenerator.class) -public class ItemModelGeneratorJsonUnbakedModelCopy { - @ModifyReturnValue(method = "create", at = @At("RETURN")) - private JsonUnbakedModel copyExtraModelData(JsonUnbakedModel original, @Local(argsOnly = true) JsonUnbakedModel oldModel) { - var extra = ((JsonUnbakedModelFirmExtra) original); - var oldExtra = ((JsonUnbakedModelFirmExtra) oldModel); - extra.setHeadModel_firmament(oldExtra.getHeadModel_firmament()); - extra.setTintOverrides_firmament(oldExtra.getTintOverrides_firmament()); - return original; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemRendererTintContextPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ItemRendererTintContextPatch.java deleted file mode 100644 index 8c5411b..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ItemRendererTintContextPatch.java +++ /dev/null @@ -1,35 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import moe.nea.firmament.features.texturepack.TintOverrides; -import net.minecraft.client.render.VertexConsumerProvider; -import net.minecraft.client.render.item.ItemRenderer; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ModelTransformationMode; -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(value = ItemRenderer.class, priority = 1010) -public class ItemRendererTintContextPatch { - @Inject(method = "renderItem(Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IILnet/minecraft/client/render/model/BakedModel;ZF)V", - at = @At(value = "HEAD"), allow = 1) - private void onStartRendering(ItemStack stack, ModelTransformationMode transformationMode, boolean leftHanded, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay, BakedModel model, boolean useInventoryModel, float z, CallbackInfo ci) { - var extra = BakedModelExtra.cast(model); - if (extra != null) { - TintOverrides.Companion.enter(extra.getTintOverrides_firmament()); - } - } - - @Inject(method = "renderItem(Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IILnet/minecraft/client/render/model/BakedModel;ZF)V", - at = @At("TAIL"), allow = 1) - private void onEndRendering(ItemStack stack, ModelTransformationMode transformationMode, boolean leftHanded, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay, BakedModel model, boolean useInventoryModel, float z, CallbackInfo ci) { - var extra = BakedModelExtra.cast(model); - if (extra != null) { - TintOverrides.Companion.exit(extra.getTintOverrides_firmament()); - } - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/JsonUnbakedModelDataHolder.java b/src/main/java/moe/nea/firmament/mixins/custommodels/JsonUnbakedModelDataHolder.java deleted file mode 100644 index a5bb34f..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/JsonUnbakedModelDataHolder.java +++ /dev/null @@ -1,130 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.features.texturepack.BakedModelExtra; -import moe.nea.firmament.features.texturepack.JsonUnbakedModelFirmExtra; -import moe.nea.firmament.features.texturepack.TintOverrides; -import moe.nea.firmament.util.ErrorUtil; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.Baker; -import net.minecraft.client.render.model.ModelRotation; -import net.minecraft.client.render.model.UnbakedModel; -import net.minecraft.client.render.model.json.JsonUnbakedModel; -import net.minecraft.util.Identifier; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.Objects; - -@Mixin(JsonUnbakedModel.class) -public abstract class JsonUnbakedModelDataHolder implements JsonUnbakedModelFirmExtra { - @Shadow - @Nullable - protected JsonUnbakedModel parent; - - @Shadow - public abstract String toString(); - - @Unique - @Nullable - public Identifier headModel; - @Unique - @Nullable - public TintOverrides tintOverrides; - @Unique - @Nullable - public TintOverrides mergedTintOverrides; - - @Override - public void setTintOverrides_firmament(@Nullable TintOverrides tintOverrides) { - this.tintOverrides = tintOverrides; - this.mergedTintOverrides = null; - } - - @Override - public @NotNull TintOverrides getTintOverrides_firmament() { - if (mergedTintOverrides != null) - return mergedTintOverrides; - var mergedTintOverrides = parent == null ? new TintOverrides() - : ((JsonUnbakedModelFirmExtra) parent).getTintOverrides_firmament(); - if (tintOverrides != null) - mergedTintOverrides = tintOverrides.mergeWithParent(mergedTintOverrides); - this.mergedTintOverrides = mergedTintOverrides; - return mergedTintOverrides; - } - - @Override - public void setHeadModel_firmament(@Nullable Identifier identifier) { - this.headModel = identifier; - } - - @Override - public @Nullable Identifier getHeadModel_firmament() { - if (this.headModel != null) return this.headModel; - if (this.parent == null) return null; - return ((JsonUnbakedModelFirmExtra) this.parent).getHeadModel_firmament(); - } - - @Inject(method = "resolve", at = @At("HEAD")) - private void addDependencies(UnbakedModel.Resolver resolver, CallbackInfo ci) { - var headModel = getHeadModel_firmament(); - if (headModel != null) { - resolver.resolve(headModel); - } - } - - private void addExtraBakeInfo(BakedModel bakedModel, Baker baker) { - if (!this.toString().contains("minecraft") && this.toString().contains("crimson")) { - System.out.println("Found non minecraft model " + this); - } - var extra = BakedModelExtra.cast(bakedModel); - if (extra != null) { - var headModel = getHeadModel_firmament(); - if (headModel != null) { - extra.setHeadModel_firmament(baker.bake(headModel, ModelRotation.X0_Y0)); - } - if (getTintOverrides_firmament().hasOverrides()) { - extra.setTintOverrides_firmament(getTintOverrides_firmament()); - } - } - } - - /** - * @see ProvideBakerToJsonUnbakedModelPatch - */ - @Override - public void storeExtraBaker_firmament(@NotNull Baker baker) { - this.storedBaker = baker; - } - - @Unique - private Baker storedBaker; - - @ModifyReturnValue( - method = "bake(Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Z)Lnet/minecraft/client/render/model/BakedModel;", - at = @At("RETURN")) - private BakedModel bakeExtraInfoWithoutBaker(BakedModel original) { - if (storedBaker != null) { - addExtraBakeInfo(original, storedBaker); - storedBaker = null; - } - return original; - } - - @ModifyReturnValue( - method = { - "bake(Lnet/minecraft/client/render/model/Baker;Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;" - }, - at = @At(value = "RETURN")) - private BakedModel bakeExtraInfo(BakedModel original, @Local(argsOnly = true) Baker baker) { - addExtraBakeInfo(original, baker); - return original; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ModelOverrideDataHolder.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ModelOverrideDataHolder.java deleted file mode 100644 index 5f9689a..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ModelOverrideDataHolder.java +++ /dev/null @@ -1,28 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.features.texturepack.FirmamentModelPredicate; -import moe.nea.firmament.features.texturepack.ModelOverrideData; -import net.minecraft.client.render.model.json.ModelOverride; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(ModelOverride.class) -public class ModelOverrideDataHolder implements ModelOverrideData { - - @Unique - private FirmamentModelPredicate[] overrides; - - @Nullable - @Override - public FirmamentModelPredicate[] getFirmamentOverrides() { - return overrides; - } - - @Override - public void setFirmamentOverrides(@NotNull FirmamentModelPredicate[] overrides) { - this.overrides = overrides; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchJsonUnbakedModelDeserializer.java b/src/main/java/moe/nea/firmament/mixins/custommodels/PatchJsonUnbakedModelDeserializer.java deleted file mode 100644 index d6c25b5..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchJsonUnbakedModelDeserializer.java +++ /dev/null @@ -1,31 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.features.texturepack.JsonUnbakedModelFirmExtra; -import moe.nea.firmament.features.texturepack.TintOverrides; -import net.minecraft.client.render.model.json.JsonUnbakedModel; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(JsonUnbakedModel.Deserializer.class) -public class PatchJsonUnbakedModelDeserializer { - @ModifyReturnValue(method = "deserialize(Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/minecraft/client/render/model/json/JsonUnbakedModel;", - at = @At("RETURN")) - private JsonUnbakedModel addHeadModel(JsonUnbakedModel original, @Local JsonObject jsonObject) { - var headModel = jsonObject.get("firmament:head_model"); - var extra = ((JsonUnbakedModelFirmExtra) original); - if (headModel instanceof JsonPrimitive prim && prim.isString()) { - extra.setHeadModel_firmament(Identifier.of(prim.getAsString())); - } - var tintOverrides = jsonObject.get("firmament:tint_overrides"); - if (tintOverrides instanceof JsonObject object) { - extra.setTintOverrides_firmament(TintOverrides.Companion.parse(object)); - } - return original; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/main/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java deleted file mode 100644 index 8c0b3f8..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java +++ /dev/null @@ -1,22 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; -import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; -import net.minecraft.client.render.entity.equipment.EquipmentRenderer; -import net.minecraft.item.equipment.EquipmentModel; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(EquipmentRenderer.class) -public class PatchLegacyArmorLayerSupport { - @WrapOperation(method = "render(Lnet/minecraft/item/equipment/EquipmentModel$LayerType;Lnet/minecraft/util/Identifier;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/util/Identifier;)Lnet/minecraft/item/equipment/EquipmentModel;")) - private EquipmentModel patchModelLayers(EquipmentModelLoader instance, Identifier id, Operation<EquipmentModel> original) { - var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(id); - if (modelOverride != null) return modelOverride; - return original.call(instance, id); - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchOverrideDeserializer.java b/src/main/java/moe/nea/firmament/mixins/custommodels/PatchOverrideDeserializer.java deleted file mode 100644 index abb1792..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchOverrideDeserializer.java +++ /dev/null @@ -1,50 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.features.texturepack.CustomModelOverrideParser; -import moe.nea.firmament.features.texturepack.ModelOverrideData; -import net.minecraft.client.render.model.json.ModelOverride; -import net.minecraft.util.Identifier; -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; - -import java.util.List; -import java.util.Map; - -@Mixin(ModelOverride.Deserializer.class) -public class PatchOverrideDeserializer { - - @ModifyReturnValue( - method = "deserialize(Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/minecraft/client/render/model/json/ModelOverride;", - at = @At(value = "RETURN")) - private ModelOverride addCustomOverrides(ModelOverride original, @Local JsonObject jsonObject) { - var originalData = (ModelOverrideData) (Object) original; - originalData.setFirmamentOverrides(CustomModelOverrideParser.parseCustomModelOverrides(jsonObject)); - return original; - } - - @ModifyExpressionValue( - method = "deserializeMinPropertyValues(Lcom/google/gson/JsonObject;)Ljava/util/List;", - at = @At(value = "INVOKE", target = "Ljava/util/Map$Entry;getValue()Ljava/lang/Object;")) - private Object removeFirmamentPredicatesFromJsonIteration(Object original, @Local Map.Entry<String, JsonElement> entry) { - if (entry.getKey().startsWith("firmament:")) return new JsonPrimitive(0F); - return original; - } - - @Inject( - method = "deserializeMinPropertyValues", - at = @At(value = "INVOKE", target = "Ljava/util/Map;entrySet()Ljava/util/Set;") - ) - private void whatever(JsonObject object, CallbackInfoReturnable<List<ModelOverride.Condition>> cir, - @Local Map<Identifier, Float> maps) { - maps.entrySet().removeIf(it -> it.getKey().getNamespace().equals("firmament")); - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ProvideBakerToJsonUnbakedModelPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ProvideBakerToJsonUnbakedModelPatch.java deleted file mode 100644 index c1ac119..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ProvideBakerToJsonUnbakedModelPatch.java +++ /dev/null @@ -1,27 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import moe.nea.firmament.features.texturepack.JsonUnbakedModelFirmExtra; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.Baker; -import net.minecraft.client.render.model.ModelBakeSettings; -import net.minecraft.client.render.model.json.JsonUnbakedModel; -import net.minecraft.client.texture.Sprite; -import net.minecraft.client.util.SpriteIdentifier; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -import java.util.function.Function; - -/** - * @see JsonUnbakedModelDataHolder#storeExtraBaker_firmament - */ -@Mixin(targets = "net.minecraft.client.render.model.ModelBaker$BakerImpl") -public abstract class ProvideBakerToJsonUnbakedModelPatch implements Baker { - @WrapOperation(method = "bake(Lnet/minecraft/client/render/model/UnbakedModel;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/json/JsonUnbakedModel;bake(Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Z)Lnet/minecraft/client/render/model/BakedModel;")) - private BakedModel provideExtraBakerToModel(JsonUnbakedModel instance, Function<SpriteIdentifier, Sprite> function, ModelBakeSettings modelBakeSettings, boolean bl, Operation<BakedModel> original) { - ((JsonUnbakedModelFirmExtra) instance).storeExtraBaker_firmament(this); - return original.call(instance, function, modelBakeSettings, bl); - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java deleted file mode 100644 index bb9cd10..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java +++ /dev/null @@ -1,36 +0,0 @@ -package moe.nea.firmament.mixins.custommodels; - -import moe.nea.firmament.events.BakeExtraModelsEvent; -import net.minecraft.client.render.model.BlockStatesLoader; -import net.minecraft.client.render.model.ItemModel; -import net.minecraft.client.render.model.ReferencedModelsCollector; -import net.minecraft.client.render.model.UnbakedModel; -import net.minecraft.client.util.ModelIdentifier; -import net.minecraft.util.Identifier; -import org.spongepowered.asm.mixin.Final; -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.CallbackInfo; - -import java.util.Map; - -@Mixin(ReferencedModelsCollector.class) -public abstract class ReferenceCustomModelsPatch { - @Shadow - protected abstract void addTopLevelModel(ModelIdentifier modelId, UnbakedModel model); - - @Shadow - @Final - private Map<Identifier, UnbakedModel> inputs; - - @Inject(method = "addBlockStates", at = @At("RETURN")) - private void addFirmamentReferencedModels( - BlockStatesLoader.BlockStateDefinition definition, CallbackInfo ci - ) { - BakeExtraModelsEvent.Companion.publish(new BakeExtraModelsEvent( - (modelIdentifier, identifier) -> addTopLevelModel(modelIdentifier, new ItemModel(identifier)))); - - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/TestForFirmamentOverridePredicatesPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/TestForFirmamentOverridePredicatesPatch.java deleted file mode 100644 index 63f3cf0..0000000 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/TestForFirmamentOverridePredicatesPatch.java +++ /dev/null @@ -1,68 +0,0 @@ - -package moe.nea.firmament.mixins.custommodels; - -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.llamalad7.mixinextras.sugar.Local; -import moe.nea.firmament.Firmament; -import moe.nea.firmament.features.texturepack.BakedOverrideData; -import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; -import moe.nea.firmament.features.texturepack.FirmamentModelPredicate; -import moe.nea.firmament.features.texturepack.ModelOverrideData; -import net.minecraft.client.render.model.json.ModelOverride; -import net.minecraft.client.render.model.json.ModelOverrideList; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Final; -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.ModifyArg; - -import java.util.List; -import java.util.Objects; - -@Mixin(ModelOverrideList.class) -public class TestForFirmamentOverridePredicatesPatch { - - @Shadow - private Identifier[] conditionTypes; - - @ModifyArg(method = "<init>(Lnet/minecraft/client/render/model/Baker;Ljava/util/List;)V", - at = @At( - value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z" - )) - public Object onInit( - Object element, - @Local ModelOverride modelOverride - ) { - var bakedOverride = (ModelOverrideList.BakedOverride) element; - var modelOverrideData = ModelOverrideData.cast(modelOverride); - BakedOverrideData.cast(bakedOverride) - .setFirmamentOverrides(modelOverrideData.getFirmamentOverrides()); - if (conditionTypes.length == 0 && - modelOverrideData.getFirmamentOverrides() != null && - modelOverrideData.getFirmamentOverrides().length > 0) { - conditionTypes = new Identifier[]{Firmament.INSTANCE.identifier("sentinel/enforce_model_override_evaluation")}; - } - return element; - } - - @ModifyExpressionValue(method = "getModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/json/ModelOverrideList$BakedOverride;test([F)Z")) - public boolean testFirmamentOverrides(boolean originalValue, - @Local ModelOverrideList.BakedOverride bakedOverride, - @Local(argsOnly = true) ItemStack stack) { - if (!originalValue) return false; - var overrideData = (BakedOverrideData) (Object) bakedOverride; - var overrides = overrideData.getFirmamentOverrides(); - if (overrides == null) return true; - if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableModelOverrides()) return false; - for (FirmamentModelPredicate firmamentOverride : overrides) { - if (!firmamentOverride.test(stack)) - return false; - } - return true; - } -} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java new file mode 100644 index 0000000..2b96e5c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java @@ -0,0 +1,62 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +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.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Applies various rendering modifications from {@link EntityRenderTintEvent} + */ +@Mixin(LivingEntityRenderer.class) +public class ChangeColorOfLivingEntities<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + @ModifyReturnValue(method = "getMixColor", at = @At("RETURN")) + private int changeColor(int original, @Local(argsOnly = true) S state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament()) + return tintState.getTint_firmament(); + return original; + } + + @ModifyArg( + method = "getOverlay", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/OverlayTexture;getU(F)I"), + allow = 1 + ) + private static float modifyLightOverlay(float originalWhiteOffset, @Local(argsOnly = true) LivingEntityRenderState state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament() || tintState.getOverlayTexture_firmament() != null) { + return 1F; // TODO: add interpolation percentage to render state extension + } + return originalWhiteOffset; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;pop()V")) + private void afterRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null && vertexConsumerProvider instanceof VertexConsumerProvider.Immediate imm) { + imm.drawCurrentLayer(); + } + EntityRenderTintEvent.overlayOverride = null; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;push()V")) + private void beforeRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null) { + EntityRenderTintEvent.overlayOverride = overlayTexture; + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java new file mode 100644 index 0000000..1019027 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java @@ -0,0 +1,55 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import moe.nea.firmament.util.render.TintedOverlayTexture; +import net.minecraft.client.render.entity.state.EntityRenderState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(EntityRenderState.class) +public class EntityRenderStateTint implements EntityRenderTintEvent.HasTintRenderState { + @Unique + int tint = -1; + @Unique + TintedOverlayTexture overlayTexture; + @Unique + boolean hasTintOverride = false; + + @Override + public int getTint_firmament() { + return tint; + } + + @Override + public void setTint_firmament(int i) { + tint = i; + hasTintOverride = true; + } + + @Override + public boolean getHasTintOverride_firmament() { + return hasTintOverride; + } + + @Override + public void setHasTintOverride_firmament(boolean b) { + hasTintOverride = b; + } + + @Override + public void reset_firmament() { + hasTintOverride = false; + overlayTexture = null; + } + + @Override + public @Nullable TintedOverlayTexture getOverlayTexture_firmament() { + return overlayTexture; + } + + @Override + public void setOverlayTexture_firmament(@Nullable TintedOverlayTexture tintedOverlayTexture) { + this.overlayTexture = tintedOverlayTexture; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java new file mode 100644 index 0000000..7938340 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java @@ -0,0 +1,30 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +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; + +/** + * Dispatches {@link EntityRenderTintEvent} to collect additional render state used by {@link ChangeColorOfLivingEntities} + */ +@Mixin(EntityRenderer.class) +public class InjectIntoRenderState<T extends Entity, S extends EntityRenderState> { + + @Inject( + method = "updateRenderState", + at = @At("RETURN")) + private void onUpdateRenderState(T entity, S state, float tickDelta, CallbackInfo ci) { + var renderState = EntityRenderTintEvent.HasTintRenderState.cast(state); + renderState.reset_firmament(); + var tintEvent = new EntityRenderTintEvent( + entity, + renderState + ); + EntityRenderTintEvent.Companion.publish(tintEvent); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java new file mode 100644 index 0000000..61e5c65 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Replaces the overlay texture used by rendering with the override specified in {@link EntityRenderTintEvent#overlayOverride} + */ +@Mixin(RenderLayer.Overlay.class) +public class ReplaceOverlayTexture { + @ModifyExpressionValue( + method = {"method_23555", "method_23556"}, + expect = 2, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getOverlayTexture()Lnet/minecraft/client/render/OverlayTexture;")) + private static OverlayTexture replaceOverlayTexture(OverlayTexture original) { + if (EntityRenderTintEvent.overlayOverride != null) + return EntityRenderTintEvent.overlayOverride; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java new file mode 100644 index 0000000..d9c174c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java @@ -0,0 +1,34 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.equipment.EquipmentRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link EquipmentRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ +@Mixin(EquipmentRenderer.class) +public class UseOverlayableEquipmentRenderer { + @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getArmorCutoutNoCull(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(Identifier texture, Operation<RenderLayer> original) { + if (EntityRenderTintEvent.overlayOverride != null) + return RenderLayer.getEntityTranslucent(texture); + return original.call(texture); + } + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java new file mode 100644 index 0000000..07bc5cf --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.feature.HeadFeatureRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link HeadFeatureRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + * @see UseOverlayableItemRenderer + */ +@Mixin(HeadFeatureRenderer.class) +public class UseOverlayableHeadFeatureRenderer { + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java new file mode 100644 index 0000000..620ab2c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderPhase; +import net.minecraft.client.render.item.ItemRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link ItemRenderState} use a {@link RenderLayer} that allows uses Minecraft's overlay texture. + * + * @see UseOverlayableHeadFeatureRenderer + */ +@Mixin(ItemRenderState.LayerRenderState.class) +public class UseOverlayableItemRenderer { + @ModifyExpressionValue(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState;renderLayer:Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(RenderLayer original) { + if (EntityRenderTintEvent.overlayOverride != null && original instanceof RenderLayer.MultiPhase multiPhase && multiPhase.phases.texture instanceof RenderPhase.Texture texture && texture.getId().isPresent()) + return RenderLayer.getEntityTranslucent(texture.getId().get()); + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java new file mode 100644 index 0000000..9905af1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link SkullBlockEntityRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ + +@Mixin(SkullBlockEntityRenderer.class) +public class UseOverlayableSkullBlockEntityRenderer { + @ModifyExpressionValue(method = "renderSkull", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private static int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } + +} diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt index 2c2a6b7..01905c7 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -1,5 +1,6 @@ package moe.nea.firmament +import com.google.gson.Gson import com.mojang.brigadier.CommandDispatcher import io.ktor.client.HttpClient import io.ktor.client.plugins.UserAgent @@ -70,6 +71,7 @@ object Firmament { ignoreUnknownKeys = true encodeDefaults = true } + val gson = Gson() val tightJson = Json(from = json) { prettyPrint = false } 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/commands/rome.kt b/src/main/kotlin/commands/rome.kt index 13acb3c..5412792 100644 --- a/src/main/kotlin/commands/rome.kt +++ b/src/main/kotlin/commands/rome.kt @@ -254,6 +254,7 @@ fun firmamentCommand() = literal("firmament") { val player = MC.player ?: return@thenExecute player.world.getOtherEntities(player, player.boundingBox.expand(12.0)) .forEach(PowerUserTools::showEntity) + PowerUserTools.showEntity(player) } } thenLiteral("callUrsa") { diff --git a/src/main/kotlin/events/BakeExtraModelsEvent.kt b/src/main/kotlin/events/BakeExtraModelsEvent.kt deleted file mode 100644 index adaa495..0000000 --- a/src/main/kotlin/events/BakeExtraModelsEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package moe.nea.firmament.events - -import java.util.function.BiConsumer -import net.minecraft.client.render.model.ReferencedModelsCollector -import net.minecraft.client.util.ModelIdentifier -import net.minecraft.util.Identifier - -// TODO: Rename this event, since it is not really directly baking models anymore -class BakeExtraModelsEvent( - private val addAnyModel: BiConsumer<ModelIdentifier, Identifier>, -) : FirmamentEvent() { - - fun addNonItemModel(modelIdentifier: ModelIdentifier, identifier: Identifier) { - this.addAnyModel.accept(modelIdentifier, identifier) - } - - fun addItemModel(modelIdentifier: ModelIdentifier) { - addNonItemModel( - modelIdentifier, - modelIdentifier.id.withPrefixedPath(ReferencedModelsCollector.ITEM_DIRECTORY)) - } - - companion object : FirmamentEventBus<BakeExtraModelsEvent>() -} diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt index 4328d77..21ee326 100644 --- a/src/main/kotlin/events/CustomItemModelEvent.kt +++ b/src/main/kotlin/events/CustomItemModelEvent.kt @@ -2,37 +2,42 @@ package moe.nea.firmament.events import java.util.Optional import kotlin.jvm.optionals.getOrNull -import net.minecraft.client.render.item.ItemModels -import net.minecraft.client.render.model.BakedModel -import net.minecraft.client.util.ModelIdentifier import net.minecraft.item.ItemStack -import moe.nea.firmament.util.ErrorUtil +import net.minecraft.util.Identifier import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.mc.IntrospectableItemModelManager +// TODO: assert an order on these events data class CustomItemModelEvent( val itemStack: ItemStack, - var overrideModel: ModelIdentifier? = null, + val itemModelManager: IntrospectableItemModelManager, + var overrideModel: Identifier? = null, ) : FirmamentEvent() { companion object : FirmamentEventBus<CustomItemModelEvent>() { - val cache = - WeakCache.memoize<ItemStack, ItemModels, Optional<BakedModel>>("CustomItemModels") { stack, models -> - val modelId = getModelIdentifier(stack) ?: return@memoize Optional.empty() - ErrorUtil.softCheck("Model Id needs to have an inventory variant", modelId.variant() == "inventory") - val bakedModel = models.getModel(modelId.id) - if (bakedModel == null || bakedModel === models.missingModelSupplier.get()) return@memoize Optional.empty() - Optional.of(bakedModel) - } + val cache = WeakCache.memoize("ItemModelIdentifier", ::getModelIdentifier0) @JvmStatic - fun getModelIdentifier(itemStack: ItemStack?): ModelIdentifier? { + fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? { if (itemStack == null) return null - return publish(CustomItemModelEvent(itemStack)).overrideModel + return cache.invoke(itemStack, itemModelManager).getOrNull() } - @JvmStatic - fun getModel(itemStack: ItemStack?, thing: ItemModels): BakedModel? { - if (itemStack == null) return null - return cache.invoke(itemStack, thing).getOrNull() + fun getModelIdentifier0( + itemStack: ItemStack, + itemModelManager: IntrospectableItemModelManager + ): Optional<Identifier> { + // TODO: add an error / warning if the model does not exist + return Optional.ofNullable(publish(CustomItemModelEvent(itemStack, itemModelManager)).overrideModel) } } + + fun overrideIfExists(overrideModel: Identifier) { + if (itemModelManager.hasModel_firmament(overrideModel)) + this.overrideModel = overrideModel + } + + fun overrideIfEmpty(identifier: Identifier) { + if (overrideModel == null) + overrideModel = identifier + } } diff --git a/src/main/kotlin/events/EntityRenderTintEvent.kt b/src/main/kotlin/events/EntityRenderTintEvent.kt new file mode 100644 index 0000000..29b888b --- /dev/null +++ b/src/main/kotlin/events/EntityRenderTintEvent.kt @@ -0,0 +1,66 @@ +package moe.nea.firmament.events + +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.entity.state.EntityRenderState +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import moe.nea.firmament.util.render.TintedOverlayTexture + +/** + * Change the tint color of a [LivingEntity] + */ +class EntityRenderTintEvent( + val entity: Entity, + val renderState: HasTintRenderState +) : FirmamentEvent.Cancellable() { + init { + if (entity !is LivingEntity) { + cancel() + } + } + + companion object : FirmamentEventBus<EntityRenderTintEvent>() { + /** + * Static variable containing an override for [GameRenderer.getOverlayTexture]. Should be only set briefly. + * + * This variable only affects render layers that naturally make use of the overlay texture, have proper overlay UVs set (`overlay u != 0`), and have a shader that makes use of the overlay (does not have the `NO_OVERLAY` flag set in its json definition). + * + * Currently supported layers: [net.minecraft.client.render.entity.equipment.EquipmentRenderer], [net.minecraft.client.render.entity.model.PlayerEntityModel], as well as some others naturally. + * + * @see moe.nea.firmament.mixins.render.entitytints.ReplaceOverlayTexture + * @see TintedOverlayTexture + */ + @JvmField + var overlayOverride: OverlayTexture? = null + } + + @Suppress("PropertyName", "FunctionName") + interface HasTintRenderState { + /** + * Multiplicative tint applied before the overlay. + */ + var tint_firmament: Int + + /** + * Must be set for [tint_firmament] to have any effect. + */ + var hasTintOverride_firmament: Boolean + + // TODO: allow for more specific selection of which layers get tinted + /** + * Specify a [TintedOverlayTexture] to be used. This does not apply to render layers not using the overlay texture. + * @see overlayOverride + */ + var overlayTexture_firmament: TintedOverlayTexture? + fun reset_firmament() + + companion object { + @JvmStatic + fun cast(state: EntityRenderState): HasTintRenderState { + return state as HasTintRenderState + } + } + } + +} diff --git a/src/main/kotlin/events/FirmamentEventBus.kt b/src/main/kotlin/events/FirmamentEventBus.kt index 71331d1..af4e16a 100644 --- a/src/main/kotlin/events/FirmamentEventBus.kt +++ b/src/main/kotlin/events/FirmamentEventBus.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.events import java.util.concurrent.CopyOnWriteArrayList import org.apache.commons.lang3.reflect.TypeUtils import moe.nea.firmament.Firmament +import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC /** @@ -48,7 +49,7 @@ open class FirmamentEventBus<T : FirmamentEvent> { val klass = e.javaClass if (!function.knownErrors.contains(klass) || Firmament.DEBUG) { function.knownErrors.add(klass) - Firmament.logger.error("Caught exception during processing event $event by $function", e) + ErrorUtil.softError("Caught exception during processing event $event by $function", e) } } } diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt index cd2b676..eac2d9b 100644 --- a/src/main/kotlin/events/IsSlotProtectedEvent.kt +++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.events import net.minecraft.item.ItemStack @@ -10,37 +8,49 @@ import moe.nea.firmament.util.CommonSoundEffects import moe.nea.firmament.util.MC data class IsSlotProtectedEvent( - val slot: Slot?, - val actionType: SlotActionType, - var isProtected: Boolean, - val itemStackOverride: ItemStack?, - var silent: Boolean = false, + val slot: Slot?, + val actionType: SlotActionType, + var isProtected: Boolean, + val itemStackOverride: ItemStack?, + val origin: MoveOrigin, + var silent: Boolean = false, ) : FirmamentEvent() { - val itemStack get() = itemStackOverride ?: slot!!.stack + val itemStack get() = itemStackOverride ?: slot!!.stack - fun protect() { - isProtected = true - } + fun protect() { + isProtected = true + silent = false + } - fun protectSilent() { - if (!isProtected) { - silent = true - } - isProtected = true - } + fun protectSilent() { + if (!isProtected) { + silent = true + } + isProtected = true + } - companion object : FirmamentEventBus<IsSlotProtectedEvent>() { - @JvmStatic - @JvmOverloads - fun shouldBlockInteraction(slot: Slot?, action: SlotActionType, itemStackOverride: ItemStack? = null): Boolean { - if (slot == null && itemStackOverride == null) return false - val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride) - publish(event) - if (event.isProtected && !event.silent) { - MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name)) - CommonSoundEffects.playFailure() - } - return event.isProtected - } - } + enum class MoveOrigin { + DROP_FROM_HOTBAR, + SALVAGE, + INVENTORY_MOVE + ; + } + companion object : FirmamentEventBus<IsSlotProtectedEvent>() { + @JvmStatic + @JvmOverloads + fun shouldBlockInteraction( + slot: Slot?, action: SlotActionType, + origin: MoveOrigin, + itemStackOverride: ItemStack? = null, + ): Boolean { + if (slot == null && itemStackOverride == null) return false + val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride, origin) + publish(event) + if (event.isProtected && !event.silent) { + MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name)) + CommonSoundEffects.playFailure() + } + return event.isProtected + } + } } 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/events/UseItemEvent.kt b/src/main/kotlin/events/UseItemEvent.kt new file mode 100644 index 0000000..e294bb1 --- /dev/null +++ b/src/main/kotlin/events/UseItemEvent.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.events + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand +import net.minecraft.world.World + +data class UseItemEvent(val playerEntity: PlayerEntity, val world: World, val hand: Hand) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus<UseItemEvent>() + val item: ItemStack = playerEntity.getStackInHand(hand) +} diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt index 4c1c63f..1dcc91a 100644 --- a/src/main/kotlin/events/registration/ChatEvents.kt +++ b/src/main/kotlin/events/registration/ChatEvents.kt @@ -1,10 +1,9 @@ - - package moe.nea.firmament.events.registration import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents import net.fabricmc.fabric.api.event.player.AttackBlockCallback import net.fabricmc.fabric.api.event.player.UseBlockCallback +import net.fabricmc.fabric.api.event.player.UseItemCallback import net.minecraft.text.Text import net.minecraft.util.ActionResult import moe.nea.firmament.events.AllowChatEvent @@ -12,43 +11,53 @@ import moe.nea.firmament.events.AttackBlockEvent import moe.nea.firmament.events.ModifyChatEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.UseBlockEvent +import moe.nea.firmament.events.UseItemEvent private var lastReceivedMessage: Text? = null fun registerFirmamentEvents() { - ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp -> - lastReceivedMessage = message - !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled - && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled - }) - ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay -> - lastReceivedMessage = message - overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled && - !AllowChatEvent.publish(AllowChatEvent(message)).cancelled) - }) - ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay -> - if (overlay) message - else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith - }) - ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay -> - if (!overlay && lastReceivedMessage !== message) { - ProcessChatEvent.publish(ProcessChatEvent(message, true)) - } - }) - ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp -> - if (lastReceivedMessage !== message) { - ProcessChatEvent.publish(ProcessChatEvent(message, true)) - } - }) + ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp -> + lastReceivedMessage = message + !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled + && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled + }) + ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay -> + lastReceivedMessage = message + overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled && + !AllowChatEvent.publish(AllowChatEvent(message)).cancelled) + }) + ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay -> + if (overlay) message + else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith + }) + ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay -> + if (!overlay && lastReceivedMessage !== message) { + ProcessChatEvent.publish(ProcessChatEvent(message, true)) + } + }) + ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp -> + if (lastReceivedMessage !== message) { + ProcessChatEvent.publish(ProcessChatEvent(message, true)) + } + }) - AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction -> - if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled) - ActionResult.CONSUME - else ActionResult.PASS - }) - UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> - if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled) - ActionResult.CONSUME - else ActionResult.PASS - }) + AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction -> + if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> + if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> + if (UseItemEvent.publish(UseItemEvent(player, world, hand)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseItemCallback.EVENT.register(UseItemCallback { playerEntity, world, hand -> + if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.CONSUME + else ActionResult.PASS + }) } diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 2110d09..9a3cbf8 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -29,7 +29,6 @@ import moe.nea.firmament.features.inventory.buttons.InventoryButtons import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay import moe.nea.firmament.features.mining.PickaxeAbility import moe.nea.firmament.features.mining.PristineProfitTracker -import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.features.world.Waypoints import moe.nea.firmament.util.data.DataHolder @@ -46,10 +45,6 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature private var hasAutoloaded = false - init { - autoload() - } - fun autoload() { synchronized(this) { if (hasAutoloaded) return @@ -70,7 +65,6 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature loadFeature(QuickCommands) loadFeature(PetFeatures) loadFeature(SaveCursorPosition) - loadFeature(CustomSkyBlockTextures) loadFeature(PriceData) loadFeature(Fixes) loadFeature(DianaWaypoints) diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt index 5bce3f4..f85825b 100644 --- a/src/main/kotlin/features/chat/ChatLinks.kt +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.chat import io.ktor.client.request.get @@ -7,16 +5,15 @@ import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.jvm.javaio.toInputStream import java.net.URL import java.util.Collections +import java.util.concurrent.atomic.AtomicInteger import moe.nea.jarvis.api.Point import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlin.math.min import net.minecraft.client.gui.screen.ChatScreen -import net.minecraft.client.render.RenderLayer import net.minecraft.client.texture.NativeImage import net.minecraft.client.texture.NativeImageBackedTexture -import net.minecraft.scoreboard.ScoreboardCriterion.RenderType import net.minecraft.text.ClickEvent import net.minecraft.text.HoverEvent import net.minecraft.text.Style @@ -35,130 +32,132 @@ import moe.nea.firmament.util.transformEachRecursively import moe.nea.firmament.util.unformattedString object ChatLinks : FirmamentFeature { - override val identifier: String - get() = "chat-links" + override val identifier: String + get() = "chat-links" - object TConfig : ManagedConfig(identifier, Category.CHAT) { - val enableLinks by toggle("links-enabled") { true } - val imageEnabled by toggle("image-enabled") { true } - val allowAllHosts by toggle("allow-all-hosts") { false } - val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" } - val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() } - val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) } - } + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val enableLinks by toggle("links-enabled") { true } + val imageEnabled by toggle("image-enabled") { true } + val allowAllHosts by toggle("allow-all-hosts") { false } + val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" } + val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() } + val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) } + } - private fun isHostAllowed(host: String) = - TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) } + private fun isHostAllowed(host: String) = + TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) } - private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) + private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) - override val config get() = TConfig - val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex() + override val config get() = TConfig + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex() + val nextTexId = AtomicInteger(0) - data class Image( - val texture: Identifier, - val width: Int, - val height: Int, - ) + data class Image( + val texture: Identifier, + val width: Int, + val height: Int, + ) - val imageCache: MutableMap<String, Deferred<Image?>> = - Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>()) + val imageCache: MutableMap<String, Deferred<Image?>> = + Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>()) - private fun tryCacheUrl(url: String) { - if (!isUrlAllowed(url)) { - return - } - if (url in imageCache) { - return - } - imageCache[url] = Firmament.coroutineScope.async { - try { - val response = Firmament.httpClient.get(URL(url)) - if (response.status.value == 200) { - val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob) - val image = NativeImage.read(inputStream) - val texture = MC.textureManager.registerDynamicTexture( - "dynamic_image_preview", - NativeImageBackedTexture(image) - ) - Image(texture, image.width, image.height) - } else - null - } catch (exc: Exception) { - exc.printStackTrace() - null - } - } - } + private fun tryCacheUrl(url: String) { + if (!isUrlAllowed(url)) { + return + } + if (url in imageCache) { + return + } + imageCache[url] = Firmament.coroutineScope.async { + try { + val response = Firmament.httpClient.get(URL(url)) + if (response.status.value == 200) { + val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob) + val image = NativeImage.read(inputStream) + val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") + MC.textureManager.registerTexture( + texId, + NativeImageBackedTexture(image) + ) + Image(texId, image.width, image.height) + } else + null + } catch (exc: Exception) { + exc.printStackTrace() + null + } + } + } - val imageExtensions = listOf("jpg", "png", "gif", "jpeg") - fun isImageUrl(url: String): Boolean { - return (url.substringAfterLast('.').lowercase() in imageExtensions) - } + val imageExtensions = listOf("jpg", "png", "gif", "jpeg") + fun isImageUrl(url: String): Boolean { + return (url.substringAfterLast('.').lowercase() in imageExtensions) + } - @Subscribe - @OptIn(ExperimentalCoroutinesApi::class) - fun onRender(it: ScreenRenderPostEvent) { - if (!TConfig.imageEnabled) return - if (it.screen !is ChatScreen) return - val hoveredComponent = - MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return - val hoverEvent = hoveredComponent.hoverEvent ?: return - val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return - val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return - if (!isImageUrl(url)) return - val imageFuture = imageCache[url] ?: return - if (!imageFuture.isCompleted) return - val image = imageFuture.getCompleted() ?: return - it.drawContext.matrices.push() - val pos = TConfig.position - pos.applyTransformations(it.drawContext.matrices) - val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) - it.drawContext.matrices.scale(scale, scale, 1F) - it.drawContext.drawTexture( - image.texture, - 0, - 0, - 1F, - 1F, - image.width, - image.height, - image.width, - image.height, - ) - it.drawContext.matrices.pop() - } + @Subscribe + @OptIn(ExperimentalCoroutinesApi::class) + fun onRender(it: ScreenRenderPostEvent) { + if (!TConfig.imageEnabled) return + if (it.screen !is ChatScreen) return + val hoveredComponent = + MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return + val hoverEvent = hoveredComponent.hoverEvent ?: return + val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return + if (!isImageUrl(url)) return + val imageFuture = imageCache[url] ?: return + if (!imageFuture.isCompleted) return + val image = imageFuture.getCompleted() ?: return + it.drawContext.matrices.push() + val pos = TConfig.position + pos.applyTransformations(it.drawContext.matrices) + val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) + it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.drawTexture( + image.texture, + 0, + 0, + 1F, + 1F, + image.width, + image.height, + image.width, + image.height, + ) + it.drawContext.matrices.pop() + } - @Subscribe - fun onModifyChat(it: ModifyChatEvent) { - if (!TConfig.enableLinks) return - it.replaceWith = it.replaceWith.transformEachRecursively { child -> - val text = child.string - if ("://" !in text) return@transformEachRecursively child - val s = Text.empty().setStyle(child.style) - var index = 0 - while (index < text.length) { - val nextMatch = urlRegex.find(text, index) - if (nextMatch == null) { - s.append(Text.literal(text.substring(index, text.length))) - break - } - val range = nextMatch.groups[0]!!.range - val url = nextMatch.groupValues[0] - s.append(Text.literal(text.substring(index, range.first))) - s.append( - Text.literal(url).setStyle( - Style.EMPTY.withUnderline(true).withColor( - Formatting.AQUA - ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) - .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) - ) - ) - if (isImageUrl(url)) - tryCacheUrl(url) - index = range.last + 1 - } - s - } - } + @Subscribe + fun onModifyChat(it: ModifyChatEvent) { + if (!TConfig.enableLinks) return + it.replaceWith = it.replaceWith.transformEachRecursively { child -> + val text = child.string + if ("://" !in text) return@transformEachRecursively child + val s = Text.empty().setStyle(child.style) + var index = 0 + while (index < text.length) { + val nextMatch = urlRegex.find(text, index) + if (nextMatch == null) { + s.append(Text.literal(text.substring(index, text.length))) + break + } + val range = nextMatch.groups[0]!!.range + val url = nextMatch.groupValues[0] + s.append(Text.literal(text.substring(index, range.first))) + s.append( + Text.literal(url).setStyle( + Style.EMPTY.withUnderline(true).withColor( + Formatting.AQUA + ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) + .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) + ) + ) + if (isImageUrl(url)) + tryCacheUrl(url) + index = range.last + 1 + } + s + } + } } diff --git a/src/main/kotlin/features/chat/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/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 13320dc..8be5d5d 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -1,20 +1,31 @@ package moe.nea.firmament.features.debug +import com.mojang.serialization.Codec +import com.mojang.serialization.DynamicOps import com.mojang.serialization.JsonOps +import com.mojang.serialization.codecs.RecordCodecBuilder import kotlin.jvm.optionals.getOrNull import net.minecraft.block.SkullBlock import net.minecraft.block.entity.SkullBlockEntity import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.ProfileComponent import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType import net.minecraft.entity.LivingEntity import net.minecraft.item.ItemStack import net.minecraft.item.Items +import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import net.minecraft.predicate.NbtPredicate import net.minecraft.text.Text import net.minecraft.text.TextCodecs +import net.minecraft.util.Identifier import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.Position +import net.minecraft.util.math.Vec3d import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent @@ -23,12 +34,13 @@ import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldKeyboardEvent import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.IntrospectableItemModelManager +import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -86,6 +98,11 @@ object PowerUserTools : FirmamentFeature { } fun showEntity(target: Entity) { + val nbt = NbtPredicate.entityToNbt(target) + nbt.remove("Inventory") + nbt.put("StyledName", TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, target.styledDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) @@ -101,6 +118,8 @@ object PowerUserTools : FirmamentFeature { } } + // TODO: leak this through some other way, maybe. + lateinit var getSkullId: (profile: ProfileComponent) -> Identifier? @Subscribe fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { @@ -116,7 +135,11 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) } else if (it.matches(TConfig.copyTexturePackId)) { - val model = CustomItemModelEvent.getModelIdentifier(item) + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: Identifier): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) return @@ -146,7 +169,7 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) return } - val skullTexture = CustomSkyBlockTextures.getSkullTexture(profile) + val skullTexture = getSkullId(profile) if (skullTexture == null) { lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) return @@ -179,7 +202,7 @@ object PowerUserTools : FirmamentFeature { MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) return } - val id = CustomSkyBlockTextures.getSkullTexture(entity.owner!!) + val id = getSkullId(entity.owner!!) if (id == null) { MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) } else { diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt new file mode 100644 index 0000000..9935051 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Style +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) { + val highlightPlayersForSlice by toggle("highlight-cake-players") { true } +// val highlightAllPlayers by toggle("highlight-all-cake-players") { true } + } + + val cakeIcon = "⛃" + + val cakeColors = listOf( + CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, Formatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, Formatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, Formatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, Formatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, Formatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: Formatting, + ) { + val searchedTextRgb = formatting.colorValue!! + val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0) + val tintOverlay by lazy { + TintedOverlayTexture().setColor(brightenedRgb) + } + } + + val sliceToColor = cakeColors.associateBy { it.id } + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightPlayersForSlice) return + val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return + // TODO: cache the requested color + val player = event.entity as? PlayerEntity ?: return + val cakeColor: Style = player.styledDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.rgb == requestedCakeTeam.searchedTextRgb) { + event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay + } + } + +} 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..7a3a152 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,7 +13,9 @@ 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.FeaturesInitializedEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyReleasedEvent @@ -37,6 +38,7 @@ import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.render.GuiRenderLayers import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyblock.DungeonUtil import moe.nea.firmament.util.skyblockUUID import moe.nea.firmament.util.unformattedString @@ -59,6 +61,18 @@ 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 } + val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } + } + + enum class SlotRenderLinesMode : StringIdentifiable { + EVERYTHING, + ONLY_BOXES, + NOTHING; + + override fun asString(): String { + return name + } } override val config: TConfig @@ -95,7 +109,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,12 +118,16 @@ 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) { val stack = inv.getStack(i) - if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + if (IsSlotProtectedEvent.shouldBlockInteraction(null, + SlotActionType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack) + ) anyBlocked = true } if (anyBlocked) { @@ -145,6 +163,19 @@ object SlotLocking : FirmamentFeature { } @Subscribe + fun onEvent(event: FeaturesInitializedEvent) { + IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") { + if (it.isProtected + && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR + && DungeonUtil.isInActiveDungeon + && TConfig.allowDroppingInDungeons + ) { + it.isProtected = false + } + } + } + + @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { val boundSlots = DConfig.data?.boundSlots ?: mapOf() val isValidAction = @@ -227,23 +258,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/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 2be798b..6092e26 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -66,6 +66,18 @@ class StorageOverlayCustom( return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) } + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return overview.keyReleased(keyCode, scanCode, modifiers) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return overview.keyPressed(keyCode, scanCode, modifiers) + } + + override fun charTyped(chr: Char, modifiers: Int): Boolean { + return overview.charTyped(chr, modifiers) + } + override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt index 4c624ef..633a8fe 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -1,15 +1,22 @@ package moe.nea.firmament.features.inventory.storageoverlay +import io.github.notenoughupdates.moulconfig.common.IMinecraft import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent import io.github.notenoughupdates.moulconfig.gui.MouseEvent import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import java.util.TreeSet import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack import net.minecraft.screen.slot.Slot import net.minecraft.text.Text import net.minecraft.util.Identifier @@ -20,10 +27,17 @@ import moe.nea.firmament.util.MC import moe.nea.firmament.util.MoulConfigUtils.adopt import moe.nea.firmament.util.MoulConfigUtils.clickMCComponentInPlace import moe.nea.firmament.util.MoulConfigUtils.drawMCComponentInPlace +import moe.nea.firmament.util.MoulConfigUtils.typeMCComponentInPlace +import moe.nea.firmament.util.StringUtil.words import moe.nea.firmament.util.assertTrueOr import moe.nea.firmament.util.customgui.customGui import moe.nea.firmament.util.mc.FakeSlot +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.render.enableScissorWithoutTranslation +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString class StorageOverlayScreen : Screen(Text.literal("")) { @@ -83,10 +97,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) { horizontalAmount: Double, verticalAmount: Double ): Boolean { - scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toFloat() + coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat()) + return true + } + fun coerceScroll(offset: Float) { + scroll = (scroll + offset) .coerceAtMost(getMaxScroll()) .coerceAtLeast(0F) - return true } fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height @@ -142,12 +159,26 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val guiContext = GuiContext(EmptyComponent()) private val knobStub = EmptyComponent() - val editButton = PanelComponent(ColumnComponent(FirmButtonComponent(TextComponent("Edit"), action = ::editPages)), - 8, PanelComponent.DefaultBackgroundRenderer.TRANSPARENT) + val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages) + val searchText = Property.of("") // TODO: sync with REI + val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true), + tr("firmament.storage-overlay.search.suggestion", "Search...").string, + IMinecraft.instance.defaultFontRenderer) + val controlComponent = PanelComponent( + ColumnComponent( + searchField, + editButton, + ), + 8, PanelComponent.DefaultBackgroundRenderer.TRANSPARENT + ) init { - guiContext.adopt(editButton) + searchText.addObserver { _, _ -> + layoutedForEach(StorageOverlay.Data.data ?: StorageData(), { _, _, _ -> }) + coerceScroll(0F) + } guiContext.adopt(knobStub) + guiContext.adopt(controlComponent) } fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) { @@ -157,7 +188,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { measurements.controlY, CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT) context.drawMCComponentInPlace( - editButton, + controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, mouseX, mouseY) @@ -211,9 +242,9 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun createScissors(context: DrawContext) { val rect = getScrollPanelInner() - context.enableScissor( - rect.minX, rect.minY, - rect.maxX, rect.maxY + context.enableScissorWithoutTranslation( + rect.minX.toFloat(), rect.minY.toFloat(), + rect.maxX.toFloat(), rect.maxY.toFloat(), ) } @@ -251,7 +282,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { knobGrabbed = false return true } - if (clickMCComponentInPlace(editButton, + if (clickMCComponentInPlace(controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, mouseX.toInt(), mouseY.toInt(), @@ -272,6 +303,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components. if (getScrollPanelInner().contains(mouseX, mouseY)) { val data = StorageOverlay.Data.data ?: StorageData() layoutedForEach(data) { rect, page, _ -> @@ -290,7 +322,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { knobGrabbed = true return true } - if (clickMCComponentInPlace(editButton, + if (clickMCComponentInPlace(controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, mouseX.toInt(), mouseY.toInt(), @@ -299,6 +331,78 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return false } + override fun charTyped(chr: Char, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.CharTyped(chr) + ) + ) { + return true + } + return super.charTyped(chr, modifiers) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.KeyPressed(keyCode, false) + ) + ) { + return true + } + return super.keyReleased(keyCode, scanCode, modifiers) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.KeyPressed(keyCode, true) + ) + ) { + return true + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + + + var searchCache: String? = null + var filteredPagesCache = setOf<StoragePageSlot>() + + fun getFilteredPages(): Set<StoragePageSlot> { + val searchValue = searchText.get() + val data = StorageOverlay.Data.data ?: return filteredPagesCache // Do not update cache if data is missing + if (searchCache == searchValue) return filteredPagesCache + val result = + data.storageInventories + .entries.asSequence() + .filter { it.value.inventory?.stacks?.any { matchesSearch(it, searchValue) } ?: true } + .map { it.key } + .toSet() + searchCache = searchValue + filteredPagesCache = result + return result + } + + + fun matchesSearch(itemStack: ItemStack, search: String): Boolean { + val searchWords = search.words().toCollection(TreeSet()) + fun removePrefixes(value: String) { + searchWords.removeIf { value.contains(it, ignoreCase = true) } + } + itemStack.displayNameAccordingToNbt.unformattedString.words().forEach(::removePrefixes) + if (searchWords.isEmpty()) return true + itemStack.loreAccordingToNbt.forEach { + it.unformattedString.words().forEach(::removePrefixes) + } + return searchWords.isEmpty() + } + private inline fun layoutedForEach( data: StorageData, func: ( @@ -309,7 +413,9 @@ class StorageOverlayScreen : Screen(Text.literal("")) { var yOffset = -scroll.toInt() var xOffset = 0 var maxHeight = 0 + val filter = getFilteredPages() for ((page, inventory) in data.storageInventories.entries) { + if (page !in filter) continue val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight } ?: 18 maxHeight = maxOf(maxHeight, currentHeight) 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 4fcf8a7..1737969 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -7,11 +7,13 @@ import net.minecraft.item.ItemStack import net.minecraft.util.DyeColor import net.minecraft.util.Hand import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.ProfileSwitchEvent import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.UseItemEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig @@ -27,10 +29,13 @@ import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.parseTimePattern +import moe.nea.firmament.util.red import moe.nea.firmament.util.render.RenderCircleProgress import moe.nea.firmament.util.render.lerp import moe.nea.firmament.util.skyblock.AbilityUtils +import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.toShedaniel +import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch @@ -43,6 +48,21 @@ object PickaxeAbility : FirmamentFeature { val cooldownEnabled by toggle("ability-cooldown") { false } val cooldownScale by integer("ability-scale", 16, 64) { 16 } val drillFuelBar by toggle("fuel-bar") { true } + val blockOnPrivateIsland by choice( + "block-on-dynamic", + ) { + BlockPickaxeAbility.ONLY_DESTRUCTIVE + } + } + + enum class BlockPickaxeAbility : StringIdentifiable { + NEVER, + ALWAYS, + ONLY_DESTRUCTIVE; + + override fun asString(): String { + return name + } } var lobbyJoinTime = TimeMark.farPast() @@ -56,6 +76,8 @@ object PickaxeAbility : FirmamentFeature { "Maniac Miner" to 59.seconds, "Vein Seeker" to 60.seconds ) + val destructiveAbilities = setOf("Pickobulus") + val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET) override val config: ManagedConfig get() = TConfig @@ -74,6 +96,27 @@ 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) + val shouldBlock = when (TConfig.blockOnPrivateIsland) { + BlockPickaxeAbility.NEVER -> false + BlockPickaxeAbility.ALWAYS -> ability.any() + BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities } + } + if (shouldBlock) { + MC.sendChat(tr("firmament.pickaxe.blocked", + "Firmament blocked a pickaxe ability from being used on a private island.") + .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic") + ) + event.cancel() + } + } + + @Subscribe fun onSlotClick(it: SlotClickEvent) { if (MC.screen?.title?.unformattedString == "Heart of the Mountain") { val name = it.stack.displayNameAccordingToNbt.unformattedString @@ -113,10 +156,21 @@ object PickaxeAbility : FirmamentFeature { fun onChatMessage(it: ProcessChatEvent) { abilityUsePattern.useMatch(it.unformattedString) { lastUsage[group("name")] = TimeMark.now() + abilityOverride = group("name") } abilitySwitchPattern.useMatch(it.unformattedString) { abilityOverride = group("ability") } + pickaxeAbilityCooldownPattern.useMatch(it.unformattedString) { + val ability = abilityOverride ?: return@useMatch + val remainingCooldown = parseTimePattern(group("remainingCooldown")) + val length = defaultAbilityDurations[ability] ?: return@useMatch + lastUsage[ability] = TimeMark.ago(length - remainingCooldown) + } + nowAvailable.useMatch(it.unformattedString) { + val ability = group("name") + lastUsage[ability] = TimeMark.farPast() + } } @Subscribe @@ -136,6 +190,7 @@ object PickaxeAbility : FirmamentFeature { val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)") val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.") + val nowAvailable = Pattern.compile("(?<name>[a-zA-Z0-9 ]+) is now available!") data class PickaxeAbilityData( val name: String, 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/features/texturepack/BakedModelExtra.kt b/src/main/kotlin/features/texturepack/BakedModelExtra.kt deleted file mode 100644 index 6305748..0000000 --- a/src/main/kotlin/features/texturepack/BakedModelExtra.kt +++ /dev/null @@ -1,30 +0,0 @@ -package moe.nea.firmament.features.texturepack - -import net.fabricmc.fabric.api.renderer.v1.model.WrapperBakedModel as WrapperBakedModelFabric -import net.minecraft.client.render.model.BakedModel -import net.minecraft.client.render.model.WrapperBakedModel -import moe.nea.firmament.util.ErrorUtil - -interface BakedModelExtra { - companion object { - @JvmStatic - fun cast(originalModel: BakedModel): BakedModelExtra? { - var p = originalModel - for (i in 0..256) { - p = when (p) { - is BakedModelExtra -> return p - is WrapperBakedModel -> p.wrapped - is WrapperBakedModelFabric -> WrapperBakedModelFabric.unwrap(p) - else -> break - } - } - ErrorUtil.softError("Could not find a baked model for $originalModel") - return null - } - } - - var tintOverrides_firmament: TintOverrides? - - fun getHeadModel_firmament(): BakedModel? - fun setHeadModel_firmament(headModel: BakedModel?) -} diff --git a/src/main/kotlin/features/texturepack/BakedOverrideData.kt b/src/main/kotlin/features/texturepack/BakedOverrideData.kt deleted file mode 100644 index e9391f1..0000000 --- a/src/main/kotlin/features/texturepack/BakedOverrideData.kt +++ /dev/null @@ -1,14 +0,0 @@ - -package moe.nea.firmament.features.texturepack - -import net.minecraft.client.render.model.json.ModelOverrideList - -interface BakedOverrideData { - fun getFirmamentOverrides(): Array<FirmamentModelPredicate>? - fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?) - companion object{ - @Suppress("CAST_NEVER_SUCCEEDS") - @JvmStatic - fun cast(bakedOverride: ModelOverrideList.BakedOverride): BakedOverrideData = bakedOverride as BakedOverrideData - } -} diff --git a/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt b/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt deleted file mode 100644 index c5fc20b..0000000 --- a/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt +++ /dev/null @@ -1,82 +0,0 @@ - -package moe.nea.firmament.features.texturepack - -import com.google.gson.JsonObject -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import moe.nea.firmament.features.texturepack.predicates.AndPredicate -import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate -import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate -import moe.nea.firmament.features.texturepack.predicates.ItemPredicate -import moe.nea.firmament.features.texturepack.predicates.LorePredicate -import moe.nea.firmament.features.texturepack.predicates.NotPredicate -import moe.nea.firmament.features.texturepack.predicates.OrPredicate -import moe.nea.firmament.features.texturepack.predicates.PetPredicate -import net.minecraft.item.ItemStack -import net.minecraft.util.Identifier - -object CustomModelOverrideParser { - object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> { - val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() - override val descriptor: SerialDescriptor - get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor) - - override fun deserialize(decoder: Decoder): FirmamentModelPredicate { - val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject - return AndPredicate(parsePredicates(json).toTypedArray()) - } - - override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) { - TODO("Cannot serialize firmament predicates") - } - } - - val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>() - - - fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) { - predicateParsers[Identifier.of("firmament", name)] = parser - } - - init { - registerPredicateParser("display_name", DisplayNamePredicate.Parser) - registerPredicateParser("lore", LorePredicate.Parser) - registerPredicateParser("all", AndPredicate.Parser) - registerPredicateParser("any", OrPredicate.Parser) - registerPredicateParser("not", NotPredicate.Parser) - registerPredicateParser("item", ItemPredicate.Parser) - registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) - registerPredicateParser("pet", PetPredicate.Parser) - } - - private val neverPredicate = listOf( - object : FirmamentModelPredicate { - override fun test(stack: ItemStack): Boolean { - return false - } - } - ) - - fun parsePredicates(predicates: JsonObject): List<FirmamentModelPredicate> { - val parsedPredicates = mutableListOf<FirmamentModelPredicate>() - for (predicateName in predicates.keySet()) { - if (!predicateName.startsWith("firmament:")) continue - val identifier = Identifier.of(predicateName) - val parser = predicateParsers[identifier] ?: return neverPredicate - val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate - parsedPredicates.add(parsedPredicate) - } - return parsedPredicates - } - - @JvmStatic - fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? { - val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null - val parsedPredicates = parsePredicates(predicates) - if (parsedPredicates.isEmpty()) - return null - return parsedPredicates.toTypedArray() - } -} diff --git a/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt b/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt deleted file mode 100644 index 627d39a..0000000 --- a/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt +++ /dev/null @@ -1,135 +0,0 @@ -package moe.nea.firmament.features.texturepack - -import com.mojang.authlib.minecraft.MinecraftProfileTexture -import com.mojang.authlib.properties.Property -import java.util.Optional -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable -import kotlin.jvm.optionals.getOrNull -import net.minecraft.block.SkullBlock -import net.minecraft.client.MinecraftClient -import net.minecraft.client.render.RenderLayer -import net.minecraft.client.util.ModelIdentifier -import net.minecraft.component.type.ProfileComponent -import net.minecraft.util.Identifier -import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.BakeExtraModelsEvent -import moe.nea.firmament.events.CustomItemModelEvent -import moe.nea.firmament.events.FinalizeResourceManagerEvent -import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.collections.WeakCache -import moe.nea.firmament.util.mc.decodeProfileTextureProperty -import moe.nea.firmament.util.skyBlockId - -object CustomSkyBlockTextures : FirmamentFeature { - override val identifier: String - get() = "custom-skyblock-textures" - - object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing? - val enabled by toggle("enabled") { true } - val skullsEnabled by toggle("skulls-enabled") { true } - val cacheForever by toggle("cache-forever") { true } - val cacheDuration by integer("cache-duration", 0, 100) { 1 } - val enableModelOverrides by toggle("model-overrides") { true } - val enableArmorOverrides by toggle("armor-overrides") { true } - val enableBlockOverrides by toggle("block-overrides") { true } - val enableLegacyCIT by toggle("legacy-cit") { true } - val allowRecoloringUiText by toggle("recolor-text") { true } - } - - override val config: ManagedConfig - get() = TConfig - - val allItemCaches by lazy { - listOf( - CustomItemModelEvent.cache.cache, - skullTextureCache.cache, - CustomGlobalTextures.overrideCache.cache, - CustomGlobalArmorOverrides.overrideCache.cache - ) - } - - fun clearAllCaches() { - allItemCaches.forEach(WeakCache<*, *, *>::clear) - } - - @Subscribe - fun onTick(it: TickEvent) { - if (TConfig.cacheForever) return - if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) { - clearAllCaches() - } - } - - @Subscribe - fun onStart(event: FinalizeResourceManagerEvent) { - event.registerOnApply("Clear firmament CIT caches") { - clearAllCaches() - } - } - - @Subscribe - fun bakeCustomFirmModels(event: BakeExtraModelsEvent) { - val resources = - MinecraftClient.getInstance().resourceManager.findResources("models/item" - ) { it: Identifier -> - "firmskyblock" == it.namespace && it.path - .endsWith(".json") - } - for (identifier in resources.keys) { - val modelId = ModelIdentifier.ofInventoryVariant( - Identifier.of( - "firmskyblock", - identifier.path.substring( - "models/item/".length, - identifier.path.length - ".json".length), - )) - event.addItemModel(modelId) - } - } - - @Subscribe - fun onCustomModelId(it: CustomItemModelEvent) { - if (!TConfig.enabled) return - val id = it.itemStack.skyBlockId ?: return - it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path)) - } - - private val skullTextureCache = - WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component -> - val id = getSkullTexture(component) ?: return@memoize Optional.empty() - if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { - return@memoize Optional.empty() - } - return@memoize Optional.of(id) - } - - private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex() - - fun getSkullId(textureProperty: Property): String? { - val texture = decodeProfileTextureProperty(textureProperty) ?: return null - val textureUrl = - texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null - val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null - return mcUrlData.groupValues[1] - } - - fun getSkullTexture(profile: ProfileComponent): Identifier? { - val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null - return Identifier.of("firmskyblock", "textures/placedskull/$id.png") - } - - fun modifySkullTexture( - type: SkullBlock.SkullType?, - component: ProfileComponent?, - cir: CallbackInfoReturnable<RenderLayer> - ) { - if (type != SkullBlock.Type.PLAYER) return - if (!TConfig.skullsEnabled) return - if (component == null) return - - val n = skullTextureCache.invoke(component).getOrNull() ?: return - cir.returnValue = RenderLayer.getEntityTranslucent(n) - } -} diff --git a/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt b/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt deleted file mode 100644 index d11fec0..0000000 --- a/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt +++ /dev/null @@ -1,8 +0,0 @@ - -package moe.nea.firmament.features.texturepack - -import net.minecraft.item.ItemStack - -interface FirmamentModelPredicate { - fun test(stack: ItemStack): Boolean -} diff --git a/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt b/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt deleted file mode 100644 index 9f641b8..0000000 --- a/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt +++ /dev/null @@ -1,16 +0,0 @@ - -package moe.nea.firmament.features.texturepack - -import net.minecraft.client.render.model.Baker -import net.minecraft.util.Identifier - -interface JsonUnbakedModelFirmExtra { - fun storeExtraBaker_firmament(baker: Baker) - - fun setHeadModel_firmament(identifier: Identifier?) - fun getHeadModel_firmament(): Identifier? - - fun setTintOverrides_firmament(tintOverrides: TintOverrides?) - fun getTintOverrides_firmament(): TintOverrides - -} diff --git a/src/main/kotlin/features/texturepack/ModelOverrideData.kt b/src/main/kotlin/features/texturepack/ModelOverrideData.kt deleted file mode 100644 index 29d9192..0000000 --- a/src/main/kotlin/features/texturepack/ModelOverrideData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.nea.firmament.features.texturepack - -import net.minecraft.client.render.model.json.ModelOverride - -interface ModelOverrideData { - companion object { - - @JvmStatic - @Suppress("CAST_NEVER_SUCCEEDS") - fun cast(override: ModelOverride) = override as ModelOverrideData - } - - fun getFirmamentOverrides(): Array<FirmamentModelPredicate>? - fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?) -} diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt index 16db059..3ebfe70 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -4,6 +4,7 @@ import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set @@ -32,6 +33,7 @@ import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.tr object Waypoints : FirmamentFeature { override val identifier: String @@ -198,11 +200,22 @@ object Waypoints : FirmamentFeature { } } } + thenLiteral("export") { + thenExecute { + val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map { + ColeWeightWaypoint(it.x, + it.y, + it.z) + }) + ClipboardUtils.setTextContent(data) + source.sendFeedback(tr("firmament.command.waypoint.export", "Copied ${waypoints.size} waypoints to clipboard")) + } + } thenLiteral("import") { thenExecute { val contents = ClipboardUtils.getTextContents() val data = try { - Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents) + Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents) } catch (ex: Exception) { Firmament.logger.error("Could not load waypoints from clipboard", ex) source.sendError(Text.translatable("firmament.command.waypoint.import.error")) diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt new file mode 100644 index 0000000..761c086 --- /dev/null +++ b/src/main/kotlin/gui/CheckboxComponent.kt @@ -0,0 +1,56 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import net.minecraft.client.render.RenderLayer +import moe.nea.firmament.Firmament + +class CheckboxComponent<T>( + val state: GetSetter<T>, + val value: T, +) : GuiComponent() { + override fun getWidth(): Int { + return 16 + } + + override fun getHeight(): Int { + return 16 + } + + fun isEnabled(): Boolean { + return state.get() == value + } + + override fun render(context: GuiImmediateContext) { + val ctx = (context.renderContext as ModernRenderContext).drawContext + ctx.drawGuiTexture( + RenderLayer::getGuiTextured, + if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked") + else Firmament.identifier("firmament:widget/checkbox_unchecked"), + 0, 0, + 16, 16 + ) + } + + var isClicking = false + + override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean { + if (mouseEvent is MouseEvent.Click) { + if (isClicking && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) { + isClicking = false + if (context.isHovered) + state.set(value) + return true + } + if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) { + requestFocus() + isClicking = true + return true + } + } + return false + } +} diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt new file mode 100644 index 0000000..2ea3efc --- /dev/null +++ b/src/main/kotlin/gui/config/ChoiceHandler.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign +import io.github.notenoughupdates.moulconfig.gui.VerticalAlign +import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import kotlinx.serialization.json.JsonElement +import kotlin.jvm.optionals.getOrNull +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.gui.CheckboxComponent +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 { + @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (universe as java.util.List<*>).toArray(arrayOfNulls<Enum<E>>(0)) as Array<E> + } + val renderer = EnumRenderer.default<E>() + + override fun toJson(element: E): JsonElement? { + return codec.encodeStart(KJsonOps.INSTANCE, element) + .promotePartial { ErrorUtil.softError("Failed to encode json element '$element': $it") }.result() + .getOrNull() + } + + override fun fromJson(element: JsonElement): E { + return codec.decode(KJsonOps.INSTANCE, element) + .promotePartial { ErrorUtil.softError("Failed to decode json element '$element': $it") } + .result() + .get() + .first + } + + override fun emitGuiElements(opt: ManagedOption<E>, guiAppender: GuiAppender) { + guiAppender.appendFullRow(TextComponent(opt.labelText.string)) + for (e in universe) { + guiAppender.appendFullRow(RowComponent( + AlignComponent(CheckboxComponent(opt, e), { HorizontalAlign.LEFT }, { VerticalAlign.CENTER }), + TextComponent(renderer.getName(opt, e).string) + )) + } + } +} diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt new file mode 100644 index 0000000..3b80b7e --- /dev/null +++ b/src/main/kotlin/gui/config/EnumRenderer.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.gui.config + +import net.minecraft.text.Text + +interface EnumRenderer<E : Any> { + fun getName(option: ManagedOption<E>, value: E): Text + + companion object { + fun <E : Enum<E>> default() = object : EnumRenderer<E> { + override fun getName(option: ManagedOption<E>, value: E): Text { + return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase()) + } + } + } +} diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt index 8222a46..7ddda9e 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.gui.config +import com.mojang.serialization.Codec import io.github.notenoughupdates.moulconfig.gui.CloseEventListener import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper import io.github.notenoughupdates.moulconfig.gui.GuiContext @@ -20,6 +21,7 @@ import kotlin.io.path.writeText import kotlin.time.Duration import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text +import net.minecraft.util.StringIdentifiable import moe.nea.firmament.Firmament import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.keybindings.SavedKeyBinding @@ -113,6 +115,41 @@ abstract class ManagedConfig( return option(propertyName, default, BooleanHandler(this)) } + protected fun <E> choice( + propertyName: String, + enumClass: Class<E>, + default: () -> E + ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable { + return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList())) + } + + 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 +// ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable { +// return choice( +// propertyName, +// enumEntries<E>().toList(), +// StringIdentifiable.createCodec { enumValues<E>() }, +// EnumRenderer.default(), +// default +// ) +// } + open fun onChange(option: ManagedOption<*>) { + } + protected fun duration( propertyName: String, min: Duration, 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..fd7a0c4 100644 --- a/src/main/kotlin/gui/entity/EntityRenderer.kt +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -111,10 +111,14 @@ object EntityRenderer { renderContext: DrawContext, posX: Int, posY: Int, - mouseX: Float, - mouseY: Float + // TODO: Add width, height properties here + width: Double, + height: Double, + mouseX: Double, + mouseY: Double, + entityScale: Double = (height - 10.0) / 2.0 ) { - var bottomOffset = 0.0F + var bottomOffset = 0.0 var currentEntity = entity val maxSize = entity.iterate { it.firstPassenger as? LivingEntity } .map { it.height } @@ -125,9 +129,9 @@ object EntityRenderer { renderContext, posX, posY, - posX + 50, - posY + 80, - minOf(2F / maxSize, 1F) * 30, + (posX + width).toInt(), + (posY + height).toInt(), + minOf(2F / maxSize, 1F) * entityScale, -bottomOffset, mouseX, mouseY, @@ -146,17 +150,19 @@ object EntityRenderer { y1: Int, x2: Int, y2: Int, - size: Float, - bottomOffset: Float, - mouseX: Float, - mouseY: Float, + size: Double, + bottomOffset: Double, + 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 hw = (x2 - x1) / 2 + val hh = (y2 - y1) / 2 + val targetYaw = atan(((centerX - mouseX) / hw)).toFloat() + val targetPitch = atan(((centerY - mouseY) / hh)).toFloat() val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat()) val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180)) rotateToFaceTheFront.mul(rotateToFaceTheCamera) @@ -170,12 +176,12 @@ object EntityRenderer { entity.pitch = -targetPitch * 20.0f entity.headYaw = entity.yaw entity.prevHeadYaw = entity.yaw - val vector3f = Vector3f(0.0f, entity.height / 2.0f + bottomOffset, 0.0f) + val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f) InventoryScreen.drawEntity( context, centerX, centerY, - size, + size.toFloat(), vector3f, rotateToFaceTheFront, rotateToFaceTheCamera, diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt index 7ec385c..ccf6b60 100644 --- a/src/main/kotlin/gui/entity/FakeWorld.kt +++ b/src/main/kotlin/gui/entity/FakeWorld.kt @@ -8,6 +8,7 @@ import net.minecraft.block.BlockState import net.minecraft.client.gui.screen.world.SelectWorldScreen import net.minecraft.component.type.MapIdComponent import net.minecraft.entity.Entity +import net.minecraft.entity.boss.dragon.EnderDragonPart import net.minecraft.entity.damage.DamageSource import net.minecraft.entity.player.PlayerEntity import net.minecraft.fluid.Fluid @@ -262,6 +263,10 @@ class FakeWorld( return null } + override fun getEnderDragonParts(): MutableCollection<EnderDragonPart> { + return mutableListOf() + } + override fun getTickManager(): TickManager { return TickManager() } diff --git a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt index e2bed8d..59b131a 100644 --- a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt +++ b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt @@ -1,26 +1,25 @@ - - package moe.nea.firmament.keybindings import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper import net.minecraft.client.option.KeyBinding import net.minecraft.client.util.InputUtil -import moe.nea.firmament.gui.config.KeyBindingHandler import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.util.TestUtil object FirmamentKeyBindings { - fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) { - val vanillaKeyBinding = KeyBindingHelper.registerKeyBinding( - KeyBinding( - name, - InputUtil.Type.KEYSYM, - -1, - "firmament.key.category" - ) - ) - keyBindings[vanillaKeyBinding] = config - } + fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) { + val vanillaKeyBinding = KeyBinding( + name, + InputUtil.Type.KEYSYM, + -1, + "firmament.key.category" + ) + if (!TestUtil.isInTest) { + KeyBindingHelper.registerKeyBinding(vanillaKeyBinding) + } + keyBindings[vanillaKeyBinding] = config + } - val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>() + val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>() } diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt index 91a6b50..6d18223 100644 --- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -1,28 +1,31 @@ - package moe.nea.firmament.repo import io.github.moulberry.repo.IReloadable import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUNpcShopRecipe import io.github.moulberry.repo.data.NEURecipe import moe.nea.firmament.util.SkyblockId +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 -> + if (recipe is NEUNpcShopRecipe) { + usages.getOrPut(recipe.isSoldBy.skyblockId, ::mutableSetOf).add(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 014de7d..9fa0083 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -24,21 +24,28 @@ import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtOps import net.minecraft.nbt.NbtString +import net.minecraft.text.MutableText +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() @@ -70,6 +77,12 @@ object ItemCache : IReloadable { val ItemStack.isBroken get() = get(FirmamentDataComponentTypes.IS_BROKEN) ?: false + + fun ItemStack.withFallback(fallback: ItemStack?): ItemStack { + if (isBroken && fallback != null) return fallback + return this + } + fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack { return ItemStack(Items.PAINTING).apply { setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) @@ -88,6 +101,35 @@ object ItemCache : IReloadable { } } + fun un189Lore(lore: String): MutableText { + 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() @@ -95,6 +137,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)) @@ -129,12 +173,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..4c01974 --- /dev/null +++ b/src/main/kotlin/repo/ReforgeStore.kt @@ -0,0 +1,125 @@ +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) + byType.getOrPut(itemType.dungeonVariant, ::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/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt index f92fe4f..617efec 100644 --- a/src/main/kotlin/repo/RepoModResourcePack.kt +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.repo import java.io.InputStream @@ -21,86 +20,86 @@ import net.minecraft.resource.ResourcePackInfo import net.minecraft.resource.ResourcePackSource import net.minecraft.resource.ResourceType import net.minecraft.resource.metadata.ResourceMetadata -import net.minecraft.resource.metadata.ResourceMetadataReader +import net.minecraft.resource.metadata.ResourceMetadataSerializer import net.minecraft.text.Text import net.minecraft.util.Identifier import net.minecraft.util.PathUtil import moe.nea.firmament.Firmament class RepoModResourcePack(val basePath: Path) : ModResourcePack { - companion object { - fun append(packs: MutableList<in ModResourcePack>) { - Firmament.logger.info("Registering mod resource pack") - packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) - } + companion object { + fun append(packs: MutableList<in ModResourcePack>) { + Firmament.logger.info("Registering mod resource pack") + packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + } - fun createResourceDirectly(identifier: Identifier): Optional<Resource> { - val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation) - return Optional.of( - Resource( - pack, - pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() - ) { - val base = - pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) - if (base == null) - ResourceMetadata.NONE - else - NamespaceResourceManager.loadMetadata(base) - } - ) - } - } + fun createResourceDirectly(identifier: Identifier): Optional<Resource> { + val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation) + return Optional.of( + Resource( + pack, + pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() + ) { + val base = + pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) + if (base == null) + ResourceMetadata.NONE + else + NamespaceResourceManager.loadMetadata(base) + } + ) + } + } - override fun close() { - } + override fun close() { + } - override fun openRoot(vararg segments: String): InputSupplier<InputStream>? { - return getFile(segments)?.let { InputSupplier.create(it) } - } + override fun openRoot(vararg segments: String): InputSupplier<InputStream>? { + return getFile(segments)?.let { InputSupplier.create(it) } + } - fun getFile(segments: Array<out String>): Path? { - PathUtil.validatePath(*segments) - val path = segments.fold(basePath, Path::resolve) - if (!path.isRegularFile()) return null - return path - } + fun getFile(segments: Array<out String>): Path? { + PathUtil.validatePath(*segments) + val path = segments.fold(basePath, Path::resolve) + if (!path.isRegularFile()) return null + return path + } - override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? { - if (type != ResourceType.CLIENT_RESOURCES) return null - if (id.namespace != "neurepo") return null - val file = getFile(id.path.split("/").toTypedArray()) - return file?.let { InputSupplier.create(it) } - } + override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? { + if (type != ResourceType.CLIENT_RESOURCES) return null + if (id.namespace != "neurepo") return null + val file = getFile(id.path.split("/").toTypedArray()) + return file?.let { InputSupplier.create(it) } + } - override fun findResources( - type: ResourceType?, - namespace: String, - prefix: String, - consumer: ResourcePack.ResultConsumer - ) { - if (namespace != "neurepo") return - if (type != ResourceType.CLIENT_RESOURCES) return + override fun findResources( + type: ResourceType?, + namespace: String, + prefix: String, + consumer: ResourcePack.ResultConsumer + ) { + if (namespace != "neurepo") return + if (type != ResourceType.CLIENT_RESOURCES) return - val prefixPath = basePath.resolve(prefix) - if (!prefixPath.exists()) - return - Files.walk(prefixPath) - .asSequence() - .map { it.relativeTo(basePath) } - .forEach { - consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it)) - } - } + val prefixPath = basePath.resolve(prefix) + if (!prefixPath.exists()) + return + Files.walk(prefixPath) + .asSequence() + .map { it.relativeTo(basePath) } + .forEach { + consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it)) + } + } - override fun getNamespaces(type: ResourceType?): Set<String> { - if (type != ResourceType.CLIENT_RESOURCES) return emptySet() - return setOf("neurepo") - } + override fun getNamespaces(type: ResourceType?): Set<String> { + if (type != ResourceType.CLIENT_RESOURCES) return emptySet() + return setOf("neurepo") + } - override fun <T> parseMetadata(metaReader: ResourceMetadataReader<T>): T? { - return AbstractFileResourcePack.parseMetadata( - metaReader, """ + override fun <T : Any?> parseMetadata(metadataSerializer: ResourceMetadataSerializer<T>?): T? { + return AbstractFileResourcePack.parseMetadata( + metadataSerializer, """ { "pack": { "pack_format": 12, @@ -108,19 +107,20 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { } } """.trimIndent().byteInputStream() - ) - } + ) + } - override fun getInfo(): ResourcePackInfo { - return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty()) - } - override fun getFabricModMetadata(): ModMetadata { - return FabricLoader.getInstance().getModContainer("firmament") - .get().metadata - } + override fun getInfo(): ResourcePackInfo { + return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty()) + } - override fun createOverlay(overlay: String): ModResourcePack { - return RepoModResourcePack(basePath.resolve(overlay)) - } + override fun getFabricModMetadata(): ModMetadata { + return FabricLoader.getInstance().getModContainer("firmament") + .get().metadata + } + + override fun createOverlay(overlay: String): ModResourcePack { + return RepoModResourcePack(basePath.resolve(overlay)) + } } diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index 18126ee..da34707 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -9,17 +9,37 @@ 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.MC +import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.blue +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.mc.modifyLore +import moe.nea.firmament.util.modifyExtraAttributes import moe.nea.firmament.util.petData +import moe.nea.firmament.util.prepend +import moe.nea.firmament.util.reconstitute 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.unformattedString +import moe.nea.firmament.util.useMatch import moe.nea.firmament.util.withColor data class SBItemStack constructor( @@ -28,8 +48,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 @@ -66,7 +87,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() ) } @@ -77,6 +100,168 @@ data class SBItemStack constructor( } return SBItemStack(neuIngredient.skyblockId, neuIngredient.amount.toInt()) } + + fun passthrough(itemStack: ItemStack): SBItemStack { + return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack) + } + + fun parseStatBlock(itemStack: ItemStack): List<StatLine> { + return itemStack.loreAccordingToNbt + .map { parseStatLine(it) } + .takeWhile { it != null } + .filterNotNull() + } + + 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 isStarAffected: Boolean = true, + ) + + 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.GREEN, false), + "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, + val isHidden: Boolean, + ) { + REFORGE(Formatting.BLUE, "(", ")", false), + STAR_BUFF(Formatting.RESET, "", "", true), + CATA_STAR_BUFF(Formatting.DARK_GRAY, "(", ")", false), + ; + } + + data class StatLine( + val statName: String, + val value: Text?, + val rest: List<Text> = listOf(), + val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.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 + + if (buffKind.isHidden) emptyList() + else 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( @@ -127,6 +312,41 @@ 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) + reforge.reforgeAbility?.get(rarity)?.let { reforgeAbility -> + val formattedReforgeAbility = ItemCache.un189Lore(reforgeAbility) + .grey() + itemStack.modifyLore { + val lastBlank = it.indexOfLast { it.unformattedString.isBlank() } + val newList = mutableListOf<Text>() + newList.addAll(it.subList(0, lastBlank)) + newList.add(Text.literal("")) + newList.add(Text.literal("${reforge.reforgeName} Bonus").blue()) + MC.font.textHandler.wrapLines(formattedReforgeAbility, 180, Style.EMPTY).mapTo(newList) { + it.reconstitute() + } + newList.addAll(it.subList(lastBlank, it.size)) + return@modifyLore newList + } + } + } + + // 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 @@ -138,10 +358,14 @@ data class SBItemStack constructor( return@run ItemStack.EMPTY val replacementData = mutableMapOf<String, String>() injectReplacementDataForPets(replacementData) - return@run neuItem.asItemStack(idHint = skyblockId, replacementData) + val baseItem = neuItem.asItemStack(idHint = skyblockId, replacementData) + .withFallback(fallback) .copyWithCount(stackSize) - .also { it.appendLore(extraLore) } - .also { enhanceStatsByStars(it, stars) } + val baseStats = parseStatBlock(baseItem) + appendReforgeInfo(baseItem) + baseItem.appendLore(extraLore) + enhanceStatsByStars(baseItem, stars, baseStats) + return@run baseItem } if (itemStack_ == null) itemStack_ = itemStack @@ -151,6 +375,7 @@ data class SBItemStack constructor( private fun starString(stars: Int): Text { if (stars <= 0) return Text.empty() + // TODO: idk master stars val tiers = listOf( LegacyFormattingCode.GOLD, LegacyFormattingCode.LIGHT_PURPLE, @@ -170,11 +395,23 @@ data class SBItemStack constructor( return starString } - private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int) { + private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int, baseStats: List<StatLine>) { if (stars == 0) return // TODO: increase stats and add the star level into the nbt data so star displays work + itemStack.modifyExtraAttributes { + it.putInt("upgrade_level", stars) + } itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() .append(starString(stars)) + val isDungeon = ItemType.fromItemStack(itemStack)?.isDungeon ?: true + val truncatedStarCount = if (isDungeon) minOf(5, stars) else stars + appendEnhancedStats(itemStack, + baseStats + .filter { it.statFormatting.isStarAffected } + .associate { + it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02)) + }, + BuffKind.STAR_BUFF) } fun asImmutableItemStack(): ItemStack { 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..acb7102 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -9,27 +9,59 @@ import kotlin.io.path.isReadable import kotlin.io.path.isRegularFile import kotlin.io.path.listDirectoryEntries import kotlin.math.absoluteValue +import kotlin.math.roundToInt import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import net.minecraft.text.Text object FirmFormatters { + + private inline fun shortIf( + value: Double, breakpoint: Double, char: String, + return_: (String) -> Nothing + ) { + if (value >= breakpoint) { + val broken = (value / breakpoint * 10).roundToInt() + if (broken > 99) + return_((broken / 10).toString() + char) + val decimals = broken.toString() + decimals.singleOrNull()?.let { + return_("0.$it$char") + } + return_("${decimals[0]}.${decimals[1]}$char") + } + } + + fun shortFormat(double: Double): String { + if (double < 0) return "-" + shortFormat(-double) + shortIf(double, 1_000_000_000_000.0, "t") { return it } + shortIf(double, 1_000_000_000.0, "b") { return it } + shortIf(double, 1_000_000.0, "m") { return it } + shortIf(double, 1_000.0, "k") { return it } + shortIf(double, 1.0, "") { return it } + return double.toString() + } + 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/JvmUtil.kt b/src/main/kotlin/util/JvmUtil.kt new file mode 100644 index 0000000..5be5ebd --- /dev/null +++ b/src/main/kotlin/util/JvmUtil.kt @@ -0,0 +1,32 @@ +package moe.nea.firmament.util + +import com.sun.tools.attach.VirtualMachine +import java.lang.management.ManagementFactory +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +object JvmUtil { + fun guessJVMPid(): String { + val name = ManagementFactory.getRuntimeMXBean().name + val pid = name.substringBefore('@') + ErrorUtil.softCheck("Not a valid PID: $pid", pid.toIntOrNull() != null) + return pid + } + + fun getVM(): VirtualMachine { + return VirtualMachine.attach(guessJVMPid()) + } + + fun useVM(block: (VirtualMachine) -> Unit) { + val vm = getVM() + block(vm) + vm.detach() + } + + fun loadAgent(jarPath: Path, options: String? = null) { + useVM { + it.loadAgent(jarPath.absolutePathString(), options) + } + } + +} 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 f7c81da..ca3742d 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -7,11 +7,13 @@ import net.minecraft.client.gui.hud.InGameHud import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.render.GameRenderer import net.minecraft.client.render.WorldRenderer import net.minecraft.client.render.item.ItemRenderer import net.minecraft.client.world.ClientWorld import net.minecraft.entity.Entity import net.minecraft.item.Item +import net.minecraft.item.ItemStack import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket import net.minecraft.registry.BuiltinRegistries import net.minecraft.registry.RegistryKeys @@ -64,6 +66,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) } @@ -83,6 +87,7 @@ object MC { inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) inline val itemRenderer: ItemRenderer get() = instance.itemRenderer inline val worldRenderer: WorldRenderer get() = instance.worldRenderer + inline val gameRenderer: GameRenderer get() = instance.gameRenderer inline val networkHandler get() = player?.networkHandler inline val instance get() = MinecraftClient.getInstance() inline val keyboard get() = instance.keyboard @@ -92,12 +97,14 @@ object MC { inline val inGameHud: InGameHud get() = instance.inGameHud inline val font get() = instance.textRenderer inline val soundManager get() = instance.soundManager - inline val player: ClientPlayerEntity? get() = instance.player + inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player } inline val camera: Entity? get() = instance.cameraEntity + inline val stackInHand: ItemStack? get() = player?.inventory?.mainHandStack inline val guiAtlasManager get() = instance.guiAtlasManager - inline val world: ClientWorld? get() = instance.world + inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } + inline val playerName: String? get() = player?.name?.unformattedString inline var screen: Screen? - get() = 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<*> @@ -105,7 +112,7 @@ object MC { inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() } inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries - val defaultItems: RegistryWrapper.Impl<Item> = defaultRegistries.getOrThrow(RegistryKeys.ITEM) + val defaultItems: RegistryWrapper.Impl<Item> by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) } var lastWorld: World? = null get() { field = world ?: field diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt index 2e52092..362a4d9 100644 --- a/src/main/kotlin/util/MoulConfigUtils.kt +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -7,6 +7,7 @@ import io.github.notenoughupdates.moulconfig.gui.GuiComponent import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper import io.github.notenoughupdates.moulconfig.gui.GuiContext import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent import io.github.notenoughupdates.moulconfig.gui.MouseEvent import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext @@ -24,6 +25,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.util.InputUtil import moe.nea.firmament.gui.BarComponent import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.gui.FirmHoverComponent @@ -247,6 +249,28 @@ object MoulConfigUtils { } } + fun typeMCComponentInPlace( + component: GuiComponent, + x: Int, + y: Int, + w: Int, + h: Int, + keyboardEvent: KeyboardEvent + ): Boolean { + val immContext = createInPlaceFullContext(null, IMinecraft.instance.mouseX, IMinecraft.instance.mouseY) + if (component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h))) + return true + if (component.context.getFocusedElement() != null) { + if (keyboardEvent is KeyboardEvent.KeyPressed + && keyboardEvent.pressed && keyboardEvent.keycode == InputUtil.GLFW_KEY_ESCAPE + ) { + component.context.setFocusedElement(null) + } + return true + } + return false + } + fun clickMCComponentInPlace( component: GuiComponent, x: Int, diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt index 051d070..b2f9449 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,75 @@ 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 + get() { + // TODO: allow unfiltered access to this somehow + if (!isOnSkyblock) return null + return field + } - 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 + val oldProfileId = profileId + locraw = Locraw(it.serverName, + it.serverType.getOrNull()?.name?.uppercase(), + it.mode.getOrNull(), + it.map.getOrNull()) + SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw)) + if(oldProfileId != profileId) { + ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfileId, profileId)) + } + 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/ScoreboardUtil.kt b/src/main/kotlin/util/ScoreboardUtil.kt index 4311971..0970892 100644 --- a/src/main/kotlin/util/ScoreboardUtil.kt +++ b/src/main/kotlin/util/ScoreboardUtil.kt @@ -1,8 +1,6 @@ - - package moe.nea.firmament.util -import java.util.* +import java.util.Optional import net.minecraft.client.gui.hud.InGameHud import net.minecraft.scoreboard.ScoreboardDisplaySlot import net.minecraft.scoreboard.Team @@ -10,36 +8,48 @@ import net.minecraft.text.StringVisitable import net.minecraft.text.Style import net.minecraft.text.Text import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent -fun getScoreboardLines(): List<Text> { - val scoreboard = MC.player?.scoreboard ?: return listOf() - val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf() - return scoreboard.getScoreboardEntries(activeObjective) - .filter { !it.hidden() } - .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR) - .take(15).map { - val team = scoreboard.getScoreHolderTeam(it.owner) - val text = it.name() - Team.decorateName(team, text) - } -} +object ScoreboardUtil { + var scoreboardLines: List<Text> = listOf() + var simplifiedScoreboardLines: List<String> = listOf() + @Subscribe + fun onTick(event: TickEvent) { + scoreboardLines = getScoreboardLinesUncached() + simplifiedScoreboardLines = scoreboardLines.map { it.unformattedString } + } + + private fun getScoreboardLinesUncached(): List<Text> { + val scoreboard = MC.player?.scoreboard ?: return listOf() + val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf() + return scoreboard.getScoreboardEntries(activeObjective) + .filter { !it.hidden() } + .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR) + .take(15).map { + val team = scoreboard.getScoreHolderTeam(it.owner) + val text = it.name() + Team.decorateName(team, text) + } + } +} fun Text.formattedString(): String { - val sb = StringBuilder() - visit(StringVisitable.StyledVisitor<Unit> { style, string -> - val c = Formatting.byName(style.color?.name) - if (c != null) { - sb.append("§${c.code}") - } - if (style.isUnderlined) { - sb.append("§n") - } - if (style.isBold) { - sb.append("§l") - } - sb.append(string) - Optional.empty() - }, Style.EMPTY) - return sb.toString().replace("§[^a-f0-9]".toRegex(), "") + val sb = StringBuilder() + visit(StringVisitable.StyledVisitor<Unit> { style, string -> + val c = Formatting.byName(style.color?.name) + if (c != null) { + sb.append("§${c.code}") + } + if (style.isUnderlined) { + sb.append("§n") + } + if (style.isBold) { + sb.append("§l") + } + sb.append(string) + Optional.empty() + }, Style.EMPTY) + return sb.toString().replace("§[^a-f0-9]".toRegex(), "") } diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt index c42a55c..a86543c 100644 --- a/src/main/kotlin/util/SkyBlockIsland.kt +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -35,6 +35,8 @@ private constructor( val PRIVATE_ISLAND = forMode("dynamic") val RIFT = forMode("rift") val MINESHAFT = forMode("mineshaft") + val GARDEN = forMode("garden") + val DUNGEON = forMode("dungeon") } val userFriendlyName diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index 1c1aa77..a99afda 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()) @@ -116,6 +119,12 @@ val ItemStack.extraAttributes: NbtCompound return customData.nbt } +fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) { + val baseNbt = get(DataComponentTypes.CUSTOM_DATA)?.copyNbt() ?: NbtCompound() + block(baseNbt) + set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(baseNbt)) +} + val ItemStack.skyblockUUIDString: String? get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() } @@ -125,10 +134,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/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt index f080d5a..68e161a 100644 --- a/src/main/kotlin/util/StringUtil.kt +++ b/src/main/kotlin/util/StringUtil.kt @@ -10,4 +10,13 @@ object StringUtil { } fun Iterable<String>.unwords() = joinToString(" ") + fun nextLexicographicStringOfSameLength(string: String): String { + val next = StringBuilder(string) + while (next.lastOrNull() == Character.MAX_VALUE) next.setLength(next.length - 1) + if (next.isEmpty()) return "" // There is no upper bound. Fall back to the empty string + val lastIdx = next.indices.last + next[lastIdx] = (next[lastIdx] + 1) + return next.toString() + } + } diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt index 68a406f..45e3dde 100644 --- a/src/main/kotlin/util/TestUtil.kt +++ b/src/main/kotlin/util/TestUtil.kt @@ -1,5 +1,9 @@ package moe.nea.firmament.util object TestUtil { - val isInTest = Thread.currentThread().stackTrace.any { it.className.startsWith("org.junit.") } + inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block() + val isInTest = + Thread.currentThread().stackTrace.any { + it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.") + } } diff --git a/src/main/kotlin/util/customgui/CustomGui.kt b/src/main/kotlin/util/customgui/CustomGui.kt index 5224448..35c60ac 100644 --- a/src/main/kotlin/util/customgui/CustomGui.kt +++ b/src/main/kotlin/util/customgui/CustomGui.kt @@ -76,4 +76,16 @@ abstract class CustomGui { open fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { return false } + + open fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return false + } + + open fun charTyped(chr: Char, modifiers: Int): Boolean { + return false + } + + open fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return false + } } diff --git a/src/main/kotlin/util/json/FirmCodecs.kt b/src/main/kotlin/util/json/FirmCodecs.kt new file mode 100644 index 0000000..c0863bc --- /dev/null +++ b/src/main/kotlin/util/json/FirmCodecs.kt @@ -0,0 +1,20 @@ +package moe.nea.firmament.util.json + +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.Lifecycle +import com.mojang.util.UndashedUuid +import net.minecraft.util.Uuids + +object FirmCodecs { + @JvmField + val UUID_LENIENT_PREFER_INT_STREAM = Codec.withAlternative(Uuids.INT_STREAM_CODEC, Codec.STRING.comapFlatMap( + { + try { + DataResult.success(UndashedUuid.fromStringLenient(it), Lifecycle.stable()) + } catch (ex: IllegalArgumentException) { + DataResult.error { "Invalid UUID $it: ${ex.message}" } + } + }, + UndashedUuid::toString)) +} diff --git a/src/main/kotlin/util/json/KJsonOps.kt b/src/main/kotlin/util/json/KJsonOps.kt new file mode 100644 index 0000000..404ea5e --- /dev/null +++ b/src/main/kotlin/util/json/KJsonOps.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.util.json + +import com.google.gson.internal.LazilyParsedNumber +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import java.util.stream.Stream +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.booleanOrNull +import kotlin.streams.asSequence + +class KJsonOps : DynamicOps<JsonElement> { + companion object { + val INSTANCE = KJsonOps() + } + + override fun empty(): JsonElement { + return JsonNull + } + + override fun createNumeric(num: Number): JsonElement { + return JsonPrimitive(num) + } + + override fun createString(str: String): JsonElement { + return JsonPrimitive(str) + } + + override fun remove(input: JsonElement, key: String): JsonElement { + if (input is JsonObject) { + return JsonObject(input.filter { it.key != key }) + } else { + return input + } + } + + override fun createList(stream: Stream<JsonElement>): JsonElement { + return JsonArray(stream.toList()) + } + + override fun getStream(input: JsonElement): DataResult<Stream<JsonElement>> { + if (input is JsonArray) + return DataResult.success(input.stream()) + return DataResult.error { "Not a json array: $input" } + } + + override fun createMap(map: Stream<Pair<JsonElement, JsonElement>>): JsonElement { + return JsonObject(map.asSequence() + .map { ((it.first as JsonPrimitive).content) to it.second } + .toMap()) + } + + override fun getMapValues(input: JsonElement): DataResult<Stream<Pair<JsonElement, JsonElement>>> { + if (input is JsonObject) { + return DataResult.success(input.entries.stream().map { Pair.of(createString(it.key), it.value) }) + } + return DataResult.error { "Not a JSON object: $input" } + } + + override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult<JsonElement> { + if (key !is JsonPrimitive || key.isString) { + return DataResult.error { "key is not a string: $key" } + } + val jKey = key.content + val extra = mapOf(jKey to value) + if (map == empty()) { + return DataResult.success(JsonObject(extra)) + } + if (map is JsonObject) { + return DataResult.success(JsonObject(map + extra)) + } + return DataResult.error { "mergeToMap called with not a map: $map" } + } + + override fun mergeToList(list: JsonElement, value: JsonElement): DataResult<JsonElement> { + if (list == empty()) + return DataResult.success(JsonArray(listOf(value))) + if (list is JsonArray) { + return DataResult.success(JsonArray(list + value)) + } + return DataResult.error { "mergeToList called with not a list: $list" } + } + + override fun getStringValue(input: JsonElement): DataResult<String> { + if (input is JsonPrimitive && input.isString) { + return DataResult.success(input.content) + } + return DataResult.error { "Not a string: $input" } + } + + override fun getNumberValue(input: JsonElement): DataResult<Number> { + if (input is JsonPrimitive && !input.isString && input.booleanOrNull == null) + return DataResult.success(LazilyParsedNumber(input.content)) + return DataResult.error { "not a number: $input" } + } + + override fun createBoolean(value: Boolean): JsonElement { + return JsonPrimitive(value) + } + + override fun getBooleanValue(input: JsonElement): DataResult<Boolean> { + if (input is JsonPrimitive) { + if (input.booleanOrNull != null) + return DataResult.success(input.boolean) + return super.getBooleanValue(input) + } + return DataResult.error { "Not a boolean: $input" } + } + + override fun <U : Any?> convertTo(output: DynamicOps<U>, input: JsonElement): U { + if (input is JsonObject) + return output.createMap( + input.entries.stream().map { Pair.of(output.createString(it.key), convertTo(output, it.value)) }) + if (input is JsonArray) + return output.createList(input.stream().map { convertTo(output, it) }) + if (input is JsonNull) + return output.empty() + if (input is JsonPrimitive) { + if (input.isString) + return output.createString(input.content) + if (input.booleanOrNull != null) + return output.createBoolean(input.boolean) + } + error("Unknown json value: $input") + } +} diff --git a/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt new file mode 100644 index 0000000..e546fd3 --- /dev/null +++ b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.util.Identifier + +interface IntrospectableItemModelManager { + fun hasModel_firmament(identifier: Identifier): Boolean +} 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/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt new file mode 100644 index 0000000..a02eccc --- /dev/null +++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt @@ -0,0 +1,44 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.platform.GlConst +import com.mojang.blaze3d.systems.RenderSystem +import me.shedaniel.math.Color +import net.minecraft.client.render.OverlayTexture +import net.minecraft.util.math.ColorHelper +import moe.nea.firmament.util.ErrorUtil + +class TintedOverlayTexture : OverlayTexture() { + companion object { + val size = 16 + } + + private var lastColor: Color? = null + fun setColor(color: Color): TintedOverlayTexture { + val image = ErrorUtil.notNullOr(texture.image, "Disposed TintedOverlayTexture written to") { return this } + if (color == lastColor) return this + lastColor = color + + for (i in 0..<size) { + for (j in 0..<size) { + if (i < 8) { + image.setColorArgb(j, i, 0xB2FF0000.toInt()) + } else { + val k = ((1F - j / 15F * 0.75F) * 255F).toInt() + image.setColorArgb(j, i, ColorHelper.withAlpha(k, color.color)) + } + } + } + + RenderSystem.activeTexture(GlConst.GL_TEXTURE1) + texture.bindTexture() + texture.setFilter(false, false) + texture.setClamp(true) + image.upload(0, + 0, 0, + 0, 0, + image.width, image.height, + false) + RenderSystem.activeTexture(GlConst.GL_TEXTURE0) + return this + } +} diff --git a/src/main/kotlin/util/render/TranslatedScissors.kt b/src/main/kotlin/util/render/TranslatedScissors.kt index c1e6544..8f8bdcf 100644 --- a/src/main/kotlin/util/render/TranslatedScissors.kt +++ b/src/main/kotlin/util/render/TranslatedScissors.kt @@ -1,11 +1,15 @@ package moe.nea.firmament.util.render +import org.joml.Matrix4f import org.joml.Vector4f import net.minecraft.client.gui.DrawContext fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) { - val pMat = matrices.peek().positionMatrix + enableScissor(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt()) +} +fun DrawContext.enableScissorWithoutTranslation(x1: Float, y1: Float, x2: Float, y2: Float) { + val pMat = matrices.peek().positionMatrix.invert(Matrix4f()) val target = Vector4f() target.set(x1, y1, 0f, 1f) diff --git a/src/main/kotlin/util/skyblock/AbilityUtils.kt b/src/main/kotlin/util/skyblock/AbilityUtils.kt index 0f0adbe..0d7d2b7 100644 --- a/src/main/kotlin/util/skyblock/AbilityUtils.kt +++ b/src/main/kotlin/util/skyblock/AbilityUtils.kt @@ -131,6 +131,7 @@ object AbilityUtils { return abilities } + // TODO: memoize fun getAbilities(itemStack: ItemStack): List<ItemAbility> { return getAbilities(itemStack.loreAccordingToNbt) } diff --git a/src/main/kotlin/util/skyblock/DungeonUtil.kt b/src/main/kotlin/util/skyblock/DungeonUtil.kt new file mode 100644 index 0000000..488b158 --- /dev/null +++ b/src/main/kotlin/util/skyblock/DungeonUtil.kt @@ -0,0 +1,33 @@ +package moe.nea.firmament.util.skyblock + +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.ScoreboardUtil +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.TIME_PATTERN + +object DungeonUtil { + val isInDungeonIsland get() = SBData.skyblockLocation == SkyBlockIsland.DUNGEON + private val timeElapsedRegex = "Time Elapsed: $TIME_PATTERN".toRegex() + val isInActiveDungeon get() = isInDungeonIsland && ScoreboardUtil.simplifiedScoreboardLines.any { it.matches( + timeElapsedRegex) } + +/*Title: + +§f§lSKYBLOCK§B§L CO-OP + +' Late Spring 7th' +' §75:20am' +' §7⏣ §cThe Catacombs §7(M3)' +' §7♲ §7Ironman' +' ' +'Keys: §c■ §c✗ §8■ §a1x' +'Time Elapsed: §a46s' +'Cleared: §660% §8(105)' +' ' +'§e[B] §b151_Dragon §e2,062§c❤' +'§e[A] §6Lennart0312 §a17,165§c' +'§e[T] §b187i §a14,581§c❤' +'§e[H] §bFlameeke §a8,998§c❤' +' ' +'§ewww.hypixel.net'*/ +} diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt index 7d53c0d..7a776b5 100644 --- a/src/main/kotlin/util/skyblock/ItemType.kt +++ b/src/main/kotlin/util/skyblock/ItemType.kt @@ -1,21 +1,75 @@ package moe.nea.firmament.util.skyblock -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.petData -class ItemType(val name: String) { +@JvmInline +value class ItemType private constructor(val name: String) { companion object { - private val generated = object : ReadOnlyProperty<Any?, ItemType> { - override fun getValue(thisRef: Any?, property: KProperty<*>): ItemType { - return ItemType.ofName(property.name) - } - } - fun ofName(name: String): ItemType { return ItemType(name) } - val SWORD by generated + 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 + val typeText = + itemStack.loreAccordingToNbt.lastOrNull() + ?.siblings?.find { + !it.style.isObfuscated && !it.directLiteralStringContent.isNullOrBlank() + }?.directLiteralStringContent + if (typeText != null) { + val type = typeText.substringAfter(' ', missingDelimiterValue = "").trim() + if (type.isEmpty()) return null + return ofName(type) + } + 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). + */ + val PET = ofName("PET") + } + + val dungeonVariant get() = ofName("DUNGEON $name") + + val isDungeon get() = name.startsWith("DUNGEON ") + + override fun toString(): String { + return name } } 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..ca2b17b 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -7,4 +7,10 @@ object SkyBlockItems { val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND") val DIAMOND = SkyblockId("DIAMOND") val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE") + val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL") + val SLICE_OF_BLUEBERRY_CAKE = SkyblockId("SLICE_OF_BLUEBERRY_CAKE") + val SLICE_OF_CHEESECAKE = SkyblockId("SLICE_OF_CHEESECAKE") + val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE") + val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE") + val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE") } diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 5d95d7a..c295ae0 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -1,72 +1,18 @@ package moe.nea.firmament.util +import java.util.Optional import net.minecraft.text.ClickEvent import net.minecraft.text.MutableText +import net.minecraft.text.OrderedText import net.minecraft.text.PlainTextContent +import net.minecraft.text.StringVisitable +import net.minecraft.text.Style import net.minecraft.text.Text import net.minecraft.text.TextColor import net.minecraft.text.TranslatableTextContent import net.minecraft.util.Formatting -import moe.nea.firmament.Firmament - - -class TextMatcher(text: Text) { - data class State( - var iterator: MutableList<Text>, - var currentText: Text?, - var offset: Int, - var textContent: String, - ) - - var state = State( - mutableListOf(text), - null, - 0, - "" - ) - - fun pollChunk(): Boolean { - val firstOrNull = state.iterator.removeFirstOrNull() ?: return false - state.offset = 0 - state.currentText = firstOrNull - state.textContent = when (val content = firstOrNull.content) { - is PlainTextContent.Literal -> content.string - else -> { - Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.") - return false - } - } - state.iterator.addAll(0, firstOrNull.siblings) - return true - } - - fun pollChunks(): Boolean { - while (state.offset !in state.textContent.indices) { - if (!pollChunk()) { - return false - } - } - return true - } - - fun pollChar(): Char? { - if (!pollChunks()) return null - return state.textContent[state.offset++] - } - fun expectString(string: String): Boolean { - var found = "" - while (found.length < string.length) { - if (!pollChunks()) return false - val takeable = state.textContent.drop(state.offset).take(string.length - found.length) - state.offset += takeable.length - found += takeable - } - return found == string - } -} - val formattingChars = "kmolnrKMOLNR".toSet() fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { var nextParagraph = indexOf('§') @@ -89,6 +35,47 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { return stringBuffer.toString() } +fun OrderedText.reconstitute(): MutableText { + val base = Text.literal("") + base.setStyle(Style.EMPTY.withItalic(false)) + var lastColorCode = Style.EMPTY + val text = StringBuilder() + this.accept { index, style, codePoint -> + if (style != lastColorCode) { + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(codePoint.toChar()) + true + } + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + return base + +} +fun StringVisitable.reconstitute(): MutableText { + val base = Text.literal("") + base.setStyle(Style.EMPTY.withItalic(false)) + var lastColorCode = Style.EMPTY + val text = StringBuilder() + this.visit({ style, string -> + if (style != lastColorCode) { + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(string) + Optional.empty<Unit>() + }, Style.EMPTY) + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + return base + +} + val Text.unformattedString: String get() = string.removeColorCodes() // TODO: maybe shortcircuit this with .visit @@ -133,7 +120,9 @@ 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.darkGrey() = withColor(Formatting.DARK_GRAY) fun MutableText.red() = withColor(Formatting.RED) fun MutableText.white() = withColor(Formatting.WHITE) fun MutableText.bold(): MutableText = styled { it.withBold(true) } @@ -142,11 +131,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/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png Binary files differnew file mode 100644 index 0000000..1b87c55 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png Binary files differnew file mode 100644 index 0000000..dcd9aa4 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index c542fc8..1805721 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -3,13 +3,10 @@ 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/client/render/item/HeldItemRenderer itemRenderer Lnet/minecraft/client/render/item/ItemRenderer; -accessible field net/minecraft/client/render/item/ItemModels missingModelSupplier Ljava/util/function/Supplier; -mutable field net/minecraft/client/render/model/json/ModelOverrideList conditionTypes [Lnet/minecraft/util/Identifier; - -accessible class net/minecraft/client/render/model/json/ModelOverride$Deserializer -accessible class net/minecraft/client/render/model/json/ModelOverrideList$BakedOverride accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData; accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V accessible field net/minecraft/entity/passive/AbstractHorseEntity items Lnet/minecraft/inventory/SimpleInventory; @@ -23,14 +20,8 @@ mutable field net/minecraft/screen/slot/Slot y I accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData; accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage; +accessible field net/minecraft/client/render/OverlayTexture texture Lnet/minecraft/client/texture/NativeImageBackedTexture; -# Fix package-private access methods -accessible method net/minecraft/registry/entry/RegistryEntry$Reference setRegistryKey (Lnet/minecraft/registry/RegistryKey;)V -accessible method net/minecraft/entity/LivingEntity getHitbox ()Lnet/minecraft/util/math/Box; -accessible method net/minecraft/registry/entry/RegistryEntryList$Named <init> (Lnet/minecraft/registry/entry/RegistryEntryOwner;Lnet/minecraft/registry/tag/TagKey;)V -accessible method net/minecraft/registry/entry/RegistryEntry$Reference setValue (Ljava/lang/Object;)V -accessible field net/minecraft/client/render/model/WrapperBakedModel wrapped Lnet/minecraft/client/render/model/BakedModel; -accessible method net/minecraft/entity/passive/TameableEntity isInSameTeam (Lnet/minecraft/entity/Entity;)Z -accessible method net/minecraft/entity/Entity isInSameTeam (Lnet/minecraft/entity/Entity;)Z -accessible method net/minecraft/registry/entry/RegistryEntry$Reference setTags (Ljava/util/Collection;)V -accessible method net/minecraft/registry/entry/RegistryEntryList$Named setEntries (Ljava/util/List;)V +accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/util/Optional; +accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters; +accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase; diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt index ee2a322..107b565 100644 --- a/src/test/kotlin/testutil/ItemResources.kt +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -2,11 +2,14 @@ package moe.nea.firmament.test.testutil import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtOps import net.minecraft.nbt.StringNbtReader +import net.minecraft.registry.RegistryOps import net.minecraft.text.Text import net.minecraft.text.TextCodecs import moe.nea.firmament.test.FirmTestBootstrap +import moe.nea.firmament.util.MC object ItemResources { init { @@ -23,15 +26,16 @@ object ItemResources { fun loadSNbt(path: String): NbtCompound { return StringNbtReader.parse(loadString(path)) } + fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE) fun loadText(name: String): Text { - return TextCodecs.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/chat/$name.snbt")) + return TextCodecs.CODEC.parse(getNbtOps(), loadSNbt("testdata/chat/$name.snbt")) .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } } fun loadItem(name: String): ItemStack { // TODO: make the load work with enchantments - return ItemStack.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/items/$name.snbt")) + return ItemStack.CODEC.parse(getNbtOps(), loadSNbt("testdata/items/$name.snbt")) .getOrThrow { IllegalStateException("Could not load test item '$name': $it") } } } diff --git a/src/test/kotlin/testutil/KotestPlugin.kt b/src/test/kotlin/testutil/KotestPlugin.kt new file mode 100644 index 0000000..6db50fb --- /dev/null +++ b/src/test/kotlin/testutil/KotestPlugin.kt @@ -0,0 +1,16 @@ +package moe.nea.firmament.test.testutil + +import io.kotest.core.config.AbstractProjectConfig +import io.kotest.core.extensions.Extension +import moe.nea.firmament.test.FirmTestBootstrap + +class KotestPlugin : AbstractProjectConfig() { + override fun extensions(): List<Extension> { + return listOf() + } + + override suspend fun beforeProject() { + FirmTestBootstrap.bootstrapMinecraft() + super.beforeProject() + } +} diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt index d9de36a..949749e 100644 --- a/src/test/kotlin/util/ColorCodeTest.kt +++ b/src/test/kotlin/util/ColorCodeTest.kt @@ -1,25 +1,23 @@ package moe.nea.firmament.test.util +import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import net.minecraft.Bootstrap -import net.minecraft.SharedConstants import moe.nea.firmament.util.removeColorCodes -class ColorCodeTest { +class ColorCodeTest : AnnotationSpec() { @Test fun testWhatever() { - Assertions.assertEquals("", "".removeColorCodes().toString()) - Assertions.assertEquals("", "§".removeColorCodes().toString()) - Assertions.assertEquals("", "§a".removeColorCodes().toString()) - Assertions.assertEquals("ab", "a§ab".removeColorCodes().toString()) - Assertions.assertEquals("ab", "a§ab§§".removeColorCodes().toString()) - Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes().toString()) - Assertions.assertEquals("bc", "§ab§§c".removeColorCodes().toString()) - Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true).toString()) - Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true).toString()) - Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true).toString()) + Assertions.assertEquals("", "".removeColorCodes()) + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("", "§a".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab§§".removeColorCodes()) + Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes()) + Assertions.assertEquals("bc", "§ab§§c".removeColorCodes()) + Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true)) + Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true)) + Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true)) } @Test diff --git a/src/test/kotlin/util/TextUtilText.kt b/src/test/kotlin/util/TextUtilText.kt index 7091f4e..46ed3b4 100644 --- a/src/test/kotlin/util/TextUtilText.kt +++ b/src/test/kotlin/util/TextUtilText.kt @@ -1,15 +1,16 @@ package moe.nea.firmament.test.util +import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.getLegacyFormatString -class TextUtilText { +class TextUtilText : AnnotationSpec() { @Test fun testThing() { // TODO: add more tests that are directly validated with 1.8.9 code val text = ItemResources.loadText("all-chat") - Assertions.assertEquals("§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r", text.getLegacyFormatString()) + Assertions.assertEquals("§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r", + text.getLegacyFormatString()) } } diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt index abe739d..206a357 100644 --- a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.test.util.skyblock +import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import net.minecraft.text.Text @@ -9,7 +9,7 @@ import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.AbilityUtils import moe.nea.firmament.util.unformattedString -class AbilityUtilsTest { +class AbilityUtilsTest : AnnotationSpec() { fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map { it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) }) diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt new file mode 100644 index 0000000..cca3d13 --- /dev/null +++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.test.util.skyblock + +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.shouldBe +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.ItemType + +class ItemTypeTest + : ShouldSpec( + { + context("ItemType.fromItemstack") { + listOf( + "pets/lion-item" to ItemType.PET, + "pets/rabbit-selected" to ItemType.PET, + "pets/mithril-golem-not-selected" to ItemType.PET, + "aspect-of-the-void" to ItemType.SWORD, + "titanium-drill" to ItemType.DRILL, + "diamond-pickaxe" to ItemType.PICKAXE, + "gemstone-gauntlet" to ItemType.GAUNTLET, + ).forEach { (name, typ) -> + should("return $typ for $name") { + ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ + } + } + } + }) diff --git a/src/test/kotlin/util/skyblock/SackUtilTest.kt b/src/test/kotlin/util/skyblock/SackUtilTest.kt index e0e3e63..f93cd2b 100644 --- a/src/test/kotlin/util/skyblock/SackUtilTest.kt +++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt @@ -1,12 +1,12 @@ package moe.nea.firmament.test.util.skyblock +import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.SackUtil import moe.nea.firmament.util.skyblock.SkyBlockItems -class SackUtilTest { +class SackUtilTest : AnnotationSpec() { @Test fun testOneRottenFlesh() { Assertions.assertEquals( diff --git a/src/test/resources/testdata/items/books/feather_falling.snbt b/src/test/resources/testdata/items/books/feather_falling.snbt new file mode 100644 index 0000000..1de4632 --- /dev/null +++ b/src/test/resources/testdata/items/books/feather_falling.snbt @@ -0,0 +1,36 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + feather_falling: 6 + }, + id: "ENCHANTED_BOOK", + timestamp: 1737123521091L, + uuid: "b8128489-9ed0-4a1a-94c0-d3279ffe45ac" + }, + "minecraft:custom_name": '{"extra":[{"color":"blue","text":"Enchanted Book"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"blue","text":"Feather Falling VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Increases how high you can fall"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"before taking fall damage by "},{"color":"green","text":"6"},{"color":"gray","text":" and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"reduces fall damage by "},{"color":"green","text":"30%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Applicable on: "},{"color":"blue","text":"Boots"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Apply Cost: "},{"color":"dark_aqua","text":"60 Exp Levels"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Use this on an item in an Anvil to"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"apply it!"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"blue","text":"RARE"}],"italic":false,"text":""}' + ] + }, + count: 1, + id: "minecraft:enchanted_book" +} diff --git a/src/test/resources/testdata/items/gemstone-gauntlet.snbt b/src/test/resources/testdata/items/gemstone-gauntlet.snbt new file mode 100644 index 0000000..92ce739 --- /dev/null +++ b/src/test/resources/testdata/items/gemstone-gauntlet.snbt @@ -0,0 +1,103 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + compact_blocks: 287507, + donated_museum: 1b, + enchantments: { + compact: 8, + critical: 5, + efficiency: 5, + experience: 3, + first_strike: 4, + fortune: 4, + giant_killer: 5, + pristine: 3, + scavenger: 3, + sharpness: 5, + telekinesis: 1 + }, + gems: { + AMBER_0: "FINE", + AMETHYST_0: "FINE", + JADE_0: "FINE", + SAPPHIRE_0: "FINE", + TOPAZ_0: "FINE" + }, + id: "GEMSTONE_GAUNTLET", + modifier: "auspicious", + originTag: "QUICK_CRAFTING", + timestamp: 1642718160000L, + uuid: "af56dd7b-c4b1-4e26-8d09-1854680a93c3" + }, + "minecraft:custom_name": '{"extra":[{"color":"gold","text":"Auspicious Gemstone Gauntlet"}],"italic":false,"text":""}', + "minecraft:enchantments": { + levels: { + "minecraft:efficiency": 5 + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 8"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+300"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+50"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+50%"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+10 "},{"color":"light_purple","text":"(+10)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+11 "},{"color":"light_purple","text":"(+11)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+886 "},{"color":"blue","text":"(+50) "},{"color":"light_purple","text":"(+36)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+2.7 "},{"color":"light_purple","text":"(+1.2)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+81 "},{"color":"blue","text":"(+16) "},{"color":"light_purple","text":"(+20)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+8"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"blue","text":"["},{"color":"green","text":"☘"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"gold","text":"⸕"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"yellow","text":"✧"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"aqua","text":"✎"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"dark_purple","text":"❈"},{"color":"blue","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Compact VIII"},{"color":"blue","text":", "},{"color":"blue","text":"Critical V"},{"color":"blue","text":", "},{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Fortune IV"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Giant Killer V"},{"color":"blue","text":", "},{"color":"blue","text":"Prismatic III"},{"color":"blue","text":", "},{"color":"blue","text":"Scavenger III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Sharpness V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Ability: Reduced To Atoms"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mobs killed on "},{"color":"aqua","text":"Mining Islands "},{"color":"gray","text":"drop the same"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"light_purple","text":"Gemstones "},{"color":"gray","text":"as your filled Gemstone Slots."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"(2s cooldown)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Ability: Kinetic"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Killing mobs on "},{"color":"aqua","text":"Mining Islands "},{"color":"gray","text":"reduces your"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Forge Timers "},{"color":"gray","text":"by "},{"color":"green","text":"0s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"(+0.5s per Perfect Gemstone)"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.8% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"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":"gold","text":"LEGENDARY GAUNTLET"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -861744046, + -959235637, + -1231724855, + 724395817 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYxODUyMTY2MzY1NCwKICAicHJvZmlsZUlkIiA6ICIxZDUyMzNkMzg4NjI0YmFmYjAwZTMxNTBhN2FhM2E4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICIwMDAwMDAwMDAwMDAwMDBKIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdiZjAxYzE5OGY2ZTE2OTY1ZTIzMDIzNWNkMjJhNWE5ZjRhNDBlNDA5NDEyMzQ0Nzg5NDhmZjlhNTZlNTE3NzUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} 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" + } diff --git a/src/test/resources/testdata/items/pets/lion-item.snbt b/src/test/resources/testdata/items/pets/lion-item.snbt new file mode 100644 index 0000000..6e92685 --- /dev/null +++ b/src/test/resources/testdata/items/pets/lion-item.snbt @@ -0,0 +1,62 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"LION","active":false,"exp":0.0,"tier":"LEGENDARY","hideInfo":false,"candyUsed":0,"uuid":"c7f57149-458e-4fde-a9bc-fcc14932310a","uniqueId":"d668f085-26a6-48fe-b75b-c7b8227b5ac8","hideRightClick":false,"noMove":false}', + timestamp: 1732719293542L, + uuid: "c7f57149-458e-4fde-a9bc-fcc14932310a" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 1] "},{"color":"gold","text":"Lion"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Foraging Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Speed: "},{"color":"green","text":"+0.25"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+0.5"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+0.05"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Primal Force"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Adds "},{"color":"red","text":"+0.2 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"red","text":"+0.2 "},{"color":"red","text":"❁"}],"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Strength "},{"color":"gray","text":"to your weapons."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"First Pounce"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"First Strike"},{"color":"gray","text":", Triple-Strike"},{"color":"gray","text":", and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"bold":true,"color":"light_purple","text":"Combo "},{"color":"gray","text":"are "},{"color":"green","text":"1% "},{"color":"gray","text":"more effective."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"King of the Jungle"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Deal "},{"color":"red","text":"+1.5% "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"against mobs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"that have attacked you."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Progress to Level 2: "},{"color":"yellow","text":"0%"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"white","strikethrough":true,"text":" "},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"color":"yellow","text":"0"},{"color":"gold","text":"/"},{"color":"yellow","text":"660"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Right-click to add this pet to your"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"pet menu!"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"gold","text":"LEGENDARY"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + 100496274, + -783272221, + -1215808471, + -1437268588 + ], + properties: [ + { + name: "textures", + signature: "", + value: "eyJ0aW1lc3RhbXAiOjE1MDMzMTY0Njg1ODAsInByb2ZpbGVJZCI6ImUzYjQ0NWM4NDdmNTQ4ZmI4YzhmYTNmMWY3ZWZiYThlIiwicHJvZmlsZU5hbWUiOiJNaW5pRGlnZ2VyVGVzdCIsInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzhmZjQ3M2JkNTJiNGRiMmMwNmYxYWM4N2ZlMTM2N2JjZTc1NzRmYWMzMzBmZmFjNzk1NjIyOWY4MmVmYmExIn19fQ==" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt new file mode 100644 index 0000000..c0ef585 --- /dev/null +++ b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt @@ -0,0 +1,52 @@ +{ + components: { + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"MITHRIL_GOLEM","active":false,"exp":9232689.767527012,"tier":"LEGENDARY","hideInfo":false,"candyUsed":0,"uuid":"2ebbdaff-c9b6-4fe1-8b87-44905eb5867d","uniqueId":"26be73f1-2955-4003-9e6a-72848cea8e08","hideRightClick":false,"noMove":false}', + uuid: "2ebbdaff-c9b6-4fe1-8b87-44905eb5867d" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 86] "},{"color":"gold","text":"Mithril Golem"}],"italic":false,"text":""}', + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Mining Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"True Defense: "},{"color":"green","text":"+43"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+21.5"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Mithril Affinity"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"gold","text":"+172⸕ Mining Speed "},{"color":"gray","text":"when"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"mining "},{"color":"dark_green","text":"Mithril"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Subterranean Battler"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"green","text":"+17.2% "},{"color":"gray","text":"of most combat stats"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"while on "},{"color":"aqua","text":"Mining Islands"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"The Smell Of Powder"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"dark_green","text":"+17.2% ᠅ Mithril Powder "},{"color":"gray","text":"from"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"all sources."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Progress to Level 87: "},{"color":"yellow","text":"89%"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_green","strikethrough":true,"text":" "},{"bold":true,"color":"white","strikethrough":true,"text":" "},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"color":"yellow","text":"593,259.8"},{"color":"gold","text":"/"},{"color":"yellow","text":"666.7k"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Left-click to summon!"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Shift Left-click to toggle as favorite!"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"Right-click to convert to an item!"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + 972784821, + 1926443553, + -1284265423, + 1586851459 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYxMDY0OTEwODI4NCwKICAicHJvZmlsZUlkIiA6ICI2ZmU4OTUxZDVhY2M0NDc3OWI2ZmYxMmU3YzFlOTQ2MyIsCiAgInByb2ZpbGVOYW1lIiA6ICJlcGhlbXJhIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2MxYjJkZmU4ZWQ1ZGZmYzViMTY4N2JjMWMyNDljMzlkZTJkOGE2YzNkOTAzMDVjOTVmNmQxYTFhMzMwYTBiMSIKICAgIH0KICB9Cn0=" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/pets/rabbit-selected.snbt b/src/test/resources/testdata/items/pets/rabbit-selected.snbt new file mode 100644 index 0000000..48a6f6f --- /dev/null +++ b/src/test/resources/testdata/items/pets/rabbit-selected.snbt @@ -0,0 +1,60 @@ +{ + components: { + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"RABBIT","active":true,"exp":3.429132435816353E7,"tier":"MYTHIC","hideInfo":false,"heldItem":"YELLOW_BANDANA","candyUsed":0,"uuid":"30a05aae-2ccd-41c0-a686-5bce0df15e2d","uniqueId":"71a8949b-7444-4ead-9464-999fe549e703","hideRightClick":false,"noMove":false}', + uuid: "30a05aae-2ccd-41c0-a686-5bce0df15e2d" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 100] "},{"color":"light_purple","text":"Rabbit"}],"italic":false,"text":""}', + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Farming Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Health: "},{"color":"green","text":"+100"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Speed: "},{"color":"green","text":"+20"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Farming Fortune: "},{"color":"green","text":"+30"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Happy Feet"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Jump potions also give "},{"color":"green","text":"+50 "},{"color":"gray","text":"speed"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Farming Wisdom Boost"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"dark_aqua","text":"+30☯ Farming Wisdom"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Efficient Farming"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Farming minions work "},{"color":"green","text":"30% "},{"color":"gray","text":"faster"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"while on your island"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Chocolate Injections"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"Chocolate Factory"}],"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":""},{"color":"gray","text":"production by "},{"color":"green","text":"+0.05x"},{"color":"gray","text":". Duplicate"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Chocolate Rabbits "},{"color":"gray","text":"that you find"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"grant "},{"color":"gold","text":"+33% Chocolate"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Held Item: "},{"color":"blue","text":"Yellow Bandana"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+30☘ Farming Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"MAX LEVEL"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"▸ 34,291,324 XP"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"red","text":"Click to despawn!"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Shift Left-click to toggle as favorite!"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"Right-click to convert to an item!"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -363097213, + -1822870885, + -1670199288, + -1615169037 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYwNDU4NjI3ODg1NywKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjM0Mzg1NTVlODk5YmQ5YTA1MWE5NWRiZWE0OWViMmVjZmE1MmE2OWRiYmE4OTk4ZjM2NzM4MTllMjc3ZmRmNSIKICAgIH0KICB9Cn0=" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/texturePacks/README.md b/src/texturePacks/README.md new file mode 100644 index 0000000..8932817 --- /dev/null +++ b/src/texturePacks/README.md @@ -0,0 +1,13 @@ +<!-- +SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe> + +SPDX-License-Identifier: CC0-1.0 +--> + +# Technical Notes for the texture pack implementation + +Relevant classes: + +`ItemModelManager` can be used to select an `ItemModel`. This is done from the `ITEM_MODEL` component which is defaulted by the `Item` class. + +The list of available `ItemModel`s (as in `Identifier` -> `ItemModel` maps) is loaded by `BakedModelManager`. To this end, item models in particular are loaded from `ItemAssetsLoader#load`. Those `ItemAssets` are found in `assets/<ns>/items/` directly (not in the model folder) and can be used to select other models, similar to how predicates used to work diff --git a/src/main/kotlin/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt index 2f7f084..dc3b109 100644 --- a/src/main/kotlin/features/texturepack/CustomBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -30,7 +30,6 @@ import net.minecraft.util.math.BlockPos import net.minecraft.util.profiler.Profiler import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.BakeExtraModelsEvent import moe.nea.firmament.events.EarlyResourceReloadEvent import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent @@ -46,9 +45,9 @@ import moe.nea.firmament.util.json.SingletonSerializableList object CustomBlockTextures { @Serializable data class CustomBlockOverride( - val modes: @Serializable(SingletonSerializableList::class) List<String>, - val area: List<Area>? = null, - val replacements: Map<Identifier, Replacement>, + val modes: @Serializable(SingletonSerializableList::class) List<String>, + val area: List<Area>? = null, + val replacements: Map<Identifier, Replacement>, ) @Serializable(with = Replacement.Serializer::class) @@ -135,8 +134,8 @@ object CustomBlockTextures { ) data class BlockReplacement( - val checks: List<Area>?, - val replacement: Replacement, + val checks: List<Area>?, + val replacement: Replacement, ) { val roughCheck by lazy(LazyThreadSafetyMode.NONE) { if (checks == null || checks.size < 3) return@lazy null @@ -238,15 +237,6 @@ object CustomBlockTextures { { prepare(event.resourceManager) }, event.preparationExecutor) } - @Subscribe - fun bakeExtraModels(event: BakeExtraModelsEvent) { - preparationFuture.join().data.values - .flatMap { it.lookup.values } - .flatten() - .mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier } - .forEach { event.addNonItemModel(it, it.id) } - } - private fun prepare(manager: ResourceManager): BakedReplacements { val resources = manager.findResources("overrides/blocks") { it.namespace == "firmskyblock" && it.path.endsWith(".json") diff --git a/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt index 54e9e11..aafc85a 100644 --- a/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt @@ -8,8 +8,12 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.UseSerializers +import net.minecraft.client.render.entity.equipment.EquipmentModel +import net.minecraft.component.type.EquippableComponent +import net.minecraft.entity.EquipmentSlot import net.minecraft.item.ItemStack -import net.minecraft.item.equipment.EquipmentModel +import net.minecraft.item.equipment.EquipmentAssetKeys +import net.minecraft.registry.RegistryKey import net.minecraft.resource.ResourceManager import net.minecraft.resource.SinglePreparationResourceReloader import net.minecraft.util.Identifier @@ -20,6 +24,7 @@ import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.intoOptional import moe.nea.firmament.util.skyBlockId object CustomGlobalArmorOverrides { @@ -33,9 +38,9 @@ object CustomGlobalArmorOverrides { ) { @Transient lateinit var modelIdentifier: Identifier - fun bake() { + fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) - overrides.forEach { it.bake() } + overrides.forEach { it.bake(manager) } } init { @@ -64,22 +69,33 @@ object CustomGlobalArmorOverrides { @Transient lateinit var modelIdentifier: Identifier - fun bake() { + fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) } } - val overrideCache = WeakCache.memoize<ItemStack, Optional<Identifier>>("ArmorOverrides") { stack -> - val id = stack.skyBlockId ?: return@memoize Optional.empty() - val override = overrides[id.neuItem] ?: return@memoize Optional.empty() - for (suboverride in override.overrides) { - if (suboverride.predicate.test(stack)) { - return@memoize Optional.of(suboverride.modelIdentifier) + private fun resolveComponent(slot: EquipmentSlot, model: Identifier): EquippableComponent { + return EquippableComponent( + slot, + null, + Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)), + Optional.empty(), + Optional.empty(), false, false, false + ) + } + + val overrideCache = + WeakCache.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot -> + val id = stack.skyBlockId ?: return@memoize Optional.empty() + val override = overrides[id.neuItem] ?: return@memoize Optional.empty() + for (suboverride in override.overrides) { + if (suboverride.predicate.test(stack)) { + return@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() + } } + return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional() } - return@memoize Optional.of(override.modelIdentifier) - } var overrides: Map<String, ArmorOverride> = mapOf() private var bakedOverrides: MutableMap<Identifier, EquipmentModel> = mutableMapOf() @@ -91,7 +107,7 @@ object CustomGlobalArmorOverrides { return model } else if (layers != null) { val idNumber = sentinelFirmRunning.incrementAndGet() - val identifier = Identifier.of("firmament:sentinel/$idNumber") + val identifier = Identifier.of("firmament:sentinel/armor/$idNumber") val equipmentLayers = layers.map { EquipmentModel.Layer( it.identifier, if (it.tint) { @@ -129,26 +145,28 @@ object CustomGlobalArmorOverrides { null } } + bakedOverrides.clear() val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } } .toMap() + associatedMap.forEach { it.value.bake(manager) } return associatedMap } override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) { - bakedOverrides.clear() - prepared.forEach { it.value.bake() } overrides = prepared } }) } @JvmStatic - fun overrideArmor(itemStack: ItemStack): Optional<Identifier> { - return overrideCache.invoke(itemStack) + fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<EquippableComponent> { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return Optional.empty() + return overrideCache.invoke(itemStack, slot) } @JvmStatic fun overrideArmorLayer(id: Identifier): EquipmentModel? { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null return bakedOverrides[id] } diff --git a/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt index 9ad8bc1..20f1fb6 100644 --- a/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -1,18 +1,14 @@ -@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class) +@file:UseSerializers(IdentifierSerializer::class, FirmamentRootPredicateSerializer::class) package moe.nea.firmament.features.texturepack -import java.util.Optional import java.util.concurrent.CompletableFuture import org.slf4j.LoggerFactory import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlin.jvm.optionals.getOrNull -import net.minecraft.client.render.item.ItemModels -import net.minecraft.client.render.model.BakedModel import net.minecraft.client.util.ModelIdentifier -import net.minecraft.item.ItemStack import net.minecraft.resource.ResourceManager import net.minecraft.resource.SinglePreparationResourceReloader import net.minecraft.text.Text @@ -20,16 +16,15 @@ import net.minecraft.util.Identifier import net.minecraft.util.profiler.Profiler import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.BakeExtraModelsEvent +import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.EarlyResourceReloadEvent import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.subscription.SubscriptionOwner import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.MC -import moe.nea.firmament.util.collections.WeakCache -import moe.nea.firmament.util.intoOptional import moe.nea.firmament.util.json.SingletonSerializableList import moe.nea.firmament.util.runNull @@ -73,15 +68,6 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText }, event.preparationExecutor) } - @Subscribe - fun onBakeModels(event: BakeExtraModelsEvent) { - for (guiClassOverride in preparationFuture.join().classes) { - for (override in guiClassOverride.overrides) { - event.addItemModel(ModelIdentifier(override.model, "inventory")) - } - } - } - @Volatile var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture( CustomGuiTextureOverride(listOf())) @@ -91,7 +77,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText } override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) { - this.guiClassOverrides = prepared + guiClassOverrides = prepared } val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java) @@ -100,7 +86,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } .mapNotNull { Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex -> - logger.error("Failed to load global item override at ${it.key}", ex) + ErrorUtil.softError("Failed to load global item override at ${it.key}", ex) null } } @@ -114,12 +100,12 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json")) .getOrNull() ?: return@mapNotNull runNull { - logger.error("Failed to locate screen filter at $key") + ErrorUtil.softError("Failed to locate screen filter at $key used by ${it.value.map { it.first }}") } val screenFilter = Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream) .getOrElse { ex -> - logger.error("Failed to load screen filter at $key", ex) + ErrorUtil.softError("Failed to load screen filter at $key used by ${it.value.map { it.first }}", ex) return@mapNotNull null } ItemOverrideCollection(screenFilter, it.value.map { it.second }) @@ -139,25 +125,19 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) } } - val overrideCache = - WeakCache.memoize<ItemStack, ItemModels, Optional<BakedModel>>("CustomGlobalTextureModelOverrides") { stack, models -> - matchingOverrides - .firstNotNullOfOrNull { - it.overrides - .asSequence() - .filter { it.predicate.test(stack) } - .map { models.getModel(it.model) } - .firstOrNull() - } - .intoOptional() - } - - @JvmStatic - fun replaceGlobalModel( - models: ItemModels, - stack: ItemStack, - ): BakedModel? { - return overrideCache.invoke(stack, models).getOrNull() + @Subscribe + fun replaceGlobalModel(event: CustomItemModelEvent) { + val override = matchingOverrides + .firstNotNullOfOrNull { + it.overrides + .asSequence() + .filter { it.predicate.test(event.itemStack) } + .map { it.model } + .firstOrNull() + } + + if (override != null) + event.overrideIfExists(override) } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt new file mode 100644 index 0000000..4529d1d --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -0,0 +1,115 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.Decoder +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.Encoder +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.features.texturepack.predicates.AndPredicate +import moe.nea.firmament.features.texturepack.predicates.CastPredicate +import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate +import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate +import moe.nea.firmament.features.texturepack.predicates.ItemPredicate +import moe.nea.firmament.features.texturepack.predicates.LorePredicate +import moe.nea.firmament.features.texturepack.predicates.NotPredicate +import moe.nea.firmament.features.texturepack.predicates.OrPredicate +import moe.nea.firmament.features.texturepack.predicates.PetPredicate +import moe.nea.firmament.features.texturepack.predicates.PullingPredicate +import moe.nea.firmament.util.json.KJsonOps + +object CustomModelOverrideParser { + + val LEGACY_CODEC: Codec<FirmamentModelPredicate> = + Codec.of( + Encoder.error("cannot encode legacy firmament model predicates"), + object : Decoder<FirmamentModelPredicate> { + override fun <T : Any?> decode( + ops: DynamicOps<T>, + input: T + ): DataResult<Pair<FirmamentModelPredicate, T>> { + try { + val pred = Firmament.json.decodeFromJsonElement( + FirmamentRootPredicateSerializer, + ops.convertTo(KJsonOps.INSTANCE, input)) + return DataResult.success(Pair.of(pred, ops.empty())) + } catch (ex: Exception) { + return DataResult.error { "Could not deserialize ${ex.message}" } + } + } + } + ) + + val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>() + + + fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) { + predicateParsers[Identifier.of("firmament", name)] = parser + } + + init { + registerPredicateParser("display_name", DisplayNamePredicate.Parser) + registerPredicateParser("lore", LorePredicate.Parser) + registerPredicateParser("all", AndPredicate.Parser) + registerPredicateParser("any", OrPredicate.Parser) + registerPredicateParser("not", NotPredicate.Parser) + registerPredicateParser("item", ItemPredicate.Parser) + registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) + registerPredicateParser("pet", PetPredicate.Parser) + } + + private val neverPredicate = listOf( + object : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return false + } + } + ) + + fun parsePredicates(predicates: JsonObject?): List<FirmamentModelPredicate> { + if (predicates == null) return neverPredicate + val parsedPredicates = mutableListOf<FirmamentModelPredicate>() + for (predicateName in predicates.keySet()) { + if (predicateName == "cast") { // 1.21.4 + parsedPredicates.add(CastPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) + } + if (predicateName == "pull") { + parsedPredicates.add(PullingPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) + } + if (predicateName == "pulling") { + parsedPredicates.add(PullingPredicate.AnyPulling) + } + if (!predicateName.startsWith("firmament:")) continue + val identifier = Identifier.of(predicateName) + val parser = predicateParsers[identifier] ?: return neverPredicate + val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate + parsedPredicates.add(parsedPredicate) + } + return parsedPredicates + } + + @JvmStatic + fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? { + val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null + val parsedPredicates = parsePredicates(predicates) + if (parsedPredicates.isEmpty()) + return null + return parsedPredicates.toTypedArray() + } + + @Subscribe + fun finalizeResources(event: FinalizeResourceManagerEvent) { + ItemModelTypes.ID_MAPPER.put( + Firmament.identifier("predicates/legacy"), + PredicateModel.Unbaked.CODEC + ) + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt new file mode 100644 index 0000000..bef52d2 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -0,0 +1,118 @@ +package moe.nea.firmament.features.texturepack + +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import com.mojang.authlib.properties.Property +import java.util.Optional +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlin.jvm.optionals.getOrNull +import net.minecraft.block.SkullBlock +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayer +import net.minecraft.component.type.ProfileComponent +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CustomItemModelEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.mc.decodeProfileTextureProperty +import moe.nea.firmament.util.skyBlockId + +object CustomSkyBlockTextures : FirmamentFeature { + override val identifier: String + get() = "custom-skyblock-textures" + + object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing? + val enabled by toggle("enabled") { true } + val skullsEnabled by toggle("skulls-enabled") { true } + val cacheForever by toggle("cache-forever") { true } + val cacheDuration by integer("cache-duration", 0, 100) { 1 } + val enableModelOverrides by toggle("model-overrides") { true } + val enableArmorOverrides by toggle("armor-overrides") { true } + val enableBlockOverrides by toggle("block-overrides") { true } + val enableLegacyMinecraftCompat by toggle("legacy-minecraft-path-support") { true } + val enableLegacyCIT by toggle("legacy-cit") { true } + val allowRecoloringUiText by toggle("recolor-text") { true } + } + + override val config: ManagedConfig + get() = TConfig + + val allItemCaches by lazy { + listOf( + skullTextureCache.cache, + CustomItemModelEvent.cache.cache, + CustomGlobalArmorOverrides.overrideCache.cache + ) + } + + init { + PowerUserTools.getSkullId = ::getSkullTexture + } + + fun clearAllCaches() { + allItemCaches.forEach(WeakCache<*, *, *>::clear) + } + + @Subscribe + fun onTick(it: TickEvent) { + if (TConfig.cacheForever) return + if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) { + clearAllCaches() + } + } + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.registerOnApply("Clear firmament CIT caches") { + clearAllCaches() + } + } + + @Subscribe + fun onCustomModelId(it: CustomItemModelEvent) { + if (!TConfig.enabled) return + val id = it.itemStack.skyBlockId ?: return + it.overrideIfEmpty(Identifier.of("firmskyblock", id.identifier.path)) + } + + private val skullTextureCache = + WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component -> + val id = getSkullTexture(component) ?: return@memoize Optional.empty() + if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { + return@memoize Optional.empty() + } + return@memoize Optional.of(id) + } + + private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex() + + fun getSkullId(textureProperty: Property): String? { + val texture = decodeProfileTextureProperty(textureProperty) ?: return null + val textureUrl = + texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null + val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null + return mcUrlData.groupValues[1] + } + + fun getSkullTexture(profile: ProfileComponent): Identifier? { + val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null + return Identifier.of("firmskyblock", "textures/placedskull/$id.png") + } + + fun modifySkullTexture( + type: SkullBlock.SkullType?, + component: ProfileComponent?, + cir: CallbackInfoReturnable<RenderLayer> + ) { + if (type != SkullBlock.Type.PLAYER) return + if (!TConfig.skullsEnabled) return + if (component == null) return + + val n = skullTextureCache.invoke(component).getOrNull() ?: return + cir.returnValue = RenderLayer.getEntityTranslucent(n) + } +} diff --git a/src/main/kotlin/features/texturepack/CustomTextColors.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt index 4ca1796..4ca1796 100644 --- a/src/main/kotlin/features/texturepack/CustomTextColors.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt new file mode 100644 index 0000000..6cef4ca --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt @@ -0,0 +1,9 @@ +package moe.nea.firmament.features.texturepack + +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack + +interface FirmamentModelPredicate { + fun test(stack: ItemStack, holder: LivingEntity?): Boolean = test(stack) + fun test(stack: ItemStack): Boolean = test(stack, null) +} diff --git a/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt index 3ed0c67..3ed0c67 100644 --- a/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt new file mode 100644 index 0000000..0b8ae8e --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt @@ -0,0 +1,23 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.features.texturepack.predicates.AndPredicate + +object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> { + val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): FirmamentModelPredicate { + val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject + return AndPredicate(CustomModelOverrideParser.parsePredicates(json).toTypedArray()) + } + + override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) { + TODO("Cannot serialize firmament predicates") + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt new file mode 100644 index 0000000..0edad4c --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt @@ -0,0 +1,106 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.client.item.ItemModelManager +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.client.render.item.model.BasicItemModel +import net.minecraft.client.render.item.model.ItemModel +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.client.render.item.tint.TintSource +import net.minecraft.client.render.model.ResolvableModel +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.ModelTransformationMode +import net.minecraft.util.Identifier +import moe.nea.firmament.features.texturepack.predicates.AndPredicate + +class PredicateModel { + data class Baked( + val fallback: ItemModel, + val overrides: List<Override> + ) : ItemModel { + data class Override( + val model: ItemModel, + val predicate: FirmamentModelPredicate, + ) + + override fun update( + state: ItemRenderState, + stack: ItemStack, + resolver: ItemModelManager, + transformationMode: ModelTransformationMode, + world: ClientWorld?, + user: LivingEntity?, + seed: Int + ) { + val model = + overrides + .findLast { it.predicate.test(stack, user) } + ?.model + ?: fallback + model.update(state, stack, resolver, transformationMode, world, user, seed) + } + } + + data class Unbaked( + val fallback: ItemModel.Unbaked, + val overrides: List<Override>, + ) : ItemModel.Unbaked { + companion object { + @JvmStatic + fun fromLegacyJson(jsonObject: JsonObject, fallback: ItemModel.Unbaked): ItemModel.Unbaked { + val legacyOverrides = jsonObject.getAsJsonArray("overrides") ?: return fallback + val newOverrides = ArrayList<Override>() + for (legacyOverride in legacyOverrides) { + legacyOverride as JsonObject + val overrideModel = Identifier.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue + val predicate = CustomModelOverrideParser.parsePredicates(legacyOverride.getAsJsonObject("predicate")) + newOverrides.add(Override( + BasicItemModel.Unbaked(overrideModel, listOf()), + AndPredicate(predicate.toTypedArray()) + )) + } + return Unbaked(fallback, newOverrides) + } + + val OVERRIDE_CODEC: Codec<Override> = RecordCodecBuilder.create { + it.group( + ItemModelTypes.CODEC.fieldOf("model").forGetter(Override::model), + CustomModelOverrideParser.LEGACY_CODEC.fieldOf("predicate").forGetter(Override::predicate), + ).apply(it, Unbaked::Override) + } + val CODEC: MapCodec<Unbaked> = + RecordCodecBuilder.mapCodec { + it.group( + ItemModelTypes.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback), + OVERRIDE_CODEC.listOf().fieldOf("overrides").forGetter(Unbaked::overrides), + ).apply(it, ::Unbaked) + } + } + + data class Override( + val model: ItemModel.Unbaked, + val predicate: FirmamentModelPredicate, + ) + + override fun resolve(resolver: ResolvableModel.Resolver) { + fallback.resolve(resolver) + overrides.forEach { it.model.resolve(resolver) } + } + + override fun getCodec(): MapCodec<out Unbaked> { + return CODEC + } + + override fun bake(context: ItemModel.BakeContext): ItemModel { + return Baked( + fallback.bake(context), + overrides.map { Baked.Override(it.model.bake(context), it.predicate) } + ) + } + } +} diff --git a/src/main/kotlin/features/texturepack/RarityMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt index 634a171..634a171 100644 --- a/src/main/kotlin/features/texturepack/RarityMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt diff --git a/src/main/kotlin/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt index 5eb86ac..2b13284 100644 --- a/src/main/kotlin/features/texturepack/StringMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -70,7 +70,7 @@ interface StringMatcher { } override fun serialize(encoder: Encoder, value: StringMatcher) { - encoder.encodeSerializableValue(delegateSerializer, Companion.serialize(value).intoKotlinJson()) + encoder.encodeSerializableValue(delegateSerializer, serialize(value).intoKotlinJson()) } } diff --git a/src/main/kotlin/features/texturepack/TintOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt index 85fcae4..53df184 100644 --- a/src/main/kotlin/features/texturepack/TintOverrides.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt @@ -43,7 +43,7 @@ data class TintOverrides( val override = (value as? JsonPrimitive) ?.takeIf(JsonPrimitive::isNumber) ?.asInt - ?.let(::Fixed) + ?.let(TintOverrides::Fixed) if (override == null) { ErrorUtil.softError("Invalid tint override for a layer: $value") continue diff --git a/src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt index 7e0ddb1..7e0ddb1 100644 --- a/src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/AndPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt index 99abaaa..70eb814 100644 --- a/src/main/kotlin/features/texturepack/predicates/AndPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt @@ -3,15 +3,16 @@ package moe.nea.firmament.features.texturepack.predicates import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity import moe.nea.firmament.features.texturepack.CustomModelOverrideParser import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import net.minecraft.item.ItemStack class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { - override fun test(stack: ItemStack): Boolean { - return children.all { it.test(stack) } - } + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return children.all { it.test(stack, holder) } + } object Parser : FirmamentModelPredicateParser { override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt new file mode 100644 index 0000000..2b79c1a --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser + +class CastPredicate : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement.asDouble >= 1) return CastPredicate() + return NotPredicate(arrayOf(CastPredicate())) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return (holder as? PlayerEntity)?.fishHook != null && holder.activeItem === stack + } + + override fun test(stack: ItemStack): Boolean { + return false + } +} diff --git a/src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt index 04c7a2b..04c7a2b 100644 --- a/src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt index 3c8023d..3c8023d 100644 --- a/src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt index 3cb80c7..4833dc0 100644 --- a/src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt @@ -17,7 +17,7 @@ class ItemPredicate( val item: Item ) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { - return stack.item == item + return stack.isOf(item) } object Parser : FirmamentModelPredicateParser { diff --git a/src/main/kotlin/features/texturepack/predicates/LorePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt index f0b4737..f0b4737 100644 --- a/src/main/kotlin/features/texturepack/predicates/LorePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/NotPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt index 4986ad9..4986ad9 100644 --- a/src/main/kotlin/features/texturepack/predicates/NotPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt index b0d5178..b0d5178 100644 --- a/src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt diff --git a/src/main/kotlin/features/texturepack/predicates/OrPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt index e3093cd..e3093cd 100644 --- a/src/main/kotlin/features/texturepack/predicates/OrPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt diff --git a/src/main/kotlin/features/texturepack/predicates/PetPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt index b30b7c9..b30b7c9 100644 --- a/src/main/kotlin/features/texturepack/predicates/PetPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt new file mode 100644 index 0000000..fa46a70 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import net.minecraft.entity.LivingEntity +import net.minecraft.item.BowItem +import net.minecraft.item.ItemStack +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser + +class PullingPredicate(val percentage: Double) : FirmamentModelPredicate { + companion object { + val AnyPulling = PullingPredicate(0.1) + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + return PullingPredicate(jsonElement.asDouble) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (holder == null) return false + return BowItem.getPullProgress(holder.itemUseTime) >= percentage + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java new file mode 100644 index 0000000..4665829 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java @@ -0,0 +1,23 @@ + +package moe.nea.firmament.mixins.custommodels; + +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.decoration.DisplayEntity; +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(LivingEntityRenderer.class) +public class ApplyHeadModelInItemRenderer<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> { + // TODO: replace head_model with a condition model (if possible, automatically) + // TODO: ItemAsset.CODEC should upgrade partials + @Inject(method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL")) + private void updateHeadState(T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci) { + + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/CustomSkullTexturePatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java index f3b616a..fede766 100644 --- a/src/main/java/moe/nea/firmament/mixins/CustomSkullTexturePatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java @@ -1,12 +1,13 @@ -package moe.nea.firmament.mixins; +package moe.nea.firmament.mixins.custommodels; import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; import net.minecraft.block.SkullBlock; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; import net.minecraft.component.type.ProfileComponent; +import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -14,8 +15,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(SkullBlockEntityRenderer.class) public class CustomSkullTexturePatch { - @Inject(method = "getRenderLayer", at = @At("HEAD"), cancellable = true) - private static void onGetRenderLayer(SkullBlock.SkullType type, ProfileComponent profile, CallbackInfoReturnable<RenderLayer> cir) { + @Inject( + method = "getRenderLayer(Lnet/minecraft/block/SkullBlock$SkullType;Lnet/minecraft/component/type/ProfileComponent;Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;", + at = @At("HEAD"), + cancellable = true + ) + private static void onGetRenderLayer(SkullBlock.SkullType type, ProfileComponent profile, Identifier texture, CallbackInfoReturnable<RenderLayer> cir) { CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir); } } diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java index 4468150..669da63 100644 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java @@ -1,29 +1,30 @@ package moe.nea.firmament.mixins.custommodels; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer; +import net.minecraft.component.ComponentType; import net.minecraft.component.type.EquippableComponent; +import net.minecraft.entity.EquipmentSlot; import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import java.util.Optional; - @Mixin(ArmorFeatureRenderer.class) public class PatchArmorTexture { - @WrapOperation( + @ModifyExpressionValue( method = "renderArmor", - at = @At(value = "INVOKE", target = "Lnet/minecraft/component/type/EquippableComponent;model()Ljava/util/Optional;")) - private Optional<Identifier> overrideLayers( - EquippableComponent instance, Operation<Optional<Identifier>> original, @Local(argsOnly = true) ItemStack itemStack + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object overrideLayers( + Object original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) EquipmentSlot slot ) { - // TODO: check that all armour items are naturally equippable and have the equppable component. otherwise our call here will not be reached. - var overrides = CustomGlobalArmorOverrides.overrideArmor(itemStack); - return overrides.or(() -> original.call(instance)); + var overrides = CustomGlobalArmorOverrides.overrideArmor(itemStack, slot); + return overrides.orElse((EquippableComponent) original); } } diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java new file mode 100644 index 0000000..81ea6cd --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java @@ -0,0 +1,23 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; +import net.minecraft.client.render.entity.equipment.EquipmentRenderer; +import net.minecraft.item.equipment.EquipmentAsset; +import net.minecraft.registry.RegistryKey; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +// TODO: auto import legacy models, maybe!!! in a later patch tho +@Mixin(EquipmentRenderer.class) +public class PatchLegacyArmorLayerSupport { + @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/registry/RegistryKey;)Lnet/minecraft/client/render/entity/equipment/EquipmentModel;")) + private EquipmentModel patchModelLayers(EquipmentModelLoader instance, RegistryKey<EquipmentAsset> assetKey, Operation<EquipmentModel> original) { + var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue()); + if (modelOverride != null) return modelOverride; + return original.call(instance, assetKey); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java new file mode 100644 index 0000000..f829da0 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java @@ -0,0 +1,38 @@ +package moe.nea.firmament.mixins.custommodels; + + +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import moe.nea.firmament.util.MC; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Final; +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(EquipmentModel.Layer.class) +public class PatchLegacyTexturePathsIntoArmorLayers { + @Shadow + @Final + private Identifier textureId; + + @Inject(method = "getFullTextureId", at = @At("HEAD"), cancellable = true) + private void replaceWith1201TextureIfExists(EquipmentModel.LayerType layerType, CallbackInfoReturnable<Identifier> cir) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) + return; + var resourceManager = MC.INSTANCE.getResourceManager(); + // legacy format: "assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png" + // suffix is sadly not available to us here. this means leather armor will look a bit shite + var legacyIdentifier = this.textureId.withPath((textureName) -> { + String var10000 = layerType.asString(); + return "textures/models/armor/" + textureName + "_layer_" + + (layerType == EquipmentModel.LayerType.HUMANOID_LEGGINGS ? 2 : 1) + + ".png"; + }); + if (resourceManager.getResource(legacyIdentifier).isPresent()) { + cir.setReturnValue(legacyIdentifier); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java index 9401889..9401889 100644 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java index f9a1d0d..f9a1d0d 100644 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java index 711b2af..711b2af 100644 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java index 53ab74a..53ab74a 100644 --- a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java new file mode 100644 index 0000000..97abd1f --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java @@ -0,0 +1,43 @@ +package moe.nea.firmament.mixins.custommodels; + + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.CustomItemModelEvent; +import moe.nea.firmament.util.mc.IntrospectableItemModelManager; +import net.minecraft.client.item.ItemModelManager; +import net.minecraft.client.render.item.model.ItemModel; +import net.minecraft.client.render.item.model.MissingItemModel; +import net.minecraft.component.ComponentType; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Function; + +@Mixin(ItemModelManager.class) +public class ReplaceItemModelPatch implements IntrospectableItemModelManager { + @Shadow + @Final + private Function<Identifier, ItemModel> modelGetter; + + @WrapOperation( + method = "update(Lnet/minecraft/client/render/item/ItemRenderState;Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;Lnet/minecraft/world/World;Lnet/minecraft/entity/LivingEntity;I)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object replaceItemModelByIdentifier(ItemStack instance, ComponentType componentType, Operation<Object> original) { + var override = CustomItemModelEvent.getModelIdentifier(instance, this); + if (override != null && hasModel_firmament(override)) { + return override; + } + return original.call(instance, componentType); + } + + @Override + public boolean hasModel_firmament(@NotNull Identifier identifier) { + return !(modelGetter.apply(identifier) instanceof MissingItemModel); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java index c9fb073..e4834e9 100644 --- a/src/main/java/moe/nea/firmament/mixins/ReplaceTextColorInHandledScreen.java +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java @@ -1,4 +1,4 @@ -package moe.nea.firmament.mixins; +package moe.nea.firmament.mixins.custommodels; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java new file mode 100644 index 0000000..850ea53 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java @@ -0,0 +1,95 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.google.gson.JsonObject; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.Firmament; +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import moe.nea.firmament.features.texturepack.PredicateModel; +import moe.nea.firmament.util.ErrorUtil; +import net.minecraft.client.item.ItemAsset; +import net.minecraft.client.item.ItemAssetsLoader; +import net.minecraft.client.render.item.model.BasicItemModel; +import net.minecraft.client.render.item.model.ItemModel; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +@Mixin(ItemAssetsLoader.class) +public class SupplyFakeModelPatch { + + @ModifyReturnValue( + method = "load", + at = @At("RETURN") + ) + private static CompletableFuture<ItemAssetsLoader.Result> injectFakeGeneratedModels( + CompletableFuture<ItemAssetsLoader.Result> original, + @Local(argsOnly = true) ResourceManager resourceManager, + @Local(argsOnly = true) Executor executor + ) { + return original.thenCompose(oldModels -> CompletableFuture.supplyAsync(() -> supplyExtraModels(resourceManager, oldModels), executor)); + } + + private static ItemAssetsLoader.Result supplyExtraModels(ResourceManager resourceManager, ItemAssetsLoader.Result oldModels) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) return oldModels; + Map<Identifier, ItemAsset> newModels = new HashMap<>(oldModels.contents()); + var resources = resourceManager.findResources( + "models/item", + id -> (id.getNamespace().equals("firmskyblock") || id.getNamespace().equals("cittofirmgenerated")) + && id.getPath().endsWith(".json")); + for (Map.Entry<Identifier, Resource> model : resources.entrySet()) { + var resource = model.getValue(); + var itemModelId = model.getKey().withPath(it -> it.substring("models/item/".length(), it.length() - ".json".length())); + var genericModelId = itemModelId.withPrefixedPath("item/"); + var itemAssetId = itemModelId.withPrefixedPath("items/"); + // TODO: inject tint indexes based on the json data here + ItemModel.Unbaked unbakedModel = new BasicItemModel.Unbaked(genericModelId, List.of()); + // TODO: add a filter using the pack.mcmeta to opt out of this behaviour + try (var is = resource.getInputStream()) { + var jsonObject = Firmament.INSTANCE.getGson().fromJson(new InputStreamReader(is), JsonObject.class); + unbakedModel = PredicateModel.Unbaked.fromLegacyJson(jsonObject, unbakedModel); + } catch (Exception e) { + ErrorUtil.INSTANCE.softError("Could not create resource for fake model supplication: " + model.getKey(), e); + } + if (resourceManager.getResource(itemAssetId.withSuffixedPath(".json")) + .map(Resource::getPack) + .map(it -> isResourcePackNewer(resourceManager, it, resource.getPack())) + .orElse(true)) { + newModels.put(itemModelId, new ItemAsset( + unbakedModel, + new ItemAsset.Properties(true) + )); + } + } + return new ItemAssetsLoader.Result(newModels); + } + + private static boolean isResourcePackNewer( + ResourceManager manager, + ResourcePack null_, ResourcePack proposal) { + var pack = manager.streamResourcePacks() + .filter(it -> it == null_ || it == proposal) + .collect(findLast()); + return pack.orElse(null_) != null_; + } + + private static <T> Collector<T, ?, Optional<T>> findLast() { + return Collectors.reducing(Optional.empty(), Optional::of, + (left, right) -> right.isPresent() ? right : left); + + } + +} |