From 1baf0fba5be2d1a5a6a432b53385ea80cfe8fe19 Mon Sep 17 00:00:00 2001 From: Dragon-Seeker Date: Sun, 7 Sep 2025 20:58:14 -0500 Subject: [PATCH 1/9] Fully rework OwoItemGroup to be an extension object rather than a extension of a ItemGroup - Adjust Icons to be more date driven for use in data driven manipulation of item groups - Separate state from item group to allow for proper state on client vs server - Adjust data format for item groups to allow for more features and use endec - Inject owo extensions on server and client - Use item groups registry key rather than item group it self for Item settings --- src/main/java/io/wispforest/owo/Owo.java | 15 + .../io/wispforest/owo/client/OwoClient.java | 4 +- .../texture/AnimatedTextureDrawable.java | 96 ---- .../client/texture/SpriteSheetMetadata.java | 32 -- .../owo/compat/emi/OwoEmiPlugin.java | 16 +- .../owo/compat/rei/OwoReiPlugin.java | 23 +- .../io/wispforest/owo/itemgroup/Icon.java | 66 --- .../owo/itemgroup/ItemGroupReference.java | 3 - .../owo/itemgroup/OwoItemGroup.java | 457 ------------------ .../owo/itemgroup/OwoItemGroupBuilder.java | 131 +++++ .../owo/itemgroup/OwoItemGroupLoader.java | 178 +++++++ .../itemgroup/OwoItemSettingsExtension.java | 7 +- .../owo/itemgroup/base/ButtonDefinition.java | 33 ++ .../wispforest/owo/itemgroup/base/Icon.java | 111 +++++ .../owo/itemgroup/base/OwoItemGroup.java | 159 ++++++ .../owo/itemgroup/base/OwoItemGroupState.java | 92 ++++ .../owo/itemgroup/core/ContentSupplier.java | 21 + .../owo/itemgroup/core/IconEndec.java | 38 ++ .../owo/itemgroup/core/ItemGroupButton.java | 108 +++++ .../itemgroup/core/ItemGroupReference.java | 6 + .../itemgroup/{gui => core}/ItemGroupTab.java | 24 +- .../owo/itemgroup/gui/IconRenderRegistry.java | 122 +++++ .../owo/itemgroup/gui/ItemGroupButton.java | 90 ---- .../itemgroup/gui/ItemGroupButtonWidget.java | 11 +- .../owo/itemgroup/gui/ScrollerTextures.java | 9 + .../owo/itemgroup/gui/TabTextures.java | 8 + .../owo/itemgroup/impl/OwoItemGroupImpl.java | 184 +++++++ .../itemgroup/impl/OwoItemGroupStateImpl.java | 169 +++++++ .../owo/itemgroup/impl/SearchOnlyEntries.java | 18 + .../itemgroup/json/OwoItemGroupLoader.java | 140 ------ .../owo/itemgroup/json/WrapperGroup.java | 72 --- .../CreativeInventoryScreenMixin.java | 123 +++-- .../owo/mixin/itemgroup/ItemGroupMixin.java | 74 +++ .../owo/mixin/itemgroup/ItemMixin.java | 11 +- .../mixin/itemgroup/ItemSettingsMixin.java | 12 +- .../OwoItemGroupBuilderAccessor.java | 12 + .../itemgroup/StatusEffectsDisplayMixin.java | 16 +- .../serialization/endec/MinecraftEndecs.java | 10 + .../owo/util/pond/OwoItemExtensions.java | 9 +- .../owo/util/pond/OwoItemGroupExtension.java | 16 + src/main/resources/owo.mixins.json | 2 + src/testmod/java/io/wispforest/uwu/Uwu.java | 99 ++-- .../uwu/items/UwuTestStickItem.java | 2 +- .../data/uwu/item_group_tabs/crab_group.json | 10 +- .../food_and_drink_button.json | 13 +- .../ingredients_extension.json | 5 +- .../ingredients_extension_2.json | 5 +- 47 files changed, 1713 insertions(+), 1149 deletions(-) delete mode 100644 src/main/java/io/wispforest/owo/client/texture/AnimatedTextureDrawable.java delete mode 100644 src/main/java/io/wispforest/owo/client/texture/SpriteSheetMetadata.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/Icon.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/ItemGroupReference.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/OwoItemGroup.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupBuilder.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupLoader.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/base/ButtonDefinition.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/base/Icon.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroup.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupState.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/core/ContentSupplier.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/core/IconEndec.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupButton.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupReference.java rename src/main/java/io/wispforest/owo/itemgroup/{gui => core}/ItemGroupTab.java (56%) create mode 100644 src/main/java/io/wispforest/owo/itemgroup/gui/IconRenderRegistry.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/gui/ItemGroupButton.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/gui/ScrollerTextures.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/gui/TabTextures.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/impl/OwoItemGroupImpl.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/impl/OwoItemGroupStateImpl.java create mode 100644 src/main/java/io/wispforest/owo/itemgroup/impl/SearchOnlyEntries.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/json/OwoItemGroupLoader.java delete mode 100644 src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java create mode 100644 src/main/java/io/wispforest/owo/mixin/itemgroup/ItemGroupMixin.java create mode 100644 src/main/java/io/wispforest/owo/mixin/itemgroup/OwoItemGroupBuilderAccessor.java create mode 100644 src/main/java/io/wispforest/owo/util/pond/OwoItemGroupExtension.java diff --git a/src/main/java/io/wispforest/owo/Owo.java b/src/main/java/io/wispforest/owo/Owo.java index 7a6835f56..3c498ff4c 100644 --- a/src/main/java/io/wispforest/owo/Owo.java +++ b/src/main/java/io/wispforest/owo/Owo.java @@ -2,13 +2,20 @@ import io.wispforest.owo.client.screens.ScreenInternals; import io.wispforest.owo.command.debug.OwoDebugCommands; +import io.wispforest.owo.itemgroup.OwoItemGroupLoader; +import io.wispforest.owo.moddata.ModDataLoader; import io.wispforest.owo.ops.LootOps; import io.wispforest.owo.text.CustomTextRegistry; import io.wispforest.owo.text.InsertingTextContent; +import io.wispforest.owo.util.OwoFreezer; import io.wispforest.owo.util.Wisdom; +import io.wispforest.owo.util.pond.OwoItemGroupExtension; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemGroups; +import net.minecraft.registry.Registries; import net.minecraft.server.MinecraftServer; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -57,6 +64,14 @@ public void onInitialize() { Wisdom.spread(); + ModDataLoader.load(OwoItemGroupLoader.INSTANCE); + + OwoFreezer.registerFreezeCallback(() -> { + ItemGroups.getGroups().forEach(group -> { + ((OwoItemGroupExtension) group).attemptToBuildExtension(); + }); + }); + if (!DEBUG) return; OwoDebugCommands.register(); diff --git a/src/main/java/io/wispforest/owo/client/OwoClient.java b/src/main/java/io/wispforest/owo/client/OwoClient.java index 07552b8c9..7a63cc23a 100644 --- a/src/main/java/io/wispforest/owo/client/OwoClient.java +++ b/src/main/java/io/wispforest/owo/client/OwoClient.java @@ -4,7 +4,7 @@ import io.wispforest.owo.client.screens.ScreenInternals; import io.wispforest.owo.command.debug.OwoDebugCommands; import io.wispforest.owo.config.OwoConfigCommand; -import io.wispforest.owo.itemgroup.json.OwoItemGroupLoader; +import io.wispforest.owo.itemgroup.OwoItemGroupLoader; import io.wispforest.owo.moddata.ModDataLoader; import io.wispforest.owo.ui.core.OwoUIPipelines; import io.wispforest.owo.ui.parsing.UIModelLoader; @@ -45,8 +45,6 @@ public class OwoClient implements ClientModInitializer { @Override public void onInitializeClient() { - ModDataLoader.load(OwoItemGroupLoader.INSTANCE); - ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new UIModelLoader()); ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new NinePatchTexture.MetadataLoader()); diff --git a/src/main/java/io/wispforest/owo/client/texture/AnimatedTextureDrawable.java b/src/main/java/io/wispforest/owo/client/texture/AnimatedTextureDrawable.java deleted file mode 100644 index f9e8726d2..000000000 --- a/src/main/java/io/wispforest/owo/client/texture/AnimatedTextureDrawable.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.wispforest.owo.client.texture; - -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.Drawable; -import net.minecraft.client.render.RenderLayer; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.util.Identifier; -import net.minecraft.util.Util; - -/** - * A drawable that can draw an animated texture, very similar to how - * .mcmeta works on stitched textures in ticked atlases - * - *

Originally from Animawid, adapted for oωo

- * - * @author Tempora - * @author glisco - */ -public class AnimatedTextureDrawable implements Drawable { - - private final SpriteSheetMetadata metadata; - private final Identifier texture; - - private final int validFrames; - private final int delay; - private final boolean loop; - private final int rows; - private long startTime = -1L; - - private final int width, height; - private int x, y; - - /** - * Creates a new animated texture widget using the width and height of the spritesheet as dimensions - * - * @see #AnimatedTextureDrawable(int, int, int, int, Identifier, SpriteSheetMetadata, int, boolean) - */ - public AnimatedTextureDrawable(int x, int y, Identifier texture, SpriteSheetMetadata metadata, int delay, boolean loop) { - this(x, y, metadata.width(), metadata.height(), texture, metadata, delay, loop); - } - - /** - * Creates a new animated texture widget that can be placed on your Screen or overlay etc. - * - * @param x The x position of the widget. - * @param y The y position of the widget. - * @param width The width of the widget. - * @param height The height of the widget. - * @param texture The identifier of the texture, eg: {@code mymod:texture/animation_spritesheet.png} - * @param metadata Metadata on the spritesheet. - * @param delay The delay, in milliseconds, between each frame. - */ - public AnimatedTextureDrawable(int x, int y, int width, int height, Identifier texture, SpriteSheetMetadata metadata, int delay, boolean loop) { - this.x = x; - this.y = y; - this.texture = texture; - this.delay = delay; - this.metadata = metadata; - this.width = width; - this.height = height; - this.loop = loop; - - int columns = metadata.width() / metadata.frameWidth(); - this.rows = metadata.height() / metadata.frameHeight(); - this.validFrames = columns * this.rows; - } - - /** - * Renders this drawable at the given position. The position - * of this drawable is mutated non-temporarily - */ - public void render(int x, int y, DrawContext context, int mouseX, int mouseY, float delta) { - this.x = x; - this.y = y; - this.render(context, mouseX, mouseY, delta); - } - - @SuppressWarnings("IntegerDivisionInFloatingPointContext") - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - if (startTime == -1L) startTime = Util.getMeasuringTimeMs(); - - long currentTime = Util.getMeasuringTimeMs(); - long frame = Math.min(validFrames - 1, (currentTime - startTime) / delay); - - if (loop && frame == validFrames - 1) { - startTime = Util.getMeasuringTimeMs(); - frame = 0; - } - - context.drawTexture(RenderPipelines.GUI_TEXTURED, this.texture, x, y, (frame / rows) * metadata.frameWidth(), (frame % rows) * metadata.frameHeight(), width, height, metadata.width(), metadata.height()); - } -} diff --git a/src/main/java/io/wispforest/owo/client/texture/SpriteSheetMetadata.java b/src/main/java/io/wispforest/owo/client/texture/SpriteSheetMetadata.java deleted file mode 100644 index 05d182b81..000000000 --- a/src/main/java/io/wispforest/owo/client/texture/SpriteSheetMetadata.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.wispforest.owo.client.texture; - - -/** - * A simple container to define the sprite sheet an {@link AnimatedTextureDrawable} uses - * - *

Originally from Animawid, adapted for oωo

- * - * @author Tempora - * @author glisco - */ -public record SpriteSheetMetadata(int width, int height, int frameWidth, int frameHeight, int offset) { - - /** - * Creates a new SpriteSheetMetadata object. - * - * @param width The width of the Sprite Sheet. - * @param height The height of the Sprite Sheet. - * @param frameWidth The width of each individual frame - * @param frameHeight The width of each individual frame - */ - public SpriteSheetMetadata(int width, int height, int frameWidth, int frameHeight) { - this(width, height, frameWidth, frameHeight, 0); - } - - /** - * Convenience constructor that assumes both the spritesheet and frames are square - */ - public SpriteSheetMetadata(int size, int frameSize) { - this(size, size, frameSize, frameSize, 0); - } -} diff --git a/src/main/java/io/wispforest/owo/compat/emi/OwoEmiPlugin.java b/src/main/java/io/wispforest/owo/compat/emi/OwoEmiPlugin.java index 16bc7ae59..bd263deef 100644 --- a/src/main/java/io/wispforest/owo/compat/emi/OwoEmiPlugin.java +++ b/src/main/java/io/wispforest/owo/compat/emi/OwoEmiPlugin.java @@ -3,7 +3,7 @@ import dev.emi.emi.api.EmiPlugin; import dev.emi.emi.api.EmiRegistry; import dev.emi.emi.api.widget.Bounds; -import io.wispforest.owo.itemgroup.OwoItemGroup; +import io.wispforest.owo.itemgroup.base.OwoItemGroupState; import io.wispforest.owo.mixin.itemgroup.CreativeInventoryScreenAccessor; import io.wispforest.owo.ui.base.BaseOwoHandledScreen; import io.wispforest.owo.util.pond.OwoCreativeInventoryScreenExtensions; @@ -13,21 +13,13 @@ public class OwoEmiPlugin implements EmiPlugin { @Override public void register(EmiRegistry registry) { registry.addExclusionArea(CreativeInventoryScreen.class, (screen, consumer) -> { - var group = CreativeInventoryScreenAccessor.owo$getSelectedTab(); - if (!(group instanceof OwoItemGroup owoGroup)) return; - if (owoGroup.getButtons().isEmpty()) return; + var state = OwoItemGroupState.getState(CreativeInventoryScreenAccessor.owo$getSelectedTab()); + if (state == null) return; int x = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootX(); int y = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootY(); - int stackHeight = owoGroup.getButtonStackHeight(); - y -= 13 * (stackHeight - 4); - - for (int i = 0; i < owoGroup.getButtons().size(); i++) { - int xOffset = x + 198 + (i / stackHeight) * 26; - int yOffset = y + 10 + (i % stackHeight) * 30; - consumer.accept(new Bounds(xOffset, yOffset, 24, 24)); - } + state.getExclusionZones(x, y).forEach(rect -> consumer.accept(new Bounds(rect.position().x(), rect.position().y(), rect.width(), rect.height()))); }); registry.addGenericExclusionArea((screen, consumer) -> { diff --git a/src/main/java/io/wispforest/owo/compat/rei/OwoReiPlugin.java b/src/main/java/io/wispforest/owo/compat/rei/OwoReiPlugin.java index 8d5344390..f1acbb302 100644 --- a/src/main/java/io/wispforest/owo/compat/rei/OwoReiPlugin.java +++ b/src/main/java/io/wispforest/owo/compat/rei/OwoReiPlugin.java @@ -1,6 +1,6 @@ package io.wispforest.owo.compat.rei; -import io.wispforest.owo.itemgroup.OwoItemGroup; +import io.wispforest.owo.itemgroup.base.OwoItemGroupState; import io.wispforest.owo.mixin.itemgroup.CreativeInventoryScreenAccessor; import io.wispforest.owo.ui.base.BaseOwoHandledScreen; import io.wispforest.owo.util.pond.OwoCreativeInventoryScreenExtensions; @@ -14,7 +14,6 @@ import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Collections; public class OwoReiPlugin implements REIClientPlugin { @@ -25,24 +24,16 @@ public class OwoReiPlugin implements REIClientPlugin { @Override public void registerExclusionZones(ExclusionZones zones) { zones.register(CreativeInventoryScreen.class, screen -> { - var group = CreativeInventoryScreenAccessor.owo$getSelectedTab(); - if (!(group instanceof OwoItemGroup owoGroup)) return Collections.emptySet(); - if (owoGroup.getButtons().isEmpty()) return Collections.emptySet(); + var state = OwoItemGroupState.getState(CreativeInventoryScreenAccessor.owo$getSelectedTab()); + if (state == null) return Collections.emptySet(); int x = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootX(); int y = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootY(); - int stackHeight = owoGroup.getButtonStackHeight(); - y -= 13 * (stackHeight - 4); - - final var rectangles = new ArrayList(); - for (int i = 0; i < owoGroup.getButtons().size(); i++) { - int xOffset = x + 198 + (i / stackHeight) * 26; - int yOffset = y + 10 + (i % stackHeight) * 30; - rectangles.add(new Rectangle(xOffset, yOffset, 24, 24)); - } - - return rectangles; + return state.getExclusionZones(x, y) + .stream() + .map(rect -> new Rectangle(rect.position().x(), rect.position().y(), rect.width(), rect.height())) + .toList(); }); zones.register(BaseOwoHandledScreen.class, screen -> { diff --git a/src/main/java/io/wispforest/owo/itemgroup/Icon.java b/src/main/java/io/wispforest/owo/itemgroup/Icon.java deleted file mode 100644 index 5d564b330..000000000 --- a/src/main/java/io/wispforest/owo/itemgroup/Icon.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.wispforest.owo.itemgroup; - -import com.mojang.blaze3d.pipeline.RenderPipeline; -import io.wispforest.owo.client.texture.AnimatedTextureDrawable; -import io.wispforest.owo.client.texture.SpriteSheetMetadata; -import net.fabricmc.api.EnvType; -import net.fabricmc.api.Environment; -import net.minecraft.client.gl.RenderPipelines; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.render.RenderLayer; -import net.minecraft.item.ItemConvertible; -import net.minecraft.item.ItemStack; -import net.minecraft.util.Identifier; - -/** - * An icon used for rendering on buttons in {@link OwoItemGroup}s - *

- * Default implementations provided for textures and item stacks - */ -@FunctionalInterface -public interface Icon { - - @Environment(EnvType.CLIENT) - void render(DrawContext context, int x, int y, int mouseX, int mouseY, float delta); - - static Icon of(ItemStack stack) { - return new Icon() { - @Override - public void render(DrawContext context, int x, int y, int mouseX, int mouseY, float delta) { - context.drawItemWithoutEntity(stack, x, y); - } - }; - } - - static Icon of(ItemConvertible item) { - return of(new ItemStack(item)); - } - - static Icon of(Identifier texture, int u, int v, int textureWidth, int textureHeight) { - return new Icon() { - @Override - public void render(DrawContext context, int x, int y, int mouseX, int mouseY, float delta) { - context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, 16, 16, textureWidth, textureHeight); - } - }; - } - - /** - * Creates an Animated ItemGroup Icon - * - * @param texture The texture to render, this is the spritesheet - * @param textureSize The size of the texture, it is assumed to be square - * @param frameDelay The delay in milliseconds between frames. - * @param loop Should the animation play once or loop? - * @return The created icon instance - */ - static Icon of(Identifier texture, int textureSize, int frameDelay, boolean loop) { - var widget = new AnimatedTextureDrawable(0, 0, 16, 16, texture, new SpriteSheetMetadata(textureSize, 16), frameDelay, loop); - return new Icon() { - @Override - public void render(DrawContext context, int x, int y, int mouseX, int mouseY, float delta) { - widget.render(x, y, context, mouseX, mouseY, delta); - } - }; - } -} diff --git a/src/main/java/io/wispforest/owo/itemgroup/ItemGroupReference.java b/src/main/java/io/wispforest/owo/itemgroup/ItemGroupReference.java deleted file mode 100644 index 7e5969ad9..000000000 --- a/src/main/java/io/wispforest/owo/itemgroup/ItemGroupReference.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.wispforest.owo.itemgroup; - -public record ItemGroupReference(OwoItemGroup group, int tab) {} diff --git a/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroup.java b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroup.java deleted file mode 100644 index 8a3e80641..000000000 --- a/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroup.java +++ /dev/null @@ -1,457 +0,0 @@ -package io.wispforest.owo.itemgroup; - -import io.wispforest.owo.itemgroup.gui.ItemGroupButton; -import io.wispforest.owo.itemgroup.gui.ItemGroupButtonWidget; -import io.wispforest.owo.itemgroup.gui.ItemGroupTab; -import io.wispforest.owo.mixin.itemgroup.ItemGroupAccessor; -import io.wispforest.owo.util.pond.OwoItemExtensions; -import it.unimi.dsi.fastutil.ints.*; -import net.fabricmc.api.EnvType; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.item.*; -import net.minecraft.registry.Registries; -import net.minecraft.registry.Registry; -import net.minecraft.registry.tag.TagKey; -import net.minecraft.resource.featuretoggle.FeatureSet; -import net.minecraft.text.Text; -import net.minecraft.util.Identifier; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Extensions for {@link ItemGroup} which support multiple sub-tabs - * within, as well as arbitrary buttons with defaults provided for links - * to places like GitHub, Modrinth, etc. - *

- * Tabs can be populated by setting the {@link OwoItemSettingsExtension#tab(int)}. - * Furthermore, tags can be used for easily populating tabs from data - *

- * The roots of this implementation originated in Biome Makeover, where it was written by Lemonszz - */ -public abstract class OwoItemGroup extends ItemGroup { - - public static final BiConsumer DEFAULT_STACK_GENERATOR = (item, stacks) -> stacks.add(item.getDefaultStack()); - - protected static final ItemGroupTab PLACEHOLDER_TAB = new ItemGroupTab(Icon.of(Items.AIR), Text.empty(), (br, uh) -> {}, ItemGroupTab.DEFAULT_TEXTURE, false); - - public final List tabs = new ArrayList<>(); - public final List buttons = new ArrayList<>(); - - private final Consumer initializer; - - private final Supplier iconSupplier; - private Icon icon; - - private final IntSet activeTabs = new IntAVLTreeSet(IntComparators.NATURAL_COMPARATOR); - private final IntSet activeTabsView = IntSets.unmodifiable(this.activeTabs); - private boolean initialized = false; - - private final @Nullable Identifier backgroundTexture; - private final @Nullable ScrollerTextures scrollerTextures; - private final @Nullable TabTextures tabTextures; - - private final int tabStackHeight; - private final int buttonStackHeight; - private final boolean useDynamicTitle; - private final boolean displaySingleTab; - private final boolean allowMultiSelect; - - protected OwoItemGroup(Identifier id, Consumer initializer, Supplier iconSupplier, int tabStackHeight, int buttonStackHeight, @Nullable Identifier backgroundTexture, @Nullable ScrollerTextures scrollerTextures, @Nullable TabTextures tabTextures, boolean useDynamicTitle, boolean displaySingleTab, boolean allowMultiSelect) { - super(null, -1, Type.CATEGORY, Text.translatable("itemGroup.%s.%s".formatted(id.getNamespace(), id.getPath())), () -> ItemStack.EMPTY, (displayContext, entries) -> {}); - this.initializer = initializer; - this.iconSupplier = iconSupplier; - this.tabStackHeight = tabStackHeight; - this.buttonStackHeight = buttonStackHeight; - this.backgroundTexture = backgroundTexture; - this.scrollerTextures = scrollerTextures; - this.tabTextures = tabTextures; - this.useDynamicTitle = useDynamicTitle; - this.displaySingleTab = displaySingleTab; - this.allowMultiSelect = allowMultiSelect; - - ((ItemGroupAccessor) this).owo$setEntryCollector((context, entries) -> { - if (!this.initialized) { - throw new IllegalStateException("oωo item group not initialized, was 'initialize()' called?"); - } - - this.activeTabs.forEach(tabIdx -> { - this.tabs.get(tabIdx).contentSupplier().addItems(context, entries); - this.collectItemsFromRegistry(entries, tabIdx); - }); - }); - } - - public static Builder builder(Identifier id, Supplier iconSupplier) { - return new Builder(id, iconSupplier); - } - - // --------- - - /** - * Executes {@link #initializer} and makes sure this item group is ready for use - *

- * Call this after all of your items have been registered to make sure your icons - * show up correctly - */ - public void initialize() { - if (this.initialized) return; - - if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) this.initializer.accept(this); - if (this.tabs.isEmpty()) this.tabs.add(PLACEHOLDER_TAB); - - if (this.allowMultiSelect) { - for (int tabIdx = 0; tabIdx < this.tabs.size(); tabIdx++) { - if (!this.tabs.get(tabIdx).primary()) continue; - this.activeTabs.add(tabIdx); - } - - if (this.activeTabs.isEmpty()) this.activeTabs.add(0); - } else { - this.activeTabs.add(0); - } - - this.initialized = true; - } - - /** - * Adds the specified button to the buttons on - * the right side of the creative menu - * - * @param button The button to add - * @see ItemGroupButton#link(ItemGroup, Icon, String, String) - * @see ItemGroupButton#curseforge(ItemGroup, String) - * @see ItemGroupButton#discord(ItemGroup, String) - */ - public void addButton(ItemGroupButton button) { - this.buttons.add(button); - } - - /** - * Adds a new tab to this group - * - * @param icon The icon to use - * @param name The name of the tab, used for the translation key - * @param contentTag The tag used for filling this tab - * @param texture The texture to use for drawing the button - * @see Icon#of(ItemConvertible) - */ - public void addTab(Icon icon, String name, @Nullable TagKey contentTag, Identifier texture, boolean primary) { - this.tabs.add(new ItemGroupTab( - icon, - ButtonDefinition.tooltipFor(this, "tab", name), - contentTag == null - ? (context, entries) -> {} - : (context, entries) -> Registries.ITEM.stream().filter(item -> item.getRegistryEntry().isIn(contentTag)).forEach(entries::add), - texture, - primary - )); - } - - /** - * Adds a new tab to this group, using the default button texture - * - * @param icon The icon to use - * @param name The name of the tab, used for the translation key - * @param contentTag The tag used for filling this tab - * @see Icon#of(ItemConvertible) - */ - public void addTab(Icon icon, String name, @Nullable TagKey contentTag, boolean primary) { - addTab(icon, name, contentTag, ItemGroupTab.DEFAULT_TEXTURE, primary); - } - - /** - * Adds a new tab to this group, using the default button texture - * - * @param icon The icon to use - * @param name The name of the tab, used for the translation key - * @param contentSupplier The function used for filling this tab - * @param texture The texture to use for drawing the button - * @see Icon#of(ItemConvertible) - */ - public void addCustomTab(Icon icon, String name, ItemGroupTab.ContentSupplier contentSupplier, Identifier texture, boolean primary) { - this.tabs.add(new ItemGroupTab( - icon, - ButtonDefinition.tooltipFor(this, "tab", name), - contentSupplier, texture, primary - )); - } - - /** - * Adds a new tab to this group - * - * @param icon The icon to use - * @param name The name of the tab, used for the translation key - * @param contentSupplier The function used for filling this tab - * @see Icon#of(ItemConvertible) - */ - public void addCustomTab(Icon icon, String name, ItemGroupTab.ContentSupplier contentSupplier, boolean primary) { - this.addCustomTab(icon, name, contentSupplier, ItemGroupTab.DEFAULT_TEXTURE, primary); - } - - @Override - public void updateEntries(DisplayContext context) { - super.updateEntries(context); - - var searchEntries = new SearchOnlyEntries(this, context.enabledFeatures()); - - this.collectItemsFromRegistry(searchEntries, -1); - this.tabs.forEach(tab -> tab.contentSupplier().addItems(context, searchEntries)); - - ((ItemGroupAccessor) this).owo$setSearchTabStacks(searchEntries.searchTabStacks); - } - - protected void collectItemsFromRegistry(Entries entries, int tab) { - Registries.ITEM.stream() - .filter(item -> ((OwoItemExtensions) item).owo$group() == this && (tab < 0 || tab == ((OwoItemExtensions) item).owo$tab())) - .forEach(item -> ((OwoItemExtensions) item).owo$stackGenerator().accept(item, entries)); - } - - // Getters and setters - - /** - * Select only {@code tab}, deselecting all other tabs, - * using {@code context} for re-population - */ - public void selectSingleTab(int tab, DisplayContext context) { - this.activeTabs.clear(); - this.activeTabs.add(tab); - - this.updateEntries(context); - } - - /** - * Select {@code tab} in addition to other currently selected - * tabs, using {@code context} for re-population. - *

- * If this group does not allow multiple selection, behaves - * like {@link #selectSingleTab(int, DisplayContext)} - */ - public void selectTab(int tab, DisplayContext context) { - if (!this.allowMultiSelect) { - this.activeTabs.clear(); - } - - this.activeTabs.add(tab); - this.updateEntries(context); - } - - /** - * Deselect {@code tab} if it is currently selected, using {@code context} for - * re-population. If this results in no tabs being selected, all tabs are - * automatically selected instead - */ - public void deselectTab(int tab, DisplayContext context) { - if (!this.allowMultiSelect) return; - - this.activeTabs.remove(tab); - if (this.activeTabs.isEmpty()) { - for (int tabIdx = 0; tabIdx < this.tabs.size(); tabIdx++) { - this.activeTabs.add(tabIdx); - } - } - - this.updateEntries(context); - } - - /** - * Shorthand for {@link #selectTab(int, DisplayContext)} or - * {@link #deselectTab(int, DisplayContext)}, depending on the tabs - * current state - */ - public void toggleTab(int tab, DisplayContext context) { - if (this.isTabSelected(tab)) { - this.deselectTab(tab, context); - } else { - this.selectTab(tab, context); - } - } - - /** - * @return A set containing the indices of all currently - * selected tabs - */ - public IntSet selectedTabs() { - return this.activeTabsView; - } - - /** - * @return {@code true} if {@code tab} is currently selected - */ - public boolean isTabSelected(int tab) { - return this.activeTabs.contains(tab); - } - - public @Nullable Identifier getOwoBackgroundTexture() { - return this.backgroundTexture; - } - - public @Nullable ScrollerTextures getScrollerTextures() { - return this.scrollerTextures; - } - - public @Nullable TabTextures getTabTextures() { - return this.tabTextures; - } - - public int getTabStackHeight() { - return tabStackHeight; - } - - public int getButtonStackHeight() { - return buttonStackHeight; - } - - public boolean hasDynamicTitle() { - return this.useDynamicTitle && (this.tabs.size() > 1 || this.shouldDisplaySingleTab()); - } - - public boolean shouldDisplaySingleTab() { - return this.displaySingleTab; - } - - public boolean canSelectMultipleTabs() { - return this.allowMultiSelect; - } - - public List getButtons() { - return buttons; - } - - public ItemGroupTab getTab(int index) { - return index < this.tabs.size() ? this.tabs.get(index) : null; - } - - public Icon icon() { - return this.icon == null - ? this.icon = this.iconSupplier.get() - : this.icon; - } - - @Override - public boolean shouldDisplay() { - return true; - } - - public Identifier id() { - return Registries.ITEM_GROUP.getId(this); - } - - public static class Builder { - - private final Identifier id; - private final Supplier iconSupplier; - - private Consumer initializer = owoItemGroup -> {}; - private int tabStackHeight = 4; - private int buttonStackHeight = 4; - private @Nullable Identifier backgroundTexture = null; - private @Nullable ScrollerTextures scrollerTextures = null; - private @Nullable TabTextures tabTextures = null; - private boolean useDynamicTitle = true; - private boolean displaySingleTab = false; - private boolean allowMultiSelect = true; - - private Builder(Identifier id, Supplier iconSupplier) { - this.id = id; - this.iconSupplier = iconSupplier; - } - - public Builder initializer(Consumer initializer) { - this.initializer = initializer; - return this; - } - - public Builder tabStackHeight(int tabStackHeight) { - this.tabStackHeight = tabStackHeight; - return this; - } - - public Builder buttonStackHeight(int buttonStackHeight) { - this.buttonStackHeight = buttonStackHeight; - return this; - } - - public Builder backgroundTexture(@Nullable Identifier backgroundTexture) { - this.backgroundTexture = backgroundTexture; - return this; - } - - public Builder scrollerTextures(ScrollerTextures scrollerTextures) { - this.scrollerTextures = scrollerTextures; - return this; - } - - public Builder tabTextures(TabTextures tabTextures) { - this.tabTextures = tabTextures; - return this; - } - - public Builder disableDynamicTitle() { - this.useDynamicTitle = false; - return this; - } - - public Builder displaySingleTab() { - this.displaySingleTab = true; - return this; - } - - public Builder withoutMultipleSelection() { - this.allowMultiSelect = false; - return this; - } - - public OwoItemGroup build() { - final var group = new OwoItemGroup(id, initializer, iconSupplier, tabStackHeight, buttonStackHeight, backgroundTexture, scrollerTextures, tabTextures, useDynamicTitle, displaySingleTab, allowMultiSelect) {}; - Registry.register(Registries.ITEM_GROUP, this.id, group); - return group; - } - } - - protected static class SearchOnlyEntries extends EntriesImpl { - - public SearchOnlyEntries(ItemGroup group, FeatureSet enabledFeatures) { - super(group, enabledFeatures); - } - - @Override - public void add(ItemStack stack, StackVisibility visibility) { - if (visibility == StackVisibility.PARENT_TAB_ONLY) return; - super.add(stack, StackVisibility.SEARCH_TAB_ONLY); - } - } - - public record ScrollerTextures(Identifier enabled, Identifier disabled) {} - public record TabTextures(Identifier topSelected, Identifier topSelectedFirstColumn, Identifier topUnselected, Identifier bottomSelected, Identifier bottomSelectedFirstColumn, Identifier bottomUnselected) {} - - // Utility - - /** - * Defines a button's appearance and translation key - *

- * Used by {@link ItemGroupButtonWidget} - */ - public interface ButtonDefinition { - - Icon icon(); - - Identifier texture(); - - Text tooltip(); - - static Text tooltipFor(ItemGroup group, String component, String componentName) { - var registryId = Registries.ITEM_GROUP.getId(group); - var groupId = registryId.getNamespace().equals("minecraft") - ? registryId.getPath() - : registryId.getNamespace() + "." + registryId.getPath(); - - return Text.translatable("itemGroup." + groupId + "." + component + "." + componentName); - } - - } -} diff --git a/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupBuilder.java b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupBuilder.java new file mode 100644 index 000000000..2fca1e1cf --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupBuilder.java @@ -0,0 +1,131 @@ +package io.wispforest.owo.itemgroup; + +import io.wispforest.owo.itemgroup.base.OwoItemGroup; +import io.wispforest.owo.itemgroup.base.OwoItemGroupState; +import io.wispforest.owo.itemgroup.base.Icon; +import io.wispforest.owo.itemgroup.gui.ScrollerTextures; +import io.wispforest.owo.itemgroup.gui.TabTextures; +import io.wispforest.owo.itemgroup.impl.OwoItemGroupImpl; +import io.wispforest.owo.itemgroup.impl.OwoItemGroupStateImpl; +import io.wispforest.owo.mixin.itemgroup.ItemGroupAccessor; +import io.wispforest.owo.util.pond.OwoItemGroupExtension; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.apache.commons.lang3.mutable.MutableObject; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class OwoItemGroupBuilder { + + private final Event onInitEvent = EventFactory.createArrayBacked( + ExtensionInitialization.class, + invokers -> extension -> { + for (var invoker : invokers) invoker.onInitialization(extension); + }); + + private final RegistryKey id; + private Supplier iconSupplier = () -> Icon.EMPTY_INSTANCE; + private int tabStackHeight = 4; + private int buttonStackHeight = 4; + private @Nullable Identifier backgroundTexture = null; + private @Nullable ScrollerTextures scrollerTextures = null; + private @Nullable TabTextures tabTextures = null; + private boolean useDynamicTitle = true; + private boolean allowMultiSelect = true; + + public OwoItemGroupBuilder(RegistryKey id) { + this.id = id; + } + + public static RegistryKey createItemGroup(Identifier id, Supplier iconSupplier, Consumer buildHandler) { + var group = FabricItemGroup.builder() + .displayName(Text.translatable("itemGroup.%s.%s".formatted(id.getNamespace(), id.getPath()))) + .build(); + + var builder = new OwoItemGroupBuilder(RegistryKey.of(RegistryKeys.ITEM_GROUP, id)); + + builder.iconSupplier = iconSupplier; + + buildHandler.accept(builder); + + ((OwoItemGroupExtension) group).owo$setBuilder(builder); + + Registry.register(Registries.ITEM_GROUP, id, group); + + return RegistryKey.of(RegistryKeys.ITEM_GROUP, id); + } + + public static void modifyItemGroup(RegistryKey itemGroupKey, Consumer buildHandler) { + var group = Registries.ITEM_GROUP.getValueOrThrow(itemGroupKey); + + var builder = ((OwoItemGroupExtension) group).owo$getBuilder(); + + if (builder == null) { + var id = Registries.ITEM_GROUP.getKey(group).orElseThrow(); + + builder = new OwoItemGroupBuilder(id); + + ((OwoItemGroupExtension) group).owo$setBuilder(builder); + } + + buildHandler.accept(builder); + } + + public Event initializationEvent() { + return this.onInitEvent; + } + + public void initializer(ExtensionInitialization invoker) { + this.onInitEvent.register(invoker); + } + + public OwoItemGroupBuilder tabStackHeight(int tabStackHeight) { + this.tabStackHeight = tabStackHeight; + return this; + } + + public OwoItemGroupBuilder buttonStackHeight(int buttonStackHeight) { + this.buttonStackHeight = buttonStackHeight; + return this; + } + + public OwoItemGroupBuilder backgroundTexture(Identifier backgroundTexture) { + this.backgroundTexture = backgroundTexture; + return this; + } + + public OwoItemGroupBuilder scrollerTextures(ScrollerTextures scrollerTextures) { + this.scrollerTextures = scrollerTextures; + return this; + } + + public OwoItemGroupBuilder tabTextures(TabTextures tabTextures) { + this.tabTextures = tabTextures; + return this; + } + + public OwoItemGroupBuilder disableDynamicTitle() { + this.useDynamicTitle = false; + return this; + } + + @ApiStatus.Internal + private OwoItemGroupImpl build() { + return new OwoItemGroupImpl(id, iconSupplier, backgroundTexture, scrollerTextures, tabTextures, tabStackHeight, buttonStackHeight, useDynamicTitle, allowMultiSelect); + } + + public interface ExtensionInitialization { + void onInitialization(OwoItemGroup extension); + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupLoader.java b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupLoader.java new file mode 100644 index 000000000..f7649aa82 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupLoader.java @@ -0,0 +1,178 @@ +package io.wispforest.owo.itemgroup; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Either; +import io.wispforest.endec.Endec; +import io.wispforest.endec.StructEndec; +import io.wispforest.endec.format.gson.GsonDeserializer; +import io.wispforest.endec.impl.StructEndecBuilder; +import io.wispforest.owo.itemgroup.base.ButtonDefinition; +import io.wispforest.owo.itemgroup.base.Icon; +import io.wispforest.owo.itemgroup.core.*; +import io.wispforest.owo.itemgroup.impl.OwoItemGroupImpl; +import io.wispforest.owo.mixin.itemgroup.ItemGroupAccessor; +import io.wispforest.owo.moddata.ModDataConsumer; +import io.wispforest.owo.serialization.CodecUtils; +import io.wispforest.owo.serialization.endec.MinecraftEndecs; +import net.fabricmc.fabric.api.event.registry.RegistryEntryAddedCallback; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemGroups; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages loading and adding JSON-based tabs to preexisting {@code ItemGroup}s + * without needing to depend on owo + *

+ * This is used instead of a {@link JsonDataLoader} because + * it needs to load on the client + */ +@ApiStatus.Internal +public class OwoItemGroupLoader implements ModDataConsumer { + + public static final String EXTENDED_TAB_NAME = "extended_base_tab"; + + public static final OwoItemGroupLoader INSTANCE = new OwoItemGroupLoader(); + + private static final Map BUFFERED_GROUPS = new HashMap<>(); + + private OwoItemGroupLoader() {} + + public static void onGroupCreated(ItemGroup group) { + var groupId = Registries.ITEM_GROUP.getId(group); + + if (!BUFFERED_GROUPS.containsKey(groupId)) return; + + INSTANCE.handleData(group, BUFFERED_GROUPS.remove(groupId)); + } + + //-- + + @Override + public void acceptParsedFile(Identifier id, JsonObject json) { + try { + var data = Data.ENDEC.decodeFully(GsonDeserializer::of, json); + + if (data.tabs().isEmpty() && data.buttons().isEmpty()) return; + + var searchGroup = ItemGroups.getGroups() + .stream() + .filter(group -> data.targetGroup().equals(Registries.ITEM_GROUP.getId(group))) + .findFirst(); + + searchGroup.ifPresentOrElse( + group -> handleData(group, data), + () -> BUFFERED_GROUPS.put(data.targetGroup(), data) + ); + } catch (Exception e) { + throw new IllegalStateException("Unable to handle OwoItemGroup [" + id + "] data due to a exception!", e); + } + } + + @Override + public String getDataSubdirectory() { + return "item_group_tabs"; + } + + //-- + + public void handleData(ItemGroup targetGroup, Data data) { + final ItemGroupTab baseGroupTab; + + if (data.extend()) { + baseGroupTab = new ItemGroupTab( + EXTENDED_TAB_NAME, + Icon.of(targetGroup.getIcon()), + targetGroup.getDisplayName(), + ((ItemGroupAccessor) targetGroup).owo$getEntryCollector()::accept, + ItemGroupTab.DEFAULT_TEXTURE, + true + ); + } else { + baseGroupTab = null; + } + + OwoItemGroupBuilder.modifyItemGroup(data.targetGroupKey(), builder -> { + builder.initializer(ext -> { + var extImpl = ((OwoItemGroupImpl) ext); + + if (baseGroupTab != null && !extImpl.hasTab(EXTENDED_TAB_NAME)) { + ((OwoItemGroupImpl) ext).addTabFirst(baseGroupTab); + } + + ext.addTabs(data.createTabs()); + ext.addButtons(data.createButtons()); + }); + }); + } + + static { + RegistryEntryAddedCallback.event(Registries.ITEM_GROUP).register((rawId, id, group) -> { + OwoItemGroupLoader.onGroupCreated(group); + }); + } +} + + +record Tab(String name, Icon icon, TagKey tag, Identifier texture) { + public static final StructEndec ENDEC = StructEndecBuilder.of( + Endec.STRING.fieldOf("name", Tab::name), + IconEndec.ENDEC.fieldOf("icon", Tab::icon), + MinecraftEndecs.unprefixedTagKey(RegistryKeys.ITEM).fieldOf("tag", Tab::tag), + MinecraftEndecs.IDENTIFIER.optionalFieldOf("texture", Tab::texture, ItemGroupTab.DEFAULT_TEXTURE), + Tab::new + ); +} + +record Button(String name, String url, Icon icon) { + public static final StructEndec