diff --git a/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.java.patch b/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.java.patch index 61f85c9dc..142b1a27c 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.java.patch +++ b/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.java.patch @@ -1,18 +1,33 @@ --- before/net/minecraft/client/gui/GuiIngameMenu.java +++ after/net/minecraft/client/gui/GuiIngameMenu.java -@@ -30,9 +30,8 @@ +@@ -13,6 +13,7 @@ + { + private int saveStep; + private int visibleTime; ++ private net.minecraftforge.client.gui.NotificationModUpdateScreen modUpdateNotification; + + @Override + public void initGui() +@@ -30,13 +31,13 @@ this.buttonList.add(new GuiButton(4, this.width / 2 - 100, this.height / 4 + 24 + -16, I18n.format("menu.returnToGame"))); this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 96 + -16, 98, 20, I18n.format("menu.options"))); - GuiButton guibutton = this.addButton( - new GuiButton(7, this.width / 2 + 2, this.height / 4 + 96 + -16, 98, 20, I18n.format("menu.shareToLan")) - ); -+ this.buttonList.add(new GuiButton(12, this.width / 2 + 2, this.height / 4 + 96 + i, 98, 20, I18n.format("fml.menu.modoptions"))); ++ GuiButton modButton = this.addButton(new GuiButton(12, this.width / 2 + 2, this.height / 4 + 96 + i, 98, 20, I18n.format("fml.menu.mods"))); + GuiButton guibutton = this.addButton(new GuiButton(7, this.width / 2 - 100, this.height / 4 + 72 + -16, 200, 20, I18n.format("menu.shareToLan", new Object[0]))); guibutton.enabled = this.mc.isSingleplayer() && !this.mc.getIntegratedServer().getPublic(); - this.buttonList - .add(new GuiButton(5, this.width / 2 - 100, this.height / 4 + 48 + -16, 98, 20, I18n.format("gui.advancements"))); -@@ -77,13 +76,19 @@ +- this.buttonList +- .add(new GuiButton(5, this.width / 2 - 100, this.height / 4 + 48 + -16, 98, 20, I18n.format("gui.advancements"))); ++ this.buttonList.add(new GuiButton(5, this.width / 2 - 100, this.height / 4 + 48 + -16, 98, 20, I18n.format("gui.advancements"))); + this.buttonList.add(new GuiButton(6, this.width / 2 + 2, this.height / 4 + 48 + -16, 98, 20, I18n.format("gui.stats"))); ++ ++ this.modUpdateNotification = net.minecraftforge.client.gui.NotificationModUpdateScreen.init(this, modButton); + } + + @Override +@@ -77,13 +78,19 @@ this.mc.setIngameFocus(); break; case 5: @@ -32,3 +47,10 @@ } } +@@ -100,5 +107,6 @@ + this.drawDefaultBackground(); + this.drawCenteredString(this.fontRenderer, I18n.format("menu.game"), this.width / 2, 40, 16777215); + super.drawScreen(mouseX, mouseY, partialTicks); ++ this.modUpdateNotification.drawScreen(mouseX, mouseY, partialTicks); + } + } diff --git a/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.java.patch b/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.java.patch index c70385ce6..5ee6665f0 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.java.patch +++ b/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.java.patch @@ -48,7 +48,7 @@ - this.realmsNotification.setGuiSize(this.width, this.height); - this.realmsNotification.initGui(); - } -+ modUpdateNotification = net.minecraftforge.client.gui.NotificationModUpdateScreen.init(this, modButton); ++ this.modUpdateNotification = net.minecraftforge.client.gui.NotificationModUpdateScreen.init(this, this.modButton); } private void addSingleplayerMultiplayerButtons(int p_73969_1_, int p_73969_2_) @@ -56,7 +56,7 @@ this.buttonList.add(new GuiButton(1, this.width / 2 - 100, p_73969_1_, I18n.format("menu.singleplayer"))); this.buttonList.add(new GuiButton(2, this.width / 2 - 100, p_73969_1_ + p_73969_2_ * 1, I18n.format("menu.multiplayer"))); - this.realmsButton = this.addButton(new GuiButton(14, this.width / 2 - 100, p_73969_1_ + p_73969_2_ * 2, I18n.format("menu.online"))); -+ this.buttonList.add(modButton = new GuiButton(6, this.width / 2 - 100, p_73969_1_ + p_73969_2_ * 2, I18n.format("fml.menu.mods"))); ++ this.buttonList.add(this.modButton = new GuiButton(6, this.width / 2 - 100, p_73969_1_ + p_73969_2_ * 2, I18n.format("fml.menu.mods"))); } private void addDemoButtons(int p_73972_1_, int p_73972_2_) diff --git a/projects/cleanroom/build.gradle b/projects/cleanroom/build.gradle index f580804a6..af646c663 100644 --- a/projects/cleanroom/build.gradle +++ b/projects/cleanroom/build.gradle @@ -252,6 +252,7 @@ configurations { dependencies { compileOnly "com.cleanroommc:lwjglx:1.0.0" + compileOnly "org.jetbrains:annotations:26.0.2-1" installer "com.cleanroommc:lwjglxx:1.1.17" lwjglLibraries[0].each { installer "org.lwjgl:$it:$props.lwjgl_version" diff --git a/src/main/java/com/cleanroommc/catalogue/CatalogueConfig.java b/src/main/java/com/cleanroommc/catalogue/CatalogueConfig.java new file mode 100644 index 000000000..400e741ea --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/CatalogueConfig.java @@ -0,0 +1,94 @@ +package com.cleanroommc.catalogue; + +import net.minecraftforge.common.config.Config; +import net.minecraftforge.common.config.ConfigManager; +import net.minecraftforge.fml.client.event.ConfigChangedEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +@Config(modid = CatalogueConstants.MOD_ID) +public class CatalogueConfig { + + @Config.Comment({ + "Whether enable Catalogue mod.", + "Setting it false will stop Catalogue redirecting Forge's mod list calls." + }) + @Config.LangKey("catalogue.config.enable_mod") + public static boolean enableMod = true; + + @Config.RequiresMcRestart + @Config.Comment({ + "The list of library mods' mod ids.", + "They will have grey names in the mod list." + }) + @Config.LangKey("catalogue.config.library_list") + public static String[] libraryList = new String[]{ + "forge", + "FML", + "mcp", + "cleanroom", + "configanytime", + "mixinbooter", + "fugue", + "scalar" + }; + + @Config.RequiresMcRestart + @Config.Comment({ + "The list of ignored dependencies' mod ids.", + "They will not be displayed when searching for dependencies/dependants." + }) + @Config.LangKey("catalogue.config.ignored_dependencies_list") + public static String[] ignoredDependenciesList = new String[]{ + "minecraft", + "forge", + "FML", + "mcp", + "cleanroom" + }; + + @Config.RequiresMcRestart + @Config.Comment({ + "Whether limit the size of mods' banners." + }) + @Config.LangKey("catalogue.config.enable_banner_limit") + public static boolean enableBannerLimit = false; + + @Config.RequiresMcRestart + @Config.Comment({ + "The maximum of banner's width. Will not work if Enable Banner Limit is set false." + }) + @Config.LangKey("catalogue.config.banner_max_width") + @Config.RangeInt(min = 0) + public static int bannerMaxWidth = 1280; + + @Config.RequiresMcRestart + @Config.Comment({ + "The maximum of banner's height. Will not work if Enable Banner Limit is set false." + }) + @Config.LangKey("catalogue.config.banner_max_height") + @Config.RangeInt(min = 0) + public static int bannerMaxHeight = 256; + + @Config.RequiresMcRestart + @Config.Comment({ + "Whether limit the size of mods' icons." + }) + @Config.LangKey("catalogue.config.enable_icon_limit") + public static boolean enableIconLimit = false; + + @Config.RequiresMcRestart + @Config.Comment({ + "The maximum of icon's width and height. Will not work if Enable Icon Limit is set false." + }) + @Config.LangKey("catalogue.config.icon_max_width_height") + @Config.RangeInt(min = 0) + public static int iconMaxWidthHeight = 256; + + @SubscribeEvent + public static void onConfigChanged(@NotNull ConfigChangedEvent.OnConfigChangedEvent event) { + if (event.getModID().equals(CatalogueConstants.MOD_ID)) { + ConfigManager.sync(CatalogueConstants.MOD_ID, Config.Type.INSTANCE); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/CatalogueConstants.java b/src/main/java/com/cleanroommc/catalogue/CatalogueConstants.java new file mode 100644 index 000000000..b1bfd13f0 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/CatalogueConstants.java @@ -0,0 +1,10 @@ +package com.cleanroommc.catalogue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CatalogueConstants { + public static final String MOD_ID = "catalogue"; + public static final String MOD_NAME = "Catalogue"; + public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME); +} diff --git a/src/main/java/com/cleanroommc/catalogue/Utils.java b/src/main/java/com/cleanroommc/catalogue/Utils.java new file mode 100644 index 000000000..977b45da0 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/Utils.java @@ -0,0 +1,39 @@ +package com.cleanroommc.catalogue; + +import net.minecraft.util.ResourceLocation; + +import java.util.function.Consumer; + +/** + * Author: MrCrayfish + */ +public class Utils { + public static ResourceLocation resource(String name) { + return new ResourceLocation(CatalogueConstants.MOD_ID, name); + } + + public static ResourceLocation withDefaultNamespace(String name) { + return resource("textures/gui/sprites/" + name + ".png"); + } + + public static T make(T object, Consumer consumer) { + consumer.accept(object); + return object; + } + + public static float lerp(float delta, float start, float end) { + return start + delta * (end - start); + } + + public static double lerp(double delta, double start, double end) { + return start + delta * (end - start); + } + + public static int roundToward(int value, int factor) { + return positiveCeilDiv(value, factor) * factor; + } + + public static int positiveCeilDiv(int x, int y) { + return -Math.floorDiv(-x, y); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/Branding.java b/src/main/java/com/cleanroommc/catalogue/client/Branding.java new file mode 100644 index 000000000..b2c557d36 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/Branding.java @@ -0,0 +1,64 @@ +package com.cleanroommc.catalogue.client; + +import com.cleanroommc.catalogue.CatalogueConstants; +import com.cleanroommc.catalogue.Utils; +import com.cleanroommc.catalogue.exception.InvalidBrandingImageException; +import com.cleanroommc.catalogue.exception.ModResourceNotFoundException; +import com.cleanroommc.catalogue.platform.ClientServices; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.util.ResourceLocation; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Function; + +/** + * Author: MrCrayfish + */ +public record Branding(String prefix, int imageWidth, int imageHeight, + BiPredicate predicate, + Function locator, boolean override) { + public static final Branding ICON = new Branding("icon", ClientServices.PLATFORM.getIconLimit(), ClientServices.PLATFORM.getIconLimit(), ImagePredicate.SQUARE.and(ClientServices.PLATFORM.getEnableIconLimit() ? ImagePredicate.LESS_THAN_OR_EQUAL : ImagePredicate.ANY), IModData::getImageIcon, false); + public static final Branding BANNER = new Branding("banner", ClientServices.PLATFORM.getBannerLimit().maxWidth(), ClientServices.PLATFORM.getBannerLimit().maxHeight(), ClientServices.PLATFORM.getEnableBannerLimit() ? ImagePredicate.LESS_THAN_OR_EQUAL : ImagePredicate.ANY, IModData::getBanner, false); + public static final Branding BACKGROUND = new Branding("background", 512, 256, ImagePredicate.EQUAL, IModData::getBackground, true); + + public Optional loadResource(IModData data) { + String resource = this.locator.apply(data); + if (resource == null || resource.isBlank()) return Optional.empty(); + + String modId = data.getModId(); + BufferedImage image; + try { + IResourcePack resourcePack = data.getResourcePack(); + if (this.equals(Branding.BANNER) && resourcePack != null && !resource.startsWith("/")) { + image = resourcePack.getPackImage(); + } else { + resource = resource.startsWith("/") ? resource : "/" + resource; + image = ClientServices.PLATFORM.loadImageFromModResource(modId, resource); + } + this.predicate.test(image, this); // An InvalidBrandingImageException will be thrown if anything is wrong + DynamicTexture texture = new DynamicTexture(image); + ResourceLocation id = this.override ? Utils.resource(this.prefix) : + Utils.resource("%s/%s".formatted(this.prefix, data.getModId())); + Minecraft.getMinecraft().getTextureManager().loadTexture(id, texture); + return Optional.of(new ImageInfo(id, image.getWidth(), image.getHeight(), () -> { + Minecraft.getMinecraft().getTextureManager().deleteTexture(id); + })); + } catch (InvalidBrandingImageException e) { + CatalogueConstants.LOG.error("Invalid {} branding resource '{}' for mod '{}'", this.prefix, resource, modId, e); + } catch (ModResourceNotFoundException e) { + CatalogueConstants.LOG.error("Unable to locate the {} branding resource '{}' for mod '{}'", this.prefix, resource, modId, e); + } catch (IOException e) { + CatalogueConstants.LOG.error("An error occurred when loading the {} branding resource '{}' for mod '{}'", this.prefix, resource, modId, e); + } + + return Optional.empty(); + } + + public record BannerLimit(int maxWidth, int maxHeight) { + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/CleanroomModData.java b/src/main/java/com/cleanroommc/catalogue/client/CleanroomModData.java new file mode 100644 index 000000000..d5310ad6f --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/CleanroomModData.java @@ -0,0 +1,250 @@ +package com.cleanroommc.catalogue.client; + +import com.cleanroommc.catalogue.CatalogueConfig; +import com.cleanroommc.catalogue.CatalogueConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.common.ForgeVersion; +import net.minecraftforge.fml.client.FMLClientHandler; +import net.minecraftforge.fml.client.IModGuiFactory; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.common.ModMetadata; +import net.minecraftforge.fml.common.versioning.ArtifactVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Author: MrCrayfish + */ +public class CleanroomModData implements IModData { + public static final ResourceLocation VERSION_CHECK_ICONS = new ResourceLocation("forge", "textures/gui/version_check_icons.png"); + public static final List LIB_MODS = Arrays.asList(CatalogueConfig.libraryList); + public static final List IGNORED_DEPENDENCIES = Arrays.asList(CatalogueConfig.ignoredDependenciesList); + + private final @NotNull ModContainer info; + private final @Nullable ModMetadata metadata; + private final Type type; + private final Set dependencies; + private final Set childMods; + + public CleanroomModData(@NotNull ModContainer info) { + this.info = info; + this.metadata = info.getMetadata(); + this.type = analyzeType(info); + this.dependencies = analyzeDependencies(info); + this.childMods = analyzeChildMods(info); + } + + @Override + public Type getType() { + return this.type; + } + + @Override + public String getModId() { + return this.info.getModId(); + } + + @Override + public String getDisplayName() { + return this.info.getName(); + } + + @Override + public String getVersion() { + return this.info.getDisplayVersion(); + } + + @Override + public String getInnerVersion() { + return this.info.getVersion(); + } + + @Nullable + @Override + public String getDescription() { + return this.metadata != null ? this.metadata.description : null; + } + + @Nullable + @Override + public String getItemIcon() { + return this.metadata != null ? this.metadata.iconItem : null; + } + + @Nullable + @Override + public String getImageIcon() { + return this.metadata != null ? this.metadata.iconFile : null; + } + + @Nullable + @Override + public String getLicense() { + return this.metadata != null ? this.metadata.license : null; + } + + @Nullable + @Override + public String getCredits() { + return this.metadata != null ? this.metadata.credits : null; + } + + @Nullable + @Override + public String getAuthors() { + return this.metadata != null ? this.metadata.getAuthorList() : null; + } + + @Nullable + @Override + public String getHomepage() { + return this.metadata != null ? this.metadata.url : null; + } + + @Nullable + @Override + public String getIssueTracker() { + return this.metadata != null ? this.metadata.issueTrackerUrl : null; + } + + @Nullable + @Override + public String getBanner() { + return this.metadata != null ? this.metadata.logoFile : null; + } + + @Nullable + @Override + public String getBackground() { + return this.metadata != null ? this.metadata.backgroundFile : null; + } + + @Nullable + @Override + public String getChildModNames() { + return this.metadata != null ? this.metadata.getChildModList() : null; + } + + @Nullable + @Override + public String getParentModName() { + return this.metadata != null && this.metadata.parentMod != null ? this.metadata.parentMod.getName() : null; + } + + @Nullable + @Override + public Update getUpdate() { + ForgeVersion.CheckResult result = ForgeVersion.getCleanResult(this.info); + if (result != null && result.status.shouldDraw()) { + return new Update(result.status.isAnimated(), result.url, result.status.getSheetOffset(), VERSION_CHECK_ICONS, result.status == ForgeVersion.Status.OUTDATED || result.status == ForgeVersion.Status.BETA_OUTDATED, result.latestFound, result.homepage); + } + return null; + } + + @NotNull + @Override + public Set getDependencies() { + return this.dependencies; + } + + @NotNull + @Override + public Set getChildMods() { + return this.childMods; + } + + @Override + public boolean hasConfig() { + IModGuiFactory guiFactory = FMLClientHandler.instance().getGuiFactoryFor(this.info); + if (guiFactory == null) return false; + return guiFactory.hasConfigGui(); + } + + @Override + public void openConfigScreen(Minecraft minecraft, GuiScreen parent) { + try { + IModGuiFactory guiFactory = FMLClientHandler.instance().getGuiFactoryFor(this.info); + GuiScreen newScreen = guiFactory.createConfigGui(parent); + minecraft.displayGuiScreen(newScreen); + } catch (Exception e) { + CatalogueConstants.LOG.error("There was a critical issue trying to build the config GUI for {}", this.getModId(), e); + } + } + + @Override + public void drawUpdateIcon(Minecraft minecraft, Update update, int x, int y) { + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + int vOffset = update.animated() && (System.currentTimeMillis() / 800 & 1) == 1 ? 8 : 0; + minecraft.getTextureManager().bindTexture(update.textures()); + Gui.drawModalRectWithCustomSizedTexture(x, y, update.texOffset() * 8, vOffset, 8, 8, 64, 16); + } + + @Nullable + @Override + public String getUpdateText(Update update) { + ForgeVersion.CheckResult result = ForgeVersion.getCleanResult(this.info); + if (result == null) return null; + switch (result.status) { + case BETA: + return TextFormatting.GOLD + I18n.format("catalogue.gui.beta"); + case AHEAD: + return TextFormatting.LIGHT_PURPLE + I18n.format("catalogue.gui.ahead", update.latestFound()); + case BETA_OUTDATED: + if (update.homepage() != null && !update.homepage().isBlank()) { + return TextFormatting.GOLD + I18n.format("catalogue.gui.beta_update_available", update.latestFound(), update.homepage()); + } else { + return TextFormatting.GOLD + I18n.format("catalogue.gui.beta_update_available_no_page", update.latestFound()); + } + case OUTDATED: + if (update.homepage() != null && !update.homepage().isBlank()) { + return TextFormatting.GREEN + I18n.format("catalogue.gui.update_available", update.latestFound(), update.homepage()); + } else { + return TextFormatting.GREEN + I18n.format("catalogue.gui.update_available_no_page", update.latestFound()); + } + } + return null; + } + + @Nullable + @Override + public IResourcePack getResourcePack() { + return FMLClientHandler.instance().getResourcePackFor(this.getModId()); + } + + private Type analyzeType(@NotNull ModContainer info) { + if (this.metadata != null && this.metadata.parentMod != null) { + return Type.CHILD; + } else if (LIB_MODS.contains(info.getModId())) { + return Type.LIBRARY; + } else { + return Type.DEFAULT; + } + } + + private static @NotNull Set analyzeDependencies(@NotNull ModContainer source) { + List versions = source.getDependencies(); + return versions.stream() + .map(ArtifactVersion::getLabel) + .filter(modid -> !IGNORED_DEPENDENCIES.contains(modid)) + .collect(Collectors.toUnmodifiableSet()); + } + + private static @NotNull Set analyzeChildMods(@NotNull ModContainer source) { + ModMetadata metadata = source.getMetadata(); + if (metadata == null) return Collections.emptySet(); + return metadata.childMods.stream() + .filter(Objects::nonNull) + .map(ModContainer::getModId) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/ClientHandler.java b/src/main/java/com/cleanroommc/catalogue/client/ClientHandler.java new file mode 100644 index 000000000..40faef8d0 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/ClientHandler.java @@ -0,0 +1,22 @@ +package com.cleanroommc.catalogue.client; + +import com.cleanroommc.catalogue.CatalogueConfig; +import com.cleanroommc.catalogue.client.screen.CatalogueModListScreen; +import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraftforge.fml.client.GuiModList; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +/** + * Author: MrCrayfish + */ +public class ClientHandler { + @SubscribeEvent + public static void onOpenScreen(@NotNull GuiOpenEvent event) { + if (!CatalogueConfig.enableMod) return; + //noinspection deprecation + if (event.getGui() instanceof GuiModList screen) { + event.setGui(new CatalogueModListScreen(screen.getParentScreen())); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/ClientHelper.java b/src/main/java/com/cleanroommc/catalogue/client/ClientHelper.java new file mode 100644 index 000000000..cd1f66461 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/ClientHelper.java @@ -0,0 +1,139 @@ +package com.cleanroommc.catalogue.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import org.lwjgl.opengl.GL11; + +/** + * Author: MrCrayfish + */ +public class ClientHelper { + /** + * Creates a scissor test using minecraft screen coordinates instead of pixel coordinates. + * + * @param screenX + * @param screenY + * @param boxWidth + * @param boxHeight + */ + public static void scissor(int screenX, int screenY, int boxWidth, int boxHeight) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledRes = new ScaledResolution(mc); + int scale = scaledRes.getScaleFactor(); + + int x = screenX * scale; + int y = mc.displayHeight - (screenY * scale + boxHeight * scale); + int width = Math.max(0, boxWidth * scale); + int height = Math.max(0, boxHeight * scale); + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor(x, y, width, height); + } + + public static boolean isMouseWithin(int x, int y, int width, int height, int mouseX, int mouseY) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + + public static void blitNineSlicedSprite(NineSlice nineSlice, int x, int y, int width, int height) { + blitNineSlicedSprite(nineSlice, x, y, 0, width, height); + } + + public static void blitNineSlicedSprite(NineSlice nineSlice, int x, int y, int blitOffset, int width, int height) { + NineSlice.Border border = nineSlice.border(); + int i = Math.min(border.left(), width / 2); + int j = Math.min(border.right(), width / 2); + int k = Math.min(border.top(), height / 2); + int l = Math.min(border.bottom(), height / 2); + if (width == nineSlice.width() && height == nineSlice.height()) { + blitSprite(nineSlice.width(), nineSlice.height(), 0, 0, x, y, blitOffset, width, height); + } else if (height == nineSlice.height()) { + blitSprite(nineSlice.width(), nineSlice.height(), 0, 0, x, y, blitOffset, i, height); + blitTiledSprite(x + i, y, blitOffset, width - j - i, height, i, 0, nineSlice.width() - j - i, nineSlice.height(), nineSlice.width(), nineSlice.height()); + blitSprite(nineSlice.width(), nineSlice.height(), nineSlice.width() - j, 0, x + width - j, y, blitOffset, j, height); + } else if (width == nineSlice.width()) { + blitSprite(nineSlice.width(), nineSlice.height(), 0, 0, x, y, blitOffset, width, k); + blitTiledSprite(x, y + k, blitOffset, width, height - l - k, 0, k, nineSlice.width(), nineSlice.height() - l - k, nineSlice.width(), nineSlice.height()); + blitSprite(nineSlice.width(), nineSlice.height(), 0, nineSlice.height() - l, x, y + height - l, blitOffset, width, l); + } else { + blitSprite(nineSlice.width(), nineSlice.height(), 0, 0, x, y, blitOffset, i, k); + blitTiledSprite(x + i, y, blitOffset, width - j - i, k, i, 0, nineSlice.width() - j - i, k, nineSlice.width(), nineSlice.height()); + blitSprite(nineSlice.width(), nineSlice.height(), nineSlice.width() - j, 0, x + width - j, y, blitOffset, j, k); + blitSprite(nineSlice.width(), nineSlice.height(), 0, nineSlice.height() - l, x, y + height - l, blitOffset, i, l); + blitTiledSprite(x + i, y + height - l, blitOffset, width - j - i, l, i, nineSlice.height() - l, nineSlice.width() - j - i, l, nineSlice.width(), nineSlice.height()); + blitSprite(nineSlice.width(), nineSlice.height(), nineSlice.width() - j, nineSlice.height() - l, x + width - j, y + height - l, blitOffset, j, l); + blitTiledSprite(x, y + k, blitOffset, i, height - l - k, 0, k, i, nineSlice.height() - l - k, nineSlice.width(), nineSlice.height()); + blitTiledSprite(x + i, y + k, blitOffset, width - j - i, height - l - k, i, k, nineSlice.width() - j - i, nineSlice.height() - l - k, nineSlice.width(), nineSlice.height()); + blitTiledSprite(x + width - j, y + k, blitOffset, i, height - l - k, nineSlice.width() - j, k, j, nineSlice.height() - l - k, nineSlice.width(), nineSlice.height()); + } + } + + private static void blitTiledSprite(int x, int y, int blitOffset, int width, int height, int uPosition, int vPosition, int spriteWidth, int spriteHeight, int nineSliceWidth, int nineSliceHeight) { + if (width > 0 && height > 0) { + if (spriteWidth <= 0 || spriteHeight <= 0) { + throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + spriteWidth + "x" + spriteHeight); + } + + for (int i = 0; i < width; i += spriteWidth) { + int j = Math.min(spriteWidth, width - i); + + for (int k = 0; k < height; k += spriteHeight) { + int l = Math.min(spriteHeight, height - k); + blitSprite(nineSliceWidth, nineSliceHeight, uPosition, vPosition, x + i, y + k, blitOffset, j, l); + } + } + } + } + + private static void blitSprite(int textureWidth, int textureHeight, int uPosition, int vPosition, int x, int y, int blitOffset, int uWidth, int vHeight) { + if (uWidth != 0 && vHeight != 0) { + float minU = (float) uPosition / (float) textureWidth; + float maxU = (float) (uPosition + uWidth) / (float) textureWidth; + float minV = (float) vPosition / (float) textureHeight; + float maxV = (float) (vPosition + vHeight) / (float) textureHeight; + + innerBlit(x, x + uWidth, y, y + vHeight, blitOffset, minU, maxU, minV, maxV); + } + } + + /** + * Performs the inner blit operation for rendering a texture with the specified coordinates and texture coordinates without color tinting. + * + * @param x1 the x-coordinate of the first corner of the blit position. + * @param x2 the x-coordinate of the second corner of the blit position + * . + * @param y1 the y-coordinate of the first corner of the blit position. + * @param y2 the y-coordinate of the second corner of the blit position + * . + * @param blitOffset the z-level offset for rendering order. + * @param minU the minimum horizontal texture coordinate. + * @param maxU the maximum horizontal texture coordinate. + * @param minV the minimum vertical texture coordinate. + * @param maxV the maximum vertical texture coordinate. + */ + static void innerBlit(int x1, int x2, int y1, int y2, int blitOffset, float minU, float maxU, float minV, float maxV) { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferbuilder = tessellator.getBuffer(); + bufferbuilder.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); + bufferbuilder.pos(x1, y1, blitOffset).tex(minU, minV).endVertex(); + bufferbuilder.pos(x1, y2, blitOffset).tex(minU, maxV).endVertex(); + bufferbuilder.pos(x2, y2, blitOffset).tex(maxU, maxV).endVertex(); + bufferbuilder.pos(x2, y1, blitOffset).tex(maxU, minV).endVertex(); + tessellator.draw(); + } + + // Info of the texture + public record NineSlice(int width, int height, Border border) { + public record Border(int left, int top, int right, int bottom) { + public Border(int border) { + this(border, border, border, border); + } + } + + public NineSlice(int width, int height, int border) { + this(width, height, new Border(border)); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/IModData.java b/src/main/java/com/cleanroommc/catalogue/client/IModData.java new file mode 100644 index 000000000..02af3a310 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/IModData.java @@ -0,0 +1,103 @@ +package com.cleanroommc.catalogue.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.TextFormatting; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +/** + * Author: MrCrayfish + */ +public interface IModData { + Type getType(); + + String getModId(); + + String getDisplayName(); + + String getVersion(); + + String getInnerVersion(); + + @Nullable + String getDescription(); + + @Nullable + String getItemIcon(); + + @Nullable + String getImageIcon(); + + @Nullable + String getLicense(); + + @Nullable + String getCredits(); + + @Nullable + String getAuthors(); + + @Nullable + String getHomepage(); + + @Nullable + String getIssueTracker(); + + @Nullable + String getBanner(); + + @Nullable + String getBackground(); + + @Nullable + String getChildModNames(); + + @Nullable + String getParentModName(); + + @Nullable + Update getUpdate(); + + @Nullable + IResourcePack getResourcePack(); + + @NotNull + Set getDependencies(); //TODO lazily + + @NotNull + Set getChildMods(); + + boolean hasConfig(); + + void openConfigScreen(Minecraft minecraft, GuiScreen parent); + + void drawUpdateIcon(Minecraft minecraft, Update update, int x, int y); + + @Nullable + String getUpdateText(Update update); + + record Update(boolean animated, String url, int texOffset, ResourceLocation textures, boolean updatable, + String latestFound, String homepage) { + } + + enum Type { + DEFAULT(TextFormatting.RESET), + LIBRARY(TextFormatting.DARK_GRAY), + CHILD(TextFormatting.AQUA); + + private final TextFormatting style; + + Type(TextFormatting style) { + this.style = style; + } + + public TextFormatting getStyle() { + return this.style; + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/ImageInfo.java b/src/main/java/com/cleanroommc/catalogue/client/ImageInfo.java new file mode 100644 index 000000000..df746526e --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/ImageInfo.java @@ -0,0 +1,9 @@ +package com.cleanroommc.catalogue.client; + +import net.minecraft.util.ResourceLocation; + +/** + * Author: MrCrayfish + */ +public record ImageInfo(ResourceLocation resource, int width, int height, Runnable unregister) { +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/ImagePredicate.java b/src/main/java/com/cleanroommc/catalogue/client/ImagePredicate.java new file mode 100644 index 000000000..6500d976d --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/ImagePredicate.java @@ -0,0 +1,32 @@ +package com.cleanroommc.catalogue.client; + +import com.cleanroommc.catalogue.exception.InvalidBrandingImageException; +import org.apache.commons.lang3.function.TriFunction; +import org.jetbrains.annotations.Nullable; + +import java.awt.image.BufferedImage; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +/** + * Author: MrCrayfish + */ +public record ImagePredicate(TriFunction predicate, + BiFunction errorMessage) implements BiPredicate { + public static final ImagePredicate SQUARE = new ImagePredicate((image, maxWidth, maxHeight) -> Objects.equals(image.getWidth(), image.getHeight()), (maxWidth, maxHeight) -> "Image must be a square"); + public static final ImagePredicate EQUAL = new ImagePredicate((image, maxWidth, maxHeight) -> image.getWidth() == maxWidth && image.getHeight() == maxHeight, "Image dimensions must be exactly %spx by %spx"::formatted); + public static final ImagePredicate LESS_THAN_OR_EQUAL = new ImagePredicate((image, maxWidth, maxHeight) -> image.getWidth() <= maxWidth && image.getHeight() <= maxHeight, "Image dimensions must be less than or equal to %spx by %spx"::formatted); + public static final ImagePredicate ANY = new ImagePredicate((image, maxWidth, maxHeight) -> true, (maxWidth, maxHeight) -> ""); + + @Override + public boolean test(@Nullable BufferedImage image, Branding branding) throws InvalidBrandingImageException { + if (image == null) { + throw new InvalidBrandingImageException("Image is null"); + } + if (!this.predicate.apply(image, branding.imageWidth(), branding.imageHeight())) { + throw new InvalidBrandingImageException(this.errorMessage.apply(branding.imageWidth(), branding.imageHeight())); + } + return true; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/CatalogueModListScreen.java b/src/main/java/com/cleanroommc/catalogue/client/screen/CatalogueModListScreen.java new file mode 100644 index 000000000..5fb7bb83d --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/CatalogueModListScreen.java @@ -0,0 +1,1387 @@ +package com.cleanroommc.catalogue.client.screen; + +import com.cleanroommc.catalogue.CatalogueConstants; +import com.cleanroommc.catalogue.Utils; +import com.cleanroommc.catalogue.client.Branding; +import com.cleanroommc.catalogue.client.ClientHelper; +import com.cleanroommc.catalogue.client.IModData; +import com.cleanroommc.catalogue.client.ImageInfo; +import com.cleanroommc.catalogue.client.screen.widget.*; +import com.cleanroommc.catalogue.platform.ClientServices; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.resources.I18n; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.text.Style; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.util.text.event.ClickEvent; +import net.minecraftforge.fml.common.registry.ForgeRegistries; +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.commons.lang3.mutable.MutableObject; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@SuppressWarnings("CodeBlock2Expr") +public class CatalogueModListScreen extends GuiScreen implements DropdownMenuHandler { + private static final Favourites FAVOURITES = new Favourites(); + private static final Comparator SORT_ALPHABETICALLY = Comparator.comparing(o -> o.getData().getDisplayName()); + private static final Comparator SORT_ALPHABETICALLY_REVERSED = SORT_ALPHABETICALLY.reversed(); + private static final Comparator SORT_FAVOURITES_FIRST = Comparator.comparing(ModListEntry::getData, Comparator.comparing(data -> FAVOURITES.has(data.getModId()))).reversed().thenComparing(SORT_ALPHABETICALLY); + private static final MutableObject OPTION_QUERY = new MutableObject<>(""); + private static final MutableBoolean OPTION_HIDE_LIBRARIES = new MutableBoolean(true); + private static final MutableBoolean OPTION_HIDE_CHILD_MODS = new MutableBoolean(true); + private static final MutableBoolean OPTION_CONFIGS_ONLY = new MutableBoolean(false); + private static final MutableBoolean OPTION_UPDATES_ONLY = new MutableBoolean(false); + private static final MutableBoolean OPTION_FAVOURITES_ONLY = new MutableBoolean(false); + private static final MutableObject> OPTION_SORT = new MutableObject<>(SORT_ALPHABETICALLY); + private static final ResourceLocation MISSING_BANNER = Utils.resource("textures/gui/missing_banner.png"); + private static final ResourceLocation MISSING_BACKGROUND = Utils.resource("textures/gui/missing_background.png"); + private static final ResourceLocation MINECRAFT_LOGO = Utils.resource("textures/gui/minecraft.png"); + private static final ImageInfo MISSING_BANNER_INFO = new ImageInfo(MISSING_BANNER, 120, 120, () -> { + }); + private static final Map BANNER_CACHE = new HashMap<>(); + private static final Map IMAGE_ICON_CACHE = new HashMap<>(); + private static final Map ITEM_ICON_CACHE = new HashMap<>(); + private static final Map CACHED_MODS = new HashMap<>(); + private static final Pattern MOD_ID_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{1,63}$"); + private static final Supplier> COUNTS = Suppliers.memoize(() -> { + int[] counts = new int[2]; + CACHED_MODS.forEach((modId, data) -> { + if (data.getType() == IModData.Type.CHILD) return; + counts[data.getType() == IModData.Type.LIBRARY ? 1 : 0]++; + }); + return Pair.of(counts[0], counts[1]); + }); + private static final Map SEARCH_FILTERS = ImmutableMap.builder() + .put("dependencies", new SearchFilter((query, data) -> { + IModData target = CACHED_MODS.get(query.toLowerCase(Locale.ENGLISH)); + return target != null && target.getDependencies().contains(data.getModId()); + })) + .put("dependents", new SearchFilter((query, data) -> { + return data.getDependencies().stream().anyMatch(query::equalsIgnoreCase); + })) + .put("childmods", new SearchFilter((query, data) -> { + IModData target = CACHED_MODS.get(query.toLowerCase(Locale.ENGLISH)); + return target != null && target.getChildMods().contains(data.getModId()); + })) + .put("parentmod", new SearchFilter((query, data) -> { + return data.getChildMods().stream().anyMatch(query::equalsIgnoreCase); + })).build(); + private static final TextFormatting SEARCH_FILTER_KEY = TextFormatting.GOLD; + private static final TextFormatting SEARCH_FILTER_VALUE = TextFormatting.WHITE; + private static @Nullable ImageInfo cachedBackground; + private static boolean loaded = false; + + private final GuiScreen parentScreen; + private CatalogueTextField searchTextField; + private ModList modList; + private StringList descriptionList; + private IModData selectedModData; + private CatalogueTextButton optionsButton; + private CatalogueIconButton modFolderButton; + private CatalogueIconButton configButton; + private CatalogueIconButton websiteButton; + private CatalogueIconButton issueButton; + private @Nullable DropdownMenu menu; + + private @Nullable List activeTooltip; + private int tooltipYOffset; + /** + * Time record of text box clicking. + */ + private long lastClickTime; + private boolean didRepeatEvents; + + public CatalogueModListScreen(GuiScreen parent) { + super(); + this.parentScreen = parent; + if (!loaded) { + ClientServices.PLATFORM.getAllModData().forEach(data -> CACHED_MODS.put(data.getModId().toLowerCase(Locale.ENGLISH), data)); + CACHED_MODS.put("minecraft", new MinecraftModData()); // Override minecraft + BANNER_CACHE.put("minecraft", new ImageInfo(MINECRAFT_LOGO, 1024, 256, () -> { + })); + FAVOURITES.load(); + loaded = true; + } + } + + @Override + public void setMenu(@Nullable DropdownMenu menu) { + if (this.menu != null && this.menu != menu) { + this.menu.hide(); + } + this.menu = menu; + } + + @Override + public void initGui() { + this.didRepeatEvents = Keyboard.areRepeatEventsEnabled(); + Keyboard.enableRepeatEvents(true); + this.searchTextField = new CatalogueTextField(0, this.fontRenderer, 11, 25, 148, 20) { + @Override + public int getWidth() { + if (this.getText().startsWith("@")) { + return super.getWidth() - 16; + } + return super.getWidth(); + } + }; + this.searchTextField.setFormatter(this::formatQuery); + this.searchTextField.setMaxStringLength(128); + this.searchTextField.setText(OPTION_QUERY.get()); + this.searchTextField.setResponder(s -> { + if (!OPTION_QUERY.get().equals(s)) { + OPTION_QUERY.setValue(s); + this.updateSearchFieldSuggestion(s); + this.modList.filterAndUpdateList(); + } + }); + + this.modList = new ModList(); + this.modList.setSlotXBoundsFromLeft(10); + + this.addButton(new CatalogueTextButton(1, 10, modList.bottom + 8, 127, 20, I18n.format("gui.back"))); + this.modFolderButton = this.addButton(new CatalogueIconButton(2, 140, modList.bottom + 8, 0, 0)); + + int padding = 10; + int contentLeft = this.modList.right + 12 + padding; + int contentWidth = this.width - contentLeft - padding; + int buttonWidth = (contentWidth - padding) / 3; + + this.configButton = this.addButton(new CatalogueIconButton(3, contentLeft, 105, 10, 0, buttonWidth, I18n.format("catalogue.gui.config"))); + this.configButton.visible = false; + + this.websiteButton = this.addButton(new CatalogueIconButton(4, contentLeft + buttonWidth + 5, 105, 20, 0, buttonWidth, I18n.format("catalogue.gui.website"))); + this.websiteButton.visible = false; + + this.issueButton = this.addButton(new CatalogueIconButton(5, contentLeft + buttonWidth + buttonWidth + 10, 105, 30, 0, buttonWidth, I18n.format("catalogue.gui.submit_bug"))); + this.issueButton.visible = false; + + this.descriptionList = new StringList(contentWidth + padding * 2, 50, contentLeft - padding, 130); + + this.optionsButton = this.addButton(new CatalogueIconButton(6, this.modList.right - 16, 6, 40, 0, 16, 16)); + + this.modList.filterAndUpdateList(); + + // Resizing window causes all widgets to be recreated, therefore need to update selected info + if (this.selectedModData != null) { + this.setSelectedModData(this.selectedModData); + ModListEntry entry = this.modList.getSelected(); + if (entry != null) { + this.modList.centerScrollOn(entry); + } + } + this.updateSearchFieldSuggestion(this.searchTextField.getText()); + } + + @Override + public void onGuiClosed() { + Keyboard.enableRepeatEvents(this.didRepeatEvents); + FAVOURITES.save(); + } + + @Override + public void actionPerformed(@NotNull GuiButton button) { + switch (button.id) { + case 1: + this.mc.displayGuiScreen(this.parentScreen); + break; + case 2: + try { + Class oclass = Class.forName("java.awt.Desktop"); + Object object = oclass.getMethod("getDesktop", new Class[0]).invoke(null); + oclass.getMethod("open", File.class).invoke(object, ClientServices.PLATFORM.getModDirectory()); + } catch (Exception e) { + CatalogueConstants.LOG.error("Problem opening mods folder", e); + } + break; + case 3: + this.selectedModData.openConfigScreen(this.mc, this); + break; + case 4: + this.openLink(this.selectedModData.getHomepage()); + break; + case 5: + this.openLink(this.selectedModData.getIssueTracker()); + break; + case 6: + DropdownMenu menu = DropdownMenu.builder(this) + .setMinItemSize(100, 16) + .setAlignment(DropdownMenu.Alignment.BELOW_RIGHT) + .addMenu(I18n.format("catalogue.gui.filters"), DropdownMenu.builder(this) + .setMinItemSize(60, 16) + .setAlignment(DropdownMenu.Alignment.END_TOP) + .addCheckbox(I18n.format("catalogue.gui.filters.configs_only"), OPTION_CONFIGS_ONLY, newValue -> { + this.modList.filterAndUpdateList(); + return false; + }) + .addCheckbox(I18n.format("catalogue.gui.filters.updates_only"), OPTION_UPDATES_ONLY, newValue -> { + this.modList.filterAndUpdateList(); + return false; + }) + .addCheckbox(I18n.format("catalogue.gui.filters.favourites"), OPTION_FAVOURITES_ONLY, newValue -> { + this.modList.filterAndUpdateList(); + return false; + })) + .addMenu(I18n.format("catalogue.gui.sort"), DropdownMenu.builder(this) + .setMinItemSize(60, 16) + .setAlignment(DropdownMenu.Alignment.END_TOP) + .addItem(I18n.format("catalogue.gui.sort.alphabetically"), () -> { + OPTION_SORT.setValue(SORT_ALPHABETICALLY); + this.modList.filterAndUpdateList(); + }) + .addItem(I18n.format("catalogue.gui.sort.alphabetically_reverse"), () -> { + OPTION_SORT.setValue(SORT_ALPHABETICALLY_REVERSED); + this.modList.filterAndUpdateList(); + }) + .addItem(I18n.format("catalogue.gui.sort.favourites_first"), () -> { + OPTION_SORT.setValue(SORT_FAVOURITES_FIRST); + this.modList.filterAndUpdateList(); + })) + .addCheckbox(I18n.format("catalogue.gui.hide_libraries"), OPTION_HIDE_LIBRARIES, newValue -> { + this.modList.filterAndUpdateList(); + return false; + }) + .addCheckbox(I18n.format("catalogue.gui.hide_child_mods"), OPTION_HIDE_CHILD_MODS, newValue -> { + this.modList.filterAndUpdateList(); + return false; + }).build(); + menu.toggle(button); + break; + default: + break; + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.activeTooltip = null; + + boolean inMenu = this.menu != null; + this.drawDefaultBackground(); + int disableableMouseX = inMenu ? -1000 : mouseX; + int disableableMouseY = inMenu ? -1000 : mouseY; + this.drawModList(disableableMouseX, disableableMouseY, partialTicks); + this.drawModInfo(disableableMouseX, disableableMouseY, partialTicks); + super.drawScreen(disableableMouseX, disableableMouseY, partialTicks); + + if (OPTION_QUERY.get().startsWith("@")) { + int iconX = this.searchTextField.x + this.searchTextField.width - 15; + int iconY = this.searchTextField.y + (this.searchTextField.height - 10) / 2; + this.mc.getTextureManager().bindTexture(CatalogueIconButton.TEXTURE); + drawModalRectWithCustomSizedTexture(iconX, iconY, 20, 10, 10, 10, 64, 64); + + if (this.menu == null && ClientHelper.isMouseWithin(iconX, iconY, 10, 10, mouseX, mouseY)) { + this.setActiveTooltip(I18n.format("catalogue.gui.advanced_search.info")); + } + } + + Optional optional = Optional.ofNullable(CACHED_MODS.get(CatalogueConstants.MOD_ID.toLowerCase(Locale.ENGLISH))); + optional.ifPresent(this::loadAndCacheLogo); + ImageInfo bannerInfo = BANNER_CACHE.get(CatalogueConstants.MOD_ID); + if (bannerInfo != null) { + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + this.mc.getTextureManager().bindTexture(bannerInfo.resource()); + drawScaledCustomSizeModalRect(10, 9, 0.0F, 0.0F, bannerInfo.width(), bannerInfo.height(), 10, 10, bannerInfo.width(), bannerInfo.height()); + GlStateManager.disableBlend(); + } + + if (this.menu != null) { + this.menu.drawScreen(this.mc, mouseX, mouseY, partialTicks); + } else { + if (ClientHelper.isMouseWithin(10, 9, 10, 10, mouseX, mouseY)) { + this.setActiveTooltip(I18n.format("catalogue.gui.info")); + this.tooltipYOffset = 10; + } + + if (this.optionsButton.isMouseOver()) { + this.setActiveTooltip(I18n.format("catalogue.gui.options")); + this.tooltipYOffset = 10; + } + + if (this.modFolderButton.isMouseOver()) { + this.setActiveTooltip(I18n.format("catalogue.gui.open_mods_folder")); + } + } + + if (this.activeTooltip != null) { + this.drawHoveringText(this.activeTooltip, mouseX, mouseY + this.tooltipYOffset); + this.tooltipYOffset = 0; + } + } + + @Override + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + this.modList.handleMouseInput(); + this.descriptionList.handleMouseInput(); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int button) throws IOException { + // Menu widget + if (this.menu != null) { + if (!this.menu.mousePressed(this.mc, mouseX, mouseY)) { + this.setMenu(null); + } + return; + } + + // Mod List + if (this.modList.mouseClicked(mouseX, mouseY, button)) return; + + // Catalogue button + if (ClientHelper.isMouseWithin(10, 9, 10, 10, mouseX, mouseY) && button == 0) { + this.openLink("https://www.curseforge.com/minecraft/mc-mods/catalogue"); + return; + } + + // Version check button + if (this.selectedModData != null) { + int contentLeft = this.modList.right + 12 + 10; + String version = I18n.format("catalogue.gui.version", this.selectedModData.getVersion()); + int versionWidth = this.fontRenderer.getStringWidth(version); + if (ClientHelper.isMouseWithin(contentLeft + versionWidth + 5, 92, 8, 8, mouseX, mouseY)) { + IModData.Update update = this.selectedModData.getUpdate(); + if (update != null && update.homepage() != null && !update.homepage().isBlank() && update.updatable()) { + this.openLink(update.homepage()); + return; + } + } + } + + // Search Text Field + this.searchTextField.mouseClicked(mouseX, mouseY, button); + if (ClientHelper.isMouseWithin(this.searchTextField.x, this.searchTextField.y, this.searchTextField.width, this.searchTextField.height, mouseX, mouseY)) { + // Right click to empty + if (button == 1) { + this.searchTextField.setText(""); + return; + } + // Left click to apply suggestions + if (button == 0) { + long currentTine = Minecraft.getSystemTime(); + String text = this.searchTextField.getText(); + String suggestion = this.searchTextField.getSuggestion(); + if (!text.isEmpty() && !this.searchTextField.isTextTruncated() && !suggestion.isEmpty() && currentTine - this.lastClickTime < 250L) { + this.searchTextField.setText(text + suggestion); + this.lastClickTime = currentTine; + return; + } + this.lastClickTime = currentTine; + } + } + + super.mouseClicked(mouseX, mouseY, button); + } + + @Override + protected void mouseReleased(int mouseX, int mouseY, int button) { + if (this.modList.mouseReleased(mouseX, mouseY, button)) return; + super.mouseReleased(mouseX, mouseY, button); + } + + @Override + protected void keyTyped(char typedChar, int key) throws IOException { + if (isKeyComboCtrlF(key) && !this.searchTextField.isFocused()) { + this.searchTextField.setFocused(true); + return; + } + if (key == Keyboard.KEY_TAB && this.searchTextField.isFocused()) { + String text = this.searchTextField.getText(); + String suggestion = this.searchTextField.getSuggestion(); + if (!text.isEmpty() && !this.searchTextField.isTextTruncated() && !suggestion.isEmpty()) { + this.searchTextField.setText(text + suggestion); + return; + } + } + if (this.searchTextField.textboxKeyTyped(typedChar, key)) return; + super.keyTyped(typedChar, key); + } + + @Override + public void updateScreen() { + super.updateScreen(); + this.searchTextField.updateCursorCounter(); + } + + /** + * Draws everything considered left of the screen; title, search bar and mod list. + * + * @param mouseX the current mouse x position + * @param mouseY the current mouse y position + * @param partialTicks the partial ticks + */ + private void drawModList(int mouseX, int mouseY, float partialTicks) { + this.modList.drawScreen(mouseX, mouseY, partialTicks); + this.searchTextField.drawTextBox(); + + String modsLabel = TextFormatting.BOLD + I18n.format("catalogue.gui.mod_list"); + Pair counts = COUNTS.get(); + int modCount = counts.getLeft(); + int libCount = counts.getRight(); + String countLabel = TextFormatting.GRAY + "(" + (modCount + libCount) + ")"; + String title = modsLabel + " " + countLabel; + int titleWidth = this.fontRenderer.getStringWidth(title); + int titleLeft = this.modList.left + (this.modList.width - titleWidth) / 2; + drawString(this.fontRenderer, title, titleLeft, 10, 0xFFFFFF); + + int countLabelWidth = this.fontRenderer.getStringWidth(countLabel); + if (ClientHelper.isMouseWithin(titleLeft + titleWidth - countLabelWidth, 10, countLabelWidth, this.fontRenderer.FONT_HEIGHT, mouseX, mouseY)) { + List lines = Arrays.asList( + I18n.format("catalogue.gui.mod_count", modCount), + I18n.format("catalogue.gui.library_count", libCount) + ); + this.setActiveTooltip(lines); + this.tooltipYOffset = 10; + } + } + + private class ModList extends CatalogueListSelection { + private static final Predicate SEARCH_PREDICATE = data -> { + String query = OPTION_QUERY.get(); + if (query.startsWith("@")) { + return performSearchFilter(query, data); + } + return data.getDisplayName() + .toLowerCase(Locale.ENGLISH) + .contains(query.toLowerCase(Locale.ENGLISH)); + }; + private static final Predicate FILTER_PREDICATE = data -> { + // We ignore filters when using special query + String query = OPTION_QUERY.get(); + if (query.startsWith("@")) { + return true; + } + if (OPTION_CONFIGS_ONLY.booleanValue() && !data.hasConfig()) { + return false; + } + if (OPTION_UPDATES_ONLY.booleanValue() && (data.getUpdate() == null || !data.getUpdate().updatable())) { + return false; + } + if (OPTION_HIDE_LIBRARIES.booleanValue() && data.getType() == IModData.Type.LIBRARY) { + return false; + } + if (OPTION_HIDE_CHILD_MODS.booleanValue() && data.getType() == IModData.Type.CHILD) { + return false; + } + //noinspection RedundantIfStatement + if (OPTION_FAVOURITES_ONLY.booleanValue() && !FAVOURITES.has(data.getModId())) { + return false; + } + return true; + }; + private boolean hideFavourites; + + public ModList() { + super(CatalogueModListScreen.this.mc, 150, CatalogueModListScreen.this.height, 46, CatalogueModListScreen.this.height - 35, 26); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + if (this.children().isEmpty()) { + String text = I18n.format("catalogue.gui.no_mods"); + int left = this.left + this.width / 2; + int top = this.top + (this.bottom - this.top - CatalogueModListScreen.this.fontRenderer.FONT_HEIGHT) / 2; + drawCenteredString(CatalogueModListScreen.this.fontRenderer, text, left, top, 0xFFFFFFFF); + } + } + + public void filterAndUpdateList() { + List entries = CACHED_MODS.values().stream() + .filter(SEARCH_PREDICATE) + .filter(FILTER_PREDICATE) + .map(data -> new ModListEntry(data, this)) + .sorted(OPTION_SORT.get()) + .collect(Collectors.toList()); + this.replaceEntries(entries); + if (CatalogueModListScreen.this.selectedModData != null) { + Optional selectedEntry = this.children().stream().filter(entry -> entry.data == CatalogueModListScreen.this.selectedModData).findFirst(); + selectedEntry.ifPresent(this::setSelected); + } + this.clampAmountScrolled(); + } + + @Override + protected int getScrollBarX() { + return this.left + this.width - 6; + } + + @Override + public int getListLeft() { + return this.left; + } + + @Override + public int getListWidth() { + return this.width - (this.isScrollBarVisible() ? 6 : 0); + } + + @Override + protected void drawContainerBackground(@NotNull Tessellator tessellator) { + if (this.mc.world != null) { + drawRect(this.left, this.top, this.right, this.bottom, 0x66000000); + return; + } + super.drawContainerBackground(tessellator); + } + + @Override + public void handleMouseInput() { + this.hideFavourites = Mouse.getEventDWheel() != 0; + super.handleMouseInput(); + } + + @Override + public boolean mouseReleased(int mouseX, int mouseY, int button) { + this.hideFavourites = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean shouldHideFavourites() { + return this.hideFavourites; + } + } + + private static boolean performSearchFilter(@NotNull String query, IModData data) { + if (!query.startsWith("@")) return false; + + int end = query.indexOf(":"); + if (end == -1) return false; + + String type = query.substring(1, end).toLowerCase(Locale.ENGLISH); + if (!SEARCH_FILTERS.containsKey(type)) return false; + + String value = query.substring(end + 1); + return SEARCH_FILTERS.get(type).predicate().test(value, data); + } + + private String formatQuery(String partial, int displayPos) { + String query = OPTION_QUERY.get(); + if (!query.startsWith("@")) { + return partial; + } + + int split = query.indexOf(":"); + if (split == -1) { + return SEARCH_FILTER_KEY + partial + TextFormatting.RESET; + } + + if (displayPos > split) { + return SEARCH_FILTER_VALUE + partial + TextFormatting.RESET; + } + + if (displayPos + partial.length() < split) { + return SEARCH_FILTER_KEY + partial + TextFormatting.RESET; + } + + split = partial.indexOf(":"); + if (split == -1) { + return SEARCH_FILTER_KEY + partial + TextFormatting.RESET; + } + + return SEARCH_FILTER_KEY + partial.substring(0, split + 1) + + SEARCH_FILTER_VALUE + partial.substring(split + 1) + TextFormatting.RESET; + } + + private class ModListEntry implements CatalogueListExtended.IListEntry { + private final IModData data; + private final ModList list; + private final PinnedButton button; + private ItemStack icon; + private boolean hovered; + + public ModListEntry(@NotNull IModData data, @NotNull ModList list) { + this.data = data; + this.list = list; + this.button = new PinnedButton(); + this.icon = this.getItemIcon(); + } + + @Override + public void drawEntry(int index, int left, int top, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float partialTicks) { + this.hovered = hovered; + // Draws mod name and version + boolean inOptionsMenu = CatalogueModListScreen.this.menu != null; + boolean drawFavouriteIcon = !inOptionsMenu && this.data.getType() != IModData.Type.CHILD && !this.list.shouldHideFavourites() && ClientHelper.isMouseWithin(left + rowWidth - rowHeight - 4, top, rowHeight + 4, rowHeight, mouseX, mouseY) || FAVOURITES.has(this.data.getModId()); + drawString(CatalogueModListScreen.this.fontRenderer, this.getFormattedModName(drawFavouriteIcon), left + 24, top + 2, 0xFFFFFF); + drawString(CatalogueModListScreen.this.fontRenderer, this.getFormattedModVersion(drawFavouriteIcon), left + 24, top + 12, 0xFFFFFF); + + // Draw image icon or fallback to item icon + this.drawIcon(top, left); + + // Draws an icon if there is an update for the mod + IModData.Update update = this.data.getUpdate(); + if (update != null) { + int iconLeft = left + rowWidth - 8 - 9 + (drawFavouriteIcon ? -14 : 0); + this.data.drawUpdateIcon(CatalogueModListScreen.this.mc, update, iconLeft, top + 7); + } + + if (drawFavouriteIcon) { + this.button.x = left + rowWidth - this.button.width - 8; + this.button.y = top + (rowHeight - this.button.height) / 2; + this.button.drawButton(CatalogueModListScreen.this.mc, mouseX, mouseY, partialTicks); + if (!inOptionsMenu && this.button.isMouseOver()) { + String label = !FAVOURITES.has(this.data.getModId()) ? + I18n.format("catalogue.gui.favourite") : + I18n.format("catalogue.gui.remove_favourite"); + CatalogueModListScreen.this.setActiveTooltip(label); + } + } + } + + private void drawIcon(int top, int left) { + CatalogueModListScreen.this.loadAndCacheIcon(this.data); + + ImageInfo iconInfo = IMAGE_ICON_CACHE.get(this.data.getModId()); + if (iconInfo != null) { + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + CatalogueModListScreen.this.mc.getTextureManager().bindTexture(iconInfo.resource()); + drawScaledCustomSizeModalRect(left + 4, top + 3, 0.0F, 0.0F, iconInfo.width(), iconInfo.height(), 16, 16, iconInfo.width(), iconInfo.height()); + GlStateManager.disableBlend(); + return; + } + try { + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableDepth(); + GlStateManager.enableRescaleNormal(); + RenderHelper.enableGUIStandardItemLighting(); + + float screenZ = CatalogueModListScreen.this.zLevel; + float itemRenderZ = CatalogueModListScreen.this.itemRender.zLevel; + CatalogueModListScreen.this.zLevel = 300.0F; + CatalogueModListScreen.this.itemRender.zLevel = 300.0F; + + CatalogueModListScreen.this.itemRender.renderItemAndEffectIntoGUI(this.icon, left + 4, top + 2); + + CatalogueModListScreen.this.zLevel = screenZ; + CatalogueModListScreen.this.itemRender.zLevel = itemRenderZ; + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.disableDepth(); + GlStateManager.disableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + } catch (Exception e) { + // Attempt to catch exceptions when rendering item. Sometime level instance isn't checked for null + CatalogueConstants.LOG.debug("Failed to draw icon for mod '{}'", this.data.getModId(), e); + ITEM_ICON_CACHE.put(this.data.getModId(), new ItemStack(Blocks.GRASS)); + this.icon = new ItemStack(Blocks.GRASS); + } + } + + private @NotNull ItemStack getItemIcon() { + if (ITEM_ICON_CACHE.containsKey(this.data.getModId())) { + return ITEM_ICON_CACHE.get(this.data.getModId()); + } + + // Put grass as default item icon + ITEM_ICON_CACHE.put(this.data.getModId(), new ItemStack(Blocks.GRASS)); + + // Minecraft is a grass block + if (this.data.getModId().equals("minecraft")) return new ItemStack(Blocks.GRASS); + + // Special case for Forge to set item icon to anvil + if (this.data.getModId().equals("forge")) { + ItemStack anvil = new ItemStack(Blocks.ANVIL); + ITEM_ICON_CACHE.put("forge", anvil); + return anvil; + } + + // Gets the raw item icon resource string + String itemIcon = this.data.getItemIcon(); + if (itemIcon != null && !itemIcon.isBlank()) { + try { + // 0:mod id 1:item name (2:metadata) + String[] parts = itemIcon.split(":"); + Item item = Item.getByNameOrId(parts[0] + ":" + parts[1]); + if (item != null) { + int meta = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + ItemStack itemStack = new ItemStack(item, 1, meta); + ITEM_ICON_CACHE.put(this.data.getModId(), itemStack); + return itemStack; + } + } catch (Exception e) { + CatalogueConstants.LOG.debug("Failed to get customized item icon for mod '{}'", this.data.getModId(), e); + } + } + + // If the mod has a creative tab, Catalogue will attempt to use the tab's icon + Optional optional = Arrays.stream(CreativeTabs.CREATIVE_TAB_ARRAY) + .filter(Objects::nonNull) + .map(tab -> { + try { + return tab.getIcon(); + } catch (Exception e) { + CatalogueConstants.LOG.debug("Failed to get creative tab icon for mod '{}'", this.data.getModId(), e); + return ItemStack.EMPTY; + } + }) + .filter(tabItem -> !tabItem.isEmpty()) + .filter(tabItem -> { + ResourceLocation resource = tabItem.getItem().getRegistryName(); + return resource != null && resource.getNamespace().equals(this.data.getModId()); + }) + .findFirst(); + + // If the mod doesn't specify an item to use, Catalogue will attempt to get an item from the mod + if (optional.isEmpty()) { + optional = ForgeRegistries.ITEMS.getValuesCollection().stream() + .filter(Objects::nonNull) + .filter(item -> { + ResourceLocation resource = item.getRegistryName(); + return resource != null && resource.getNamespace().equals(this.data.getModId()); + }) + .map(ItemStack::new) + .findFirst(); + } + + if (optional.isPresent()) { + ItemStack item = optional.get(); + if (!item.isEmpty()) { + ITEM_ICON_CACHE.put(this.data.getModId(), item); + return item; + } + } + + return new ItemStack(Blocks.GRASS); + } + + private String getFormattedModName(boolean favouriteIconVisible) { + String name = this.data.getDisplayName(); + name = this.getFormattedText(name, favouriteIconVisible); + return data.getType().getStyle() + name; + } + + @NotNull + private String getFormattedModVersion(boolean favouriteIconVisible) { + String version = this.data.getVersion(); + return TextFormatting.GRAY + this.getFormattedText(version, favouriteIconVisible); + } + + private String getFormattedText(String text, boolean favouriteIconVisible) { + int paddingEnd = 4; + int trimWidth = this.list.getListWidth() - 24 - paddingEnd; + IModData.Update update = this.data.getUpdate(); + if (update != null) { + trimWidth -= 12; + } + if (favouriteIconVisible) { + trimWidth -= 18; + } + if (CatalogueModListScreen.this.fontRenderer.getStringWidth(text) > trimWidth) { + text = CatalogueModListScreen.this.fontRenderer.trimStringToWidth(text, trimWidth - 8).trim() + "..."; + } + return text; + } + + public IModData getData() { + return this.data; + } + + @Override + public boolean mousePressed(int slotIndex, int mouseX, int mouseY, int mouseButton, int relativeX, int relativeY) { + if (mouseButton == 1) { + DropdownMenu.Builder builder = DropdownMenu.builder(CatalogueModListScreen.this) + .setMinItemSize(0, 16) + .setAlignment(DropdownMenu.Alignment.BELOW_LEFT) + .addItem(I18n.format("catalogue.gui.show_dependencies"), () -> { + String filter = "@dependencies:" + this.data.getModId(); + CatalogueModListScreen.this.searchTextField.setText(filter); + }) + .addItem(I18n.format("catalogue.gui.show_dependents"), () -> { + String filter = "@dependents:" + this.data.getModId(); + CatalogueModListScreen.this.searchTextField.setText(filter); + }); + if (this.data.getType() == IModData.Type.CHILD) { + builder.addItem(I18n.format("catalogue.gui.show_parent_mod"), () -> { + String filter = "@parentmod:" + this.data.getModId(); + CatalogueModListScreen.this.searchTextField.setText(filter); + }); + } else if (!this.data.getChildMods().isEmpty()) { + builder.addItem(I18n.format("catalogue.gui.show_child_mods"), () -> { + String filter = "@childmods:" + this.data.getModId(); + CatalogueModListScreen.this.searchTextField.setText(filter); + }); + } + DropdownMenu menu = builder.build(); + menu.toggle(mouseX, mouseY); + return true; + } else if (mouseButton == 0) { + if (this.button.mousePressed(CatalogueModListScreen.this.mc, mouseX, mouseY)) { + FAVOURITES.toggle(this.data.getModId()); + ModListEntry.this.list.filterAndUpdateList(); + this.button.playPressSound(mc.getSoundHandler()); + return true; + } + CatalogueModListScreen.this.setSelectedModData(this.data); + this.list.setSelected(this); + return true; + } + return false; + } + + public boolean isMouseOver() { + return this.hovered; + } + + private class PinnedButton extends GuiButton { + private static final ResourceLocation TEXTURE = new ResourceLocation(CatalogueConstants.MOD_ID, "textures/gui/icons.png"); + + public PinnedButton() { + super(0, 0, 0, 10, 10, ""); + } + + @Override + public void drawButton(@NotNull Minecraft mc, int mouseX, int mouseY, float partialTick) { + if (!this.visible) return; + this.hovered = ModListEntry.this.isMouseOver() && ClientHelper.isMouseWithin(this.x, this.y, this.width, this.height, mouseX, mouseY); + this.mouseDragged(mc, mouseX, mouseY); + int textureU = FAVOURITES.has(ModListEntry.this.data.getModId()) ? 10 : 0; + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + mc.getTextureManager().bindTexture(TEXTURE); + drawModalRectWithCustomSizedTexture(this.x, this.y, textureU, 10, 10, 10, 64, 64); + GlStateManager.disableBlend(); + } + + @Override + public boolean mousePressed(@NotNull Minecraft mc, int mouseX, int mouseY) { + return super.mousePressed(mc, mouseX, mouseY) && ModListEntry.this.data.getType() != IModData.Type.CHILD && !ModListEntry.this.list.shouldHideFavourites(); + } + } + } + + /** + * Draws everything considered right of the screen; logo, mod title, description and more. + * + * @param mouseX the current mouse x position + * @param mouseY the current mouse y position + * @param partialTicks the partial ticks + */ + private void drawModInfo(int mouseX, int mouseY, float partialTicks) { + int listRight = this.modList.right; + this.drawVerticalLine(listRight + 11, -1, this.height, 0xFF707070); + drawRect(listRight + 12, 0, this.width, this.height, 0x66000000); + this.descriptionList.drawScreen(mouseX, mouseY, partialTicks); + + int contentLeft = listRight + 12 + 10; + int contentWidth = this.width - contentLeft - 10; + + if (this.selectedModData != null) { + this.drawBackground(this.width - contentLeft + 10, listRight + 12, 0); + + // Draw mod logo + this.drawBanner(contentWidth, contentLeft, 10, this.width - (listRight + 12 + 10) - 10, 50); + + // Draw mod name + GlStateManager.pushMatrix(); + GlStateManager.translate(contentLeft, 70, 0); + GlStateManager.scale(2.0F, 2.0F, 2.0F); + drawString(this.fontRenderer, this.selectedModData.getDisplayName(), 0, 0, 0xFFFFFF); + GlStateManager.popMatrix(); + + // Draw mod id + String modId = TextFormatting.DARK_GRAY + I18n.format("catalogue.gui.mod_id", this.selectedModData.getModId()); + int modIdWidth = this.fontRenderer.getStringWidth(modId); + drawString(this.fontRenderer, modId, contentLeft + contentWidth - modIdWidth, 92, 0xFFFFFF); + + // Draw version + String displayVersion = this.selectedModData.getVersion(); + this.drawStringWithLabel("catalogue.gui.version", displayVersion, contentLeft, 92, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + + // Draw inner version if the display version is different from it + int versionWidth = this.fontRenderer.getStringWidth(I18n.format("catalogue.gui.version", displayVersion)); + String innerVersion = this.selectedModData.getInnerVersion(); + if (!displayVersion.equals(innerVersion) && ClientHelper.isMouseWithin(contentLeft, 92, versionWidth, this.fontRenderer.FONT_HEIGHT, mouseX, mouseY)) { + this.setActiveTooltip(I18n.format("catalogue.gui.inner_version", innerVersion)); + } + + // Draws an icon if there is an update for the mod + IModData.Update update = this.selectedModData.getUpdate(); + if (update != null && update.url() != null && !update.url().isBlank()) { + this.selectedModData.drawUpdateIcon(this.mc, update, contentLeft + versionWidth + 5, 92); + if (ClientHelper.isMouseWithin(contentLeft + versionWidth + 5, 92, 8, 8, mouseX, mouseY)) { + String message = this.selectedModData.getUpdateText(update); + this.setActiveTooltip(message); + } + } + + // Draw fade from the bottom + drawGradientRect(listRight + 12, this.height - 50, this.width, this.height, 0x00000000, 0x66000000); + + int labelOffset = this.height - 18; + + // Draw child mods + String childMods = this.selectedModData.getChildModNames(); + if (childMods != null && !childMods.isBlank()) { + this.drawStringWithLabel("catalogue.gui.child_mods", childMods, contentLeft, labelOffset, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + labelOffset -= 15; + } + + String parentMod = this.selectedModData.getParentModName(); + if (parentMod != null && !parentMod.isBlank()) { + this.drawStringWithLabel("catalogue.gui.parent_mod", parentMod, contentLeft, labelOffset, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + labelOffset -= 15; + } + + // Draw license + String license = this.selectedModData.getLicense(); + if (license != null && !license.isBlank()) { + this.drawStringWithLabel("catalogue.gui.licenses", license, contentLeft, labelOffset, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + labelOffset -= 15; + } + + // Draw credits + String credits = this.selectedModData.getCredits(); + if (credits != null && !credits.isBlank()) { + this.drawStringWithLabel("catalogue.gui.credits", credits, contentLeft, labelOffset, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + labelOffset -= 15; + } + + // Draw authors + String authors = this.selectedModData.getAuthors(); + if (authors != null && !authors.isBlank()) { + this.drawStringWithLabel("catalogue.gui.authors", authors, contentLeft, labelOffset, contentWidth, mouseX, mouseY, TextFormatting.GRAY, TextFormatting.WHITE); + } + } else { + String message = TextFormatting.GRAY + I18n.format("catalogue.gui.no_selection"); + drawCenteredString(this.fontRenderer, message, contentLeft + contentWidth / 2, this.height / 2 - 5, 0xFFFFFF); + } + } + + private class StringList extends CatalogueListExtended { + + public StringList(int width, int height, int left, int top) { + super(CatalogueModListScreen.this.mc, width, height, top, top + height, 10); + this.setSlotXBoundsFromLeft(left + 8); + this.visible = false; + } + + public void setTextFromInfo(@NotNull IModData data) { + this.clearEntries(); + this.visible = true; + if (data.getDescription() == null) return; + if (data.getDescription().trim().isBlank()) { + this.visible = false; + return; + } + List lines = CatalogueModListScreen.this.fontRenderer.listFormattedStringToWidth(data.getDescription().trim(), this.getListWidth()); + for (String line : lines) { + this.addEntry(new StringEntry(line.replace("\n", "").replace("\r", "").trim())); + } + } + + @Override + protected void drawContainerBackground(@Nullable Tessellator tessellator) { + int x = this.left; + int y = this.top; + int width = this.width; + int height = this.height; + drawRect(x, y + 1, x + 1, y + height - 1, 0x77000000); + drawRect(x + 1, y, x + width - 1, y + height, 0x77000000); + drawRect(x + width - 1, y + 1, x + width, y + height - 1, 0x77000000); + } + + @Override + protected int getScrollBarX() { + return this.left + this.width - 7; + } + + @Override + public int getListLeft() { + return this.left + 8; + } + + @Override + public int getListWidth() { + return this.width - 16; + } + + @Override + protected int getRowTop(int slotIndex) { + return super.getRowTop(slotIndex) + 4; + } + + @Override + public int getMaxScroll() { + return Math.max(0, this.getContentHeight() - (this.height - 12)); + } + } + + private class StringEntry implements CatalogueListExtended.IListEntry { + private final String line; + + public StringEntry(String line) { + this.line = line; + } + + @Override + public void drawEntry(int index, int left, int top, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean hovered, float partialTicks) { + drawString(CatalogueModListScreen.this.fontRenderer, this.line, left, top, 0xFFFFFF); + } + } + + /** + * Draws a string and prepends a label. If the formed string and label is longer than the + * specified max width, it will automatically be trimmed and allows the user to hover the + * string with their mouse to read the full contents. + * + * @param format a string to prepend to the content + * @param text the string to render + * @param x the x position + * @param y the y position + * @param maxWidth the maximum width the string can render + * @param mouseX the current mouse x position + * @param mouseY the current mouse y position + */ + @SuppressWarnings("SameParameterValue") + private void drawStringWithLabel(String format, String text, int x, int y, int maxWidth, int mouseX, int mouseY, TextFormatting labelColor, TextFormatting contentColor) { + String formatted = labelColor + I18n.format(format, contentColor + text); + if (this.fontRenderer.getStringWidth(formatted) > maxWidth) { + formatted = this.fontRenderer.trimStringToWidth(formatted, maxWidth - 7) + "..."; + if (ClientHelper.isMouseWithin(x, y, maxWidth, 9, mouseX, mouseY)) { // Sets the active tool tip if string is too long so users can still read it + this.setActiveTooltip(text); + } + } + drawString(this.fontRenderer, formatted, x, y, 0xFFFFFF); + } + + private ImageInfo getBanner(String modId) { + // Try getting the banner for the mod + ImageInfo bannerInfo = BANNER_CACHE.get(modId); + if (bannerInfo != null) return bannerInfo; + + // Try using the icon image for the banner + ImageInfo iconInfo = IMAGE_ICON_CACHE.get(modId); + if (iconInfo != null) { + // Hack to make icon fill max banner height + int expandedWidth = iconInfo.width() * 10; + int expandedHeight = iconInfo.height() * 10; + return new ImageInfo(iconInfo.resource(), expandedWidth, expandedHeight, iconInfo.unregister()); + } + + // Fallback and just use missing banner + return MISSING_BANNER_INFO; + } + + private void loadAndCacheLogo(@NotNull IModData data) { + if (BANNER_CACHE.containsKey(data.getModId())) return; + + // Fills an empty logo as logo may not be present + BANNER_CACHE.put(data.getModId(), null); + + // Load the banner resource if present + Branding.BANNER.loadResource(data).ifPresent(info -> { + BANNER_CACHE.put(data.getModId(), info); + }); + } + + private void loadAndCacheIcon(@NotNull IModData data) { + if (IMAGE_ICON_CACHE.containsKey(data.getModId())) return; + + // Fills an empty icon as icon may not be present + IMAGE_ICON_CACHE.put(data.getModId(), null); + + // Load the icon branding + Branding.ICON.loadResource(data).ifPresentOrElse(info -> { + IMAGE_ICON_CACHE.put(data.getModId(), info); + }, () -> { + // If no icon, try and use the loaded banner if a square + ImageInfo bannerInfo = BANNER_CACHE.get(data.getModId()); + if (bannerInfo != null) { + if (bannerInfo.width() == bannerInfo.height()) { + IMAGE_ICON_CACHE.put(data.getModId(), bannerInfo); + } + } else { + // Otherwise temporarily load the banner, use if square, otherwise free the resource + Branding.BANNER.loadResource(data).ifPresent(info -> { + if (info.width() == info.height()) { + IMAGE_ICON_CACHE.put(data.getModId(), info); + BANNER_CACHE.put(data.getModId(), info); // Saves loading later + } else { + info.unregister().run(); + } + }); + } + }); + } + + private void reloadBackground(IModData data) { + Branding.BACKGROUND.loadResource(data).ifPresentOrElse(info -> { + cachedBackground = info; + }, () -> { + if (cachedBackground != null) { + cachedBackground.unregister().run(); + cachedBackground = null; + } + }); + } + + /** + * Draws the background that is visible when a mod is selected. Backgrounds are programmatically + * faded out to the bottom of the image. + * + * @param contentWidth the widget of the content area + * @param contentLeft the x position of the content area + * @param contentTop the y position of the content area + */ + @SuppressWarnings("SameParameterValue") + private void drawBackground(int contentWidth, int contentLeft, int contentTop) { + if (this.selectedModData == null) return; + + ResourceLocation texture = cachedBackground != null ? cachedBackground.resource() : MISSING_BACKGROUND; + this.mc.getTextureManager().bindTexture(texture); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GlStateManager.disableAlpha(); + GlStateManager.shadeModel(GL11.GL_SMOOTH); + GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder builder = tessellator.getBuffer(); + builder.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + builder.pos(contentLeft, contentTop, this.zLevel).tex(0, 0).color(1.0F, 1.0F, 1.0F, 1.0F).endVertex(); + builder.pos(contentLeft, contentTop + 128, this.zLevel).tex(0, 1).color(0.0F, 0.0F, 0.0F, 0.0F).endVertex(); + builder.pos(contentLeft + contentWidth, contentTop + 128, this.zLevel).tex(1, 1).color(0.0F, 0.0F, 0.0F, 0.0F).endVertex(); + builder.pos(contentLeft + contentWidth, contentTop, this.zLevel).tex(1, 0).color(1.0F, 1.0F, 1.0F, 1.0F).endVertex(); + tessellator.draw(); + + GlStateManager.disableBlend(); + GlStateManager.enableAlpha(); + GlStateManager.shadeModel(GL11.GL_FLAT); + } + + @SuppressWarnings("SameParameterValue") + private void drawBanner(int contentWidth, int x, int y, int maxWidth, int maxHeight) { + if (this.selectedModData != null) { + ImageInfo info = this.getBanner(this.selectedModData.getModId()); + int displayWidth = info.width(); + int displayHeight = info.height(); + if (info.width() > maxWidth) { + displayWidth = maxWidth; + displayHeight = (displayWidth * info.height()) / info.width(); + } + if (displayHeight > maxHeight) { + displayHeight = maxHeight; + displayWidth = (displayHeight * info.width()) / info.height(); + } + + x += (contentWidth - displayWidth) / 2; + y += (maxHeight - displayHeight) / 2; + + // Fix for minecraft logo + if (info.resource() == MINECRAFT_LOGO) { + y += 8; + } + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + this.mc.getTextureManager().bindTexture(info.resource()); + drawScaledCustomSizeModalRect(x, y, 0.0F, 0.0F, info.width(), info.height(), displayWidth, displayHeight, info.width(), info.height()); + + GlStateManager.disableBlend(); + } + } + + private void setActiveTooltip(String content) { + this.activeTooltip = this.fontRenderer.listFormattedStringToWidth(content, 200); + this.tooltipYOffset = 0; + } + + private void setActiveTooltip(List activeTooltip) { + this.activeTooltip = activeTooltip; + this.tooltipYOffset = 0; + } + + /** + * Sets the selected mod data. This handles loading the logo and background, updates the states + * of widgets, like the config button enable state (if the mod has a config), and the description + * test. + * + * @param data the mod data to set as selected + */ + private void setSelectedModData(IModData data) { + this.selectedModData = data; + this.loadAndCacheLogo(data); + this.reloadBackground(data); + this.configButton.visible = true; + this.websiteButton.visible = true; + this.issueButton.visible = true; + + this.configButton.enabled = data.hasConfig(); + this.websiteButton.enabled = data.getHomepage() != null && !data.getHomepage().isBlank(); + this.issueButton.enabled = data.getIssueTracker() != null && !data.getIssueTracker().isBlank(); + + int contentLeft = this.modList.right + 12 + 10; + int contentWidth = this.width - contentLeft - 10; + int labelCount = this.getFooterTextElementCount(data); + this.descriptionList.setWidth(contentWidth); + this.descriptionList.setHeight(this.height - 135 - labelCount * 15 - 9); + this.descriptionList.setSlotXBoundsFromLeft(contentLeft); + this.descriptionList.setTextFromInfo(data); + this.descriptionList.setAmountScrolled(0); + } + + /** + * Gets the count of the footer text elements. This is used to correctly set the height of + * the description widget. + * + * @param data the mod data + * @return the count of footer text elements + */ + private int getFooterTextElementCount(@NotNull IModData data) { + int count = 0; + if (data.getChildModNames() != null && !data.getChildModNames().isBlank()) count++; + if (data.getParentModName() != null && !data.getParentModName().isBlank()) count++; + if (data.getLicense() != null && !data.getLicense().isBlank()) count++; + if (data.getCredits() != null && !data.getCredits().isBlank()) count++; + if (data.getAuthors() != null && !data.getAuthors().isBlank()) count++; + return count; + } + + private void updateSearchFieldSuggestion(@NotNull String value) { + if (value.isEmpty()) { + this.searchTextField.setSuggestion(I18n.format("catalogue.gui.search") + "..."); + } else if (value.startsWith("@")) { + // Mark as special search + int end = value.indexOf(":"); + if (end != -1) { + String type = value.substring(1, end); + Optional optional = SEARCH_FILTERS.keySet().stream().filter(filter -> { + return filter.startsWith(type.toLowerCase(Locale.ENGLISH)); + }).min(Comparator.comparing(String::length)); + if (optional.isPresent()) { + int length = type.length(); + this.searchTextField.setSuggestion(optional.get().substring(length)); + } else { + this.searchTextField.setSuggestion(""); + } + } else { + this.searchTextField.setSuggestion(""); + } + } else { + Optional optional = CACHED_MODS.values().stream().filter(data -> { + return ModList.FILTER_PREDICATE.test(data) && data.getDisplayName().toLowerCase(Locale.ENGLISH).startsWith(value.toLowerCase(Locale.ENGLISH)); + }).min(Comparator.comparing(IModData::getDisplayName)); + if (optional.isPresent()) { + int length = value.length(); + String displayName = optional.get().getDisplayName(); + this.searchTextField.setSuggestion(displayName.substring(length)); + } else { + this.searchTextField.setSuggestion(""); + } + } + } + + /** + * Creates a confirmation screen to open a link + * + * @param url the url to open + */ + private void openLink(@Nullable String url) { + if (url == null) return; + Style style = new Style().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)); + this.handleComponentClick(new TextComponentString("").setStyle(style)); + } + + private static boolean isKeyComboCtrlF(int keyID) { + return keyID == Keyboard.KEY_F && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private record SearchFilter(BiPredicate predicate) { + } + + private static class Favourites { + private final Set mods = new HashSet<>(); + private boolean needsSave; + private Path file; + + public void toggle(String modId) { + if (!this.mods.remove(modId)) { + this.mods.add(modId); + } + this.needsSave = true; + } + + public boolean has(String modId) { + return this.mods.contains(modId); + } + + private void init() { + try { + Path configDir = ClientServices.PLATFORM.getConfigDirectory(); + Path file = configDir.resolve("catalogue_favourites.txt"); + if (!Files.exists(file)) { + Files.createFile(file); + } + this.file = file; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void load() { + try { + this.init(); + this.mods.clear(); + Predicate modIdRegex = MOD_ID_PATTERN.asMatchPredicate(); + Files.readAllLines(file).forEach(s -> { + if (modIdRegex.test(s) && ClientServices.PLATFORM.isModLoaded(s)) { + this.mods.add(s); + } + }); + // Save immediately to remove invalid lines + this.needsSave = true; + this.save(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void save() { + if (!this.needsSave) return; + + try { + this.needsSave = false; + this.init(); + Files.write(this.file, this.mods, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/DropdownMenuHandler.java b/src/main/java/com/cleanroommc/catalogue/client/screen/DropdownMenuHandler.java new file mode 100644 index 000000000..626ced855 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/DropdownMenuHandler.java @@ -0,0 +1,11 @@ +package com.cleanroommc.catalogue.client.screen; + +import com.cleanroommc.catalogue.client.screen.widget.DropdownMenu; +import org.jetbrains.annotations.Nullable; + +/** + * Author: MrCrayfish + */ +public interface DropdownMenuHandler { + void setMenu(@Nullable DropdownMenu menu); +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/MinecraftModData.java b/src/main/java/com/cleanroommc/catalogue/client/screen/MinecraftModData.java new file mode 100644 index 000000000..f0cc524be --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/MinecraftModData.java @@ -0,0 +1,159 @@ +package com.cleanroommc.catalogue.client.screen; + +import com.cleanroommc.catalogue.client.IModData; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiOptions; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.IResourcePack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Set; + +/** + * Author: MrCrayfish + */ +public class MinecraftModData implements IModData { + @Override + public Type getType() { + return Type.LIBRARY; + } + + @Override + public String getModId() { + return "minecraft"; + } + + @Override + public String getDisplayName() { + return "Minecraft"; + } + + @Override + public String getVersion() { + return "1.12.2"; + } + + @Override + public String getInnerVersion() { + return this.getVersion(); + } + + @Nullable + @Override + public String getDescription() { + // Description provided by minecraft.wiki (CC BY-NC-SA 3.0) + return "Minecraft is a 3D sandbox adventure game developed by Mojang Studios where players can interact with a fully customizable three-dimensional world made of blocks and entities. Its diverse gameplay options allow players to choose the way they play, creating countless possibilities."; + } + + @Nullable + @Override + public String getItemIcon() { + return null; + } + + @Nullable + @Override + public String getImageIcon() { + return null; + } + + @Nullable + @Override + public String getLicense() { + return "All Rights Reserved"; + } + + @Nullable + @Override + public String getCredits() { + return null; + } + + @Nullable + @Override + public String getAuthors() { + return "Mojang AB"; + } + + @Nullable + @Override + public String getHomepage() { + return "https://www.minecraft.net"; + } + + @Nullable + @Override + public String getIssueTracker() { + return "https://bugs.mojang.com/projects/MC/issues"; + } + + @Nullable + @Override + public String getBanner() { + return null; + } + + @Nullable + @Override + public String getBackground() { + return null; + } + + @Nullable + @Override + public String getChildModNames() { + return null; + } + + @Nullable + @Override + public String getParentModName() { + return null; + } + + @Nullable + @Override + public Update getUpdate() { + return null; + } + + @Nullable + @Override + public IResourcePack getResourcePack() { + return null; + } + + @NotNull + @Override + public Set getDependencies() { + return Collections.emptySet(); + } + + @NotNull + @Override + public Set getChildMods() { + return Collections.emptySet(); + } + + @Override + public boolean hasConfig() { + return true; + } + + @Override + public void openConfigScreen(Minecraft minecraft, GuiScreen parent) { + minecraft.displayGuiScreen(new GuiOptions(parent, minecraft.gameSettings)); + } + + @Override + public void drawUpdateIcon(Minecraft minecraft, Update update, int x, int y) { + } + + @Nullable + @Override + public String getUpdateText(Update update) { + return null; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/AbstractLayout.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/AbstractLayout.java new file mode 100644 index 000000000..17bd8839f --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/AbstractLayout.java @@ -0,0 +1,91 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import com.cleanroommc.catalogue.Utils; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public abstract class AbstractLayout implements Layout { + private int x; + private int y; + protected int width; + protected int height; + + public AbstractLayout(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void setX(int x) { + this.visitChildren(p_265043_ -> { + int i = p_265043_.getX() + (x - this.getX()); + p_265043_.setX(i); + }); + this.x = x; + } + + @Override + public void setY(int y) { + this.visitChildren(p_265586_ -> { + int i = p_265586_.getY() + (y - this.getY()); + p_265586_.setY(i); + }); + this.y = y; + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getY() { + return this.y; + } + + @Override + public int getWidth() { + return this.width; + } + + @Override + public int getHeight() { + return this.height; + } + + @SideOnly(Side.CLIENT) + protected abstract static class AbstractChildWrapper { + public final LayoutElement child; + public final LayoutSettings.LayoutSettingsImpl layoutSettings; + + protected AbstractChildWrapper(LayoutElement child, LayoutSettings layoutSettings) { + this.child = child; + this.layoutSettings = layoutSettings.getExposed(); + } + + public int getHeight() { + return this.child.getHeight() + this.layoutSettings.paddingTop + this.layoutSettings.paddingBottom; + } + + public int getWidth() { + return this.child.getWidth() + this.layoutSettings.paddingLeft + this.layoutSettings.paddingRight; + } + + public void setX(int x, int width) { + float f = (float) this.layoutSettings.paddingLeft; + float f1 = (float) (width - this.child.getWidth() - this.layoutSettings.paddingRight); + int i = (int) Utils.lerp(this.layoutSettings.xAlignment, f, f1); + this.child.setX(i + x); + } + + public void setY(int y, int height) { + float f = (float) this.layoutSettings.paddingTop; + float f1 = (float) (height - this.child.getHeight() - this.layoutSettings.paddingBottom); + int i = Math.round(Utils.lerp(this.layoutSettings.yAlignment, f, f1)); + this.child.setY(i + y); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/BorderedLinearLayout.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/BorderedLinearLayout.java new file mode 100644 index 000000000..42ee94e2c --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/BorderedLinearLayout.java @@ -0,0 +1,54 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +/** + * Author: MrCrayfish + */ +public class BorderedLinearLayout extends LinearLayout { + private int border; + + public BorderedLinearLayout(Orientation orientation) { + super(0, 0, orientation); + } + + public BorderedLinearLayout border(int size) { + int diff = size - this.border; + this.setX(this.getX() + diff); + this.setY(this.getY() + diff); + this.border = size; + return this; + } + + @Override + public int getX() { + return super.getX() - this.border; + } + + @Override + public int getY() { + return super.getY() - this.border; + } + + @Override + public void setX(int x) { + super.setX(x + this.border); + } + + @Override + public void setY(int y) { + super.setY(y + this.border); + } + + @Override + public int getWidth() { + return super.getWidth() + this.border + this.border; + } + + @Override + public int getHeight() { + return super.getHeight() + this.border + this.border; + } + + public static BorderedLinearLayout vertical() { + return new BorderedLinearLayout(LinearLayout.Orientation.VERTICAL); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Divisor.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Divisor.java new file mode 100644 index 000000000..2104f6444 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Divisor.java @@ -0,0 +1,51 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import com.google.common.annotations.VisibleForTesting; +import it.unimi.dsi.fastutil.ints.IntIterator; + +import java.util.NoSuchElementException; + +public class Divisor implements IntIterator { + private final int denominator; + private final int quotient; + private final int mod; + private int returnedParts; + private int remainder; + + public Divisor(int numerator, int denominator) { + this.denominator = denominator; + if (denominator > 0) { + this.quotient = numerator / denominator; + this.mod = numerator % denominator; + } else { + this.quotient = 0; + this.mod = 0; + } + + } + + public boolean hasNext() { + return this.returnedParts < this.denominator; + } + + public int nextInt() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } else { + int i = this.quotient; + this.remainder += this.mod; + if (this.remainder >= this.denominator) { + this.remainder -= this.denominator; + ++i; + } + + ++this.returnedParts; + return i; + } + } + + @VisibleForTesting + public static Iterable asIterable(int numerator, int denominator) { + return () -> new Divisor(numerator, denominator); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/GridLayout.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/GridLayout.java new file mode 100644 index 000000000..4c88f288f --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/GridLayout.java @@ -0,0 +1,227 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import com.cleanroommc.catalogue.Utils; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@SideOnly(Side.CLIENT) +public class GridLayout extends AbstractLayout { + private final List children = new ArrayList<>(); + private final List cellInhabitants = new ArrayList<>(); + private final LayoutSettings defaultCellSettings = LayoutSettings.defaults(); + private int rowSpacing = 0; + private int columnSpacing = 0; + + public GridLayout() { + this(0, 0); + } + + public GridLayout(int x, int y) { + super(x, y, 0, 0); + } + + @Override + public void arrangeElements() { + super.arrangeElements(); + int i = 0; + int j = 0; + + for (GridLayout.CellInhabitant gridlayout$cellinhabitant : this.cellInhabitants) { + i = Math.max(gridlayout$cellinhabitant.getLastOccupiedRow(), i); + j = Math.max(gridlayout$cellinhabitant.getLastOccupiedColumn(), j); + } + + int[] aint = new int[j + 1]; + int[] aint1 = new int[i + 1]; + + for (GridLayout.CellInhabitant gridlayout$cellinhabitant1 : this.cellInhabitants) { + int k = gridlayout$cellinhabitant1.getHeight() - (gridlayout$cellinhabitant1.occupiedRows - 1) * this.rowSpacing; + Divisor divisor = new Divisor(k, gridlayout$cellinhabitant1.occupiedRows); + + for (int l = gridlayout$cellinhabitant1.row; l <= gridlayout$cellinhabitant1.getLastOccupiedRow(); l++) { + aint1[l] = Math.max(aint1[l], divisor.nextInt()); + } + + int l1 = gridlayout$cellinhabitant1.getWidth() - (gridlayout$cellinhabitant1.occupiedColumns - 1) * this.columnSpacing; + Divisor divisor1 = new Divisor(l1, gridlayout$cellinhabitant1.occupiedColumns); + + for (int i1 = gridlayout$cellinhabitant1.column; i1 <= gridlayout$cellinhabitant1.getLastOccupiedColumn(); i1++) { + aint[i1] = Math.max(aint[i1], divisor1.nextInt()); + } + } + + int[] aint2 = new int[j + 1]; + int[] aint3 = new int[i + 1]; + aint2[0] = 0; + + for (int j1 = 1; j1 <= j; j1++) { + aint2[j1] = aint2[j1 - 1] + aint[j1 - 1] + this.columnSpacing; + } + + aint3[0] = 0; + + for (int k1 = 1; k1 <= i; k1++) { + aint3[k1] = aint3[k1 - 1] + aint1[k1 - 1] + this.rowSpacing; + } + + for (GridLayout.CellInhabitant gridlayout$cellinhabitant2 : this.cellInhabitants) { + int i2 = 0; + + for (int j2 = gridlayout$cellinhabitant2.column; j2 <= gridlayout$cellinhabitant2.getLastOccupiedColumn(); j2++) { + i2 += aint[j2]; + } + + i2 += this.columnSpacing * (gridlayout$cellinhabitant2.occupiedColumns - 1); + gridlayout$cellinhabitant2.setX(this.getX() + aint2[gridlayout$cellinhabitant2.column], i2); + int k2 = 0; + + for (int l2 = gridlayout$cellinhabitant2.row; l2 <= gridlayout$cellinhabitant2.getLastOccupiedRow(); l2++) { + k2 += aint1[l2]; + } + + k2 += this.rowSpacing * (gridlayout$cellinhabitant2.occupiedRows - 1); + gridlayout$cellinhabitant2.setY(this.getY() + aint3[gridlayout$cellinhabitant2.row], k2); + } + + this.width = aint2[j] + aint[j]; + this.height = aint3[i] + aint1[i]; + } + + public T addChild(T child, int row, int column) { + return this.addChild(child, row, column, this.newCellSettings()); + } + + public T addChild(T child, int row, int column, LayoutSettings layoutSettings) { + return this.addChild(child, row, column, 1, 1, layoutSettings); + } + + public T addChild(T child, int row, int column, Consumer layoutSettingsFactory) { + return this.addChild(child, row, column, 1, 1, Utils.make(this.newCellSettings(), layoutSettingsFactory)); + } + + public T addChild(T child, int row, int column, int occupiedRows, int occupiedColumns) { + return this.addChild(child, row, column, occupiedRows, occupiedColumns, this.newCellSettings()); + } + + public T addChild(T child, int row, int column, int occupiedRows, int occupiedColumns, LayoutSettings layoutSettings) { + if (occupiedRows < 1) { + throw new IllegalArgumentException("Occupied rows must be at least 1"); + } else if (occupiedColumns < 1) { + throw new IllegalArgumentException("Occupied columns must be at least 1"); + } else { + this.cellInhabitants.add(new GridLayout.CellInhabitant(child, row, column, occupiedRows, occupiedColumns, layoutSettings)); + this.children.add(child); + return child; + } + } + + public T addChild(T child, int row, int column, int occupiedRows, int occupiedColumns, Consumer layoutSettingsFactory) { + return this.addChild(child, row, column, occupiedRows, occupiedColumns, Utils.make(this.newCellSettings(), layoutSettingsFactory)); + } + + public GridLayout columnSpacing(int columnSpacing) { + this.columnSpacing = columnSpacing; + return this; + } + + public GridLayout rowSpacing(int rowSpacing) { + this.rowSpacing = rowSpacing; + return this; + } + + public GridLayout spacing(int spacing) { + return this.columnSpacing(spacing).rowSpacing(spacing); + } + + @Override + public void visitChildren(Consumer visitor) { + this.children.forEach(visitor); + } + + public LayoutSettings newCellSettings() { + return this.defaultCellSettings.copy(); + } + + public LayoutSettings defaultCellSetting() { + return this.defaultCellSettings; + } + + public GridLayout.RowHelper createRowHelper(int columns) { + return new GridLayout.RowHelper(columns); + } + + @SideOnly(Side.CLIENT) + static class CellInhabitant extends AbstractLayout.AbstractChildWrapper { + final int row; + final int column; + final int occupiedRows; + final int occupiedColumns; + + CellInhabitant(LayoutElement child, int row, int column, int occupiedRows, int occupiedColumns, LayoutSettings layoutSettings) { + super(child, layoutSettings.getExposed()); + this.row = row; + this.column = column; + this.occupiedRows = occupiedRows; + this.occupiedColumns = occupiedColumns; + } + + public int getLastOccupiedRow() { + return this.row + this.occupiedRows - 1; + } + + public int getLastOccupiedColumn() { + return this.column + this.occupiedColumns - 1; + } + } + + @SideOnly(Side.CLIENT) + public final class RowHelper { + private final int columns; + private int index; + + RowHelper(int columns) { + this.columns = columns; + } + + public T addChild(T child) { + return this.addChild(child, 1); + } + + public T addChild(T child, int occupiedColumns) { + return this.addChild(child, occupiedColumns, this.defaultCellSetting()); + } + + public T addChild(T child, LayoutSettings layoutSettings) { + return this.addChild(child, 1, layoutSettings); + } + + public T addChild(T child, int occupiedColumns, LayoutSettings layoutSettings) { + int i = this.index / this.columns; + int j = this.index % this.columns; + if (j + occupiedColumns > this.columns) { + i++; + j = 0; + this.index = Utils.roundToward(this.index, this.columns); + } + + this.index += occupiedColumns; + return GridLayout.this.addChild(child, i, j, 1, occupiedColumns, layoutSettings); + } + + public GridLayout getGrid() { + return GridLayout.this; + } + + public LayoutSettings newCellSettings() { + return GridLayout.this.newCellSettings(); + } + + public LayoutSettings defaultCellSetting() { + return GridLayout.this.defaultCellSetting(); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Layout.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Layout.java new file mode 100644 index 000000000..85e5693f6 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/Layout.java @@ -0,0 +1,24 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.function.Consumer; + +@SideOnly(Side.CLIENT) +public interface Layout extends LayoutElement { + void visitChildren(Consumer var1); + + default void visitWidgets(Consumer consumer) { + this.visitChildren((p_270634_) -> p_270634_.visitWidgets(consumer)); + } + + default void arrangeElements() { + this.visitChildren((p_270565_) -> { + if (p_270565_ instanceof Layout layout) { + layout.arrangeElements(); + } + + }); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutElement.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutElement.java new file mode 100644 index 000000000..b571531c6 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutElement.java @@ -0,0 +1,32 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.function.Consumer; + +@SideOnly(Side.CLIENT) +public interface LayoutElement { + void setX(int x); + + void setY(int y); + + int getX(); + + int getY(); + + int getWidth(); + + int getHeight(); + + default void setPosition(int x, int y) { + setX(x); + setY(y); + } + + default ScreenRectangle getRectangle() { + return new ScreenRectangle(this.getX(), this.getY(), this.getWidth(), this.getHeight()); + } + + void visitWidgets(Consumer consumer); +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutSettings.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutSettings.java new file mode 100644 index 000000000..d1b663c8e --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LayoutSettings.java @@ -0,0 +1,150 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public interface LayoutSettings { + LayoutSettings padding(int padding); + + LayoutSettings padding(int horizontalPadding, int verticalPadding); + + LayoutSettings padding(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom); + + LayoutSettings paddingLeft(int paddingLeft); + + LayoutSettings paddingTop(int paddingTop); + + LayoutSettings paddingRight(int paddingRight); + + LayoutSettings paddingBottom(int paddingBottom); + + LayoutSettings paddingHorizontal(int horizontalPadding); + + LayoutSettings paddingVertical(int verticalPadding); + + LayoutSettings align(float xAlignment, float yAlignment); + + LayoutSettings alignHorizontally(float xAlignment); + + LayoutSettings alignVertically(float yAlignment); + + default LayoutSettings alignHorizontallyLeft() { + return this.alignHorizontally(0.0F); + } + + default LayoutSettings alignHorizontallyCenter() { + return this.alignHorizontally(0.5F); + } + + default LayoutSettings alignHorizontallyRight() { + return this.alignHorizontally(1.0F); + } + + default LayoutSettings alignVerticallyTop() { + return this.alignVertically(0.0F); + } + + default LayoutSettings alignVerticallyMiddle() { + return this.alignVertically(0.5F); + } + + default LayoutSettings alignVerticallyBottom() { + return this.alignVertically(1.0F); + } + + LayoutSettings copy(); + + LayoutSettings.LayoutSettingsImpl getExposed(); + + static LayoutSettings defaults() { + return new LayoutSettings.LayoutSettingsImpl(); + } + + @SideOnly(Side.CLIENT) + public static class LayoutSettingsImpl implements LayoutSettings { + public int paddingLeft; + public int paddingTop; + public int paddingRight; + public int paddingBottom; + public float xAlignment; + public float yAlignment; + + public LayoutSettingsImpl() { + } + + public LayoutSettingsImpl(LayoutSettings.LayoutSettingsImpl other) { + this.paddingLeft = other.paddingLeft; + this.paddingTop = other.paddingTop; + this.paddingRight = other.paddingRight; + this.paddingBottom = other.paddingBottom; + this.xAlignment = other.xAlignment; + this.yAlignment = other.yAlignment; + } + + public LayoutSettings.LayoutSettingsImpl padding(int padding) { + return this.padding(padding, padding); + } + + public LayoutSettings.LayoutSettingsImpl padding(int horizontalPadding, int verticalPadding) { + return this.paddingHorizontal(horizontalPadding).paddingVertical(verticalPadding); + } + + public LayoutSettings.LayoutSettingsImpl padding(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { + return this.paddingLeft(paddingLeft).paddingRight(paddingRight).paddingTop(paddingTop).paddingBottom(paddingBottom); + } + + public LayoutSettings.LayoutSettingsImpl paddingLeft(int paddingLeft) { + this.paddingLeft = paddingLeft; + return this; + } + + public LayoutSettings.LayoutSettingsImpl paddingTop(int paddingTop) { + this.paddingTop = paddingTop; + return this; + } + + public LayoutSettings.LayoutSettingsImpl paddingRight(int paddingRight) { + this.paddingRight = paddingRight; + return this; + } + + public LayoutSettings.LayoutSettingsImpl paddingBottom(int paddingBottom) { + this.paddingBottom = paddingBottom; + return this; + } + + public LayoutSettings.LayoutSettingsImpl paddingHorizontal(int horizontalPadding) { + return this.paddingLeft(horizontalPadding).paddingRight(horizontalPadding); + } + + public LayoutSettings.LayoutSettingsImpl paddingVertical(int verticalPadding) { + return this.paddingTop(verticalPadding).paddingBottom(verticalPadding); + } + + public LayoutSettings.LayoutSettingsImpl align(float xAlignment, float yAlignment) { + this.xAlignment = xAlignment; + this.yAlignment = yAlignment; + return this; + } + + public LayoutSettings.LayoutSettingsImpl alignHorizontally(float xAlignment) { + this.xAlignment = xAlignment; + return this; + } + + public LayoutSettings.LayoutSettingsImpl alignVertically(float yAlignment) { + this.yAlignment = yAlignment; + return this; + } + + public LayoutSettings.LayoutSettingsImpl copy() { + return new LayoutSettings.LayoutSettingsImpl(this); + } + + @Override + public LayoutSettings.LayoutSettingsImpl getExposed() { + return this; + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LinearLayout.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LinearLayout.java new file mode 100644 index 000000000..f714f68f9 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/LinearLayout.java @@ -0,0 +1,119 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +import com.cleanroommc.catalogue.Utils; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.function.Consumer; + +@SideOnly(Side.CLIENT) +public class LinearLayout implements Layout { + private final GridLayout wrapped; + private final LinearLayout.Orientation orientation; + private int nextChildIndex = 0; + + private LinearLayout(LinearLayout.Orientation orientation) { + this(0, 0, orientation); + } + + public LinearLayout(int width, int height, LinearLayout.Orientation orientation) { + this.wrapped = new GridLayout(width, height); + this.orientation = orientation; + } + + public LinearLayout spacing(int spacing) { + this.orientation.setSpacing(this.wrapped, spacing); + return this; + } + + public LayoutSettings newCellSettings() { + return this.wrapped.newCellSettings(); + } + + public LayoutSettings defaultCellSetting() { + return this.wrapped.defaultCellSetting(); + } + + public T addChild(T child, LayoutSettings layoutSettings) { + return this.orientation.addChild(this.wrapped, child, this.nextChildIndex++, layoutSettings); + } + + public T addChild(T child) { + return this.addChild(child, this.newCellSettings()); + } + + public T addChild(T child, Consumer layoutSettingsFactory) { + return this.orientation.addChild(this.wrapped, child, this.nextChildIndex++, Utils.make(this.newCellSettings(), layoutSettingsFactory)); + } + + @Override + public void visitChildren(Consumer visitor) { + this.wrapped.visitChildren(visitor); + } + + @Override + public void arrangeElements() { + this.wrapped.arrangeElements(); + } + + @Override + public int getWidth() { + return this.wrapped.getWidth(); + } + + @Override + public int getHeight() { + return this.wrapped.getHeight(); + } + + @Override + public void setX(int x) { + this.wrapped.setX(x); + } + + @Override + public void setY(int y) { + this.wrapped.setY(y); + } + + @Override + public int getX() { + return this.wrapped.getX(); + } + + @Override + public int getY() { + return this.wrapped.getY(); + } + + public static LinearLayout vertical() { + return new LinearLayout(LinearLayout.Orientation.VERTICAL); + } + + public static LinearLayout horizontal() { + return new LinearLayout(LinearLayout.Orientation.HORIZONTAL); + } + + @SideOnly(Side.CLIENT) + public static enum Orientation { + HORIZONTAL, + VERTICAL; + + void setSpacing(GridLayout layout, int spacing) { + switch (this) { + case HORIZONTAL: + layout.columnSpacing(spacing); + break; + case VERTICAL: + layout.rowSpacing(spacing); + } + } + + public T addChild(GridLayout layout, T element, int index, LayoutSettings layoutSettings) { + return (T) (switch (this) { + case HORIZONTAL -> (LayoutElement) layout.addChild(element, 0, index, layoutSettings); + case VERTICAL -> (LayoutElement) layout.addChild(element, index, 0, layoutSettings); + }); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/layout/ScreenRectangle.java b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/ScreenRectangle.java new file mode 100644 index 000000000..d4311fc64 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/layout/ScreenRectangle.java @@ -0,0 +1,11 @@ +package com.cleanroommc.catalogue.client.screen.layout; + +public record ScreenRectangle(int left, int top, int width, int height) { + public int right() { + return left + width; + } + + public int bottom() { + return top + height; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueIconButton.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueIconButton.java new file mode 100644 index 000000000..737f42398 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueIconButton.java @@ -0,0 +1,57 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import com.cleanroommc.catalogue.CatalogueConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +/** + * Author: MrCrayfish + */ +public class CatalogueIconButton extends CatalogueTextButton { + public static final ResourceLocation TEXTURE = new ResourceLocation(CatalogueConstants.MOD_ID, "textures/gui/icons.png"); + + private final String label; + private final int u, v; + + public CatalogueIconButton(int id, int x, int y, int u, int v) { + this(id, x, y, u, v, 20, ""); + } + + public CatalogueIconButton(int id, int x, int y, int u, int v, int width, int height) { + this(id, x, y, u, v, width, height, ""); + } + + public CatalogueIconButton(int id, int x, int y, int u, int v, int width, String label) { + this(id, x, y, u, v, width, 20, label); + } + + public CatalogueIconButton(int id, int x, int y, int u, int v, int width, int height, String label) { + super(id, x, y, width, height, ""); + this.label = label; + this.u = u; + this.v = v; + } + + @Override + public void drawButton(@NotNull Minecraft minecraft, int mouseX, int mouseY, float partialTicks) { + // Draw bg + super.drawButton(minecraft, mouseX, mouseY, partialTicks); + // Draw icon and text + if (this.visible) { + FontRenderer fontrenderer = minecraft.fontRenderer; + minecraft.getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + int contentWidth = 10 + fontrenderer.getStringWidth(this.label) + (!this.label.isEmpty() ? 4 : 0); + int iconX = this.x + (this.width - contentWidth) / 2; + int iconY = this.y + (this.height - 10) / 2; + float brightness = this.enabled ? 1.0F : 0.5F; + GlStateManager.color(brightness, brightness, brightness, 1.0F); + drawModalRectWithCustomSizedTexture(iconX, iconY, this.u, this.v, 10, 10, 64, 64); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + drawString(fontrenderer, this.label, iconX + 14, iconY + 1, this.getFGColor()); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListExtended.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListExtended.java new file mode 100644 index 000000000..8bd94092a --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListExtended.java @@ -0,0 +1,397 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import com.cleanroommc.catalogue.client.ClientHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiListExtended; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class CatalogueListExtended extends GuiListExtended { + private boolean scrollBarVisible; + + public CatalogueListExtended(Minecraft mc, int width, int height, int top, int bottom, int slotHeight) { + super(mc, width, height, top, bottom, slotHeight); + } + + // Values renamed by deepseek. Comments are handwrite. + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + if (!this.visible) return; + + this.mouseX = mouseX; + this.mouseY = mouseY; + + // Customized background. Empty by default. + this.drawBackground(); + + this.bindAmountScrolled(); + int maxScroll = this.getMaxScroll(); + this.scrollBarVisible = maxScroll > 0 && this.getContentHeight() != 0; + + ClientHelper.scissor(this.left, this.top, this.width, this.bottom - this.top); + + GlStateManager.disableLighting(); + GlStateManager.disableFog(); + Tessellator tessellator = Tessellator.getInstance(); + + // Shadowed dirt background. Scroll with the entries. + this.drawContainerBackground(tessellator); + + // Customized header. Empty by default + if (this.hasListHeader) { + this.drawListHeader(this.getListLeft(), this.getListTop(), tessellator); + } + + this.renderListItems(mouseX, mouseY, partialTicks); + + GlStateManager.disableDepth(); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE); + GlStateManager.disableAlpha(); + GlStateManager.shadeModel(GL11.GL_SMOOTH); + GlStateManager.disableTexture2D(); + + // Scroll Bar + if (this.scrollBarVisible) { + this.drawScrollBar(maxScroll); + } + + // Customized decorations. Empty by default. + this.renderDecorations(mouseX, mouseY); + + GlStateManager.enableTexture2D(); + GlStateManager.shadeModel(GL11.GL_FLAT); + GlStateManager.enableAlpha(); + GlStateManager.disableBlend(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + protected void drawScrollBar(int maxScroll) { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + + int scrollBarLeft = this.getScrollBarX(); + int scrollBarRight = scrollBarLeft + 6; + + int scrollThumbHeight = (this.bottom - this.top) * (this.bottom - this.top) / this.getContentHeight(); + scrollThumbHeight = MathHelper.clamp(scrollThumbHeight, 32, this.bottom - this.top - 8); + int scrollThumbTop = (int) this.amountScrolled * (this.bottom - this.top - scrollThumbHeight) / maxScroll + this.top; + scrollThumbTop = Math.max(scrollThumbTop, this.top); + + // Background + buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + buffer.pos(scrollBarLeft, this.bottom, 0).tex(0, 1).color(0, 0, 0, 255).endVertex(); + buffer.pos(scrollBarRight, this.bottom, 0).tex(1, 1).color(0, 0, 0, 255).endVertex(); + buffer.pos(scrollBarRight, this.top, 0).tex(1, 0).color(0, 0, 0, 255).endVertex(); + buffer.pos(scrollBarLeft, this.top, 0).tex(0, 0).color(0, 0, 0, 255).endVertex(); + tessellator.draw(); + + // Main + buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + buffer.pos(scrollBarLeft, scrollThumbTop + scrollThumbHeight, 0).tex(0, 1).color(128, 128, 128, 255).endVertex(); + buffer.pos(scrollBarRight, scrollThumbTop + scrollThumbHeight, 0).tex(1, 1).color(128, 128, 128, 255).endVertex(); + buffer.pos(scrollBarRight, scrollThumbTop, 0).tex(1, 0).color(128, 128, 128, 255).endVertex(); + buffer.pos(scrollBarLeft, scrollThumbTop, 0).tex(0, 0).color(128, 128, 128, 255).endVertex(); + tessellator.draw(); + + // Border + buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR); + buffer.pos(scrollBarLeft, scrollThumbTop + scrollThumbHeight - 1, 0).tex(0, 1).color(192, 192, 192, 255).endVertex(); + buffer.pos(scrollBarRight - 1, scrollThumbTop + scrollThumbHeight - 1, 0).tex(1, 1).color(192, 192, 192, 255).endVertex(); + buffer.pos(scrollBarRight - 1, scrollThumbTop, 0).tex(1, 0).color(192, 192, 192, 255).endVertex(); + buffer.pos(scrollBarLeft, scrollThumbTop, 0).tex(0, 0).color(192, 192, 192, 255).endVertex(); + tessellator.draw(); + } + + protected void renderListItems(int mouseX, int mouseY, float partialTicks) { + for (int index = 0; index < this.getSize(); ++index) { + int rowLeft = this.getListLeft(); + int rowRight = this.getListRight(); + int rowTop = this.getRowTop(index); + int rowBottom = this.getRowBottom(index) - 4; + + if (rowTop > this.bottom || rowBottom < this.top) { + this.updateItemPos(index, rowLeft, rowTop, partialTicks); + } + + if (rowTop + this.slotHeight >= this.top && rowTop <= this.bottom) { + this.renderItem(index, rowLeft, rowTop, rowRight, rowBottom, mouseX, mouseY, partialTicks); + } + } + } + + protected void renderItem(int slotIndex, int rowLeft, int rowTop, int rowRight, int rowBottom, int mouseX, int mouseY, float partialTicks) { + this.drawSlot(slotIndex, rowLeft, rowTop, rowBottom - rowTop, mouseX, mouseY, partialTicks); + } + + @Override + public void handleMouseInput() { + if (this.isMouseYWithinSlotBounds(this.mouseY)) { + + if (Mouse.getEventButton() == 0 && Mouse.getEventButtonState() && this.mouseY >= this.top && this.mouseY <= this.bottom) { + int listLeft = this.getListLeft(); + int listRight = this.getListRight(); + + int relativeY = this.mouseY - this.top - this.headerPadding + (int) this.amountScrolled - 4; + int slotIndex = relativeY / this.slotHeight; + + if (slotIndex < this.getSize() && this.mouseX >= listLeft && this.mouseX <= listRight && slotIndex >= 0 && relativeY >= 0) { + this.elementClicked(slotIndex, false, this.mouseX, this.mouseY); + this.selectedElement = slotIndex; + } else if (this.mouseX >= listLeft && this.mouseX <= listRight && relativeY < 0) { + this.clickedHeader(this.mouseX - listLeft, this.mouseY - this.top + (int) this.amountScrolled - 4); + } + } + + if (Mouse.isButtonDown(0) && this.getEnabled()) { + if (this.initialClickY == -1) { + boolean clickedOnList = true; + + if (this.mouseY >= this.top && this.mouseY <= this.bottom) { + int listLeft = this.getListLeft(); + int listRight = this.getListRight(); + int relativeY = this.mouseY - this.top - this.headerPadding + (int) this.amountScrolled - 4; + int slotIndex = relativeY / this.slotHeight; + + if (slotIndex < this.getSize() && this.mouseX >= listLeft && this.mouseX <= listRight && slotIndex >= 0 && relativeY >= 0) { + boolean isDoubleClick = slotIndex == this.selectedElement && Minecraft.getSystemTime() - this.lastClicked < 250L; + this.elementClicked(slotIndex, isDoubleClick, this.mouseX, this.mouseY); + this.selectedElement = slotIndex; + this.lastClicked = Minecraft.getSystemTime(); + } else if (this.mouseX >= listLeft && this.mouseX <= listRight && relativeY < 0) { + this.clickedHeader(this.mouseX - listLeft, this.mouseY - this.top + (int) this.amountScrolled - 4); + clickedOnList = false; + } + + int scrollBarLeft = this.getScrollBarX(); + int scrollBarRight = scrollBarLeft + 6; + + if (this.mouseX >= scrollBarLeft && this.mouseX <= scrollBarRight) { + this.scrollMultiplier = -1.0F; + + int maxScroll = Math.max(1, this.getMaxScroll()); + + int viewHeight = this.bottom - this.top; + int scrollBarHeight = (int) ((float) (viewHeight * viewHeight) / (float) this.getContentHeight()); + + scrollBarHeight = MathHelper.clamp(scrollBarHeight, 32, viewHeight - 8); + + this.scrollMultiplier /= (float) (viewHeight - scrollBarHeight) / (float) maxScroll; + } else { + this.scrollMultiplier = 1.0F; + } + + if (clickedOnList) { + this.initialClickY = this.mouseY; + } else { + this.initialClickY = -2; + } + } else { + this.initialClickY = -2; + } + } else if (this.initialClickY >= 0) { + this.amountScrolled -= (float) (this.mouseY - this.initialClickY) * this.scrollMultiplier; + this.initialClickY = this.mouseY; + } + } else { + this.initialClickY = -1; + } + + int dWheel = Mouse.getEventDWheel(); + + if (dWheel != 0) { + dWheel = dWheel > 0 ? -1 : 1; + this.amountScrolled += (float) (dWheel * this.slotHeight / 2); + } + } + } + + @Deprecated + @Override + protected void drawSelectionBox(int contentLeft, int contentTop, int mouseX, int mouseY, float partialTicks) { + } + + @Override + public boolean mouseClicked(int mouseX, int mouseY, int mouseEvent) { + if (this.isMouseYWithinSlotBounds(mouseY)) { + int slotIndex = this.getSlotIndexFromScreenCoords(mouseX, mouseY); + if (slotIndex >= 0) { + int j = this.left + this.getListLeft(); + int k = this.top + 4 - this.getAmountScrolled() + slotIndex * this.slotHeight + this.headerPadding; + int relativeX = mouseX - j; + int relativeY = mouseY - k; + if (this.getListEntry(slotIndex).mousePressed(slotIndex, mouseX, mouseY, mouseEvent, relativeX, relativeY)) { + this.setEnabled(false); + return true; + } + } + } + return false; + } + + @Override + public boolean mouseReleased(int mouseX, int mouseY, int mouseEvent) { + for (int slotIndex = 0; slotIndex < this.getSize(); ++slotIndex) { + int j = this.left + this.getListLeft(); + int k = this.top + 4 - this.getAmountScrolled() + slotIndex * this.slotHeight + this.headerPadding; + int relativeX = mouseX - j; + int relativeY = mouseY - k; + this.getListEntry(slotIndex).mouseReleased(slotIndex, mouseX, mouseY, mouseEvent, relativeX, relativeY); + } + this.setEnabled(true); + return false; + } + + @Override + public int getSlotIndexFromScreenCoords(int mouseX, int mouseY) { + int i = this.getListWidth() / 2; + int j = this.left + this.width / 2; + int k = j - i; + int l = j + i; + int i1 = MathHelper.floor(mouseY - this.top) - this.headerPadding + this.getAmountScrolled() - 4; + int j1 = i1 / this.slotHeight; + return mouseX < this.getScrollBarX() && mouseX >= k && mouseX <= l && j1 >= 0 && i1 >= 0 && j1 < this.getSize() ? j1 : -1; + } + + public void setClampedAmountScrolled(float scroll) { + this.amountScrolled = MathHelper.clamp(scroll, 0.0F, this.getMaxScroll()); + } + + public void setAmountScrolled(float scroll) { + this.setClampedAmountScrolled(scroll); + } + + public void clampAmountScrolled() { + this.setClampedAmountScrolled(this.getAmountScrolled()); + } + + public void setWidth(int width) { + this.width = width; + this.right = this.left + this.width; + } + + public void setHeight(int height) { + this.height = height; + this.bottom = this.top + height; + } + + protected boolean isScrollBarVisible() { + return this.scrollBarVisible; + } + + protected int getListLeft() { + return this.width / 2 - this.getListWidth() / 2 + 2; + } + + protected int getListRight() { + return this.getListLeft() + this.getListWidth(); + } + + protected int getListTop() { + return this.top + 4 - (int) this.amountScrolled; + } + + protected int getRowTop(int slotIndex) { + return this.top + 4 - (int) this.amountScrolled + slotIndex * this.slotHeight + this.headerPadding; + } + + protected int getRowBottom(int slotIndex) { + return this.getRowTop(slotIndex) + this.slotHeight; + } + + /* + Some helpers. + */ + + private final List children = new ArrayList<>(); + + public final List children() { + return this.children; + } + + @NotNull + @Override + public E getListEntry(int slotIndex) { + return this.children().get(slotIndex); + } + + @Override + protected int getSize() { + return this.children().size(); + } + + public void centerScrollOn(E pEntry) { + this.setAmountScrolled((float) (this.children().indexOf(pEntry) * this.slotHeight + this.slotHeight / 2 - (this.bottom - this.top) / 2)); + } + + public void addEntry(E pEntry) { + this.children().add(pEntry); + } + + public void clearEntries() { + this.children().clear(); + } + + public void replaceEntries(Collection entries) { + this.clearEntries(); + this.children().addAll(entries); + } + + public void removeEntries(@NotNull List entries) { + entries.forEach(this::removeEntry); + } + + public void removeEntry(E entry) { + this.children.remove(entry); + } + + public void clearEntriesExcept(E entry) { + this.children.removeIf((e) -> e != entry); + } + + public interface IListEntry extends IGuiListEntry { + + /** + * Called when the entry's position is moved. + */ + @Override + default void updatePosition(int slotIndex, int x, int y, float partialTicks) { + } + + /** + * Called when the mouse is clicked within this entry. + * + * @param mouseX the current mouse x position + * @param mouseY the current mouse y position + * @param relativeX the current x position of the mouse relative to the top-left corner of the entry + * @param relativeY the current y position of the mouse relative to the top-left corner of the entry + * @return true means that something within this entry was clicked and the list should not be dragged. + */ + @Override + default boolean mousePressed(int slotIndex, int mouseX, int mouseY, int mouseButton, int relativeX, int relativeY) { + return true; + } + + /** + * Called when the mouse button is released. + * + * @param mouseX the current mouse x position + * @param mouseY the current mouse y position + * @param relativeX the current x position of the mouse relative to the top-left corner of the entry + * @param relativeY the current y position of the mouse relative to the top-left corner of the entry + */ + @Override + default void mouseReleased(int slotIndex, int mouseX, int mouseY, int mouseButton, int relativeX, int relativeY) { + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListSelection.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListSelection.java new file mode 100644 index 000000000..21305fdd2 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueListSelection.java @@ -0,0 +1,66 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class CatalogueListSelection extends CatalogueListExtended { + private @Nullable E selected; + + public CatalogueListSelection(Minecraft mcIn, int widthIn, int heightIn, int topIn, int bottomIn, int slotHeightIn) { + super(mcIn, widthIn, heightIn, topIn, bottomIn, slotHeightIn); + } + + @Override + public void clearEntries() { + super.clearEntries(); + this.setSelected(null); + } + + @Override + protected boolean isSelected(int slotIndex) { + return Objects.equals(this.getSelected(), this.getListEntry(slotIndex)); + } + + @Nullable + public E getSelected() { + return this.selected; + } + + public void setSelected(@Nullable E selected) { + this.selected = selected; + } + + @Override + public void removeEntry(E entry) { + if (this.children().remove(entry) && entry == this.getSelected()) { + this.setSelected(null); + } + } + + @Override + public void clearEntriesExcept(E entry) { + super.clearEntriesExcept(entry); + if (this.selected != entry) { + this.setSelected(null); + } + } + + @Override + protected void renderItem(int slotIndex, int rowLeft, int rowTop, int rowRight, int rowBottom, int mouseX, int mouseY, float partialTicks) { + if (this.showSelectionBox && this.isSelected(slotIndex)) { + renderSelection(rowLeft, rowTop, rowRight, rowBottom); + } + super.renderItem(slotIndex, rowLeft, rowTop, rowRight, rowBottom, mouseX, mouseY, partialTicks); + } + + @SuppressWarnings("SameParameterValue") + protected void renderSelection(int left, int top, int right, int bottom) { + top -= 2; + bottom += 2; + Gui.drawRect(left, top, right, bottom, 0xFF808080); + Gui.drawRect(left + 1, top + 1, right - 1, bottom - 1, -16777216); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextButton.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextButton.java new file mode 100644 index 000000000..4bd85331a --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextButton.java @@ -0,0 +1,79 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import com.cleanroommc.catalogue.Utils; +import com.cleanroommc.catalogue.client.ClientHelper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +public class CatalogueTextButton extends GuiButton { + protected static final WidgetSprites SPRITES = new WidgetSprites(Utils.withDefaultNamespace("widget/button"), Utils.withDefaultNamespace("widget/button_disabled"), Utils.withDefaultNamespace("widget/button_highlighted")); + + public CatalogueTextButton(int buttonId, int x, int y, int widthIn, int heightIn, String buttonText) { + super(buttonId, x, y, widthIn, heightIn, buttonText); + } + + public void drawButton(@NotNull Minecraft mc, int mouseX, int mouseY, float partialTicks) { + if (!this.visible) return; + this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height; + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + mc.getTextureManager().bindTexture(SPRITES.get(this.enabled, this.hovered)); + ClientHelper.blitNineSlicedSprite(new ClientHelper.NineSlice(200, 20, 3), this.x, this.y, this.width, this.height); + + this.mouseDragged(mc, mouseX, mouseY); + this.renderString(mc.fontRenderer, this.getFGColor()); + } + + public void renderString(FontRenderer font, int color) { + this.renderScrollingString(font, 2, color); + } + + protected void renderScrollingString(FontRenderer font, int width, int color) { + int i = this.x + width; + int j = this.x + this.width - width; + renderScrollingString(font, this.displayString, i, this.y, j, this.y + this.height, color); + } + + public void renderScrollingString(FontRenderer font, String text, int minX, int minY, int maxX, int maxY, int color) { + renderScrollingString(font, text, (minX + maxX) / 2, minX, minY, maxX, maxY, color); + } + + public void renderScrollingString(FontRenderer font, String text, int centerX, int minX, int minY, int maxX, int maxY, int color) { + int i = font.getStringWidth(text); + int j = (minY + maxY - 9) / 2 + 1; + int k = maxX - minX; + if (i > k) { + int l = i - k; + double d0 = (double) System.currentTimeMillis() / (double) 1000.0F; + double d1 = Math.max((double) l * (double) 0.5F, (double) 3.0F); + double d2 = Math.sin((Math.PI / 2D) * Math.cos((Math.PI * 2D) * d0 / d1)) / (double) 2.0F + (double) 0.5F; + double d3 = Utils.lerp(d2, 0.0F, l); + ClientHelper.scissor(minX, minY, maxX - minX, maxY - minY); + drawString(font, text, minX - (int) d3, j, color); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } else { + int i1 = MathHelper.clamp(centerX, minX + i / 2, maxX - i / 2); + drawCenteredString(font, text, i1, j, color); + } + } + + protected int getFGColor() { + if (this.packedFGColour != 0) { + return this.packedFGColour; + } else if (!this.enabled) { + return 10526880; + } else if (this.hovered) { + return 16777120; + } else { + return 14737632; + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextField.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextField.java new file mode 100644 index 000000000..84e229f3d --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/CatalogueTextField.java @@ -0,0 +1,155 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiTextField; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +public class CatalogueTextField extends GuiTextField { + private final FontRenderer fontRenderer; + private boolean isTextTruncated; + @NotNull + private String suggestion = ""; + @Nullable + private Consumer responder; + @Nullable + private BiFunction formatter; + + public CatalogueTextField(int id, FontRenderer fontRenderer, int x, int y, int width, int height) { + super(id, fontRenderer, x, y, width, height); + this.fontRenderer = fontRenderer; + } + + // Values renamed by deepseek. Comments are handwrite. + @Override + public void drawTextBox() { + if (!this.getVisible()) return; + + if (this.getEnableBackgroundDrawing()) { + int borderColor = this.isFocused() ? 0xFFFFFFFF : 0xFFA0A0A0; + drawRect(this.x - 1, this.y - 1, this.x + this.width + 1, this.y + this.height + 1, borderColor); + drawRect(this.x, this.y, this.x + this.width, this.y + this.height, 0xFF000000); + } + + int textColor = this.isEnabled ? this.enabledColor : this.disabledColor; + + int cursorPosRelative = this.cursorPosition - this.lineScrollOffset; + int selectionEndRelative = this.selectionEnd - this.lineScrollOffset; + + String visibleText = this.fontRenderer.trimStringToWidth(this.getText().substring(this.lineScrollOffset), this.getWidth()); + + boolean isCursorVisible = cursorPosRelative >= 0 && cursorPosRelative <= visibleText.length(); + boolean shouldDrawCursor = this.isFocused() && this.cursorCounter / 6 % 2 == 0 && isCursorVisible; + + int textStartX = this.getEnableBackgroundDrawing() ? this.x + 4 : this.x; + int textStartY = this.getEnableBackgroundDrawing() ? this.y + (this.height - 8) / 2 : this.y; + int currentDrawX = textStartX; + + if (selectionEndRelative > visibleText.length()) { + selectionEndRelative = visibleText.length(); + } + + // Draw text before cursor + if (!visibleText.isEmpty()) { + String rawTextBeforeCursor = isCursorVisible ? visibleText.substring(0, cursorPosRelative) : visibleText; + currentDrawX = this.fontRenderer.drawStringWithShadow(formatText(rawTextBeforeCursor, this.lineScrollOffset), (float) textStartX, (float) textStartY, textColor); + } + + this.isTextTruncated = this.cursorPosition < this.getText().length() || this.getText().length() >= this.getMaxStringLength(); + int cursorDrawX = currentDrawX; + + if (!isCursorVisible) { + cursorDrawX = cursorPosRelative > 0 ? textStartX + this.width : textStartX; + } else if (this.isTextTruncated) { + cursorDrawX = currentDrawX - 1; + --currentDrawX; + } + + // Draw text after cursor + if (!visibleText.isEmpty() && isCursorVisible && cursorPosRelative < visibleText.length()) { + String rawTextAfterCursor = visibleText.substring(cursorPosRelative); + currentDrawX = this.fontRenderer.drawStringWithShadow(formatText(rawTextAfterCursor, this.cursorPosition), (float) currentDrawX, (float) textStartY, textColor); + } + + if (!this.isTextTruncated && !this.suggestion.isEmpty()) { + int suggestionDrawX = this.getText().isEmpty() ? currentDrawX : currentDrawX - 1; + String suggestion = this.fontRenderer.trimStringToWidth(this.suggestion, textStartX + this.getWidth() - suggestionDrawX); + this.fontRenderer.drawStringWithShadow(suggestion, (float) suggestionDrawX, (float) textStartY, 0x808080); + } + + if (shouldDrawCursor) { + if (this.isTextTruncated) { + Gui.drawRect(cursorDrawX, textStartY - 1, cursorDrawX + 1, textStartY + 1 + this.fontRenderer.FONT_HEIGHT, 0xFFCFCFD0); + } else { + this.fontRenderer.drawStringWithShadow("_", (float) cursorDrawX, (float) textStartY, textColor); + } + } + + if (selectionEndRelative != cursorPosRelative) { + int selectionEndX = textStartX + this.fontRenderer.getStringWidth(visibleText.substring(0, selectionEndRelative)); + this.drawSelectionBox(cursorDrawX, textStartY - 1, selectionEndX - 1, textStartY + 1 + this.fontRenderer.FONT_HEIGHT); + } + } + + // Formatter + public void setFormatter(@Nullable BiFunction pFormatter) { + this.formatter = pFormatter; + } + + private String formatText(String text, int cursorPos) { + return this.formatter != null ? this.formatter.apply(text, cursorPos) : text; + } + + // Responder + public void setResponder(@Nullable Consumer pResponder) { + this.responder = pResponder; + } + + // Patch vanilla missing methods + @Override + public void setText(@NotNull String textIn) { + String previousText = this.getText(); + super.setText(textIn); + if (!Objects.equals(this.getText(), previousText)) { + this.setResponderEntryValue(this.getId(), this.getText()); + } + } + + @Override + public void setMaxStringLength(int length) { + String previousText = this.getText(); + super.setMaxStringLength(length); + if (!Objects.equals(this.getText(), previousText)) { + this.setResponderEntryValue(this.getId(), this.getText()); + } + } + + // Call consumer responder + @Override + public void setResponderEntryValue(int idIn, @NotNull String textIn) { + if (this.responder != null) { + this.responder.accept(textIn); + } + super.setResponderEntryValue(idIn, textIn); + } + + // Suggestion + public void setSuggestion(@NotNull String suggestion) { + this.suggestion = suggestion; + } + + @NotNull + public String getSuggestion() { + return this.suggestion; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isTextTruncated() { + return this.isTextTruncated; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/DropdownMenu.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/DropdownMenu.java new file mode 100644 index 000000000..9418b5e39 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/DropdownMenu.java @@ -0,0 +1,503 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import com.cleanroommc.catalogue.CatalogueConstants; +import com.cleanroommc.catalogue.Utils; +import com.cleanroommc.catalogue.client.ClientHelper; +import com.cleanroommc.catalogue.client.screen.DropdownMenuHandler; +import com.cleanroommc.catalogue.client.screen.layout.BorderedLinearLayout; +import com.cleanroommc.catalogue.client.screen.layout.LayoutElement; +import com.cleanroommc.catalogue.client.screen.layout.ScreenRectangle; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.init.SoundEvents; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Author: MrCrayfish + */ +public class DropdownMenu extends Gui implements LayoutElement { + private final DropdownMenuHandler handler; + private final BorderedLinearLayout layout = (BorderedLinearLayout) + BorderedLinearLayout.vertical().border(1).spacing(1); + private final List items = new ArrayList<>(); + private Alignment alignment = Alignment.BELOW_LEFT; + private @Nullable DropdownMenu parent; + private @Nullable DropdownMenu subMenu; + + public boolean active; + public boolean visible; + + private int x; + private int y; + private int width; + private int height; + + private DropdownMenu(DropdownMenuHandler handler) { + super(); + this.handler = handler; + this.visible = false; + this.active = true; + } + + private void setAlignment(Alignment alignment) { + this.alignment = alignment; + } + + public void toggle(int mouseX, int mouseY) { + this.toggle(new ScreenRectangle(mouseX, mouseY, 0, 0)); + } + + public void toggle(GuiButton widget) { + this.toggle(new ScreenRectangle(widget.x, widget.y, widget.width, widget.height)); + } + + public void toggle(ScreenRectangle rect) { + if (!this.visible) { + this.show(rect); + } else { + this.hide(); + } + } + + private void show(ScreenRectangle rect) { + this.updatePosition(rect); + this.items.forEach(child -> child.visible = true); + this.visible = true; + if (this.parent == null) { + this.handler.setMenu(this); + } + } + + public void hide() { + this.items.forEach(child -> { + child.visible = false; + if (child instanceof DropdownItem menu) { + menu.subMenu.hide(); + } + }); + this.subMenu = null; + this.visible = false; + } + + private void updatePosition(ScreenRectangle rect) { + this.layout.arrangeElements(); + this.width = this.layout.getWidth(); + this.height = this.layout.getHeight(); + this.alignment.aligner.accept(this, rect); + this.layout.setX(this.getX()); + this.layout.setY(this.getY()); + } + + public void addItem(MenuItem item) { + this.layout.addChild(item); + this.items.add(item); + item.visible = false; + } + + private void deepClose() { + this.handler.setMenu(null); + } + + public void drawScreen(Minecraft minecraft, int mouseX, int mouseY, float deltaTick) { + GlStateManager.pushMatrix(); + final ScaledResolution sr = new ScaledResolution(minecraft); + drawRect(0, 0, sr.getScaledWidth(), sr.getScaledHeight(), 0x50000000); + drawRect(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), 0xAA000000); + this.items.forEach(widget -> widget.drawWidget(minecraft, mouseX, mouseY, deltaTick)); + if (this.subMenu != null) { + this.subMenu.drawScreen(minecraft, mouseX, mouseY, deltaTick); + } + GlStateManager.popMatrix(); + } + + public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY) { + if (!this.active || !this.visible) return false; + + AtomicBoolean clicked = new AtomicBoolean(); + this.layout.visitWidgets(widget -> { + if (widget instanceof MenuItem item && item.mousePressed(minecraft, mouseX, mouseY)) { + clicked.set(true); + } + }); + if (this.subMenu != null && this.subMenu.mousePressed(minecraft, mouseX, mouseY)) { + clicked.set(true); + } + return clicked.get(); + } + + @Override + public void visitWidgets(Consumer consumer) { + this.layout.visitWidgets(consumer); + } + + public static class MenuItem extends Gui implements LayoutElement { + static final WidgetSprites SPRITES = new WidgetSprites( + Utils.withDefaultNamespace("dropdown/item"), + Utils.withDefaultNamespace("dropdown/item_highlighted") + ); + protected final DropdownMenu parent; + private final Runnable onClick; + + private int width; + private int height; + private int x; + private int y; + public String label; + public boolean enabled; + public boolean visible; + protected boolean hovered; + + public MenuItem(DropdownMenu menu, String label, Runnable onClick) { + super(); + this.x = 0; + this.y = 0; + this.width = 100; + this.height = 20; + this.enabled = true; + this.visible = true; + this.label = label; + this.parent = menu; + this.onClick = onClick; + } + + protected boolean selected() { + return false; + } + + public void drawWidget(Minecraft minecraft, int mouseX, int mouseY, float deltaTick) { + if (!this.visible) return; + this.hovered = mouseX >= this.getX() && mouseY >= this.getY() && mouseX < this.getX() + this.getWidth() && mouseY < this.getY() + this.height; + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + minecraft.getTextureManager().bindTexture(SPRITES.get(this.enabled, this.hovered || this.selected())); + ClientHelper.blitNineSlicedSprite(new ClientHelper.NineSlice(12, 12, 2), this.getX(), this.getY(), this.getWidth(), this.getHeight()); + + FontRenderer font = minecraft.fontRenderer; + int offset = (this.getHeight() - font.FONT_HEIGHT) / 2 + 1; + drawString(font, this.label, this.getX() + offset, this.getY() + offset, 0xFFFFFFFF); + } + + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + if (this.enabled && this.visible && this.hovered) { + this.onClick(mouseX, mouseY); + this.playPressSound(mc.getSoundHandler()); + return true; + } + return false; + } + + public void onClick(int mouseX, int mouseY) { + this.onClick.run(); + this.parent.deepClose(); + } + + protected int calculateWidth() { + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + int labelOffset = (this.height - font.FONT_HEIGHT) / 2 + 1; + int labelWidth = font.getStringWidth(this.label); + return labelOffset + labelWidth + labelOffset; + } + + public boolean isMouseOver() { + return this.hovered; + } + + public void playPressSound(SoundHandler soundHandlerIn) { + soundHandlerIn.playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } + + @Override + public void setX(int pX) { + this.x = pX; + } + + @Override + public void setY(int pY) { + this.y = pY; + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getY() { + return this.y; + } + + @Override + public int getWidth() { + return this.width; + } + + @Override + public int getHeight() { + return this.height; + } + + public void setWidth(int width) { + this.width = width; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setSize(int width, int height) { + this.setWidth(width); + this.setHeight(height); + } + + @Override + public void setPosition(int pX, int pY) { + LayoutElement.super.setPosition(pX, pY); + } + + @Override + public void visitWidgets(Consumer pConsumer) { + pConsumer.accept(this); + } + } + + private static class CheckboxMenuItem extends MenuItem { + private static final ResourceLocation TEXTURE = new ResourceLocation(CatalogueConstants.MOD_ID, "textures/gui/checkbox.png"); + + private final MutableBoolean holder; + private final Function callback; + + public CheckboxMenuItem(DropdownMenu menu, String label, MutableBoolean holder, Function callback) { + super(menu, label, () -> { + }); + this.holder = holder; + this.callback = callback; + } + + @Override + public void drawWidget(Minecraft minecraft, int mouseX, int mouseY, float deltaTick) { + super.drawWidget(minecraft, mouseX, mouseY, deltaTick); + int offset = (this.getHeight() - 14) / 2; + minecraft.getTextureManager().bindTexture(TEXTURE); + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + drawModalRectWithCustomSizedTexture(this.getX() + this.getWidth() - 14 - offset, this.getY() + offset, this.hovered ? 14 : 0, this.holder.get() ? 14 : 0, 14, 14, 64, 64); + } + + @Override + public void onClick(int mouseX, int mouseY) { + boolean newValue = !this.holder.get(); + this.holder.setValue(newValue); + if (this.callback.apply(newValue)) { + this.parent.deepClose(); + } + } + + @Override + protected int calculateWidth() { + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + int labelOffset = (this.getHeight() - font.FONT_HEIGHT) / 2 + 1; + int labelWidth = font.getStringWidth(this.label); + int checkboxOffset = (this.getHeight() - 14) / 2; + return labelOffset + labelWidth + labelOffset + 14 + checkboxOffset; + } + } + + private static class DropdownItem extends MenuItem { + private final DropdownMenu subMenu; + + public DropdownItem(DropdownMenu menu, DropdownMenu subMenu, String label) { + super(menu, label, () -> { + }); + this.subMenu = subMenu; + } + + @Override + public void drawWidget(Minecraft minecraft, int mouseX, int mouseY, float deltaTick) { + super.drawWidget(minecraft, mouseX, mouseY, deltaTick); + FontRenderer font = minecraft.fontRenderer; + int top = this.getY() + (this.getHeight() - font.FONT_HEIGHT) / 2 + 1; + drawString(font, ">", this.getX() + this.getWidth() - 10, top, 0xFFFFFFFF); + } + + @Override + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { + return super.mousePressed(mc, mouseX, mouseY) || this.subMenu.mousePressed(mc, mouseX, mouseY); + } + + @Override + public void onClick(int mouseX, int mouseY) { + if (this.parent.subMenu != null) { + this.parent.subMenu.hide(); + if (this.parent.subMenu == this.subMenu) { + this.parent.subMenu = null; + return; + } + } + this.parent.subMenu = this.subMenu; + this.subMenu.show(this.getRectangle()); + } + + @Override + public void visitWidgets(Consumer consumer) { + consumer.accept(this); + this.subMenu.visitWidgets(consumer); + } + + @Override + protected boolean selected() { + return this.parent.subMenu == this.subMenu; + } + + @Override + protected int calculateWidth() { + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + int labelOffset = (this.getHeight() - font.FONT_HEIGHT) / 2 + 1; + int labelWidth = font.getStringWidth(this.label); + int arrowWidth = font.getStringWidth(">"); + return labelOffset + labelWidth + labelOffset + arrowWidth + labelOffset; + } + } + + private interface MenuAligner { + void accept(DropdownMenu menu, ScreenRectangle rectangle); + } + + public enum Alignment { + ABOVE_LEFT((menu, rectangle) -> { + menu.setX(rectangle.left()); + menu.setY(rectangle.top() - menu.getHeight()); + }), + ABOVE_RIGHT((menu, rectangle) -> { + menu.setX(rectangle.right() - menu.getWidth()); + menu.setY(rectangle.top() - menu.getHeight()); + }), + BELOW_LEFT((menu, rectangle) -> { + menu.setX(rectangle.left() - 1); + menu.setY(rectangle.bottom()); + }), + BELOW_RIGHT((menu, rectangle) -> { + menu.setX(rectangle.right() - menu.getWidth() + 1); + menu.setY(rectangle.bottom()); + }), + END_TOP((menu, rectangle) -> { + menu.setX(rectangle.right()); + menu.setY(rectangle.top() - 1); + }), + END_BOTTOM((menu, rectangle) -> { + menu.setX(rectangle.right()); + menu.setY(rectangle.bottom() - menu.getHeight() + 1); + }); + + + private final MenuAligner aligner; + + Alignment(MenuAligner positioner) { + this.aligner = positioner; + } + + } + + public static Builder builder(DropdownMenuHandler handler) { + return new Builder(handler); + } + + public static class Builder { + private final DropdownMenuHandler handler; + private final DropdownMenu base; + private final List items = new ArrayList<>(); + private int minItemWidth = 0; + private int minItemHeight = 20; + + private Builder(DropdownMenuHandler handler) { + this.handler = handler; + this.base = new DropdownMenu(handler); + } + + public Builder setMinItemSize(int width, int height) { + this.minItemWidth = width; + this.minItemHeight = height; + return this; + } + + public Builder setAlignment(Alignment alignment) { + this.base.setAlignment(alignment); + return this; + } + + public Builder addItem(String label, Runnable onClick) { + this.items.add(new MenuItem(this.base, label, onClick)); + return this; + } + + public Builder addCheckbox(String label, MutableBoolean holder, Function callback) { + this.items.add(new CheckboxMenuItem(this.base, label, holder, callback)); + return this; + } + + public Builder addMenu(String label, Builder builder) { + DropdownMenu menu = builder.build(); + menu.parent = this.base; + this.items.add(new DropdownItem(this.base, menu, label)); + return this; + } + + public DropdownMenu build() { + int maxWidth = this.items.stream().mapToInt(MenuItem::calculateWidth).max().orElse(100); + this.items.forEach(widget -> { + widget.setSize(Math.max(maxWidth, this.minItemWidth), this.minItemHeight); + this.base.addItem(widget); + }); + return this.base; + } + } + + @Override + public void setX(int x) { + this.x = x; + } + + @Override + public void setY(int y) { + this.y = y; + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getY() { + return this.y; + } + + @Override + public int getWidth() { + return this.width; + } + + @Override + public int getHeight() { + return this.height; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/client/screen/widget/WidgetSprites.java b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/WidgetSprites.java new file mode 100644 index 000000000..84ade66b4 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/client/screen/widget/WidgetSprites.java @@ -0,0 +1,25 @@ +package com.cleanroommc.catalogue.client.screen.widget; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public record WidgetSprites(ResourceLocation enabled, ResourceLocation disabled, ResourceLocation enabledFocused, + ResourceLocation disabledFocused) { + public WidgetSprites(ResourceLocation normal, ResourceLocation focused) { + this(normal, normal, focused, focused); + } + + public WidgetSprites(ResourceLocation enabled, ResourceLocation disabled, ResourceLocation focused) { + this(enabled, disabled, focused, disabled); + } + + public ResourceLocation get(boolean enabled, boolean focused) { + if (enabled) { + return focused ? this.enabledFocused : this.enabled; + } else { + return focused ? this.disabledFocused : this.disabled; + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/exception/InvalidBrandingImageException.java b/src/main/java/com/cleanroommc/catalogue/exception/InvalidBrandingImageException.java new file mode 100644 index 000000000..9fd249375 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/exception/InvalidBrandingImageException.java @@ -0,0 +1,10 @@ +package com.cleanroommc.catalogue.exception; + +/** + * Author: MrCrayfish + */ +public class InvalidBrandingImageException extends RuntimeException { + public InvalidBrandingImageException(String message) { + super(message); + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/exception/ModResourceNotFoundException.java b/src/main/java/com/cleanroommc/catalogue/exception/ModResourceNotFoundException.java new file mode 100644 index 000000000..7e09ad006 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/exception/ModResourceNotFoundException.java @@ -0,0 +1,10 @@ +package com.cleanroommc.catalogue.exception; + +import java.io.IOException; + +/** + * Author: MrCrayfish + */ +public class ModResourceNotFoundException extends IOException { + +} diff --git a/src/main/java/com/cleanroommc/catalogue/platform/CleanroomPlatformHelper.java b/src/main/java/com/cleanroommc/catalogue/platform/CleanroomPlatformHelper.java new file mode 100644 index 000000000..c3b9d07ba --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/platform/CleanroomPlatformHelper.java @@ -0,0 +1,171 @@ +package com.cleanroommc.catalogue.platform; + +import com.cleanroommc.catalogue.CatalogueConfig; +import com.cleanroommc.catalogue.client.Branding; +import com.cleanroommc.catalogue.client.CleanroomModData; +import com.cleanroommc.catalogue.client.IModData; +import com.cleanroommc.catalogue.platform.services.IPlatformHelper; +import com.google.common.base.Strings; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiVideoSettings; +import net.minecraft.client.renderer.texture.TextureUtil; +import net.minecraftforge.fml.client.FMLClientHandler; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.common.ModMetadata; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Author: MrCrayfish + */ +public class CleanroomPlatformHelper implements IPlatformHelper { + + @Override + public List getAllModData() { + List containerList = Loader.instance().getActiveModList(); + + // Add special mod containers + ArrayList specialContainerList = new ArrayList<>(); + FMLClientHandler.instance().addSpecialModEntries(specialContainerList); + containerList.addAll(specialContainerList); + + // Add child mods to metadata + for (ModContainer container : containerList) { + if (container == null) continue; + ModMetadata metadata = container.getMetadata(); + if (metadata != null && metadata.parentMod == null && !Strings.isNullOrEmpty(metadata.parent)) { + ModContainer parentContainer = Loader.instance().getIndexedModList().get(metadata.parent); + if (parentContainer != null) { + metadata.parentMod = parentContainer; + parentContainer.getMetadata().childMods.add(container); + } + } + } + + List dataList = new ArrayList<>(); + containerList.removeIf(modContainer -> { + if (modContainer.getModId().equals("optifine")) { + dataList.add(new OFData(modContainer)); + return true; + } + return false; + }); + dataList.addAll(containerList.stream().map(CleanroomModData::new).collect(Collectors.toList())); + return dataList; + } + + @Override + public File getModDirectory() { + return Loader.instance().getModsDir(); + } + + @Override + public Path getConfigDirectory() { + return Loader.instance().getConfigDir().toPath(); + } + + @Override + public BufferedImage loadImageFromModResource(String modid, String resource) throws IOException { + InputStream is = getClass().getResourceAsStream(resource); + return is != null ? TextureUtil.readBufferedImage(is) : null; + } + + @Override + public boolean isModLoaded(String modId) { + return Loader.isModLoaded(modId); + } + + @Override + public boolean getEnableBannerLimit() { + return CatalogueConfig.enableBannerLimit; + } + + @Override + public Branding.BannerLimit getBannerLimit() { + return new Branding.BannerLimit(CatalogueConfig.bannerMaxWidth, CatalogueConfig.bannerMaxHeight); + } + + @Override + public boolean getEnableIconLimit() { + return CatalogueConfig.enableIconLimit; + } + + @Override + public int getIconLimit() { + return CatalogueConfig.iconMaxWidthHeight; + } + + private static class OFData extends CleanroomModData { + private final String version; + + private OFData(ModContainer info) { + super(info); + this.version = this.getDisplayVersion(); + } + + private @NotNull String getDisplayVersion() { + String version = this.getInnerVersion(); + return version.substring(version.indexOf("OptiFine_1.12.2_") + 16); + } + + @Override + public String getVersion() { + return this.version; + } + + @Nullable + @Override + public String getDescription() { + // Copied from https://www.optifine.net/home + return """ + OptiFine is a Minecraft optimization mod. + It allows Minecraft to run faster and look better with full support for HD textures and many configuration options. + """; + } + + @Nullable + @Override + public String getLicense() { + return "All Rights Reserved"; + } + + @Nullable + @Override + public String getAuthors() { + return "sp614x"; + } + + @Nullable + @Override + public String getHomepage() { + return "https://www.optifine.net/home"; + } + + @Nullable + @Override + public String getIssueTracker() { + return "https://github.com/sp614x/optifine/issues"; + } + + @Override + public boolean hasConfig() { + return true; + } + + @Override + public void openConfigScreen(Minecraft minecraft, GuiScreen parent) { + minecraft.displayGuiScreen(new GuiVideoSettings(parent, minecraft.gameSettings)); + } + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/platform/ClientServices.java b/src/main/java/com/cleanroommc/catalogue/platform/ClientServices.java new file mode 100644 index 000000000..26fc04a84 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/platform/ClientServices.java @@ -0,0 +1,16 @@ +package com.cleanroommc.catalogue.platform; + +import com.cleanroommc.catalogue.CatalogueConstants; +import com.cleanroommc.catalogue.platform.services.IPlatformHelper; + +import java.util.ServiceLoader; + +public class ClientServices { + public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class); + + public static T load(Class clazz) { + final T loadedService = ServiceLoader.load(clazz).findFirst().orElseThrow(() -> new NullPointerException("Failed to load service for " + clazz.getName())); + CatalogueConstants.LOG.debug("Loaded {} for service {}", loadedService, clazz); + return loadedService; + } +} diff --git a/src/main/java/com/cleanroommc/catalogue/platform/services/IPlatformHelper.java b/src/main/java/com/cleanroommc/catalogue/platform/services/IPlatformHelper.java new file mode 100644 index 000000000..273f72153 --- /dev/null +++ b/src/main/java/com/cleanroommc/catalogue/platform/services/IPlatformHelper.java @@ -0,0 +1,30 @@ +package com.cleanroommc.catalogue.platform.services; + +import com.cleanroommc.catalogue.client.Branding; +import com.cleanroommc.catalogue.client.IModData; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public interface IPlatformHelper { + List getAllModData(); + + File getModDirectory(); + + Path getConfigDirectory(); + + BufferedImage loadImageFromModResource(String modid, String resource) throws IOException; + + boolean isModLoaded(String modId); + + boolean getEnableBannerLimit(); + + Branding.BannerLimit getBannerLimit(); + + boolean getEnableIconLimit(); + + int getIconLimit(); +} diff --git a/src/main/java/com/cleanroommc/common/CatalogueContainer.java b/src/main/java/com/cleanroommc/common/CatalogueContainer.java new file mode 100644 index 000000000..c643dade6 --- /dev/null +++ b/src/main/java/com/cleanroommc/common/CatalogueContainer.java @@ -0,0 +1,38 @@ +package com.cleanroommc.common; + +import com.cleanroommc.catalogue.CatalogueConfig; +import com.cleanroommc.catalogue.CatalogueConstants; +import com.cleanroommc.catalogue.client.ClientHandler; +import com.google.common.eventbus.EventBus; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.ConfigManager; +import net.minecraftforge.fml.common.DummyModContainer; +import net.minecraftforge.fml.common.LoadController; +import net.minecraftforge.fml.common.ModMetadata; + +import java.util.Arrays; + +public class CatalogueContainer extends DummyModContainer { + public CatalogueContainer() { + super(new ModMetadata()); + ModMetadata meta = this.getMetadata(); + meta.modId = CatalogueConstants.MOD_ID; + meta.name = CatalogueConstants.MOD_NAME; + meta.description = "Updates Forge's mod list with a new and improved design"; + meta.version = "1.0.0"; + meta.authorList = Arrays.asList("MrCrayfish", "RuiXuqi"); + meta.credits = "Hatsondogs for creating icons"; + meta.license = "MIT"; + meta.logoFile = "/assets/catalogue/icon.png"; + meta.iconFile = "/assets/catalogue/icon.png"; + meta.backgroundFile = "/assets/catalogue/background.png"; + ConfigManager.register(CatalogueConfig.class); + MinecraftForge.EVENT_BUS.register(CatalogueConfig.class); + MinecraftForge.EVENT_BUS.register(ClientHandler.class); + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + return true; + } +} diff --git a/src/main/java/com/cleanroommc/common/CleanroomContainer.java b/src/main/java/com/cleanroommc/common/CleanroomContainer.java index 94ba529b7..ba41180d6 100644 --- a/src/main/java/com/cleanroommc/common/CleanroomContainer.java +++ b/src/main/java/com/cleanroommc/common/CleanroomContainer.java @@ -15,11 +15,13 @@ public CleanroomContainer() { meta.name = "Cleanroom"; meta.description = """ Cleanroom is a 1.12.2 Forge fork, providing newer toolchain, new APIs and 99% compatibility. - Our plan is to make 1.12.2 up-to-date with latest toolchain while adding more feature to - Forge & vanilla, so we don't need to care about higher versions' X point release * Y Mod API headache. + Our plan is to make 1.12.2 up-to-date with latest toolchain while adding more feature to Forge & vanilla, so we don't need to care about higher versions' X point release * Y Mod API headache. """; meta.version = CleanroomVersion.VERSION; meta.authorList = Arrays.asList("LexManos", "cpw", "fry", "Rongmario", "kappa_maintainer", "Li"); + meta.logoFile = "/cleanroom_banner.png"; + meta.iconFile = "/cleanroom_icon.png"; + meta.backgroundFile = "/cleanroom_background.png"; } @Override diff --git a/src/main/java/com/cleanroommc/common/ConfigAnytimeContainer.java b/src/main/java/com/cleanroommc/common/ConfigAnytimeContainer.java index eda4f2bdc..60cf6952a 100644 --- a/src/main/java/com/cleanroommc/common/ConfigAnytimeContainer.java +++ b/src/main/java/com/cleanroommc/common/ConfigAnytimeContainer.java @@ -6,8 +6,6 @@ import net.minecraftforge.fml.common.LoadController; import net.minecraftforge.fml.common.ModMetadata; -import java.util.Arrays; - public class ConfigAnytimeContainer extends DummyModContainer { public ConfigAnytimeContainer() { super(new ModMetadata()); @@ -17,6 +15,7 @@ public ConfigAnytimeContainer() { meta.description = "Allows Forge configurations to be setup at any point in time."; meta.version = ForgeEarlyConfig.CUSTOM_BUILT_IN_MOD_VERSION ? ForgeEarlyConfig.CONFIG_ANY_TIME_VERSION : "3.0"; meta.authorList.add("Rongmario"); + meta.logoFile = "/configanytime_icon.png"; } @Override diff --git a/src/main/java/com/cleanroommc/common/MixinContainer.java b/src/main/java/com/cleanroommc/common/MixinContainer.java index 870d2cb8f..293cf2695 100644 --- a/src/main/java/com/cleanroommc/common/MixinContainer.java +++ b/src/main/java/com/cleanroommc/common/MixinContainer.java @@ -15,6 +15,7 @@ public MixinContainer() { meta.description = "A Mixin library and loader."; meta.version = ForgeEarlyConfig.CUSTOM_BUILT_IN_MOD_VERSION ? ForgeEarlyConfig.MIXIN_BOOTER_VERSION : "10.7"; meta.authorList.add("Rongmario"); + meta.logoFile = "/mixinbooter_icon.png"; } @Override diff --git a/src/main/java/net/minecraftforge/client/gui/NotificationModUpdateScreen.java b/src/main/java/net/minecraftforge/client/gui/NotificationModUpdateScreen.java index 7f3cba538..ffbee19ee 100644 --- a/src/main/java/net/minecraftforge/client/gui/NotificationModUpdateScreen.java +++ b/src/main/java/net/minecraftforge/client/gui/NotificationModUpdateScreen.java @@ -91,10 +91,10 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) GlStateManager.popMatrix(); } - public static NotificationModUpdateScreen init(GuiMainMenu guiMainMenu, GuiButton modButton) + public static NotificationModUpdateScreen init(GuiScreen guiScreen, GuiButton modButton) { NotificationModUpdateScreen notificationModUpdateScreen = new NotificationModUpdateScreen(modButton); - notificationModUpdateScreen.setGuiSize(guiMainMenu.width, guiMainMenu.height); + notificationModUpdateScreen.setGuiSize(guiScreen.width, guiScreen.height); notificationModUpdateScreen.initGui(); return notificationModUpdateScreen; } diff --git a/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java b/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java index b59f675ae..6a7a81218 100644 --- a/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java +++ b/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java @@ -91,6 +91,7 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.ConfigManager; import net.minecraftforge.common.util.CompoundDataFixer; +import com.cleanroommc.catalogue.client.screen.CatalogueModListScreen; import net.minecraftforge.fml.client.registry.RenderingRegistry; import net.minecraftforge.fml.common.DummyModContainer; import net.minecraftforge.fml.common.DuplicateModsFoundException; @@ -760,7 +761,7 @@ public void tryLoadExistingWorld(GuiWorldSelection selectWorldGUI, WorldSummary public void showInGameModOptions(GuiIngameMenu guiIngameMenu) { - showGuiScreen(new GuiModList(guiIngameMenu)); + showGuiScreen(new CatalogueModListScreen(guiIngameMenu)); } public IModGuiFactory getGuiFactoryFor(ModContainer selectedMod) diff --git a/src/main/java/net/minecraftforge/fml/client/GuiErrorBase.java b/src/main/java/net/minecraftforge/fml/client/GuiErrorBase.java index 038749580..8bc6f944d 100644 --- a/src/main/java/net/minecraftforge/fml/client/GuiErrorBase.java +++ b/src/main/java/net/minecraftforge/fml/client/GuiErrorBase.java @@ -59,8 +59,7 @@ protected void actionPerformed(GuiButton button) { try { - File modsDir = new File(minecraftDir, "mods"); - Desktop.getDesktop().open(modsDir); + Desktop.getDesktop().open(Loader.instance().getModsDir()); } catch (Exception e) { diff --git a/src/main/java/net/minecraftforge/fml/client/GuiModList.java b/src/main/java/net/minecraftforge/fml/client/GuiModList.java index 0b925267e..181c0b806 100644 --- a/src/main/java/net/minecraftforge/fml/client/GuiModList.java +++ b/src/main/java/net/minecraftforge/fml/client/GuiModList.java @@ -69,8 +69,10 @@ /** * @author cpw - * + * @deprecated Use {@link com.cleanroommc.catalogue.client.screen.CatalogueModListScreen} */ +@Deprecated +@SuppressWarnings("DeprecatedIsStillUsed") public class GuiModList extends GuiScreen { private enum SortType implements Comparator @@ -625,4 +627,8 @@ protected void clickHeader(int x, int y) } } } + + public GuiScreen getParentScreen() { + return this.mainMenu; + } } diff --git a/src/main/java/net/minecraftforge/fml/common/Loader.java b/src/main/java/net/minecraftforge/fml/common/Loader.java index e45ec0cf5..4308b4881 100644 --- a/src/main/java/net/minecraftforge/fml/common/Loader.java +++ b/src/main/java/net/minecraftforge/fml/common/Loader.java @@ -40,6 +40,7 @@ import com.cleanroommc.common.CleanroomContainer; import com.cleanroommc.common.MixinContainer; import com.cleanroommc.common.ConfigAnytimeContainer; +import com.cleanroommc.common.CatalogueContainer; import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.ForgeVersion; import net.minecraftforge.common.capabilities.CapabilityManager; @@ -170,6 +171,9 @@ public class Loader * The canonical configuration directory */ private File canonicalConfigDir; + /** + * The canonical mods directory + */ private File canonicalModsDir; private LoadController modController; private MinecraftDummyContainer minecraft; @@ -375,6 +379,7 @@ private ModDiscoverer identifyMods(List additionalContainers) mods.add(new InjectedModContainer(new CleanroomContainer(), FMLSanityChecker.fmlLocation)); mods.add(new InjectedModContainer(new MixinContainer(), FMLSanityChecker.fmlLocation)); mods.add(new InjectedModContainer(new ConfigAnytimeContainer(), FMLSanityChecker.fmlLocation)); + mods.add(new InjectedModContainer(new CatalogueContainer(), FMLSanityChecker.fmlLocation)); for (String cont : injectedContainers) { @@ -693,6 +698,11 @@ public File getConfigDir() return canonicalConfigDir; } + public File getModsDir() + { + return canonicalModsDir; + } + public String getCrashInformation() { // Handle being called before we've begun setup diff --git a/src/main/java/net/minecraftforge/fml/common/MinecraftDummyContainer.java b/src/main/java/net/minecraftforge/fml/common/MinecraftDummyContainer.java index 78d287bf6..1318d3a1f 100644 --- a/src/main/java/net/minecraftforge/fml/common/MinecraftDummyContainer.java +++ b/src/main/java/net/minecraftforge/fml/common/MinecraftDummyContainer.java @@ -21,6 +21,7 @@ import java.io.File; import java.security.cert.Certificate; +import java.util.List; import com.google.common.eventbus.EventBus; import net.minecraftforge.fml.common.versioning.VersionParser; @@ -37,9 +38,17 @@ public class MinecraftDummyContainer extends DummyModContainer public MinecraftDummyContainer(String actualMCVersion) { super(new ModMetadata()); - getMetadata().modId = "minecraft"; - getMetadata().name = "Minecraft"; - getMetadata().version = actualMCVersion; + ModMetadata meta = this.getMetadata(); + meta.modId = "minecraft"; + meta.name = "Minecraft"; + meta.version = actualMCVersion; + // Description provided by minecraft.wiki (CC BY-NC-SA 3.0) + meta.description = "Minecraft is a 3D sandbox adventure game developed by Mojang Studios where players can interact with a fully customizable three-dimensional world made of blocks and entities. Its diverse gameplay options allow players to choose the way they play, creating countless possibilities."; + meta.authorList = List.of("Mojang AB"); + meta.url = "https://www.minecraft.net"; + meta.issueTrackerUrl = "https://bugs.mojang.com/projects/MC/issues"; + meta.license = "All Rights Reserved"; + staticRange = VersionParser.parseRange("["+actualMCVersion+"]"); } diff --git a/src/main/java/net/minecraftforge/fml/common/ModMetadata.java b/src/main/java/net/minecraftforge/fml/common/ModMetadata.java index ceb56e4fe..41259ee8e 100644 --- a/src/main/java/net/minecraftforge/fml/common/ModMetadata.java +++ b/src/main/java/net/minecraftforge/fml/common/ModMetadata.java @@ -49,11 +49,16 @@ public class ModMetadata * URL to update json file. Format is defined here: https://gist.github.com/LexManos/7aacb9aa991330523884 */ public String updateJSON = ""; + public String issueTrackerUrl = ""; public String logoFile = ""; + public String iconFile = ""; + public String iconItem = ""; // Customized item or block as icon. Will not applied when iconFile is valid. + public String backgroundFile = ""; public String version = ""; public List authorList = Lists.newArrayList(); public String credits = ""; + public String license = ""; public String parent = ""; public String[] screenshots; diff --git a/src/main/resources/META-INF/services/com.cleanroommc.catalogue.platform.services.IPlatformHelper b/src/main/resources/META-INF/services/com.cleanroommc.catalogue.platform.services.IPlatformHelper new file mode 100644 index 000000000..9d58866a9 --- /dev/null +++ b/src/main/resources/META-INF/services/com.cleanroommc.catalogue.platform.services.IPlatformHelper @@ -0,0 +1 @@ +com.cleanroommc.catalogue.platform.CleanroomPlatformHelper \ No newline at end of file diff --git a/src/main/resources/assets/catalogue/background.png b/src/main/resources/assets/catalogue/background.png new file mode 100644 index 000000000..a14604c70 Binary files /dev/null and b/src/main/resources/assets/catalogue/background.png differ diff --git a/src/main/resources/assets/catalogue/icon.png b/src/main/resources/assets/catalogue/icon.png new file mode 100644 index 000000000..ab238d3ac Binary files /dev/null and b/src/main/resources/assets/catalogue/icon.png differ diff --git a/src/main/resources/assets/catalogue/lang/de_de.lang b/src/main/resources/assets/catalogue/lang/de_de.lang new file mode 100644 index 000000000..a3b9db1d0 --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/de_de.lang @@ -0,0 +1,2 @@ +catalogue.gui.no_selection=No mod selected... +catalogue.gui.info=This menu was redesigned by Catalogue. Click here to open the CurseForge page for this mod! diff --git a/src/main/resources/assets/catalogue/lang/en_au.lang b/src/main/resources/assets/catalogue/lang/en_au.lang new file mode 100644 index 000000000..5f9fb879d --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/en_au.lang @@ -0,0 +1,62 @@ +catalogue.gui.no_selection=No mod selected... +catalogue.gui.mod_list=Mods +catalogue.gui.search=Search +catalogue.gui.config=Config +catalogue.gui.open_mods_folder=Open Mods Folder +catalogue.gui.child_mods=Child Mod(s): %s +catalogue.gui.parent_mod=Parent Mod: %s +catalogue.gui.licenses=License(s): %s +catalogue.gui.authors=Author(s): %s +catalogue.gui.contributors=Contributor(s): %s +catalogue.gui.credits=Credits: %s +catalogue.gui.version=Version: %s +catalogue.gui.inner_version=Inner Version: %s +catalogue.gui.beta=This version is a beta version +catalogue.gui.ahead=This version is ahead of the latest version found (%s) +catalogue.gui.update_available=Update (%s): %s +catalogue.gui.update_available_no_page=Update (%s) +catalogue.gui.beta_update_available=Beta Update (%s): %s +catalogue.gui.beta_update_available_no_page=Beta Update (%s) +catalogue.gui.info=This menu is provided by Catalogue. Click here to open the CurseForge page for this mod! +catalogue.gui.favourite=Mark as Favourite +catalogue.gui.remove_favourite=Remove as Favourite +catalogue.gui.options=Options +catalogue.gui.filters=Filters +catalogue.gui.filters.configs_only=Mods with Configs +catalogue.gui.filters.updates_only=Mods with Updates +catalogue.gui.filters.favourites=Favourites Only +catalogue.gui.sort=Sort +catalogue.gui.sort.alphabetically=A to Z +catalogue.gui.sort.alphabetically_reverse=Z to A +catalogue.gui.sort.favourites_first=Favourites First +catalogue.gui.hide_libraries=Hide Libraries +catalogue.gui.hide_child_mods=Hide Child Mods +catalogue.gui.no_mods=No mods... +catalogue.gui.mod_count=%s Mods +catalogue.gui.library_count=%s Libraries +catalogue.gui.child_mod_count=%s Child Mods +catalogue.gui.advanced_search.info=Advanced search queries will ignore any active filters +catalogue.gui.show_dependencies=Show Dependencies +catalogue.gui.show_dependents=Show Dependents +catalogue.gui.show_child_mods=Show Child Mods +catalogue.gui.show_parent_mod=Show Parent Mod +catalogue.gui.website=Website +catalogue.gui.submit_bug=Submit Bug +catalogue.gui.mod_id=Mod ID: %s + +catalogue.config.enable_mod=Enable Mod +catalogue.config.enable_mod.tooltip=Whether enable Catalogue mod. \nSetting it false will stop Catalogue redirecting Forge's mod list calls. +catalogue.config.library_list=Library List +catalogue.config.library_list.tooltip=The list of library mods' mod ids.\nThey will have grey names in the mod list. +catalogue.config.ignored_dependencies_list=Ignored Dependencies List +catalogue.config.ignored_dependencies_list.tooltip=The list of ignored dependencies' mod ids.\nThey will not be displayed when searching for dependencies/dependants. +catalogue.config.enable_banner_limit=Enable Banner Limit +catalogue.config.enable_banner_limit.tooltip=Whether limit the size of mods' banners. +catalogue.config.banner_max_width=Banner Max Width +catalogue.config.banner_max_width.tooltip=The maximum of banner's width. Will not work if Enable Banner Limit is set false. +catalogue.config.banner_max_height=Banner Max Height +catalogue.config.banner_max_height.tooltip=The maximum of banner's height. Will not work if Enable Banner Limit is set false. +catalogue.config.enable_icon_limit=Enable Icon Limit +catalogue.config.enable_icon_limit.tooltip=Whether limit the size of mods' icons. +catalogue.config.icon_max_width_height=Icon Max Width/Height +catalogue.config.icon_max_width_height.tooltip=The maximum of icon's width and height. Will not work if Enable Icon Limit is set false. diff --git a/src/main/resources/assets/catalogue/lang/en_us.lang b/src/main/resources/assets/catalogue/lang/en_us.lang new file mode 100644 index 000000000..b0d0579bc --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/en_us.lang @@ -0,0 +1,62 @@ +catalogue.gui.no_selection=No mod selected... +catalogue.gui.mod_list=Mods +catalogue.gui.search=Search +catalogue.gui.config=Config +catalogue.gui.open_mods_folder=Open Mods Folder +catalogue.gui.child_mods=Child Mod(s): %s +catalogue.gui.parent_mod=Parent Mod: %s +catalogue.gui.licenses=License(s): %s +catalogue.gui.authors=Author(s): %s +catalogue.gui.contributors=Contributor(s): %s +catalogue.gui.credits=Credits: %s +catalogue.gui.version=Version: %s +catalogue.gui.inner_version=Inner Version: %s +catalogue.gui.beta=This version is a beta version +catalogue.gui.ahead=This version is ahead of the latest version found (%s) +catalogue.gui.update_available=Update (%s): %s +catalogue.gui.update_available_no_page=Update (%s) +catalogue.gui.beta_update_available=Beta Update (%s): %s +catalogue.gui.beta_update_available_no_page=Beta Update (%s) +catalogue.gui.info=This menu is provided by Catalogue. Click here to open the CurseForge page for this mod! +catalogue.gui.favourite=Mark as Favorite +catalogue.gui.remove_favourite=Remove as Favorite +catalogue.gui.options=Options +catalogue.gui.filters=Filters +catalogue.gui.filters.configs_only=Mods with Configs +catalogue.gui.filters.updates_only=Mods with Updates +catalogue.gui.filters.favourites=Favorites Only +catalogue.gui.sort=Sort +catalogue.gui.sort.alphabetically=A to Z +catalogue.gui.sort.alphabetically_reverse=Z to A +catalogue.gui.sort.favourites_first=Favorites First +catalogue.gui.hide_libraries=Hide Libraries +catalogue.gui.hide_child_mods=Hide Child Mods +catalogue.gui.no_mods=No mods... +catalogue.gui.mod_count=%s Mods +catalogue.gui.library_count=%s Libraries +catalogue.gui.child_mod_count=%s Child Mods +catalogue.gui.advanced_search.info=Advanced search queries will ignore any active filters +catalogue.gui.show_dependencies=Show Dependencies +catalogue.gui.show_dependents=Show Dependents +catalogue.gui.show_child_mods=Show Child Mods +catalogue.gui.show_parent_mod=Show Parent Mod +catalogue.gui.website=Website +catalogue.gui.submit_bug=Submit Bug +catalogue.gui.mod_id=Mod ID: %s + +catalogue.config.enable_mod=Enable Mod +catalogue.config.enable_mod.tooltip=Whether enable Catalogue mod. \nSetting it false will stop Catalogue redirecting Forge's mod list calls. +catalogue.config.library_list=Library List +catalogue.config.library_list.tooltip=The list of library mods' mod ids.\nThey will have grey names in the mod list. +catalogue.config.ignored_dependencies_list=Ignored Dependencies List +catalogue.config.ignored_dependencies_list.tooltip=The list of ignored dependencies' mod ids.\nThey will not be displayed when searching for dependencies/dependants. +catalogue.config.enable_banner_limit=Enable Banner Limit +catalogue.config.enable_banner_limit.tooltip=Whether limit the size of mods' banners. +catalogue.config.banner_max_width=Banner Max Width +catalogue.config.banner_max_width.tooltip=The maximum of banner's width. Will not work if Enable Banner Limit is false. +catalogue.config.banner_max_height=Banner Max Height +catalogue.config.banner_max_height.tooltip=The maximum of banner's height. Will not work if Enable Banner Limit is false. +catalogue.config.enable_icon_limit=Enable Icon Limit +catalogue.config.enable_icon_limit.tooltip=Whether limit the size of mods' icons. +catalogue.config.icon_max_width_height=Icon Max Width/Height +catalogue.config.icon_max_width_height.tooltip=The maximum of icon's width and height. Will not work if Enable Icon Limit is set false. diff --git a/src/main/resources/assets/catalogue/lang/pl_pl.lang b/src/main/resources/assets/catalogue/lang/pl_pl.lang new file mode 100644 index 000000000..683ffbe39 --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/pl_pl.lang @@ -0,0 +1,2 @@ +catalogue.gui.no_selection=Brak wybranego moda... +catalogue.gui.info=To menu zostało przeprojektowane przez moda Catalogue. Kliknij tutaj, aby otworzyć stronę tego moda w serwisie CurseForge! diff --git a/src/main/resources/assets/catalogue/lang/ru_ru.lang b/src/main/resources/assets/catalogue/lang/ru_ru.lang new file mode 100644 index 000000000..43a759b8b --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/ru_ru.lang @@ -0,0 +1,32 @@ +catalogue.gui.no_selection=Нет выбранного мода… +catalogue.gui.mod_list=Моды +catalogue.gui.search=Поиск +catalogue.gui.config=Настройки +catalogue.gui.website=Веб-сайт +catalogue.gui.submit_bug=Сообщить баг +catalogue.gui.open_mods_folder=Открыть папку модов +catalogue.gui.licenses=Лицензия(и): %s +catalogue.gui.authors=Автор(ы): %s +catalogue.gui.contributors=Участник(ы): %s +catalogue.gui.credits=Приписание: %s +catalogue.gui.version=Версия: %s +catalogue.gui.update_available=Обновление: %s +catalogue.gui.info=Это меню предоставлено Catalogue. Нажмите, чтобы открыть страницу CurseForge для этого мода! +catalogue.gui.favourite=Отметить как избранное +catalogue.gui.remove_favourite=Удалить из избранного +catalogue.gui.options=Настройки +catalogue.gui.filters=Фильтры +catalogue.gui.filters.configs_only=Моды с конфигами +catalogue.gui.filters.updates_only=Моды с обновлениями +catalogue.gui.filters.favourites=Только избранное +catalogue.gui.sort=Сортировка +catalogue.gui.sort.alphabetically_reverse=Z до A +catalogue.gui.sort.alphabetically=A до Z +catalogue.gui.sort.favourites_first=Сначала избранное +catalogue.gui.hide_libraries=Скрыть библиотеки +catalogue.gui.no_mods=Нет модов... +catalogue.gui.mod_count=%s модов +catalogue.gui.library_count=%s библиотек +catalogue.gui.advanced_search.info=Расширенные поисковые запросы будут игнорировать все активные фильтры +catalogue.gui.show_dependencies=Показывать зависимости +catalogue.gui.show_dependents=Показывать зависимых diff --git a/src/main/resources/assets/catalogue/lang/zh_cn.lang b/src/main/resources/assets/catalogue/lang/zh_cn.lang new file mode 100644 index 000000000..d9609612d --- /dev/null +++ b/src/main/resources/assets/catalogue/lang/zh_cn.lang @@ -0,0 +1,62 @@ +catalogue.gui.no_selection=没有选择模组…… +catalogue.gui.mod_list=模组 +catalogue.gui.search=搜索 +catalogue.gui.config=配置 +catalogue.gui.open_mods_folder=打开模组文件夹 +catalogue.gui.child_mods=子模组:%s +catalogue.gui.parent_mod=父模组:%s +catalogue.gui.licenses=许可协议:%s +catalogue.gui.authors=作者:%s +catalogue.gui.contributors=贡献者:%s +catalogue.gui.credits=致谢:%s +catalogue.gui.version=版本:%s +catalogue.gui.inner_version=内部版本:%s +catalogue.gui.beta=此版本为Beta版 +catalogue.gui.ahead=此版本高于搜索到的最新版(%s) +catalogue.gui.update_available=更新(%s):%s +catalogue.gui.update_available_no_page=更新(%s) +catalogue.gui.beta_update_available=Beta更新(%s):%s +catalogue.gui.beta_update_available_no_page=Beta更新(%s) +catalogue.gui.info=此菜单由Catalogue提供。单击此处打开此模组的CurseForge页面! +catalogue.gui.favourite=标记为收藏 +catalogue.gui.remove_favourite=取消收藏 +catalogue.gui.options=选项 +catalogue.gui.filters=筛选 +catalogue.gui.filters.configs_only=仅显示有配置的模组 +catalogue.gui.filters.updates_only=仅显示有更新的模组 +catalogue.gui.filters.favourites=仅显示收藏 +catalogue.gui.sort=排序 +catalogue.gui.sort.alphabetically=A到Z +catalogue.gui.sort.alphabetically_reverse=Z到A +catalogue.gui.sort.favourites_first=收藏优先 +catalogue.gui.hide_libraries=隐藏库模组 +catalogue.gui.hide_child_mods=隐藏子模组 +catalogue.gui.no_mods=没有模组…… +catalogue.gui.mod_count=%s个模组 +catalogue.gui.library_count=%s个库 +catalogue.gui.child_mod_count=%s个子模组 +catalogue.gui.advanced_search.info=高级搜索查询将忽略任何激活的筛选器 +catalogue.gui.show_dependencies=显示前置模组 +catalogue.gui.show_dependents=显示依赖模组 +catalogue.gui.show_child_mods=显示子模组 +catalogue.gui.show_parent_mod=显示父模组 +catalogue.gui.website=网站 +catalogue.gui.submit_bug=提交Bug +catalogue.gui.mod_id=Mod ID:%s + +catalogue.config.enable_mod=启用模组 +catalogue.config.enable_mod.tooltip=控制是否启用Catalogue模组。 \n设置为否会阻止Catalogue重定向Forge模组列表的调用。 +catalogue.config.library_list=库列表 +catalogue.config.library_list.tooltip=库模组的Mod ID列表。\n它们会在模组列表中显示灰色名字。 +catalogue.config.ignored_dependencies_list=忽略前置列表 +catalogue.config.ignored_dependencies_list.tooltip=忽略的前置模组的Mod ID列表。\n它们不会在搜索前置/依赖模组时显示。 +catalogue.config.enable_banner_limit=启用横幅限制 +catalogue.config.enable_banner_limit.tooltip=控制是否限制横幅尺寸。 +catalogue.config.banner_max_width=横幅最大宽度 +catalogue.config.banner_max_width.tooltip=横幅的最大宽度。当横幅限制被禁用时不会生效。 +catalogue.config.banner_max_height=横幅最大长度 +catalogue.config.banner_max_height.tooltip=横幅的最大长度。当横幅限制被禁用时不会生效。 +catalogue.config.enable_icon_limit=启用图标限制 +catalogue.config.enable_icon_limit.tooltip=控制是否限制图标尺寸。 +catalogue.config.icon_max_width_height=图标最大长度/宽度 +catalogue.config.icon_max_width_height.tooltip=图标的最大长度/宽度。当图标限制被禁用时不会生效。 diff --git a/src/main/resources/assets/catalogue/textures/gui/checkbox.png b/src/main/resources/assets/catalogue/textures/gui/checkbox.png new file mode 100644 index 000000000..805910f45 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/checkbox.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/icons.png b/src/main/resources/assets/catalogue/textures/gui/icons.png new file mode 100644 index 000000000..784bb0d40 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/icons.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/minecraft.png b/src/main/resources/assets/catalogue/textures/gui/minecraft.png new file mode 100644 index 000000000..7fd8316e4 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/minecraft.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/missing_background.png b/src/main/resources/assets/catalogue/textures/gui/missing_background.png new file mode 100644 index 000000000..acf473c6b Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/missing_background.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/missing_banner.png b/src/main/resources/assets/catalogue/textures/gui/missing_banner.png new file mode 100644 index 000000000..173e377ad Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/missing_banner.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item.png b/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item.png new file mode 100644 index 000000000..02dd3b13f Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item_highlighted.png b/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item_highlighted.png new file mode 100644 index 000000000..3abe659b6 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/sprites/dropdown/item_highlighted.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button.png b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button.png new file mode 100644 index 000000000..96f6e3d00 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_disabled.png b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_disabled.png new file mode 100644 index 000000000..053025371 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_disabled.png differ diff --git a/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_highlighted.png b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_highlighted.png new file mode 100644 index 000000000..ba1a7fe61 Binary files /dev/null and b/src/main/resources/assets/catalogue/textures/gui/sprites/widget/button_highlighted.png differ diff --git a/src/main/resources/assets/forge/lang/en_us.lang b/src/main/resources/assets/forge/lang/en_us.lang index af2b057c6..aeba234e4 100644 --- a/src/main/resources/assets/forge/lang/en_us.lang +++ b/src/main/resources/assets/forge/lang/en_us.lang @@ -237,7 +237,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=Mods fml.menu.mods.normal=Normal fml.menu.mods.search=Search: -fml.menu.modoptions=Mod Options... item.forge.bucketFilled.name=%s Bucket diff --git a/src/main/resources/assets/forge/lang/es_es.lang b/src/main/resources/assets/forge/lang/es_es.lang index 06682c277..2e8260661 100644 --- a/src/main/resources/assets/forge/lang/es_es.lang +++ b/src/main/resources/assets/forge/lang/es_es.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=Mods fml.menu.mods.normal=Normal fml.menu.mods.search=Buscar\: -fml.menu.modoptions=Opciones de mods... item.forge.bucketFilled.name=Cubo de %s diff --git a/src/main/resources/assets/forge/lang/fr_fr.lang b/src/main/resources/assets/forge/lang/fr_fr.lang index a2391ea17..4fe5f0b43 100644 --- a/src/main/resources/assets/forge/lang/fr_fr.lang +++ b/src/main/resources/assets/forge/lang/fr_fr.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=Mods fml.menu.mods.normal=Normal fml.menu.mods.search=Rechercher \: -fml.menu.modoptions=Options de mods... item.forge.bucketFilled.name=Seau de %s diff --git a/src/main/resources/assets/forge/lang/ja_jp.lang b/src/main/resources/assets/forge/lang/ja_jp.lang index 93acd988f..33acf3941 100644 --- a/src/main/resources/assets/forge/lang/ja_jp.lang +++ b/src/main/resources/assets/forge/lang/ja_jp.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=Alt + %s fml.menu.mods=Mod fml.menu.mods.normal=通常 fml.menu.mods.search=検索: -fml.menu.modoptions=Mod 設定… item.forge.bucketFilled.name=%s入りバケツ diff --git a/src/main/resources/assets/forge/lang/ko_kr.lang b/src/main/resources/assets/forge/lang/ko_kr.lang index 5ab39cc2f..6b6cb8308 100644 --- a/src/main/resources/assets/forge/lang/ko_kr.lang +++ b/src/main/resources/assets/forge/lang/ko_kr.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=알트 + %s fml.menu.mods=모드 fml.menu.mods.normal=일반 fml.menu.mods.search=검색: -fml.menu.modoptions=모드 설정... item.forge.bucketFilled.name=%s 양동이 diff --git a/src/main/resources/assets/forge/lang/ru_ru.lang b/src/main/resources/assets/forge/lang/ru_ru.lang index d4c3c7556..581e4534d 100644 --- a/src/main/resources/assets/forge/lang/ru_ru.lang +++ b/src/main/resources/assets/forge/lang/ru_ru.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=Моды fml.menu.mods.normal=Обычная fml.menu.mods.search=Поиск\: -fml.menu.modoptions=Настройки модов... item.forge.bucketFilled.name=Ведро %s diff --git a/src/main/resources/assets/forge/lang/zh_cn.lang b/src/main/resources/assets/forge/lang/zh_cn.lang index 55d639586..cd8fa7a65 100644 --- a/src/main/resources/assets/forge/lang/zh_cn.lang +++ b/src/main/resources/assets/forge/lang/zh_cn.lang @@ -185,7 +185,7 @@ fml.config.sample.title=这只是配置界面的示例,不会保存修改结 fml.configgui.applyGlobally=应用到全局 fml.configgui.confirmRestartMessage=我知道了 fml.configgui.gameRestartRequired=有一个或多个设置需要重新启动 Minecraft 才能生效。 -fml.configgui.gameRestartTitle=需要重新启动Minecraft +fml.configgui.gameRestartTitle=需要重新启动 Minecraft fml.configgui.sampletext=示例文本 fml.configgui.tooltip.addNewEntryAbove=在上面添加新条目 fml.configgui.tooltip.applyGlobally=在全局应用撤消更改或重置为默认值。 @@ -205,7 +205,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=模组 fml.menu.mods.normal=普通 fml.menu.mods.search=搜索: -fml.menu.modoptions=模组设置 item.forge.bucketFilled.name=%s桶 diff --git a/src/main/resources/assets/forge/lang/zh_tw.lang b/src/main/resources/assets/forge/lang/zh_tw.lang index a7c0b41e6..34bd5403d 100644 --- a/src/main/resources/assets/forge/lang/zh_tw.lang +++ b/src/main/resources/assets/forge/lang/zh_tw.lang @@ -203,7 +203,6 @@ forge.controlsgui.alt=ALT + %s fml.menu.mods=模組 fml.menu.mods.normal=一般 fml.menu.mods.search=搜尋: -fml.menu.modoptions=模組選項... item.forge.bucketFilled.name=%s桶 diff --git a/src/main/resources/cleanroom_background.png b/src/main/resources/cleanroom_background.png new file mode 100644 index 000000000..48e12821e Binary files /dev/null and b/src/main/resources/cleanroom_background.png differ diff --git a/src/main/resources/cleanroom_banner.png b/src/main/resources/cleanroom_banner.png new file mode 100644 index 000000000..ddc6f701a Binary files /dev/null and b/src/main/resources/cleanroom_banner.png differ diff --git a/src/main/resources/cleanroom_icon.png b/src/main/resources/cleanroom_icon.png new file mode 100644 index 000000000..39754085d Binary files /dev/null and b/src/main/resources/cleanroom_icon.png differ diff --git a/src/main/resources/configanytime_icon.png b/src/main/resources/configanytime_icon.png new file mode 100644 index 000000000..4509a0471 Binary files /dev/null and b/src/main/resources/configanytime_icon.png differ diff --git a/src/main/resources/forge_at.cfg b/src/main/resources/forge_at.cfg index 0c22ae72f..aaa51ab58 100644 --- a/src/main/resources/forge_at.cfg +++ b/src/main/resources/forge_at.cfg @@ -217,6 +217,15 @@ public net.minecraft.client.gui.GuiButton field_146121_g # height - needed for c # GuiTextField public-f net.minecraft.client.gui.GuiTextField field_146218_h # width - needed for config GUI stuff public-f net.minecraft.client.gui.GuiTextField field_146219_i # height - needed for config GUI stuff + +protected net.minecraft.client.gui.GuiTextField field_146214_l # cursorCounter - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146221_u # disabledColor - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146222_t # enabledColor - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146223_s # selectionEnd - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146224_r # cursorPosition - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146225_q # lineScrollOffset - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField field_146226_p # isEnabled - needed for mod list GUI stuff +protected net.minecraft.client.gui.GuiTextField func_146188_c(IIII)V # drawSelectionBox - needed for mod list GUI stuff # GuiSlot public net.minecraft.client.gui.GuiSlot field_148149_f # slotHeight - needed for config GUI stuff public net.minecraft.client.gui.GuiSlot field_148151_d # right - needed for config GUI stuff diff --git a/src/main/resources/forge_logo.png b/src/main/resources/forge_logo.png index 7129008d1..49db5c8a7 100644 Binary files a/src/main/resources/forge_logo.png and b/src/main/resources/forge_logo.png differ diff --git a/src/main/resources/mcplogo.png b/src/main/resources/mcplogo.png index fd5eba5dc..aa76f2152 100644 Binary files a/src/main/resources/mcplogo.png and b/src/main/resources/mcplogo.png differ diff --git a/src/main/resources/mixinbooter_icon.png b/src/main/resources/mixinbooter_icon.png new file mode 100644 index 000000000..014608ef4 Binary files /dev/null and b/src/main/resources/mixinbooter_icon.png differ