From 7b09cb7bdf58ea08352f2aca8eef66f58929cde0 Mon Sep 17 00:00:00 2001 From: IotaBread Date: Tue, 3 Oct 2023 22:51:35 -0300 Subject: [PATCH] Add sub-category support Fixes #24 --- src/main/java/me/bymartrixx/vtd/VTDMod.java | 17 ++- .../java/me/bymartrixx/vtd/data/Category.java | 108 ++++++++++++++++++ .../vtd/data/DownloadPackRequestData.java | 2 +- .../java/me/bymartrixx/vtd/data/Pack.java | 5 + .../me/bymartrixx/vtd/data/RpCategories.java | 24 +++- .../bymartrixx/vtd/gui/VTDownloadScreen.java | 9 +- .../vtd/gui/widget/PackSelectionHelper.java | 30 +++-- .../gui/widget/PackSelectionListWidget.java | 83 +++++++++++++- .../gui/widget/SelectedPacksListWidget.java | 86 +++++++++++++- 9 files changed, 339 insertions(+), 25 deletions(-) diff --git a/src/main/java/me/bymartrixx/vtd/VTDMod.java b/src/main/java/me/bymartrixx/vtd/VTDMod.java index 9175cb4..739abc1 100644 --- a/src/main/java/me/bymartrixx/vtd/VTDMod.java +++ b/src/main/java/me/bymartrixx/vtd/VTDMod.java @@ -58,7 +58,7 @@ public class VTDMod implements ClientModInitializer { // DEBUG - private static final boolean USE_LOCAL_CATEGORIES = false; + public static final boolean USE_LOCAL_CATEGORIES = false; private static final ThreadFactory DOWNLOAD_THREAD_FACTORY = new ThreadFactoryBuilder() .setNameFormat("VT Download %d").build(); @@ -122,11 +122,18 @@ public static HttpResponse executeRequest(R request) public static void loadRpCategories() { try { - RpCategories categories; + RpCategories categories = null; String file = System.getProperty("vtd.debug.rpCategoriesFile"); if (USE_LOCAL_CATEGORIES && file != null) { - categories = GSON.fromJson(Files.newBufferedReader(Path.of(file)), RpCategories.class); - } else { + try (BufferedReader reader = Files.newBufferedReader(Path.of(file))) { + categories = GSON.fromJson(reader, RpCategories.class); + } catch (IOException e) { + LOGGER.warn("Failed to load debug categories", e); + categories = null; + } + } + + if (categories == null) { HttpResponse response = executeRequest(createHttpGet("/assets/resources/json/" + VT_VERSION + "/rpcategories.json")); try (InputStream stream = new BufferedInputStream(response.getEntity().getContent())) { categories = GSON.fromJson(new InputStreamReader(stream), RpCategories.class); @@ -148,6 +155,8 @@ public static void loadRpCategories() { public static CompletableFuture executePackDownload( DownloadPackRequestData requestData, Consumer progressCallback, Path downloadPath, @Nullable String userFileName) { + LOGGER.debug("Downloading resource packs: {}", GSON.toJson(requestData)); + return CompletableFuture.supplyAsync(() -> { try { HttpPost request = createHttpPost("/assets/server/zipresourcepacks.php"); diff --git a/src/main/java/me/bymartrixx/vtd/data/Category.java b/src/main/java/me/bymartrixx/vtd/data/Category.java index d3f9fc9..1f6d6c8 100644 --- a/src/main/java/me/bymartrixx/vtd/data/Category.java +++ b/src/main/java/me/bymartrixx/vtd/data/Category.java @@ -1,18 +1,30 @@ package me.bymartrixx.vtd.data; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +@JsonAdapter(Category.CustomTypeAdapterFactory.class) public class Category { public static final List HARD_INCOMPATIBLE_CATEGORIES = List.of("Menu Panoramas", "Options Backgrounds", "Colorful Slime"); @SerializedName("category") private final String name; + @SerializedName("categories") + @Nullable + private List subCategories; private final List packs; @Nullable private Warning warning = null; @@ -28,16 +40,31 @@ public Category(String name, List packs) { this.buildPacksById(); } + public Category(String name, @Nullable List subCategories, List packs) { + this(name, packs); + this.subCategories = subCategories; + } + public Category(String name, List packs, @Nullable Warning warning) { this(name, packs); this.warning = warning; } + public Category(String name, @Nullable List subCategories, List packs, @Nullable Warning warning) { + this(name, subCategories, packs); + this.warning = warning; + } + public Category(String name, List packs, @Nullable Warning warning, boolean hardIncompatible) { this(name, packs, warning); this.hardIncompatible = hardIncompatible; } + public Category(String name, @Nullable List subCategories, List packs, @Nullable Warning warning, boolean hardIncompatible) { + this(name, subCategories, packs, warning); + this.hardIncompatible = hardIncompatible; + } + private void buildPacksById() { if (this.packsById != null) { return; @@ -53,6 +80,11 @@ public String getName() { return this.name; } + @Nullable + public List getSubCategories() { + return this.subCategories; + } + public List getPacks() { return this.packs; } @@ -83,6 +115,11 @@ public String getId() { return this.name.toLowerCase(Locale.ROOT).replaceAll("\\s", "-"); } + @Override + public String toString() { + return this.name; + } + @SuppressWarnings("ClassCanBeRecord") // Gson doesn't support records public static class Warning { private final String text; @@ -101,4 +138,75 @@ public String getColor() { return color; } } + + public static class SubCategory extends Category { + private Category parent; + + public SubCategory(String name, List packs) { + super(name, packs); + } + + public SubCategory(String name, List packs, @Nullable Warning warning) { + super(name, packs, warning); + } + + public SubCategory(String name, List packs, @Nullable Warning warning, boolean hardIncompatible) { + super(name, packs, warning, hardIncompatible); + } + + public Category getParent() { + if (this.parent == null) { + throw new IllegalStateException("Parent category for '" + this.getName() + "' is null"); + } + + return this.parent; + } + + @Override + @Nullable + public List getSubCategories() { + return null; + } + + @Override + public String getId() { + return this.getParent().getId() + "." + super.getId(); + } + + @Override + public String toString() { + return this.getParent().toString() + " > " + super.toString(); + } + } + + static class CustomTypeAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + TypeAdapter defaultAdapter = gson.getDelegateAdapter(this, type); + + // Delegate writing and reading to the default adapter + return new TypeAdapter<>() { + @Override + public void write(JsonWriter out, T value) throws IOException { + defaultAdapter.write(out, value); + } + + @Override + public T read(JsonReader in) throws IOException { + T result = defaultAdapter.read(in); + + if (result instanceof Category category) { + // Link sub categories to their parents post-deserialization + if (category.getSubCategories() != null) { + for (SubCategory subCategory : category.getSubCategories()) { + subCategory.parent = category; + } + } + } + + return result; + } + }; + } + } } diff --git a/src/main/java/me/bymartrixx/vtd/data/DownloadPackRequestData.java b/src/main/java/me/bymartrixx/vtd/data/DownloadPackRequestData.java index 4648b23..159ff5b 100644 --- a/src/main/java/me/bymartrixx/vtd/data/DownloadPackRequestData.java +++ b/src/main/java/me/bymartrixx/vtd/data/DownloadPackRequestData.java @@ -20,7 +20,7 @@ private DownloadPackRequestData(Map> packs) { @Contract("_ -> new") public static DownloadPackRequestData create(Map> selectedPacks) { Map> packs = selectedPacks.entrySet().stream().collect(Collectors.toMap( - entry -> entry.getKey().getName(), + entry -> entry.getKey().getId(), entry -> entry.getValue().stream().map(Pack::getId).toList() )); return new DownloadPackRequestData(packs); diff --git a/src/main/java/me/bymartrixx/vtd/data/Pack.java b/src/main/java/me/bymartrixx/vtd/data/Pack.java index 1c075e4..243bd37 100644 --- a/src/main/java/me/bymartrixx/vtd/data/Pack.java +++ b/src/main/java/me/bymartrixx/vtd/data/Pack.java @@ -76,4 +76,9 @@ public boolean isCompatible(Pack pack) { public String getIcon() { return this.icon == null ? this.getId() : this.icon; } + + @Override + public String toString() { + return this.name; + } } diff --git a/src/main/java/me/bymartrixx/vtd/data/RpCategories.java b/src/main/java/me/bymartrixx/vtd/data/RpCategories.java index b28571d..9af2a10 100644 --- a/src/main/java/me/bymartrixx/vtd/data/RpCategories.java +++ b/src/main/java/me/bymartrixx/vtd/data/RpCategories.java @@ -7,17 +7,35 @@ public class RpCategories { private final List categories; + @Nullable + private List allCategories; + public RpCategories(List categories) { this.categories = categories; } public List getCategories() { - return categories; + return this.categories; + } + + private List getAllCategories() { + if (this.allCategories != null) { + return this.allCategories; + } + + this.allCategories = this.categories.stream().mapMulti((category, consumer) -> { + consumer.accept(category); + if (category.getSubCategories() != null) { + category.getSubCategories().forEach(consumer); + } + }).toList(); + + return this.allCategories; } @Nullable public Pack findPack(String id) { - for (Category category : categories) { + for (Category category : this.getAllCategories()) { Pack pack = category.getPack(id); if (pack != null) { return pack; @@ -29,7 +47,7 @@ public Pack findPack(String id) { @Nullable public Category getCategory(Pack pack) { - for (Category category : categories) { + for (Category category : this.getAllCategories()) { if (category.getPacks().contains(pack)) { return category; } diff --git a/src/main/java/me/bymartrixx/vtd/gui/VTDownloadScreen.java b/src/main/java/me/bymartrixx/vtd/gui/VTDownloadScreen.java index 3040ea2..0b4e3a0 100644 --- a/src/main/java/me/bymartrixx/vtd/gui/VTDownloadScreen.java +++ b/src/main/java/me/bymartrixx/vtd/gui/VTDownloadScreen.java @@ -166,7 +166,7 @@ private String getPackName() { private void download() { this.changed = false; - if (DOWNLOAD_DISABLED) return; + if (DOWNLOAD_DISABLED || VTDMod.USE_LOCAL_CATEGORIES) return; this.downloadProgress = 0.0F; this.progressBar.show(PROGRESS_BAR_MAX_TIME, () -> this.downloadProgress, () -> this.downloadProgress = -1.0F); @@ -266,9 +266,14 @@ private void readResourcePack() { } public boolean selectCategory(Category category) { + Category selectorCategory = category; + if (category instanceof Category.SubCategory subCategory) { + selectorCategory = subCategory.getParent(); + } + if (this.currentCategory != category) { this.currentCategory = category; - this.categorySelector.setSelectedCategory(category); + this.categorySelector.setSelectedCategory(selectorCategory); this.packSelector.setCategory(category); return true; } diff --git a/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionHelper.java b/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionHelper.java index 1150d66..5a3147d 100644 --- a/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionHelper.java +++ b/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionHelper.java @@ -38,20 +38,34 @@ public void buildIncompatibilityGroups(List categories) { this.usedColors.clear(); // Create all incompatibility groups + List packs = new ArrayList<>(); for (Category c : categories) { + if (c.getSubCategories() != null) { + for (Category.SubCategory subCategory : c.getSubCategories()) { + if (subCategory.isHardIncompatible()) { + this.allIncompatibilityGroups.add(new CategoryIncompatibilityGroup(subCategory)); + continue; + } + + packs.addAll(subCategory.getPacks()); + } + } + if (c.isHardIncompatible()) { this.allIncompatibilityGroups.add(new CategoryIncompatibilityGroup(c)); continue; } - for (Pack pack : c.getPacks()) { - int i; - // noinspection SuspiciousMethodCalls DefaultIncompatibilityGroup#equals also works for packs - if ((i = this.allIncompatibilityGroups.indexOf(pack)) == -1) { - this.allIncompatibilityGroups.add(new DefaultIncompatibilityGroup(pack)); - } else { - ((DefaultIncompatibilityGroup) this.allIncompatibilityGroups.get(i)).bases.add(pack.getId()); - } + packs.addAll(c.getPacks()); + } + + for (Pack pack : packs) { + int i; + // noinspection SuspiciousMethodCalls DefaultIncompatibilityGroup#equals also works for packs + if ((i = this.allIncompatibilityGroups.indexOf(pack)) == -1) { + this.allIncompatibilityGroups.add(new DefaultIncompatibilityGroup(pack)); + } else { + ((DefaultIncompatibilityGroup) this.allIncompatibilityGroups.get(i)).bases.add(pack.getId()); } } diff --git a/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionListWidget.java b/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionListWidget.java index 20e021a..3a63c34 100644 --- a/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionListWidget.java +++ b/src/main/java/me/bymartrixx/vtd/gui/widget/PackSelectionListWidget.java @@ -13,7 +13,11 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.EntryListWidget; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.sound.SoundEvents; import net.minecraft.text.ClickEvent; import net.minecraft.text.OrderedText; import net.minecraft.text.Style; @@ -76,8 +80,9 @@ public PackSelectionListWidget(MinecraftClient client, VTDownloadScreen screen, public void setCategory(Category category) { this.category = category; + this.setFocusedChild(null); this.replaceEntries(this.getPackEntries(category)); - this.setScrollAmount(this.getScrollAmount()); // Clamp scroll amount to the new max value + this.setScrollAmount(0.0); } public void updateCategories(List categories) { @@ -100,6 +105,10 @@ private List getPackEntries(Category category) { entries.add(new WarningEntry(this.client, this.screen, category.getWarning())); } + if (category instanceof Category.SubCategory subCategory) { + entries.add(new ParentCategoryButtonEntry(this.client, this.screen, subCategory)); + } + for (Pack pack : category.getPacks()) { // Experimental packs aren't shown in the web page, at least for now if (pack.isExperimental()) { @@ -113,6 +122,12 @@ private List getPackEntries(Category category) { } } + if (category.getSubCategories() != null) { + for (Category.SubCategory subCategory : category.getSubCategories()) { + entries.add(new SubCategoryButtonEntry(this.client, this.screen, subCategory)); + } + } + this.entryCache.put(category, entries); return entries; @@ -554,13 +569,77 @@ private void renderText(GuiGraphics graphics, int x, int y, int width) { } // endregion - @Override public String toString() { return "Warning"; } } + public static class SubCategoryButtonEntry extends CategoryButtonEntry { + protected SubCategoryButtonEntry(MinecraftClient client, VTDownloadScreen screen, Category.SubCategory category) { + super(client, screen, category); + } + + @Override + public String toString() { + return "Sub category " + this.category.getName(); + } + } + + public static class ParentCategoryButtonEntry extends CategoryButtonEntry { + protected ParentCategoryButtonEntry(MinecraftClient client, VTDownloadScreen screen, Category.SubCategory subCategory) { + super(client, screen, subCategory.getParent()); + } + + @Override + public String toString() { + return "Parent category " + this.category.getName(); + } + } + + public abstract static class CategoryButtonEntry extends AbstractEntry { + protected static final int TEXTURE_V = 66; + protected static final int BUTTON_HEIGHT = 20; + protected static final int BUTTON_HORIZONTAL_PADDING = 32; + + protected final Category category; + protected final Text name; + + protected CategoryButtonEntry(MinecraftClient client, VTDownloadScreen screen, Category category) { + super(client, screen); + this.category = category; + this.name = Text.literal(category.getName()).formatted(Formatting.BOLD); + } + + @Override + protected List getTooltipText(int width) { + return Collections.emptyList(); + } + + private void playDownSound(SoundManager soundManager) { + soundManager.play(PositionedSoundInstance.create(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == GLFW.GLFW_MOUSE_BUTTON_1) { + this.screen.selectCategory(this.category); + this.playDownSound(this.client.getSoundManager()); + return true; + } + + return false; + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + graphics.drawNineSlicedTexture(ClickableWidget.WIDGETS_TEXTURE, + x + BUTTON_HORIZONTAL_PADDING, y + (entryHeight - BUTTON_HEIGHT) / 2, entryWidth - BUTTON_HORIZONTAL_PADDING * 2, BUTTON_HEIGHT, + 20, 4, 200, 20, 0, TEXTURE_V); + graphics.drawCenteredShadowedText(this.client.textRenderer, this.name, x + entryWidth / 2, y + (entryHeight - this.client.textRenderer.fontHeight) / 2, 0xFFFFFF); + } + } + public static abstract class AbstractEntry extends EntryListWidget.Entry { protected final MinecraftClient client; protected final VTDownloadScreen screen; diff --git a/src/main/java/me/bymartrixx/vtd/gui/widget/SelectedPacksListWidget.java b/src/main/java/me/bymartrixx/vtd/gui/widget/SelectedPacksListWidget.java index b70382f..6fe46ec 100644 --- a/src/main/java/me/bymartrixx/vtd/gui/widget/SelectedPacksListWidget.java +++ b/src/main/java/me/bymartrixx/vtd/gui/widget/SelectedPacksListWidget.java @@ -67,7 +67,7 @@ private void updateSelection(Pack pack, Category category, boolean selected) { if (selected) { CategoryEntry categoryEntry = this.getOrCreateCategoryEntry(category); PackEntry entry = this.getPackEntry(pack, category); - int i = this.getLastChildIndex(category); + int i = this.getLastChildEntryIndex(category); if (i == -1) { i = this.children().indexOf(categoryEntry); } @@ -103,6 +103,7 @@ private void addPacks(Map> packs) { private int getCategoryEntryIndex(Category category) { for (int i = 0; i < this.children().size(); i++) { + //noinspection EqualsBetweenInconvertibleTypes - CategoryEntry overrides equals() if (this.children().get(i).equals(category)) { return i; } @@ -114,8 +115,20 @@ private int getCategoryEntryIndex(Category category) { private CategoryEntry getOrCreateCategoryEntry(Category category) { int i = this.getCategoryEntryIndex(category); if (i == -1) { - CategoryEntry entry = new CategoryEntry(this, category); - this.addEntry(entry); + CategoryEntry entry; + if (category instanceof Category.SubCategory subCategory) { + entry = new SubCategoryEntry(this, subCategory); + CategoryEntry parentEntry = this.getOrCreateCategoryEntry(subCategory.getParent()); + int index = this.getLastChildIndex(parentEntry.getCategory()); + if (index != -1) { + this.children().add(index + 1, entry); + } else { + this.addEntry(entry); + } + } else { + entry = new CategoryEntry(this, category); + this.addEntry(entry); + } return entry; } @@ -124,6 +137,7 @@ private CategoryEntry getOrCreateCategoryEntry(Category category) { private int getPackEntryIndex(Pack pack) { for (int i = 0; i < this.children().size(); i++) { + //noinspection EqualsBetweenInconvertibleTypes - PackEntry overrides equals() if (this.children().get(i).equals(pack)) { return i; } @@ -141,7 +155,7 @@ private PackEntry getPackEntry(Pack pack, Category category) { return (PackEntry) this.children().get(i); } - private int getLastChildIndex(Category category) { + private int getLastChildEntryIndex(Category category) { int index = -1; int i = this.getCategoryEntryIndex(category); if (i != -1) { @@ -161,6 +175,44 @@ private int getLastChildIndex(Category category) { return index; } + private int getLastSubCategoryIndex(Category category) { + int index = -1; + int i = this.getCategoryEntryIndex(category); + if (i != -1 && !(category instanceof Category.SubCategory)) { + for (i++; i < this.children().size(); i++) { + AbstractEntry entry = this.children().get(i); + if (entry instanceof SubCategoryEntry subCategoryEntry) { + if (subCategoryEntry.getParentCategory().equals(category)) { + index = i; + } + continue; + } + + break; + } + } + + return index; + } + + private int getLastChildIndex(Category category) { + int subCat = this.getLastSubCategoryIndex(category); + if (subCat != -1) { + SubCategoryEntry entry = (SubCategoryEntry) this.children().get(subCat); + int subCatIndex = this.getLastChildEntryIndex(entry.getParentCategory()); + if (subCatIndex != -1) { + return subCatIndex; + } + } + + int index = this.getLastChildEntryIndex(category); + if (index == -1 && subCat != -1) { + return subCat; + } + + return index; + } + @Override protected boolean isSelectedEntry(int index) { return this.getFocused() == this.getEntry(index); @@ -307,7 +359,7 @@ public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth } public static class CategoryEntry extends AbstractEntry { - private final Category category; + protected final Category category; private long lastClickTime = -1; public CategoryEntry(SelectedPacksListWidget widget, Category category) { @@ -339,6 +391,10 @@ protected void selectEntry() { this.widget.screen.selectCategory(this.category); } + public Category getCategory() { + return this.category; + } + @Override public boolean equals(Object obj) { if (obj == this) { @@ -353,6 +409,26 @@ public boolean equals(Object obj) { } } + public static class SubCategoryEntry extends CategoryEntry { + public SubCategoryEntry(SelectedPacksListWidget widget, Category.SubCategory category) { + super(widget, category); + } + + @Override + protected String getTextString() { + return "| " + super.getTextString(); + } + + @Override + public Category.SubCategory getCategory() { + return (Category.SubCategory) super.getCategory(); + } + + public Category getParentCategory() { + return this.getCategory().getParent(); + } + } + public static class PackEntry extends AbstractEntry { private final Category category; private final Pack pack;