diff --git a/build.gradle b/build.gradle index 2f0a052ab..ab1c63055 100644 --- a/build.gradle +++ b/build.gradle @@ -63,19 +63,23 @@ allprojects { } } - publishing { - publications { - mavenJava(MavenPublication) { - from components.java + var validPublishProjects = ["owo-lib", "owo-sentinel"]; + + if (validPublishProjects.contains(project.archives_base_name)) { + publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } } - } - repositories { - maven { - url ENV.MAVEN_URL - credentials { - username ENV.MAVEN_USER - password ENV.MAVEN_PASSWORD + repositories { + maven { + url ENV.MAVEN_URL + credentials { + username ENV.MAVEN_USER + password ENV.MAVEN_PASSWORD + } } } } @@ -103,6 +107,46 @@ sourceSets { } } +//-- Used to pull in the given AP files to be shipped under owolib as single jar + +configurations { + owoConfigAPJava { canBeResolved = true } + owoConfigAPResources { canBeResolved = true } +} + +tasks.named('compileJava', JavaCompile) { + dependsOn(configurations.owoConfigAPJava) + source(configurations.owoConfigAPJava) +} + +processResources { + dependsOn(configurations.owoConfigAPResources) + from(configurations.owoConfigAPResources) +} + +tasks.named('javadoc', Javadoc).configure { + dependsOn(configurations.owoConfigAPJava) + source(configurations.owoConfigAPJava) +} + +tasks.named('sourcesJar', Jar) { + dependsOn(configurations.owoConfigAPJava) + dependsOn(configurations.owoConfigAPResources) + from(configurations.owoConfigAPJava) + from(configurations.owoConfigAPResources) +} + +dependencies { + compileOnly(annotationProcessor(project(':owo-config-ap'))) + + owoConfigAPJava project(path: ':owo-config-ap', configuration: 'owoConfigAPJava') + owoConfigAPResources project(path: ':owo-config-ap', configuration: 'owoConfigAPResources') + + //testmodImplementation(testmodAnnotationProcessor(project(':owo-config-ap'))) +} + +//-- + loom { runs { testmodClient { @@ -148,8 +192,7 @@ dependencies { modCompileOnly("xyz.nucleoid:server-translations-api:${project.stapi_version}") - testmodImplementation sourceSets.main.output - testmodAnnotationProcessor sourceSets.main.output + testmodImplementation(testmodAnnotationProcessor(sourceSets.main.output)) } javadoc { diff --git a/gradle.properties b/gradle.properties index a6a8273f1..7647dfb0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ archives_base_name=owo-lib fabric_version=0.131.0+1.21.8 # https://maven.shedaniel.me/me/shedaniel/RoughlyEnoughItems-fabric/ -rei_version=19.0.809 +rei_version=20.0.811 # https://maven.terraformersmc.com/releases/dev/emi/emi-fabric/ emi_version=1.1.18+1.21.1 @@ -27,3 +27,6 @@ modmenu_version=15.0.0-beta.3 # https://maven.nucleoid.xyz/xyz/nucleoid/server-translations-api/ stapi_version=2.5.0+1.21.5-rc1 + +# Enabled ItemViewers (rei or emi in a comma separated list) +item_viewers=rei diff --git a/owo-config-ap/build.gradle b/owo-config-ap/build.gradle new file mode 100644 index 000000000..9419e4868 --- /dev/null +++ b/owo-config-ap/build.gradle @@ -0,0 +1,19 @@ +loom { + runConfigs.client.ideConfigGenerated = true +} + +configurations { + owoConfigAPJava { + canBeResolved = false + canBeConsumed = true + } + owoConfigAPResources { + canBeResolved = false + canBeConsumed = true + } +} + +artifacts { + owoConfigAPJava sourceSets.main.java.sourceDirectories.singleFile + owoConfigAPResources sourceSets.main.resources.sourceDirectories.singleFile +} diff --git a/owo-config-ap/gradle.properties b/owo-config-ap/gradle.properties new file mode 100644 index 000000000..8db73db8e --- /dev/null +++ b/owo-config-ap/gradle.properties @@ -0,0 +1 @@ +archives_base_name=owo-config-ap diff --git a/src/main/java/io/wispforest/owo/config/ConfigAP.java b/owo-config-ap/src/main/java/io/wispforest/owo/config/ConfigAP.java similarity index 82% rename from src/main/java/io/wispforest/owo/config/ConfigAP.java rename to owo-config-ap/src/main/java/io/wispforest/owo/config/ConfigAP.java index 479e20512..2e29787cb 100644 --- a/src/main/java/io/wispforest/owo/config/ConfigAP.java +++ b/owo-config-ap/src/main/java/io/wispforest/owo/config/ConfigAP.java @@ -126,7 +126,7 @@ public boolean process(Set annotations, RoundEnvironment try { var file = this.processingEnv.getFiler().createSourceFile(wrapperName); try (var writer = new PrintWriter(file.openWriter())) { - writer.println(makeWrapper(wrapperName, className, this.collectFields(Option.Key.ROOT, clazz, clazz.getAnnotation(Config.class).defaultHook()))); + writer.println(makeWrapper(wrapperName, className, this.collectFields(Key.ROOT, clazz, clazz.getAnnotation(Config.class).defaultHook()))); } } catch (IOException e) { throw new RuntimeException("Failed to generate config wrapper", e); @@ -137,7 +137,7 @@ public boolean process(Set annotations, RoundEnvironment return true; } - private List collectFields(Option.Key parent, TypeElement clazz, boolean defaultHook) { + private List collectFields(Key parent, TypeElement clazz, boolean defaultHook) { var messager = this.processingEnv.getMessager(); var list = new ArrayList(); @@ -209,28 +209,28 @@ private String makeWrapper(String wrapperClassName, String configClassName, List .replace("{accessors}\n", accessorMethods.finish()); } - private String makeGetAccessor(String fieldName, Option.Key fieldKey, TypeMirror fieldType) { + private String makeGetAccessor(String fieldName, Key fieldKey, TypeMirror fieldType) { return GET_ACCESSOR_TEMPLATE .replace("{option_instance}", constantNameOf(fieldKey)) .replace("{field_name}", fieldName) .replace("{field_type}", fieldType.toString()); } - private String makeSetAccessor(String fieldName, Option.Key fieldKey, TypeMirror fieldType) { + private String makeSetAccessor(String fieldName, Key fieldKey, TypeMirror fieldType) { return SET_ACCESSOR_TEMPLATE .replace("{option_instance}", constantNameOf(fieldKey)) .replace("{field_name}", fieldName) .replace("{field_type}", fieldType.toString()); } - private String makeSubscribe(String fieldName, Option.Key fieldKey, TypeMirror fieldType) { + private String makeSubscribe(String fieldName, Key fieldKey, TypeMirror fieldType) { return SUBSCRIBE_TEMPLATE .replace("{option_instance}", constantNameOf(fieldKey)) .replace("{field_name}", fieldName) .replace("{field_type}", this.primitivesToWrappers.getOrDefault(fieldType, fieldType).toString()); } - private String constantNameOf(Option.Key key) { + private String constantNameOf(Key key) { return key.asString().replace(".", "_"); } @@ -240,11 +240,11 @@ private interface ConfigField { private final class ValueField implements ConfigField { private final String name; - private final Option.Key key; + private final Key key; private final TypeMirror type; private final boolean makeSubscribe; - private ValueField(String name, Option.Key key, TypeMirror type, boolean makeSubscribe) { + private ValueField(String name, Key key, TypeMirror type, boolean makeSubscribe) { this.name = name; this.key = key; this.type = type; @@ -357,4 +357,86 @@ public char charAt(int index) { return this.builder.toString(); } } + + private record Key(String[] path) { + + public static final Key ROOT = new Key(new String[0]); + + public Key(List path) { + this(path.toArray(String[]::new)); + } + + public Key(String key) { + this(key.split("\\.")); + } + + /** + * @return The immediate parent of this key, + * or {@link #ROOT} if the parent is the root key + */ + public Key parent() { + if (this.path.length <= 1) return ROOT; + + var newPath = new String[this.path.length - 1]; + System.arraycopy(this.path, 0, newPath, 0, this.path.length - 1); + return new Key(newPath); + } + + /** + * Create the key for a child of this key + * + * @param childName The name of the child + */ + public Key child(String childName) { + var newPath = new String[this.path.length + 1]; + System.arraycopy(this.path, 0, newPath, 0, this.path.length); + newPath[this.path.length] = childName; + return new Key(newPath); + } + + /** + * @return The segments of this key joined with {@code .} + */ + public String asString() { + return String.join(".", this.path); + } + + /** + * @return The name of the element this key describes, + * without any of its parents + */ + public String name() { + if (this.path.length < 1) return ""; + return this.path[this.path.length - 1]; + } + + /** + * @return {@code true} if and only if this + * key is reference-equal to {@link #ROOT} + */ + public boolean isRoot() { + return this == ROOT; + } + + // Records don't play nicely with arrays, thus need to manually + // declare all the record autogenerated stuff here + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key = (Key) o; + return Arrays.equals(path, key.path); + } + + @Override + public int hashCode() { + return Arrays.hashCode(path); + } + + @Override + public String toString() { + return "Key{" + "path=" + Arrays.toString(path) + '}'; + } + } } diff --git a/src/main/java/io/wispforest/owo/config/annotation/Config.java b/owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Config.java similarity index 95% rename from src/main/java/io/wispforest/owo/config/annotation/Config.java rename to owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Config.java index aeafacb23..34da64d14 100644 --- a/src/main/java/io/wispforest/owo/config/annotation/Config.java +++ b/owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Config.java @@ -1,5 +1,7 @@ package io.wispforest.owo.config.annotation; +import com.mojang.datafixers.types.templates.Hook; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/io/wispforest/owo/config/annotation/Hook.java b/owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Hook.java similarity index 100% rename from src/main/java/io/wispforest/owo/config/annotation/Hook.java rename to owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Hook.java diff --git a/src/main/java/io/wispforest/owo/config/annotation/Nest.java b/owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Nest.java similarity index 100% rename from src/main/java/io/wispforest/owo/config/annotation/Nest.java rename to owo-config-ap/src/main/java/io/wispforest/owo/config/annotation/Nest.java diff --git a/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/owo-config-ap/src/main/resources/META-INF/services/javax.annotation.processing.Processor similarity index 100% rename from src/main/resources/META-INF/services/javax.annotation.processing.Processor rename to owo-config-ap/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/settings.gradle b/settings.gradle index eb1e54ddf..6501d7e51 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,4 +9,5 @@ pluginManagement { } } -include 'owo-sentinel' \ No newline at end of file +include 'owo-sentinel' +include 'owo-config-ap' \ No newline at end of file diff --git a/src/main/java/io/wispforest/owo/Owo.java b/src/main/java/io/wispforest/owo/Owo.java index 7a6835f56..7707d7343 100644 --- a/src/main/java/io/wispforest/owo/Owo.java +++ b/src/main/java/io/wispforest/owo/Owo.java @@ -2,13 +2,19 @@ import io.wispforest.owo.client.screens.ScreenInternals; import io.wispforest.owo.command.debug.OwoDebugCommands; +import io.wispforest.owo.impl.OwoConfigImpl; +import io.wispforest.owo.itemgroup.data.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.ItemGroups; import net.minecraft.server.MinecraftServer; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -45,6 +51,8 @@ public class Owo implements ModInitializer { DEBUG = debug; } + public static final OwoConfigImpl CONFIG = OwoConfigImpl.createAndLoad(); + @Override @ApiStatus.Internal public void onInitialize() { @@ -57,6 +65,12 @@ public void onInitialize() { Wisdom.spread(); + OwoFreezer.registerFreezeCallback(() -> { + ModDataLoader.load(OwoItemGroupLoader.INSTANCE); + + 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..2efffdbdc 100644 --- a/src/main/java/io/wispforest/owo/client/OwoClient.java +++ b/src/main/java/io/wispforest/owo/client/OwoClient.java @@ -4,8 +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.moddata.ModDataLoader; +import io.wispforest.owo.itemgroup.data.CondensedEntryLoader; import io.wispforest.owo.ui.core.OwoUIPipelines; import io.wispforest.owo.ui.parsing.UIModelLoader; import io.wispforest.owo.ui.renderstate.OwoSpecialGuiElementRenderers; @@ -45,10 +44,9 @@ 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()); + ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(CondensedEntryLoader.INSTANCE); OwoUIPipelines.register(); 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..25d5b69ee 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.get(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/modmenu/OwoModMenuPlugin.java b/src/main/java/io/wispforest/owo/compat/modmenu/OwoModMenuPlugin.java index bb7308ff6..52235d855 100644 --- a/src/main/java/io/wispforest/owo/compat/modmenu/OwoModMenuPlugin.java +++ b/src/main/java/io/wispforest/owo/compat/modmenu/OwoModMenuPlugin.java @@ -3,6 +3,7 @@ import com.google.common.collect.ForwardingMap; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; +import io.wispforest.owo.Owo; import io.wispforest.owo.config.ui.ConfigScreenProviders; import net.minecraft.util.Util; import org.jetbrains.annotations.ApiStatus; @@ -28,4 +29,9 @@ public class OwoModMenuPlugin implements ModMenuApi { public Map> getProvidedConfigScreenFactories() { return OWO_FACTORIES; } + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> OWO_FACTORIES.getOrDefault("owo", parent1 -> null).create(parent); + } } 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..4a68abd38 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.get(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/impl/OwoConfigModel.java b/src/main/java/io/wispforest/owo/impl/OwoConfigModel.java new file mode 100644 index 000000000..a62cdefbb --- /dev/null +++ b/src/main/java/io/wispforest/owo/impl/OwoConfigModel.java @@ -0,0 +1,19 @@ +package io.wispforest.owo.impl; + +import io.wispforest.owo.config.annotation.*; +import io.wispforest.owo.ui.core.Color; +import io.wispforest.owo.util.Wisdom; + +@Modmenu(modId = "owo") +@Config(wrapperName = "OwoConfigImpl", name = "owo") +public class OwoConfigModel { + + @SectionHeader("condensed_entries") + public boolean expandedCondensedEntries = false; + + public boolean showBackgroundColor = true; + public boolean showBorderColor = true; + + @WithAlpha + public Color borderColor = Color.ofArgb(0xFF3955e5); +} 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..ffe9f353f --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/OwoItemGroupBuilder.java @@ -0,0 +1,128 @@ +package io.wispforest.owo.itemgroup; + +import io.wispforest.owo.itemgroup.base.OwoItemGroup; +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.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.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +// TODO [ItemGroupPR]: DOCUMENT +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.NONE; + 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/OwoItemSettingsExtension.java b/src/main/java/io/wispforest/owo/itemgroup/OwoItemSettingsExtension.java index 3fb937cc4..5f5cbc8c7 100644 --- a/src/main/java/io/wispforest/owo/itemgroup/OwoItemSettingsExtension.java +++ b/src/main/java/io/wispforest/owo/itemgroup/OwoItemSettingsExtension.java @@ -1,8 +1,11 @@ package io.wispforest.owo.itemgroup; +import io.wispforest.owo.itemgroup.base.OwoItemGroup; +import io.wispforest.owo.itemgroup.core.ItemGroupReference; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.Item; import net.minecraft.item.ItemGroup; +import net.minecraft.registry.RegistryKey; import net.minecraft.util.Hand; import net.minecraft.world.World; @@ -17,11 +20,11 @@ default Item.Settings group(ItemGroupReference ref) { /** * @param group The item group this item should appear in */ - default Item.Settings group(OwoItemGroup group) { + default Item.Settings group(RegistryKey group) { throw new IllegalStateException("Implemented in mixin."); } - default OwoItemGroup group() { + default RegistryKey group() { throw new IllegalStateException("Implemented in mixin."); } diff --git a/src/main/java/io/wispforest/owo/itemgroup/base/ButtonDefinition.java b/src/main/java/io/wispforest/owo/itemgroup/base/ButtonDefinition.java new file mode 100644 index 000000000..650f29505 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/ButtonDefinition.java @@ -0,0 +1,33 @@ +package io.wispforest.owo.itemgroup.base; + +import io.wispforest.owo.itemgroup.gui.ItemGroupButtonWidget; +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.RegistryKey; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +/** + * Defines a button's appearance and translation key + *

+ * Used by {@link ItemGroupButtonWidget} + */ +public interface ButtonDefinition { + + String name(); + + Icon icon(); + + Identifier texture(); + + Text tooltip(); + + static Text tooltipFor(RegistryKey group, String component, String componentName) { + var registryId = group.getValue(); + 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/base/Icon.java b/src/main/java/io/wispforest/owo/itemgroup/base/Icon.java new file mode 100644 index 000000000..83f9d110d --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/Icon.java @@ -0,0 +1,117 @@ +package io.wispforest.owo.itemgroup.base; + +import com.mojang.datafixers.util.Either; +import io.wispforest.endec.Endec; +import io.wispforest.endec.StructEndec; +import io.wispforest.endec.impl.StructEndecBuilder; +import io.wispforest.owo.serialization.CodecUtils; +import io.wispforest.owo.serialization.DispatchedEndec; +import io.wispforest.owo.serialization.IdentifiedData; +import io.wispforest.owo.serialization.endec.MinecraftEndecs; +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +/// +/// An icon used for rendering on buttons in {@link OwoItemGroup} with +/// default implementations for [ItemStack], textures, and animated textures +/// +public interface Icon extends IdentifiedData { + + @ApiStatus.Internal + Icon NONE = () -> DispatchedEndec.EMPTY_ID; + + DispatchedEndec ENDEC = DispatchedEndec.of() + .idEndec(MinecraftEndecs.identifierEndec("owo")) + .emptyValue(() -> NONE) + .allowTypelessData() + .baseClasses(ItemIcon.class, TextureIcon.class, AnimatedTextureIcon.class) + .create(); + + static Icon of(ItemStack stack) { + return new ItemIcon(stack); + } + + static Icon of(ItemConvertible item) { + return of(new ItemStack(item)); + } + + record ItemIcon(ItemStack stack) implements Icon { + public static final Identifier ID = Identifier.of("owo", "itemstack"); + + public static final StructEndec ENDEC = Icon.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + CodecUtils.eitherEndec(MinecraftEndecs.ITEM_STACK, MinecraftEndecs.ofRegistry(Registries.ITEM)) + .xmap(either -> Either.unwrap(either.mapRight(Item::getDefaultStack)), Either::left) + .fieldOf("stack", ItemIcon::stack), + ItemIcon::new + )); + + @Override + public Identifier getTypeId() { + return ID; + } + } + + static Icon of(Identifier texture, int u, int v, int textureWidth, int textureHeight) { + return new TextureIcon(texture, u, v, textureWidth, textureHeight); + } + + record TextureIcon(Identifier texture, int u, int v, int textureWidth, int textureHeight) implements Icon { + public static final Identifier ID = Identifier.of("owo", "texture"); + + public static final StructEndec ENDEC = Icon.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + MinecraftEndecs.IDENTIFIER.fieldOf("texture", TextureIcon::texture), + Endec.INT.fieldOf("u", TextureIcon::u), + Endec.INT.fieldOf("v", TextureIcon::v), + Endec.INT.fieldOf("texture_width", TextureIcon::textureWidth), + Endec.INT.fieldOf("texture_height", TextureIcon::textureHeight), + TextureIcon::new + )); + + @Override + public Identifier getTypeId() { + return ID; + } + } + + /** + * Creates an Animated ItemGroup Icon + * + * @param texture The texture to render, this is the spritesheet + * @param textureWidth The width of the texture + * @param textureHeight The width of the texture + * @param frameDelay The delay in milliseconds between frames + * @param loop Whether the given animation should loop or only play once + * @return The created icon instance + */ + static Icon of(Identifier texture, int textureWidth, int textureHeight, int frameDelay, boolean loop, boolean reversible) { + return new AnimatedTextureIcon(texture, textureWidth, textureHeight, frameDelay, loop, reversible); + } + + record AnimatedTextureIcon(Identifier texture, int textureWidth, int textureHeight, int frameDelay, boolean loop, boolean reversible) implements Icon { + public static final Identifier ID = Identifier.of("owo", "animated_texture"); + + public static final StructEndec ENDEC = Icon.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + MinecraftEndecs.IDENTIFIER.fieldOf("texture", AnimatedTextureIcon::texture), + Endec.INT.fieldOf("texture_width", AnimatedTextureIcon::textureWidth), + Endec.INT.fieldOf("texture_height", AnimatedTextureIcon::textureHeight), + Endec.INT.fieldOf("frame_delay", AnimatedTextureIcon::frameDelay), + Endec.BOOLEAN.optionalFieldOf("should_loop", AnimatedTextureIcon::loop, true), + Endec.BOOLEAN.optionalFieldOf("reversible", AnimatedTextureIcon::reversible, false), + AnimatedTextureIcon::new + )); + + @Override + public Identifier getTypeId() { + return ID; + } + } + + Identifier getTypeId(); +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/base/ItemStacksSupplier.java b/src/main/java/io/wispforest/owo/itemgroup/base/ItemStacksSupplier.java new file mode 100644 index 000000000..008d6714c --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/ItemStacksSupplier.java @@ -0,0 +1,189 @@ +package io.wispforest.owo.itemgroup.base; + +import io.wispforest.endec.Endec; +import io.wispforest.endec.StructEndec; +import io.wispforest.endec.impl.StructEndecBuilder; +import io.wispforest.owo.itemgroup.util.ItemStackOps; +import io.wispforest.owo.serialization.DispatchedEndec; +import io.wispforest.owo.serialization.IdentifiedData; +import io.wispforest.owo.serialization.endec.MinecraftEndecs; +import net.minecraft.block.Block; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collections; +import java.util.List; +import java.util.SequencedCollection; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public interface ItemStacksSupplier extends Supplier> { + + @ApiStatus.Internal + ItemStacksSupplier EMPTY = Collections::emptyList; + + DispatchedEndec ENDEC = DispatchedEndec.ofOptionalIdentifiedData(() -> ItemStacksSupplier.EMPTY) + .baseClasses(StackCollection.class, RegistryTag.class, ItemVariants.class) + .allowTypelessData() + .create(); + + static ItemStacksSupplier compound(SequencedCollection suppliers) { + return new SupplierCollection(suppliers); + } + + static ItemStacksSupplier stacks(SequencedCollection stacks) { + return new StackCollection(stacks); + } + + static ItemStacksSupplier tag(TagKey tagKey) { + return new RegistryTag(tagKey); + } + + static ItemStacksSupplier of(Predicate predicate) { + return new ItemPredicate(predicate); + } + + static ItemStacksSupplier itemVariants(ItemConvertible item) { + return new ItemVariants(item, false); + } + + static ItemStacksSupplier itemVariants(ItemConvertible item, boolean overrideClassSupplier) { + return new ItemVariants(item, overrideClassSupplier); + } + + static ItemStacksSupplier supplier(ItemStacksSupplier supplier) { + return (supplier instanceof ItemConvertible convertible) + ? new ItemVariants(convertible, false) + : supplier; + } + + record StackCollection(SequencedCollection stacks) implements ItemStacksSupplier, IdentifiedData { + public static final Identifier ID = Identifier.of("owo", "itemstacks"); + + public static final StructEndec ENDEC = ItemStacksSupplier.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + MinecraftEndecs.ITEM_STACK.listOf().fieldOf("stacks", s -> { + return (s.stacks() instanceof List list) ? list : List.copyOf(s.stacks()); + }), + StackCollection::new + )); + + @Override + public SequencedCollection get() { + return stacks; + } + + @Override + public Identifier getTypeId() { + return ID; + } + } + + record RegistryTag(TagKey tagKey) implements ItemStacksSupplier, IdentifiedData { + + public RegistryTag { + var registryKey = tagKey.registryRef(); + + if (!(registryKey == RegistryKeys.ITEM || registryKey == RegistryKeys.BLOCK)) { + throw new IllegalStateException("Unable to handle the given registry type for a Tag ItemStackSupplier: " + registryKey.getValue()); + } + } + + public static final Identifier ID = Identifier.of("owo", "tag"); + + public static final StructEndec ENDEC = ItemStacksSupplier.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + MinecraftEndecs.IDENTIFIER.optionalFieldOf("registry", s -> s.tagKey().registryRef().getValue(), RegistryKeys.ITEM.getValue()), + MinecraftEndecs.IDENTIFIER.fieldOf("tag", s -> s.tagKey().id()), + (registry, tagId) -> { + var key = RegistryKey.ofRegistry(registry); + + return new RegistryTag((TagKey) (Object) TagKey.of(key, tagId)); + } + )); + + @Override + public SequencedCollection get() { + return ItemStackOps.getStacks(tagKey()); + } + + @Override + public Identifier getTypeId() { + return ID; + } + } + + // TODO: CHANGE TO ItemConvertible? + record ItemPredicate(Predicate predicate) implements ItemStacksSupplier { + @Override + public SequencedCollection get() { + return Registries.ITEM.stream() + .filter(predicate) + .map(net.minecraft.item.Item::getDefaultStack) + .toList(); + } + } + + record ItemVariants(ItemConvertible item, boolean overrideClassSupplier) implements ItemStacksSupplier, IdentifiedData { + public ItemVariants { + if (!(item instanceof net.minecraft.item.Item || item instanceof Block)) { + throw new IllegalStateException("Unable to handle the given registry type for a ItemConvertible ItemStackSupplier: " + item); + } + } + + public static final Identifier ID = Identifier.of("owo", "item"); + + public static final StructEndec ENDEC = ItemStacksSupplier.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + MinecraftEndecs.IDENTIFIER.optionalFieldOf("registry", s -> (s.item instanceof Block) ? RegistryKeys.BLOCK.getValue() : RegistryKeys.ITEM.getValue(), RegistryKeys.ITEM.getValue()), + MinecraftEndecs.IDENTIFIER.fieldOf("entry", s -> (s.item instanceof Block block) ? Registries.BLOCK.getId(block) : Registries.ITEM.getId(s.item.asItem())), + Endec.BOOLEAN.optionalFieldOf("override_class_supplier", ItemVariants::overrideClassSupplier, false), + (registry, entryId, overrideClassSupplier) -> new ItemVariants((registry == RegistryKeys.BLOCK.getValue()) ? Registries.BLOCK.get(entryId) : Registries.ITEM.get(entryId), overrideClassSupplier) + )); + + @Override + public SequencedCollection get() { + if (!overrideClassSupplier && item instanceof ItemStacksSupplier supplier) return supplier.get(); + + var itemClazz = item.getClass(); + + return Registries.ITEM.stream() + .filter(itemClazz::isInstance) + .map(net.minecraft.item.Item::getDefaultStack) + .toList(); + } + + @Override + public Identifier getTypeId() { + return ID; + } + } + + record SupplierCollection(SequencedCollection suppliers) implements ItemStacksSupplier, IdentifiedData { + public static final Identifier ID = Identifier.of("owo", "compound"); + + public static final StructEndec ENDEC = ItemStacksSupplier.ENDEC.registerEndec(ID, + StructEndecBuilder.of( + ItemStacksSupplier.ENDEC.listOf().fieldOf("suppliers", s -> { + return (s.suppliers() instanceof List list) ? list : List.copyOf(s.suppliers()); + }), + SupplierCollection::new + )); + + @Override + public Identifier getTypeId() { + return ID; + } + + @Override + public SequencedCollection get() { + return suppliers.stream().flatMap(supplier -> supplier.get().stream()).toList(); + } + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroup.java b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroup.java new file mode 100644 index 000000000..459c840df --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroup.java @@ -0,0 +1,161 @@ +package io.wispforest.owo.itemgroup.base; + +import io.wispforest.owo.itemgroup.core.OwoEntryCollector; +import io.wispforest.owo.itemgroup.gui.ScrollerTextures; +import io.wispforest.owo.itemgroup.gui.TabTextures; +import io.wispforest.owo.itemgroup.core.ItemGroupButton; +import io.wispforest.owo.itemgroup.core.ItemGroupTab; +import io.wispforest.owo.util.pond.OwoItemGroupExtension; +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; + +/// +/// The bases for the owo's extension on top of [ItemGroup] storing static data +/// where as [OwoItemGroupState] holds state of active tabs used to gather entries +/// for the group +/// +public interface OwoItemGroup { + + BiConsumer DEFAULT_STACK_GENERATOR = (item, stacks) -> stacks.add(item.getDefaultStack()); + + @Nullable + static OwoItemGroup get(ItemGroup group) { + return ((OwoItemGroupExtension) group).owo$getExtension(); + } + + //-- + + /** + * Adds the specified button to the buttons on + * the right side of the creative menu + * + * @param button The button to add + * @see ItemGroupButton#link(RegistryKey, Icon, String, String) + * @see ItemGroupButton#curseforge(RegistryKey, String) + * @see ItemGroupButton#discord(RegistryKey, String) + */ + void addButton(ItemGroupButton 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) + */ + void addTab(Icon icon, String name, @Nullable TagKey contentTag, Identifier texture, boolean 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) + */ + void addTab(Icon icon, String name, @Nullable TagKey contentTag, boolean 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) + */ + void addCustomTab(Icon icon, String name, OwoEntryCollector contentSupplier, Identifier texture, boolean 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) + */ + void addCustomTab(Icon icon, String name, OwoEntryCollector contentSupplier, boolean primary); + + void addTabs(Collection tabs); + + void addButtons(Collection buttons); + + //-- + + /// + /// @return Alternative textures for when rendering the background for the given [ItemGroup] + /// + @Nullable + Identifier backgroundTexture(); + + /// + /// @return Alternative textures for when rendering the scrollbar for the given [ItemGroup] + /// + @Nullable + ScrollerTextures scrollerTextures(); + + /// + /// @return Alternative textures for when rendering the tabs for the given [ItemGroup] + /// + @Nullable + TabTextures tabTextures(); + + /// + /// @return The max height of tab to be stacked when ordering such when rendering the [ItemGroup] + /// + int tabStackHeight(); + + /// + /// @return The max height of buttons to be stacked when ordering such when rendering the [ItemGroup] + /// + int buttonStackHeight(); + + /// + /// Whether the given title for [ItemGroup#getDisplayName()] should be adjusted when + /// rendering the name to show a pathed name based on the current opened tab + /// + boolean useDynamicTitle(); + + /// + /// @return Whether the end user has the ability to select multiple tabs at once to allow + /// for seeing multiple tabs entries at once + /// + boolean allowMultiSelect(); + + //-- + + /// + /// @return A replacement for [ItemGroup#getIcon()] with higher capabilities for rendering + /// or [Icon#NONE] if not used + /// + Icon icon(); + + List getTabs(); + + /// + /// @return The given [ItemGroupTab] for the following index if present + /// + @Nullable + default ItemGroupTab getTab(int index) { + var tabs = this.getTabs(); + return index < tabs.size() ? tabs.get(index) : null; + } + + /// + /// @return All [ItemGroupButton]s added to the given extension + /// + List getButtons(); + + RegistryKey itemGroupId(); +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupEntries.java b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupEntries.java new file mode 100644 index 000000000..7ee592e3f --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupEntries.java @@ -0,0 +1,83 @@ +package io.wispforest.owo.itemgroup.base; + +import io.wispforest.owo.itemgroup.core.CondensedEntries; +import io.wispforest.owo.itemgroup.core.CondensedEntry; +import io.wispforest.owo.itemgroup.util.ItemStackOps; +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; + +import java.util.SequencedCollection; +import java.util.function.Predicate; + +// TODO [ItemGroupPR]: DOCUMENT? +public interface OwoItemGroupEntries extends CondensedEntries.RegistrationCallback { + + default OwoItemGroupEntries addAll(TagKey tagKey) { + return this.addAll(tagKey, ItemGroup.StackVisibility.PARENT_AND_SEARCH_TABS); + } + + default OwoItemGroupEntries addAll(TagKey tagKey, ItemGroup.StackVisibility visibility) { + return addAll(ItemStackOps.getStacks(tagKey), visibility); + } + + default OwoItemGroupEntries add(ItemStack stack) { + return this.add(stack, ItemGroup.StackVisibility.PARENT_AND_SEARCH_TABS); + } + + default OwoItemGroupEntries add(ItemConvertible item, ItemGroup.StackVisibility visibility) { + return this.add(new ItemStack(item), visibility); + } + + default OwoItemGroupEntries add(ItemConvertible item) { + return this.add(new ItemStack(item), ItemGroup.StackVisibility.PARENT_AND_SEARCH_TABS); + } + + default OwoItemGroupEntries addAll(SequencedCollection stacks, ItemGroup.StackVisibility visibility) { + stacks.forEach((stack) -> this.add(stack, visibility)); + + return this; + } + + default OwoItemGroupEntries addAll(SequencedCollection stacks) { + this.addAll(stacks, ItemGroup.StackVisibility.PARENT_AND_SEARCH_TABS); + + return this; + } + + OwoItemGroupEntries add(ItemStack stack, ItemGroup.StackVisibility visibility); + + //-- + + @Override + default OwoItemGroupEntries addEntry(Identifier identifier, Predicate predicate) { + return addEntry(identifier, ItemStacksSupplier.of(predicate)); + } + + @Override + default CondensedEntries.RegistrationCallback addEntry(Identifier identifier, ItemConvertible item) { + return addEntry(identifier, ItemStacksSupplier.itemVariants(item)); + } + + OwoItemGroupEntries addEntry(TagKey tagKey); + + @Override + default OwoItemGroupEntries addEntry(Identifier identifier, TagKey tagKey) { + return addEntry(identifier, ItemStacksSupplier.tag(tagKey)); + } + + @Override + default OwoItemGroupEntries addEntry(Identifier identifier, SequencedCollection stacks) { + return addEntry(identifier, ItemStacksSupplier.stacks(stacks)); + } + + @Override + default OwoItemGroupEntries addEntry(Identifier identifier, ItemStacksSupplier supplier) { + return addEntry(new CondensedEntry(identifier, supplier, false)); + } + + OwoItemGroupEntries addEntry(CondensedEntry entry); +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupState.java b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupState.java new file mode 100644 index 000000000..4a0d0ffd1 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/base/OwoItemGroupState.java @@ -0,0 +1,111 @@ +package io.wispforest.owo.itemgroup.base; + +import io.wispforest.owo.Owo; +import io.wispforest.owo.itemgroup.impl.OwoItemGroupStateImpl; +import io.wispforest.owo.mixin.itemgroup.CreativeInventoryScreenAccessor; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.client.gui.ScreenRect; +import net.minecraft.item.ItemGroup; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/// +/// The state for the given [OwoItemGroup] with the currently selected +/// tabs and acts as the primary access for the given entries of the +/// group +/// +public interface OwoItemGroupState extends ItemGroup.EntryCollector { + + @Nullable + static OwoItemGroupState get(ItemGroup group) { + return get(group, null); + } + + @Nullable + static OwoItemGroupState get(ItemGroup group, @Nullable ItemGroup.DisplayContext context) { + var isClientSide = true; + + if (context != null) { + var server = Owo.currentServer(); + + isClientSide = server == null || server.getRegistryManager() != context.lookup(); + } + + return get(group, isClientSide); + } + + static OwoItemGroupState get(ItemGroup group, boolean isClientSide) { + return OwoItemGroupStateImpl.getState(group, isClientSide); + } + + OwoItemGroup getExtension(); + + void updateSearchEntries(ItemGroup group, ItemGroup.DisplayContext context); + + /** + * Select only {@code tab}, deselecting all other tabs, + * using {@code context} for re-population + */ + void selectSingleTab(int tab, ItemGroup.DisplayContext 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, ItemGroup.DisplayContext)} + */ + void selectTab(int tab, ItemGroup.DisplayContext 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 + */ + void deselectTab(int tab, ItemGroup.DisplayContext context); + + /** + * Shorthand for {@link #selectTab(int, ItemGroup.DisplayContext)} or + * {@link #deselectTab(int, ItemGroup.DisplayContext)}, depending on the tabs + * current state + */ + void toggleTab(int tab, ItemGroup.DisplayContext context); + + /** + * @return A set containing the indices of all currently + * selected tabs + */ + IntSet selectedTabs(); + + /** + * @return {@code true} if {@code tab} is currently selected + */ + default boolean isTabSelected(int tab) { + return selectedTabs().contains(tab); + } + + Text getDisplayName(Text baseDisplayName); + + default Collection getExclusionZones(int x, int y) { + var extension = OwoItemGroup.get(CreativeInventoryScreenAccessor.owo$getSelectedTab()); + + if (extension == null || extension.getButtons().isEmpty()) return Collections.emptySet(); + + int stackHeight = extension.buttonStackHeight(); + y -= 13 * (stackHeight - 4); + + final var rectangles = new ArrayList(); + + for (int i = 0; i < extension.getButtons().size(); i++) { + int xOffset = x + 198 + (i / stackHeight) * 26; + int yOffset = y + 10 + (i % stackHeight) * 30; + rectangles.add(new ScreenRect(xOffset, yOffset, 24, 24)); + } + + return rectangles; + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntries.java b/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntries.java new file mode 100644 index 000000000..5e7b95f14 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntries.java @@ -0,0 +1,134 @@ +package io.wispforest.owo.itemgroup.core; + +import io.wispforest.owo.itemgroup.base.ItemStacksSupplier; +import io.wispforest.owo.itemgroup.base.OwoItemGroupState; +import io.wispforest.owo.itemgroup.impl.OwoItemGroupStateImpl; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.item.Item; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +import java.util.*; +import java.util.function.Predicate; + +// TODO [ItemGroupPR]: DOCUMENT +public class CondensedEntries { + + private static final Map, Map>> ENTRIES = new LinkedHashMap<>(); + private static final Map, Map>> DATA_LOADED_ENTRIES = new LinkedHashMap<>(); + + public static void registerEntry(RegistryKey targetGroup, int tabIndex, CondensedEntry entry) { + var entriesMap = ENTRIES.computeIfAbsent(targetGroup, key -> new LinkedHashMap<>()) + .computeIfAbsent(tabIndex, integer -> new LinkedHashMap<>()); + + entriesMap.put(entry.id(), entry); + } + + @ApiStatus.Internal + public static void registerDataEntries(Map>>> data) { + DATA_LOADED_ENTRIES.clear(); + + data.forEach((identifier, groupedEntries) -> { + groupedEntries.forEach((itemGroupId, tabEntries) -> { + var groupKey = RegistryKey.of(RegistryKeys.ITEM_GROUP, itemGroupId); + + tabEntries.forEach((tabIndex, entries) -> { + CondensedEntries.registerFor(groupKey, tabIndex); + + var entriesMap = DATA_LOADED_ENTRIES.computeIfAbsent(groupKey, key -> new LinkedHashMap<>()) + .computeIfAbsent(tabIndex, integer -> new LinkedHashMap<>()); + + entries.forEach(entry -> entriesMap.put(entry.id(), entry)); + }); + }); + }); + } + + public static RegistrationCallback registerFor(RegistryKey groupKey) { + return registerFor(groupKey, 0); + } + + public static RegistrationCallback registerFor(RegistryKey groupKey, int tabIndex) { + return new RegistrationCallback() { + @Override + public RegistrationCallback addEntry(CondensedEntry entry) { + registerEntry(groupKey, tabIndex, entry); + + return this; + } + }; + } + + public static List> getEntriesFor(ItemGroup group, IntSet tabIndex) { + var key = Registries.ITEM_GROUP.getKey(group).orElseThrow(); + + var list = new ArrayList>(); + + // -- Code registered Entries -- + + var groupEntries = CondensedEntries.ENTRIES.get(key); + + if (groupEntries != null) { + list.addAll(tabIndex.intStream().mapToObj(groupEntries::get).filter(Objects::nonNull).toList()); + } + + // -- Data Driven Entries -- + + var dataGroupEntries = CondensedEntries.DATA_LOADED_ENTRIES.get(key); + + if (dataGroupEntries != null) { + list.addAll(tabIndex.intStream().mapToObj(dataGroupEntries::get).filter(Objects::nonNull).toList()); + } + + // -- State Entries -- + + var state = OwoItemGroupState.get(group); + + if (state != null) { + var entries = ((OwoItemGroupStateImpl) state).activeCondensedEntries(); + + list.addAll(tabIndex.intStream().mapToObj(entries::get).filter(Objects::nonNull).toList()); + } + + //-- + + return list; + } + + public interface RegistrationCallback { + default RegistrationCallback addEntry(Identifier identifier, Predicate predicate) { + return addEntry(identifier, ItemStacksSupplier.of(predicate)); + } + + default RegistrationCallback addEntry(Identifier identifier, TagKey tagKey) { + return addEntry(identifier, ItemStacksSupplier.tag(tagKey)); + } + + default RegistrationCallback addEntry(Identifier identifier, ItemConvertible item) { + return addEntry(identifier, ItemStacksSupplier.itemVariants(item)); + } + +// default RegistrationCallback addItems(Identifier identifier, SequencedCollection items) { +// return addEntry(identifier, ItemStacksSupplier.of(ItemStackUtils.convertItems(items))); +// } + + default RegistrationCallback addEntry(Identifier identifier, SequencedCollection stacks) { + return addEntry(identifier, ItemStacksSupplier.stacks(stacks)); + } + + default RegistrationCallback addEntry(Identifier identifier, ItemStacksSupplier supplier) { + return addEntry(new CondensedEntry(identifier, supplier, false)); + } + + RegistrationCallback addEntry(CondensedEntry entry); + } +} + + diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntry.java b/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntry.java new file mode 100644 index 000000000..2c0f66363 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/CondensedEntry.java @@ -0,0 +1,131 @@ +package io.wispforest.owo.itemgroup.core; + +import io.wispforest.owo.itemgroup.base.ItemStacksSupplier; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.Language; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +/// +/// An entry within an [ItemGroup] that is condensable allowing the user to +/// expand or shrink the entry to show or hide its entries saving space by +/// grouping similar entries +/// +public record CondensedEntry(Identifier id, ItemStacksSupplier childrenEntries, boolean useItemMatching) { + + public String getTranslationKey() { + return (this.childrenEntries() instanceof ItemStacksSupplier.RegistryTag registryTag) + ? registryTag.tagKey().getTranslationKey() + : "condensed_entry." + this.id().toTranslationKey(); + } + + public void addExtraInfo(Consumer tooltipAddCallback, TooltipType type) { + if (type.isAdvanced()) { + tooltipAddCallback.accept(Text.empty()); + + if (childrenEntries instanceof ItemStacksSupplier.RegistryTag registryTag) { + tooltipAddCallback.accept(Text.translatable("text.owo.condensed_entries.tag_key", Text.translatable(registryTag.tagKey().getTranslationKey()))); + } + + tooltipAddCallback.accept(Text.translatable("text.owo.condensed_entries.entry_id", id)); + } + } + + @ApiStatus.Internal + public State createState(ItemStack parent, List children) { + return new State(parent, children); + } + + /* + * Ordering of children entries will be first based on any existing entry orderers and then based on the order in which the entries are given + */ + public class State { + + private final ItemStack parent; + private final List children; + + public State(ItemStack parent, List children) { + this.parent = parent; + this.children = children; + } + + private boolean showChildren = false; + + @Nullable + private ItemStack iconStack = null; + + private double totalTime; + + public CondensedEntry entry() { + return CondensedEntry.this; + } + + public ItemStack getDisplayStack(double delta) { + this.totalTime += delta * 50; + + // TODO: ADJUSTABLE TIME? + if (this.iconStack == null || (this.totalTime > 1500)) { + this.totalTime = 0; + + ItemStack chosenIconStack = null; + + while (chosenIconStack == null) { + // TODO: ADD ABILITY TO TOGGLE DISPLAY ROTATION ON AND OFF + int index = new Random().nextInt(0, this.children.size()); + + var entry = this.children.get(index); + + if (this.iconStack != entry) { + chosenIconStack = entry; + } + } + + this.iconStack = chosenIconStack; + } + + return this.iconStack; + } + + public boolean showChildren() { + return this.showChildren; + } + + public void toggleChildren(List displayStacks) { + var startingIndex = displayStacks.indexOf(parent); + + if (this.showChildren) { + displayStacks.removeAll(children); + } else { + displayStacks.addAll(startingIndex + 1, children); + } + + toggleChildren(); + } + + public void toggleChildren() { + this.showChildren = !this.showChildren; + } + + public Text title() { + return Text.translatable(entry().getTranslationKey()).formatted(Formatting.WHITE); + } + + @Nullable + public Text description() { + var key = entry().getTranslationKey() + ".tooltip"; + + return Language.getInstance().hasTranslation(key) + ? Text.translatable(key).formatted(Formatting.GRAY) + : null; + } + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupButton.java b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupButton.java new file mode 100644 index 000000000..b9a40dde3 --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupButton.java @@ -0,0 +1,108 @@ +package io.wispforest.owo.itemgroup.core; + +import io.wispforest.owo.itemgroup.base.ButtonDefinition; +import io.wispforest.owo.itemgroup.base.Icon; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ConfirmLinkScreen; +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.RegistryKey; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +/** + * A button placed to the right side of the creative inventory. Provides defaults + * for linking to sites, but can execute arbitrary actions + */ +public class ItemGroupButton implements ButtonDefinition { + + public static final Identifier ICONS_TEXTURE = Identifier.of("owo", "textures/gui/icons.png"); + + private final String name; + private final Icon icon; + private final Text tooltip; + private final Identifier texture; + private final Runnable action; + + public ItemGroupButton(RegistryKey groupKey, Icon icon, String name, Identifier texture, Runnable action) { + this.name = name; + this.icon = icon; + this.tooltip = ButtonDefinition.tooltipFor(groupKey, "button", name); + this.action = action; + this.texture = texture; + } + + public ItemGroupButton(RegistryKey groupKey, Icon icon, String name, Runnable action) { + this(groupKey, icon, name, ItemGroupTab.DEFAULT_TEXTURE, action); + } + + + public static Icon iconFromType(String type) { + return switch (type) { + case "modrinth" -> Icon.of(ICONS_TEXTURE, 16, 0, 64, 64); + case "curseforge" -> Icon.of(ICONS_TEXTURE, 32, 0, 64, 64); + case "github" -> Icon.of(ICONS_TEXTURE, 0, 0, 64, 64); + case "discord" -> Icon.of(ICONS_TEXTURE, 48, 0, 64, 64); + default -> Icon.NONE; + }; + } + + public static ItemGroupButton github(RegistryKey groupKey, String url) { + return link(groupKey, iconFromType("github"), "github", url); + } + + public static ItemGroupButton modrinth(RegistryKey groupKey, String url) { + return link(groupKey, iconFromType("modrinth"), "modrinth", url); + } + + public static ItemGroupButton curseforge(RegistryKey groupKey, String url) { + return link(groupKey, iconFromType("curseforge"), "curseforge", url); + } + + public static ItemGroupButton discord(RegistryKey groupKey, String url) { + return link(groupKey, iconFromType("discord"), "discord", url); + } + + /** + * Creates a button that opens the given link when clicked + * + * @param icon The icon for this button to use + * @param name The name of this button, used for the translation key + * @param url The url to open + * @return The created button + */ + public static ItemGroupButton link(RegistryKey groupKey, Icon icon, String name, String url) { + return new ItemGroupButton(groupKey, icon, name, () -> { + final var client = MinecraftClient.getInstance(); + var screen = client.currentScreen; + client.setScreen(new ConfirmLinkScreen(confirmed -> { + if (confirmed) Util.getOperatingSystem().open(url); + client.setScreen(screen); + }, url, true)); + }); + } + + @Override + public String name() { + return this.name; + } + + @Override + public Identifier texture() { + return this.texture; + } + + @Override + public Icon icon() { + return this.icon; + } + + @Override + public Text tooltip() { + return this.tooltip; + } + + public Runnable action() { + return this.action; + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupReference.java b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupReference.java new file mode 100644 index 000000000..0f3bc85cd --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupReference.java @@ -0,0 +1,9 @@ +package io.wispforest.owo.itemgroup.core; + +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.RegistryKey; + +/// +/// A key object representing a specific tab for a given [ItemGroup] +/// +public record ItemGroupReference(RegistryKey group, int tab) {} diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupTab.java b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupTab.java new file mode 100644 index 000000000..07e04779d --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/ItemGroupTab.java @@ -0,0 +1,28 @@ +package io.wispforest.owo.itemgroup.core; + +import io.wispforest.owo.itemgroup.OwoItemSettingsExtension; +import io.wispforest.owo.itemgroup.base.ButtonDefinition; +import io.wispforest.owo.itemgroup.base.Icon; +import io.wispforest.owo.itemgroup.base.OwoItemGroup; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + + +/// +/// Represents a tab inside a given {@link OwoItemGroup} with +/// its entries being gathered in the tabs [OwoEntryCollector]. +/// +/// When adding the tabs for your [OwoItemGroup] and +/// If you want to use {@link OwoItemSettingsExtension#tab(int)} to +/// define the contents, use {@code null} as the tag +/// +public record ItemGroupTab( + String name, + Icon icon, + Text tooltip, + OwoEntryCollector contentSupplier, + Identifier texture, + boolean primary +) implements ButtonDefinition { + public static final Identifier DEFAULT_TEXTURE = Identifier.of("owo", "textures/gui/tabs.png"); +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/core/OwoEntryCollector.java b/src/main/java/io/wispforest/owo/itemgroup/core/OwoEntryCollector.java new file mode 100644 index 000000000..62e237d6a --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/core/OwoEntryCollector.java @@ -0,0 +1,22 @@ +package io.wispforest.owo.itemgroup.core; + +import io.wispforest.owo.itemgroup.base.OwoItemGroup; +import io.wispforest.owo.itemgroup.base.OwoItemGroupEntries; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.registry.tag.TagKey; + +/// +/// Supplies entries for [OwoItemGroup] tab similar to [ItemGroup.EntryCollector] but uses +/// [OwoItemGroupEntries] instead of [ItemGroup.Entries] +/// +@FunctionalInterface +public interface OwoEntryCollector { + OwoEntryCollector EMPTY = (context, entries) -> {}; + + static OwoEntryCollector fromTag(TagKey tag) { + return (context, entries) -> entries.addAll(tag); + } + + void addItems(ItemGroup.DisplayContext context, OwoItemGroupEntries entries); +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/data/CondensedEntryLoader.java b/src/main/java/io/wispforest/owo/itemgroup/data/CondensedEntryLoader.java new file mode 100644 index 000000000..9539d502d --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/data/CondensedEntryLoader.java @@ -0,0 +1,79 @@ +package io.wispforest.owo.itemgroup.data; + +import com.mojang.datafixers.util.Either; +import io.wispforest.endec.Endec; +import io.wispforest.endec.StructEndec; +import io.wispforest.endec.impl.StructEndecBuilder; +import io.wispforest.owo.itemgroup.base.ItemStacksSupplier; +import io.wispforest.owo.itemgroup.core.CondensedEntries; +import io.wispforest.owo.itemgroup.core.CondensedEntry; +import io.wispforest.owo.serialization.CodecUtils; +import io.wispforest.owo.serialization.EndecDataLoader; +import net.minecraft.resource.ResourceFinder; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.profiler.Profiler; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class CondensedEntryLoader extends EndecDataLoader>>> { + + public static final CondensedEntryLoader INSTANCE = new CondensedEntryLoader(); + + public static final Identifier ID = Identifier.of("owo", "condensed_entry_loader"); + + protected CondensedEntryLoader() { + super(ENTRIES_ENDEC, ResourceFinder.json("owo/condensed_entries")); + } + + @Override + protected void apply(Map>>> data, ResourceManager manager, Profiler profiler) { + CondensedEntries.registerDataEntries(data); + } + + @Override + public Identifier getFabricId() { + return ID; + } + + private static final Endec> CONDENSED_ENTRIES_ENDEC = Endec.map(Identifier::toString, Identifier::of, RawEntry.ENTRY_ENDEC) + .xmap( + map -> map.entrySet().stream().map(entry -> new CondensedEntry(entry.getKey(), entry.getValue().childrenEntries(), entry.getValue().useItemMatching())).toList(), + list -> list.stream().collect(Collectors.toMap(CondensedEntry::id, entry -> new RawEntry(entry.childrenEntries(), entry.useItemMatching()))) + ); + + private static final Endec>>> ENTRIES_ENDEC = + map(Identifier::toString, Identifier::of, + CodecUtils.eitherEndec(map(Object::toString, Integer::valueOf, CONDENSED_ENTRIES_ENDEC), CONDENSED_ENTRIES_ENDEC) + .xmap( + either -> Either.unwrap(either.mapRight(entries -> Util.make(new LinkedHashMap<>(), map -> map.put(0, entries)))), + Either::left + ) + ); + + private static Endec> map(Function keyToString, Function stringToKey, Endec valueEndec) { + return Endec.of((ctx, serializer, map) -> { + try (var mapState = serializer.map(ctx, valueEndec, map.size())) { + map.forEach((k, v) -> mapState.entry(keyToString.apply(k), v)); + } + }, (ctx, deserializer) -> { + var mapState = deserializer.map(ctx, valueEndec); + + Map map = new LinkedHashMap<>(mapState.estimatedSize()); + mapState.forEachRemaining(entry -> map.put(stringToKey.apply(entry.getKey()), entry.getValue())); + + return map; + }); + } + + private record RawEntry(ItemStacksSupplier childrenEntries, boolean useItemMatching) { + public static final StructEndec ENTRY_ENDEC = StructEndecBuilder.of( + ItemStacksSupplier.ENDEC.fieldOf("stacks", RawEntry::childrenEntries), + Endec.BOOLEAN.optionalFieldOf("use_item_matching", RawEntry::useItemMatching, false), + RawEntry::new + ); + } +} diff --git a/src/main/java/io/wispforest/owo/itemgroup/data/OwoItemGroupLoader.java b/src/main/java/io/wispforest/owo/itemgroup/data/OwoItemGroupLoader.java new file mode 100644 index 000000000..ac8ebe44f --- /dev/null +++ b/src/main/java/io/wispforest/owo/itemgroup/data/OwoItemGroupLoader.java @@ -0,0 +1,265 @@ +package io.wispforest.owo.itemgroup.data; + +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.OwoItemGroupBuilder; +import io.wispforest.owo.itemgroup.base.ButtonDefinition; +import io.wispforest.owo.itemgroup.base.Icon; +import io.wispforest.owo.itemgroup.base.ItemStacksSupplier; +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.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * 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()) { + var collector = ((ItemGroupAccessor) targetGroup).owo$getEntryCollector(); + + baseGroupTab = new ItemGroupTab( + EXTENDED_TAB_NAME, + Icon.of(targetGroup.getIcon()), + targetGroup.getDisplayName(), + (context, entries) -> collector.accept(context, entries::add), + 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); + }); + } + + /* + * { + * "entries": [ + * "minecraft:air", + * { + * "item": "minecraft:air" + * }, + * { + * "items": [ + * ... + * ] + * }, + * { + * "tag": "minecraft:wools" + * }, + * { + * "registry": "block" + * "entry": "minecraft:shulker_block" + * } + * ] + * } + */ + private record Tab(String name, Icon icon, List suppliers, Identifier texture, boolean areTagsCondensable) { + public static final StructEndec ENDEC = StructEndecBuilder.of( + Endec.STRING.fieldOf("name", Tab::name), + Icon.ENDEC.fieldOf("icon", Tab::icon), + CodecUtils.eitherStructEndec(RawItemStacksSupplier.LIST_ENDEC.structOf("entries"), ItemStacksSupplier.ENDEC) + .xmap( + either -> mapAndUnwrap(either, supplier -> List.of(new RawItemStacksSupplier(supplier))), + Either::left + ).flatFieldOf(Tab::suppliers), + MinecraftEndecs.IDENTIFIER.optionalFieldOf("texture", Tab::texture, ItemGroupTab.DEFAULT_TEXTURE), + Endec.BOOLEAN.optionalFieldOf("are_tags_condensable", Tab::areTagsCondensable, false), + Tab::new + ); + + private record RawItemStacksSupplier(ItemStacksSupplier stackSupplier, @Nullable Identifier condensedId) { + private RawItemStacksSupplier(ItemStacksSupplier stackSupplier) { + this(stackSupplier, null); + } + + public static final Endec CONDENSED_ITEM_STACK = CodecUtils.eitherEndec(MinecraftEndecs.ITEM_STACK, MinecraftEndecs.ofRegistry(Registries.ITEM)) + .xmap( + either -> mapAndUnwrap(either, Item::getDefaultStack), + stack -> (stack.getCount() > 1 || !stack.getComponentChanges().isEmpty()) ? Either.left(stack) : Either.right(stack.getItem())); + + private static final Endec SUPPLIER_ENDEC = CodecUtils.eitherEndec( + CONDENSED_ITEM_STACK.xmap(stack -> ItemStacksSupplier.stacks(List.of(stack)), supplier1 -> supplier1.get().getFirst()), + ItemStacksSupplier.ENDEC + ).xmap( + Either::unwrap, + supplier1 -> (supplier1 instanceof ItemStacksSupplier.StackCollection(var stacks) && stacks.size() == 1) ? Either.left(supplier1) : Either.right(supplier1)); + + public static final StructEndec BASE_ENDEC = StructEndecBuilder.of( + ItemStacksSupplier.ENDEC.flatFieldOf(RawItemStacksSupplier::stackSupplier), + MinecraftEndecs.IDENTIFIER.optionalFieldOf("condensed_id", RawItemStacksSupplier::condensedId, () -> null), + RawItemStacksSupplier::new); + + public static final Endec ENDEC = CodecUtils.eitherEndec(BASE_ENDEC, SUPPLIER_ENDEC) + .xmap(either -> mapAndUnwrap(either, RawItemStacksSupplier::new), Either::left); + + public static final Endec> LIST_ENDEC = CodecUtils.eitherEndec(RawItemStacksSupplier.ENDEC.listOf(), RawItemStacksSupplier.ENDEC) + .xmap(either -> mapAndUnwrap(either, List::of), list -> list.size() == 1 ? Either.right(list.getFirst()) : Either.left(list)); + } + } + + private record Button(String name, String url, Icon icon) { + public static final StructEndec