diff --git a/src/main/java/io/github/prospector/modmenu/ModMenu.java b/src/main/java/io/github/prospector/modmenu/ModMenu.java index 69c195d7d..ccff00d87 100644 --- a/src/main/java/io/github/prospector/modmenu/ModMenu.java +++ b/src/main/java/io/github/prospector/modmenu/ModMenu.java @@ -1,13 +1,16 @@ package io.github.prospector.modmenu; - import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.github.prospector.modmenu.api.ModMenuApi; +import io.github.prospector.modmenu.config.ModMenuConfig; import io.github.prospector.modmenu.config.ModMenuConfigManager; -import io.github.prospector.modmenu.util.HardcodedUtil; -import net.fabricmc.api.ModInitializer; +import io.github.prospector.modmenu.util.TestModContainer; +import io.github.prospector.modmenu.util.UpdateCheckerUtil; +import io.github.prospector.modmenu.util.mod.FabricMod; +import io.github.prospector.modmenu.util.mod.Mod; +import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.CustomValue; @@ -17,15 +20,18 @@ import net.minecraft.client.gui.options.ScreenOptions; import net.minecraft.client.gui.options.data.OptionsPages; import net.minecraft.core.data.registry.Registries; +import net.minecraft.core.lang.I18n; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.NumberFormat; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -public class ModMenu implements ModInitializer { +public class ModMenu implements ClientModInitializer { public static final String MOD_ID = "modmenu"; + public static final String GITHUB_REF = "Turnip-Labs/ModMenu"; static { Registries.NAMESPACES.register(MOD_ID, MOD_ID); } @@ -35,11 +41,19 @@ public class ModMenu implements ModInitializer { private static final Map LEGACY_CONFIG_SCREEN_TASKS = new HashMap<>(); public static final List LIBRARY_MODS = new ArrayList<>(); public static final Set CLIENTSIDE_MODS = new HashSet<>(); - public static final Set DEPRECATED_MODS = new HashSet<>(); + public static final Set DEPRECATED_MODS = new HashSet<>(); public static final Set PATCHWORK_FORGE_MODS = new HashSet<>(); - public static final Map>> CUSTOM_BADGE_MODS = new HashMap<>(); + public static final Map>> CUSTOM_BADGE_MODS = new HashMap<>(); public static final Map> PARENT_MAP = new HashMap<>(); - private static Map> configScreenFactories = new HashMap<>(); + private static final Map> configScreenFactories = new HashMap<>(); + public static final Map MODS = new ConcurrentHashMap<>(); + public static final boolean DEV_ENVIRONMENT = FabricLoader.getInstance().isDevelopmentEnvironment(); + + private static int cachedDisplayedModCount = -1; + + public static void clearModCountCache() { + cachedDisplayedModCount = -1; + } public static boolean hasConfigScreenFactory(String modid) { return configScreenFactories.containsKey(modid); @@ -55,6 +69,72 @@ public static void openConfigScreen(String modid) { if (opener != null) opener.run(); } + public static String getDisplayedModCount() { + if (cachedDisplayedModCount == -1) { + boolean includeChildren = ModMenuConfig.COUNT_CHILDREN.getValue(); + boolean includeLibraries = ModMenuConfig.COUNT_LIBRARIES.getValue(); + boolean includeHidden = ModMenuConfig.COUNT_HIDDEN_MODS.getValue(); + + long count = MODS.values().stream().filter(mod -> { + boolean isChild = mod.getParent() != null; + if (!includeChildren && isChild) { + return false; + } + + boolean isLibrary = ModMenu.LIBRARY_MODS.contains(mod.getId()); + if (!includeLibraries && isLibrary) { + return false; + } + + return includeHidden || !mod.isHidden(); + }).count(); + + if (Boolean.getBoolean("modmenu.debug")) { + count += TestModContainer.getTestModContainers().size(); + } + + cachedDisplayedModCount = Math.toIntExact(count); + } + return NumberFormat.getInstance().format(cachedDisplayedModCount); + } + + public static String createModsButtonText(boolean title) { + var titleStyle = ModMenuConfig.MODS_BUTTON_STYLE.getValue(); + var gameMenuStyle = ModMenuConfig.GAME_MENU_BUTTON_STYLE.getValue(); + + boolean isIcon = title ? + titleStyle == ModMenuConfig.TitleMenuButtonStyle.ICON : + gameMenuStyle == ModMenuConfig.GameMenuButtonStyle.ICON; + + boolean isShort = title ? + titleStyle == ModMenuConfig.TitleMenuButtonStyle.INSERT : + gameMenuStyle == ModMenuConfig.GameMenuButtonStyle.INSERT; + + I18n i18n = I18n.getInstance(); + String modsText = i18n.translateKey("modmenu.title"); + + if (ModMenuConfig.MOD_COUNT_LOCATION.getValue().isOnModsButton() && !isIcon) { + String count = getDisplayedModCount(); + if (!isShort) { + modsText += " " + i18n.translateKeyAndFormat("modmenu.loaded", count); + } else { + String specificKey = "modmenu.loaded." + count; + String key; + + String nKey = i18n.translateKey(specificKey); + boolean hasSpecific = !nKey.equals(specificKey); + key = hasSpecific ? specificKey : "modmenu.loaded"; + + String secretKey = specificKey + ".secret"; + if (ModMenuConfig.EASTER_EGGS.getValue() && !i18n.translateKey(secretKey).equals(secretKey)) { + key = secretKey; + } + modsText += " " + i18n.translateKeyAndFormat(key, count); + } + } + return modsText; + } + public static void addLegacyConfigScreenTask(String modid, Runnable task) { LEGACY_CONFIG_SCREEN_TASKS.putIfAbsent(modid, task); } @@ -69,29 +149,35 @@ public static void addLibraryMod(String modid) { LIBRARY_MODS.add(modid); } - @SuppressWarnings("RedundantCollectionOperation") @Override - public void onInitialize() { + public void onInitializeClient() { ModMenuConfigManager.initializeConfig(); - FabricLoader.getInstance().getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> { + FabricLoader loader = FabricLoader.getInstance(); + loader.getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> { ModMenuApi api = entrypoint.getEntrypoint(); ModContainer mod = entrypoint.getProvider(); - try { - api.getClass().getDeclaredMethod("getConfigScreenFactory"); // Make sure the method is implemented - configScreenFactories.put(mod.getMetadata().getId(), api.getConfigScreenFactory()); - } catch (NoSuchMethodException ignored) {} - api.attachCustomBadges((name, outlineColor, fillColor) -> { - Map> map = new HashMap<>(); - map.put(name, new AbstractMap.SimpleEntry<>(outlineColor, fillColor)); - CUSTOM_BADGE_MODS.put(mod.getMetadata().getId(), map); - }); - }); + try { + api.getClass().getDeclaredMethod("getModConfigScreenFactory"); + var factory = api.getModConfigScreenFactory(); + if (factory != null) { + configScreenFactories.put(mod.getMetadata().getId(), factory::create); + } + } catch (NoSuchMethodException ignored) {} + api.attachCustomBadges((name, outlineColor, fillColor) -> { + Map> map = new HashMap<>(); + map.put(name, new AbstractMap.SimpleEntry<>(outlineColor, fillColor)); + CUSTOM_BADGE_MODS.put(mod.getMetadata().getId(), map); + }); + }); configScreenFactories.put("minecraft", (screenBase -> new ScreenOptions(screenBase, OptionsPages.GENERAL))); - Collection mods = FabricLoader.getInstance().getAllMods(); - HardcodedUtil.initializeHardcodings(); + Collection mods = loader.getAllMods(); for (ModContainer mod : mods) { ModMetadata metadata = mod.getMetadata(); String id = metadata.getId(); + + MODS.put(id, new FabricMod(mod)); + UpdateCheckerUtil.checkForUpdates(); + // API badges if (metadata.containsCustomValue("modmenu:api") && metadata.getCustomValue("modmenu:api").getAsBoolean()) { addLibraryMod(id); @@ -107,9 +193,9 @@ public void onInitialize() { } // Deprecated badges - if (metadata.containsCustomValue("modmenu:deprecated") && metadata.getCustomValue("modmenu:deprecated").getAsBoolean()) { - DEPRECATED_MODS.add(id); - } + if (metadata.containsCustomValue("modmenu:deprecated") && metadata.getCustomValue("modmenu:deprecated").getAsBoolean()) { + DEPRECATED_MODS.add(id); + } // Patchwork (unused) if (metadata.containsCustomValue("patchwork:source") && metadata.getCustomValue("patchwork:source").getAsObject() != null) { @@ -123,19 +209,29 @@ public void onInitialize() { if (metadata.containsCustomValue("modmenu:parent")) { String parentId = metadata.getCustomValue("modmenu:parent").getAsString(); if (parentId != null) { - Optional parent = FabricLoader.getInstance().getModContainer(parentId); + Optional parent = loader.getModContainer(parentId); parent.ifPresent(modContainer -> { final List list = PARENT_MAP.computeIfAbsent(modContainer, k -> new ArrayList<>()); list.add(mod); }); } - } else { - HardcodedUtil.hardcodeModuleMetadata(mod, metadata, id); + } + + // Hardcode marking as library + if (id.equals("fabricloader") + || id.equals("java") + || id.equals("mixinextras")) { + addLibraryMod(id); } } } - public static String getFormattedModCount() { - return NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()); + public static boolean areModUpdatesAvailable() { + for (Mod mod : MODS.values()) { + if (mod.getUpdateInfo() != null && mod.getUpdateInfo().isUpdateAvailable()) { + return true; + } + } + return false; } } diff --git a/src/main/java/io/github/prospector/modmenu/api/ConfigScreenFactory.java b/src/main/java/io/github/prospector/modmenu/api/ConfigScreenFactory.java new file mode 100644 index 000000000..e9e8cceaf --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/api/ConfigScreenFactory.java @@ -0,0 +1,8 @@ +package io.github.prospector.modmenu.api; + +import net.minecraft.client.gui.Screen; + +@FunctionalInterface +public interface ConfigScreenFactory { + S create(Screen parent); +} diff --git a/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java b/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java index c9cc10f99..39a1744da 100644 --- a/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java +++ b/src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java @@ -1,67 +1,88 @@ package io.github.prospector.modmenu.api; - import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.util.NullScreenFactory; import io.github.prospector.modmenu.util.TriConsumer; import net.minecraft.client.gui.Screen; -import org.jetbrains.annotations.ApiStatus; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.Map; +@SuppressWarnings("unused") public interface ModMenuApi { /** - * Replaced with {@link ModMenuApi#getConfigScreen(Screen)}, with - * the ModMenuApi implemented onto a class that is added as an - * entry point to your fabric mod metadata. + * Used for creating a {@link Screen} instance of the Mod Menu + * "Mods" screen * - * @deprecated Will be removed in 1.15 snapshots. + * @param previous The screen before opening + * @return A "Mods" Screen */ - @Deprecated - static void addConfigOverride(String modid, Runnable action) { - ModMenu.addLegacyConfigScreenTask(modid, action); + static Screen createModsScreen(Screen previous) { + return new ModsScreen(previous); } /** - * Used to determine the owner of this API implementation. + * Used for creating text just like what would appear + * on a Mod Menu Mods button * - * @deprecated This method is deprecated as Fabric Loader - * itself provides provider info for entrypoints. + * @return The text that would be displayed on a Mods button */ - @Deprecated - String getModId(); + static String createModsButtonText() { + return ModMenu.createModsButtonText(true); + } /** - * Replaced with {@link ModMenuApi#getConfigScreenFactory()}, which - * now allows ModMenu to open the screen for you, rather than depending - * on you to open it, and gets rid of the messy Optional->Supplier wrapping. + * Used to construct a new config screen instance when your mod's + * configuration button is selected on the mod menu screen. The + * screen instance parameter is the active mod menu screen. * - * @deprecated For internal use only. + * @return A factory for constructing config screen instances. */ - @Deprecated - @ApiStatus.Internal - default Optional> getConfigScreen(Screen screen) { - return Optional.empty(); + default ConfigScreenFactory getModConfigScreenFactory() { + return new NullScreenFactory<>(); } /** - * Used to construct a new config screen instance when your mod's - * configuration button is selected on the mod menu screen. The - * screen instance parameter is the active mod menu screen. + * Used for mods that have their own update checking logic. + * By returning your own {@link UpdateChecker} instance, you can override ModMenus built-in update checking logic. * - * @return A factory function for constructing config screen instances. + * @return An {@link UpdateChecker} or null if ModMenu should handle update checking. */ - default Function getConfigScreenFactory() { - return screen -> getConfigScreen(screen).map(Supplier::get).orElse(null); + default UpdateChecker getUpdateChecker() { + return null; } + /** + * Used to provide config screen factories for other mods. This takes second + * priority to a mod's own config screen factory provider. For example, if + * mod `xyz` supplies a config screen factory, mod `abc` providing a config + * screen to `xyz` will be pointless, as the one provided by `xyz` will be + * used. + *

+ * This method is NOT meant to be used to add a config screen factory to + * your own mod. + * + * @return a map of mod ids to screen factories. + */ + default Map> getProvidedConfigScreenFactories() { + return Map.of(); + } + + /** + * Used to provide update checkers for other mods. A mod registering its own + * update checker will take priority over any provided ones should both exist. + * + * @return a map of mod ids to update checkers. + */ + default Map getProvidedUpdateCheckers() { + return Map.of(); + } - /** - * Add custom badges for mods to use. - * - * @param consumer the consumer used to add new badges. - */ - default void attachCustomBadges(TriConsumer consumer) { - } + /** + * Add custom badges for mods to use. + * + * @param consumer the consumer used to add new badges. + */ + default void attachCustomBadges(TriConsumer consumer) { + } } diff --git a/src/main/java/io/github/prospector/modmenu/api/UpdateChannel.java b/src/main/java/io/github/prospector/modmenu/api/UpdateChannel.java new file mode 100644 index 000000000..2de2e7b8b --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/api/UpdateChannel.java @@ -0,0 +1,19 @@ +package io.github.prospector.modmenu.api; + +import io.github.prospector.modmenu.config.ModMenuConfig; + +/** + * Supported update channels, in ascending order by stability. + */ +public enum UpdateChannel { + ALPHA, + BETA, + RELEASE; + + /** + * @return the user's preferred update channel. + */ + public static UpdateChannel getUserPreference() { + return ModMenuConfig.UPDATE_CHANNEL.getValue(); + } +} diff --git a/src/main/java/io/github/prospector/modmenu/api/UpdateChecker.java b/src/main/java/io/github/prospector/modmenu/api/UpdateChecker.java new file mode 100644 index 000000000..9c03302f6 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/api/UpdateChecker.java @@ -0,0 +1,16 @@ +package io.github.prospector.modmenu.api; + +import org.jetbrains.annotations.Nullable; + +@SuppressWarnings("unused") +public interface UpdateChecker { + /** + * Gets called when ModMenu is checking for updates. + * This is done in a separate thread, so this call can/should be blocking. + * + *

Your update checker should aim to return an update on the same or a more stable channel than the user's preference which you can get via {@link UpdateChannel#getUserPreference()}.

+ * + * @return The update info + */ + @Nullable UpdateInfo checkForUpdates(); +} diff --git a/src/main/java/io/github/prospector/modmenu/api/UpdateInfo.java b/src/main/java/io/github/prospector/modmenu/api/UpdateInfo.java new file mode 100644 index 000000000..4390353ad --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/api/UpdateInfo.java @@ -0,0 +1,29 @@ +package io.github.prospector.modmenu.api; + +import org.jetbrains.annotations.Nullable; + +@SuppressWarnings("unused") +public interface UpdateInfo { + /** + * @return If an update for the mod is available. + */ + boolean isUpdateAvailable(); + + /** + * @return The message that is getting displayed when an update is available or null to let ModMenu handle displaying the message. + */ + @Nullable + default String getUpdateMessage() { + return null; + } + + /** + * @return The URL to the mod download. + */ + String getDownloadLink(); + + /** + * @return The update channel this update is available for. + */ + UpdateChannel getUpdateChannel(); +} diff --git a/src/main/java/io/github/prospector/modmenu/config/FileOnlyConfig.java b/src/main/java/io/github/prospector/modmenu/config/FileOnlyConfig.java new file mode 100644 index 000000000..a8566c9b5 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/FileOnlyConfig.java @@ -0,0 +1,11 @@ +package io.github.prospector.modmenu.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FileOnlyConfig { +} diff --git a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java index 128a3df5d..8d7c998b3 100644 --- a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java +++ b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfig.java @@ -1,52 +1,149 @@ -package io.github.prospector.modmenu.config; - - -import io.github.prospector.modmenu.util.HardcodedUtil; -import net.fabricmc.loader.api.ModContainer; -import net.minecraft.core.lang.I18n; - -import java.util.Comparator; - -public class ModMenuConfig { - private boolean showLibraries = false; - private Sorting sorting = Sorting.ASCENDING; - - public void toggleShowLibraries() { - this.showLibraries = !this.showLibraries; - ModMenuConfigManager.save(); - } - - public void toggleSortMode() { - this.sorting = Sorting.values()[(sorting.ordinal() + 1) % Sorting.values().length]; - ModMenuConfigManager.save(); - } - - public boolean showLibraries() { - return showLibraries; - } - - public Sorting getSorting() { - return sorting; - } - - public enum Sorting { - ASCENDING(Comparator.comparing(modContainer -> HardcodedUtil.formatFabricModuleName(modContainer.getMetadata().getName())), "modmenu.sorting.ascending"), - DECENDING(ASCENDING.getComparator().reversed(), "modmenu.sorting.decending"); - - final Comparator comparator; - final String key; - - Sorting(Comparator comparator, String key) { - this.comparator = comparator; - this.key = key; - } - - public Comparator getComparator() { - return comparator; - } - - public String getName() { - return I18n.getInstance().translateKey(key); - } - } -} +package io.github.prospector.modmenu.config; + +import com.google.gson.annotations.SerializedName; +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.api.UpdateChannel; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.config.option.BooleanConfigOption; +import io.github.prospector.modmenu.config.option.EnumConfigOption; +import io.github.prospector.modmenu.config.option.OptionConvertible; +import io.github.prospector.modmenu.config.option.StringSetConfigOption; +import io.github.prospector.modmenu.util.mod.Mod; +import net.fabricmc.loader.api.ModContainer; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +public class ModMenuConfig { + public static final EnumConfigOption SORTING = new EnumConfigOption<>("sorting", Sorting.ASCENDING); + public static final BooleanConfigOption COUNT_LIBRARIES = new BooleanConfigOption("count_libraries", true); + public static final BooleanConfigOption COMPACT_LIST = new BooleanConfigOption("compact_list", false); + public static final BooleanConfigOption COUNT_CHILDREN = new BooleanConfigOption("count_children", true); + public static final EnumConfigOption MODS_BUTTON_STYLE = new EnumConfigOption<>("mods_button_style", TitleMenuButtonStyle.ICON); + public static final EnumConfigOption GAME_MENU_BUTTON_STYLE = new EnumConfigOption<>("game_menu_button_style", GameMenuButtonStyle.ICON); + public static final BooleanConfigOption COUNT_HIDDEN_MODS = new BooleanConfigOption("count_hidden_mods", true); + public static final EnumConfigOption MOD_COUNT_LOCATION = new EnumConfigOption<>("mod_count_location", ModCountLocation.TITLE_SCREEN); + public static final BooleanConfigOption HIDE_MOD_LINKS = new BooleanConfigOption("hide_mod_links", false); + public static final EnumConfigOption SHOW_LIBRARIES = new EnumConfigOption<>("show_libraries", LibraryVisibility.WITH_CONFIG); + public static final BooleanConfigOption HIDE_MOD_LICENSE = new BooleanConfigOption("hide_mod_license", false); + public static final BooleanConfigOption HIDE_BADGES = new BooleanConfigOption("hide_badges", false); + public static final BooleanConfigOption HIDE_MOD_CREDITS = new BooleanConfigOption("hide_mod_credits", false); + public static final BooleanConfigOption EASTER_EGGS = new BooleanConfigOption("easter_eggs", true); + public static final BooleanConfigOption RANDOM_JAVA_COLORS = new BooleanConfigOption("random_java_colors", false); + public static final BooleanConfigOption TRANSLATE_NAMES = new BooleanConfigOption("translate_names", true); + public static final BooleanConfigOption TRANSLATE_DESCRIPTIONS = new BooleanConfigOption("translate_descriptions", true); + public static final BooleanConfigOption UPDATE_CHECKER = new BooleanConfigOption("update_checker", true); + public static final BooleanConfigOption BUTTON_UPDATE_BADGE = new BooleanConfigOption("button_update_badge", true); + public static final EnumConfigOption UPDATE_CHANNEL = new EnumConfigOption<>("update_channel", UpdateChannel.RELEASE); + public static final BooleanConfigOption QUICK_CONFIGURE = new BooleanConfigOption("quick_configure", true); + + @FileOnlyConfig + public static final BooleanConfigOption MODIFY_TITLE_SCREEN = new BooleanConfigOption("modify_title_screen", true); + @FileOnlyConfig + public static final BooleanConfigOption MODIFY_GAME_MENU = new BooleanConfigOption("modify_game_menu", true); + @FileOnlyConfig + public static final BooleanConfigOption HIDE_CONFIG_BUTTONS = new BooleanConfigOption("hide_config_buttons", false); + @FileOnlyConfig + public static final BooleanConfigOption CONFIG_MODE = new BooleanConfigOption("config_mode", false); + @FileOnlyConfig + public static final BooleanConfigOption DISABLE_DRAG_AND_DROP = new BooleanConfigOption("disable_drag_and_drop", false); + @FileOnlyConfig + public static final StringSetConfigOption HIDDEN_MODS = new StringSetConfigOption("hidden_mods", new HashSet<>()); + @FileOnlyConfig + public static final StringSetConfigOption HIDDEN_CONFIGS = new StringSetConfigOption("hidden_configs", new HashSet<>()); + @FileOnlyConfig + public static final StringSetConfigOption DISABLE_UPDATE_CHECKER = new StringSetConfigOption("disable_update_checker", new HashSet<>()); + + public static List asOptions() { + List options = new ArrayList<>(); + for (Field field : ModMenuConfig.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()) && + OptionConvertible.class.isAssignableFrom(field.getType()) && + !field.isAnnotationPresent(FileOnlyConfig.class)) { + try { + options.add((OptionConvertible) field.get(null)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + return options; + } + + public enum Sorting { + ASCENDING(Comparator.comparing(c -> c.getMetadata().getName().toLowerCase(Locale.ROOT))), + DESCENDING(ASCENDING.getComparator().reversed()), + HAS_UPDATE(Comparator.comparing((ModContainer c) -> { + Mod mod = ModMenu.MODS.get(c.getMetadata().getId()); + if (mod == null) return false; + UpdateInfo info = mod.getUpdateInfo(); + return info != null && info.isUpdateAvailable(); + }).reversed()); + + private final Comparator comparator; + + Sorting(Comparator comparator) { + this.comparator = comparator; + } + + public Comparator getComparator() { + return comparator; + } + } + + public enum LibraryVisibility { + FALSE, + WITH_CONFIG, + TRUE; + + public boolean hideMod(Mod mod, boolean hasConfig) { + boolean isLibrary = ModMenu.LIBRARY_MODS.contains(mod.getId()); + return switch (this) { + case TRUE -> false; + case FALSE -> isLibrary; + case WITH_CONFIG -> isLibrary && !hasConfig; + }; + } + + public boolean isTrue() { + return this == TRUE; + } + } + + public enum ModCountLocation { + TITLE_SCREEN(true, false), + MODS_BUTTON(false, true), + TITLE_SCREEN_AND_MODS_BUTTON(true, true), + NONE(false, false); + + private final boolean titleScreen, modsButton; + + ModCountLocation(boolean titleScreen, boolean modsButton) { + this.titleScreen = titleScreen; + this.modsButton = modsButton; + } + + public boolean isOnTitleScreen() { + return titleScreen; + } + + public boolean isOnModsButton() { + return modsButton; + } + } + + public enum TitleMenuButtonStyle { + @SerializedName(value = "insert", alternate = {"below_asset_packs"}) INSERT, + @SerializedName(value = "icon", alternate = {"replace", "replace_asset_packs"}) ICON + } + + public enum GameMenuButtonStyle { + @SerializedName(value = "insert", alternate = {"below_bugs"}) INSERT, + @SerializedName(value = "icon", alternate = {"replace", "replace_bugs"}) ICON + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java index ea589446b..180c0e60f 100644 --- a/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java +++ b/src/main/java/io/github/prospector/modmenu/config/ModMenuConfigManager.java @@ -1,65 +1,133 @@ -package io.github.prospector.modmenu.config; - - -import io.github.prospector.modmenu.ModMenu; -import net.fabricmc.loader.api.FabricLoader; - -import java.io.*; - -public class ModMenuConfigManager { - private static File file; - private static ModMenuConfig config; - - private static void prepareBiomeConfigFile() { - if (file != null) { - return; - } - file = new File(FabricLoader.getInstance().getConfigDir().toFile(), ModMenu.MOD_ID + ".json"); - } - - public static ModMenuConfig initializeConfig() { - if (config != null) { - return config; - } - - config = new ModMenuConfig(); - load(); - - return config; - } - - private static void load() { - prepareBiomeConfigFile(); - - try { - if (!file.exists()) { - save(); - } - if (file.exists()) { - BufferedReader br = new BufferedReader(new FileReader(file)); - - config = ModMenu.GSON.fromJson(br, ModMenuConfig.class); - } - } catch (FileNotFoundException e) { - System.err.println("Couldn't load Mod Menu configuration file; reverting to defaults"); - e.printStackTrace(); - } - } - - public static void save() { - prepareBiomeConfigFile(); - - String jsonString = ModMenu.GSON.toJson(config); - - try (FileWriter fileWriter = new FileWriter(file)) { - fileWriter.write(jsonString); - } catch (IOException e) { - System.err.println("Couldn't save Mod Menu configuration file"); - e.printStackTrace(); - } - } - - public static ModMenuConfig getConfig() { - return config; - } -} +package io.github.prospector.modmenu.config; + +import com.google.gson.*; +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.option.BooleanConfigOption; +import io.github.prospector.modmenu.config.option.ConfigOptionStorage; +import io.github.prospector.modmenu.config.option.EnumConfigOption; +import io.github.prospector.modmenu.config.option.StringSetConfigOption; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public class ModMenuConfigManager { + private static Path path; + + private static void prepareConfigPath() { + if (path == null) { + path = FabricLoader.getInstance().getConfigDir().resolve(ModMenu.MOD_ID + ".json"); + } + } + + public static void initializeConfig() { + load(); + } + + @SuppressWarnings("unchecked") + private static void load() { + prepareConfigPath(); + + try { + if (!Files.exists(path)) { + save(); + } + + if (Files.exists(path)) { + BufferedReader br = Files.newBufferedReader(path); + JsonObject json = JsonParser.parseReader(br).getAsJsonObject(); + for (Field field : ModMenuConfig.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) { + if (StringSetConfigOption.class.isAssignableFrom(field.getType())) { + JsonArray jsonArray = json.getAsJsonArray(field.getName().toLowerCase(Locale.ROOT)); + if (jsonArray != null) { + StringSetConfigOption option = (StringSetConfigOption) field.get(null); + Set values = new HashSet<>(); + for (JsonElement element : jsonArray) { + values.add(element.getAsString()); + } + ConfigOptionStorage.setStringSet(option.getKey(), values); + } + } else if (BooleanConfigOption.class.isAssignableFrom(field.getType())) { + JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive(field.getName().toLowerCase(Locale.ROOT)); + if (jsonPrimitive != null && jsonPrimitive.isBoolean()) { + BooleanConfigOption option = (BooleanConfigOption) field.get(null); + ConfigOptionStorage.setBoolean(option.getKey(), jsonPrimitive.getAsBoolean()); + } + } else if (EnumConfigOption.class.isAssignableFrom(field.getType()) && field.getGenericType() instanceof ParameterizedType) { + JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive(field.getName().toLowerCase(Locale.ROOT)); + if (jsonPrimitive != null && jsonPrimitive.isString()) { + Type generic = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; + if (generic instanceof Class) { + EnumConfigOption option = (EnumConfigOption) field.get(null); + Enum found = null; + for (Enum value : ((Class>) generic).getEnumConstants()) { + if (value.name().toLowerCase(Locale.ROOT).equals(jsonPrimitive.getAsString())) { + found = value; + break; + } + } + + if (found != null) { + ConfigOptionStorage.setEnumTypeless(option.getKey(), found); + } + } + } + } + } + } + } + } catch (IOException | IllegalAccessException e) { + System.err.println("Couldn't load Mod Menu configuration file; reverting to defaults"); + e.printStackTrace(); + } + } + + @SuppressWarnings("unchecked") + public static void save() { + ModMenu.clearModCountCache(); + prepareConfigPath(); + JsonObject config = new JsonObject(); + try { + for (Field field : ModMenuConfig.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) { + if (BooleanConfigOption.class.isAssignableFrom(field.getType())) { + BooleanConfigOption option = (BooleanConfigOption) field.get(null); + config.addProperty(field.getName().toLowerCase(Locale.ROOT), ConfigOptionStorage.getBoolean(option.getKey())); + } else if (StringSetConfigOption.class.isAssignableFrom(field.getType())) { + StringSetConfigOption option = (StringSetConfigOption) field.get(null); + JsonArray array = new JsonArray(); + ConfigOptionStorage.getStringSet(option.getKey()).forEach(array::add); + config.add(field.getName().toLowerCase(Locale.ROOT), array); + } else if (EnumConfigOption.class.isAssignableFrom(field.getType()) && field.getGenericType() instanceof ParameterizedType) { + Type generic = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; + if (generic instanceof Class) { + EnumConfigOption option = (EnumConfigOption) field.get(null); + config.addProperty(field.getName().toLowerCase(Locale.ROOT), ConfigOptionStorage.getEnumTypeless(option.getKey(), (Class>) generic).name().toLowerCase(Locale.ROOT)); + } + } + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + String jsonString = ModMenu.GSON.toJson(config); + try (BufferedWriter fileWriter = Files.newBufferedWriter(path)) { + fileWriter.write(jsonString); + } catch (IOException e) { + System.err.println("Couldn't save Mod Menu configuration file"); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/io/github/prospector/modmenu/config/option/BooleanConfigOption.java b/src/main/java/io/github/prospector/modmenu/config/option/BooleanConfigOption.java new file mode 100644 index 000000000..cf8070959 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/option/BooleanConfigOption.java @@ -0,0 +1,65 @@ +package io.github.prospector.modmenu.config.option; + +import io.github.prospector.modmenu.util.TranslationUtil; +import net.minecraft.core.lang.I18n; + +public class BooleanConfigOption implements OptionConvertible { + private final String key; + private final String translationKey; + private final boolean defaultValue; + + public BooleanConfigOption(String key, boolean defaultValue, String enabledKey, String disabledKey) { + ConfigOptionStorage.setBoolean(key, defaultValue); + this.key = key; + this.translationKey = TranslationUtil.translationKeyOf("option", key); + this.defaultValue = defaultValue; + } + + public BooleanConfigOption(String key, boolean defaultValue) { + this(key, defaultValue, "true", "false"); + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getTranslationKey() { + return translationKey; + } + + public boolean getValue() { + return ConfigOptionStorage.getBoolean(key); + } + + public void setValue(boolean value) { + ConfigOptionStorage.setBoolean(key, value); + } + + public void toggleValue() { + ConfigOptionStorage.toggleBoolean(key); + } + + public boolean getDefaultValue() { + return defaultValue; + } + + @Override + public String getButtonText() { + I18n i18n = I18n.getInstance(); + String name = i18n.translateKey(translationKey); + String value = i18n.translateKey(translationKey + (getValue() ? ".true" : ".false")); + return name + ": " + value; + } + + @Override + public void cycle(int direction) { + toggleValue(); + } + + @Override + public boolean isDefault() { + return getValue() == defaultValue; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/config/option/ConfigOptionStorage.java b/src/main/java/io/github/prospector/modmenu/config/option/ConfigOptionStorage.java new file mode 100644 index 000000000..802251aa7 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/option/ConfigOptionStorage.java @@ -0,0 +1,60 @@ +package io.github.prospector.modmenu.config.option; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class ConfigOptionStorage { + private static final Map BOOLEAN_OPTIONS = new HashMap<>(); + private static final Map> ENUM_OPTIONS = new HashMap<>(); + private static final Map> STRING_SET_OPTIONS = new HashMap<>(); + + public static void setStringSet(String key, Set value) { + STRING_SET_OPTIONS.put(key, value); + } + + public static Set getStringSet(String key) { + return STRING_SET_OPTIONS.get(key); + } + + public static void setBoolean(String key, boolean value) { + BOOLEAN_OPTIONS.put(key, value); + } + + public static void toggleBoolean(String key) { + setBoolean(key, !getBoolean(key)); + } + + public static boolean getBoolean(String key) { + return BOOLEAN_OPTIONS.get(key); + } + + @SuppressWarnings("unchecked") + public static > E getEnum(String key, Class typeClass) { + return (E) ENUM_OPTIONS.get(key); + } + + public static Enum getEnumTypeless(String key, Class> typeClass) { + return ENUM_OPTIONS.get(key); + } + + public static > void setEnum(String key, E value) { + ENUM_OPTIONS.put(key, value); + } + + public static void setEnumTypeless(String key, Enum value) { + ENUM_OPTIONS.put(key, value); + } + + public static > E cycleEnum(String key, Class typeClass) { + return cycleEnum(key, typeClass, 1); + } + + public static > E cycleEnum(String key, Class typeClass, int amount) { + E[] values = typeClass.getEnumConstants(); + E currentValue = getEnum(key, typeClass); + E newValue = values[(currentValue.ordinal() + values.length + amount) % values.length]; + setEnum(key, newValue); + return newValue; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/config/option/EnumConfigOption.java b/src/main/java/io/github/prospector/modmenu/config/option/EnumConfigOption.java new file mode 100644 index 000000000..34e082e67 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/option/EnumConfigOption.java @@ -0,0 +1,63 @@ +package io.github.prospector.modmenu.config.option; + +import io.github.prospector.modmenu.util.TranslationUtil; +import net.minecraft.core.lang.I18n; + +import java.util.Locale; + +public class EnumConfigOption> implements OptionConvertible { + private final String key; + private final String translationKey; + private final Class enumClass; + private final E defaultValue; + + public EnumConfigOption(String key, E defaultValue) { + ConfigOptionStorage.setEnum(key, defaultValue); + this.key = key; + this.translationKey = TranslationUtil.translationKeyOf("option", key); + this.enumClass = defaultValue.getDeclaringClass(); + this.defaultValue = defaultValue; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getTranslationKey() { + return translationKey; + } + + public E getValue() { + return ConfigOptionStorage.getEnum(key, enumClass); + } + + public void setValue(E value) { + ConfigOptionStorage.setEnum(key, value); + } + + public void cycleValue(int amount) { + ConfigOptionStorage.cycleEnum(key, enumClass, amount); + } + + public String getValueText(E value) { + return I18n.getInstance().translateKey(translationKey + "." + value.name().toLowerCase(Locale.ROOT)); + } + + @Override + public String getButtonText() { + String name = I18n.getInstance().translateKey(translationKey); + return name + ": " + getValueText(getValue()); + } + + @Override + public void cycle(int direction) { + cycleValue(direction); + } + + @Override + public boolean isDefault() { + return getValue() == defaultValue; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/config/option/OptionConvertible.java b/src/main/java/io/github/prospector/modmenu/config/option/OptionConvertible.java new file mode 100644 index 000000000..c6c94bf23 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/option/OptionConvertible.java @@ -0,0 +1,13 @@ +package io.github.prospector.modmenu.config.option; + +public interface OptionConvertible { + String getKey(); + + String getTranslationKey(); + + String getButtonText(); + + void cycle(int direction); + + boolean isDefault(); +} diff --git a/src/main/java/io/github/prospector/modmenu/config/option/StringSetConfigOption.java b/src/main/java/io/github/prospector/modmenu/config/option/StringSetConfigOption.java new file mode 100644 index 000000000..314da6897 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/config/option/StringSetConfigOption.java @@ -0,0 +1,30 @@ +package io.github.prospector.modmenu.config.option; + +import io.github.prospector.modmenu.util.TranslationUtil; + +import java.util.Set; + +public class StringSetConfigOption { + private final String key; + private final String translationKey; + private final Set defaultValue; + + public StringSetConfigOption(String key, Set defaultValue) { + ConfigOptionStorage.setStringSet(key, defaultValue); + this.key = key; + this.translationKey = TranslationUtil.translationKeyOf("option", key); + this.defaultValue = defaultValue; + } + + public String getKey() { + return key; + } + + public Set getValue() { + return ConfigOptionStorage.getStringSet(key); + } + + public void setValue(Set value) { + ConfigOptionStorage.setStringSet(key, value); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java deleted file mode 100644 index 24d758deb..000000000 --- a/src/main/java/io/github/prospector/modmenu/gui/DescriptionListWidget.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.github.prospector.modmenu.gui; - - -import io.github.prospector.modmenu.util.HardcodedUtil; -import io.github.prospector.modmenu.util.RenderUtils; -import net.fabricmc.loader.api.metadata.Person; -import net.minecraft.client.Minecraft; -import net.minecraft.client.render.font.FontRenderer; -import net.minecraft.core.lang.I18n; - -import java.util.Collection; - -public class DescriptionListWidget extends EntryListWidget { - private final ModListScreen parent; - private final FontRenderer textRenderer; - private ModListEntry lastSelected = null; - - public DescriptionListWidget(Minecraft client, int width, int height, int top, int bottom, int entryHeight, ModListScreen parent) { - super(client, width, height, top, bottom, entryHeight); - this.parent = parent; - this.textRenderer = client.font; - } - - @Override - public DescriptionEntry getSelected() { - return null; - } - - @Override - public int getRowWidth() { - return this.width - 10; - } - - @Override - protected int getScrollbarPosition() { - return this.width - 6 + left; - } - - @Override - public void render(int mouseX, int mouseY, float delta) { - I18n i18n = I18n.getInstance(); - ModListEntry selectedEntry = parent.getSelectedEntry(); - if (selectedEntry != lastSelected) { - lastSelected = selectedEntry; - clearEntries(); - setScrollAmount(-Double.MAX_VALUE); - String description = lastSelected.getMetadata().getDescription(); - String id = lastSelected.getMetadata().getId(); - Collection authors = lastSelected.getMetadata().getAuthors(); - Collection contributors = lastSelected.getMetadata().getContributors(); - Collection licenses = lastSelected.getMetadata().getLicense(); - if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(id)) { - description = HardcodedUtil.getHardcodedDescription(id); - } - if (lastSelected != null && description != null && !description.isEmpty()) { - for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, description.replaceAll("\n", "\n\n"), getRowWidth())) { - children().add(new DescriptionEntry(line)); - } - } - if (!authors.isEmpty()) { - if (!children().isEmpty()) children().add(new DescriptionEntry("")); - children().add(new DescriptionEntry(i18n.translateKey("modmenu.authors"))); - for (Person person : authors) { - children().add(new DescriptionEntry(" " + person.getName())); - } - } - if (!contributors.isEmpty()) { - if (!children().isEmpty()) children().add(new DescriptionEntry("")); - children().add(new DescriptionEntry(i18n.translateKey("modmenu.contributors"))); - for (Person person : contributors) { - children().add(new DescriptionEntry(" " + person.getName())); - } - } - if (!licenses.isEmpty()) { - if (!children().isEmpty()) children().add(new DescriptionEntry("")); - children().add(new DescriptionEntry(i18n.translateKey("modmenu.licenses"))); - for (String license : licenses) { - children().add(new DescriptionEntry(" " + license)); - } - } - } - super.render(mouseX, mouseY, delta); - } - - @Override - protected void renderHoleBackground(int y1, int y2, int startAlpha, int endAlpha) { - // Awful hack but it makes the background "seamless" - parent.overlayBackground(left, y1, right, y2, 64, 64, 64, startAlpha, endAlpha); - } - - protected class DescriptionEntry extends Entry { - protected String text; - - public DescriptionEntry(String text) { - this.text = text; - } - - @Override - public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - this.drawStringShadow(this.fontRenderer, text, x, y, 0xAAAAAA); - } - } - -} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java b/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java deleted file mode 100644 index ccc28f42f..000000000 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListEntry.java +++ /dev/null @@ -1,159 +0,0 @@ -package io.github.prospector.modmenu.gui; - - -import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.util.BadgeRenderer; -import io.github.prospector.modmenu.util.HardcodedUtil; -import io.github.prospector.modmenu.util.RenderUtils; -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.metadata.ModMetadata; -import net.minecraft.client.Minecraft; -import net.minecraft.client.render.font.FontRenderer; -import net.minecraft.client.render.renderer.GLRenderer; -import net.minecraft.client.render.renderer.Shaders; -import net.minecraft.client.render.renderer.State; -import net.minecraft.client.render.tessellator.TessellatorGeneral; -import org.lwjgl.opengl.GL11; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -public class ModListEntry extends AlwaysSelectedEntryListWidget.Entry { - public static final String UNKNOWN_ICON = "/gui/unknown_pack.png"; - private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); - - protected final Minecraft client; - protected final ModContainer container; - protected final ModMetadata metadata; - protected final ModListWidget list; - protected Integer iconLocation; - - public ModListEntry(Minecraft mc, ModContainer container, ModListWidget list) { - this.container = container; - this.list = list; - this.metadata = container.getMetadata(); - this.client = mc; - } - - @Override - public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { - x += getXOffset(); - rowWidth -= getXOffset(); - - GLRenderer.pushFrame(); - - GLRenderer.setColor4f(1, 1, 1, 1); // We LOVE random color calls - this.bindIconTexture(); - internalRender(y, x); - String name = metadata.getName(); - if (name.equals("Minecraft")){ // BAD CODE - name = "Better than Adventure"; - } - name = HardcodedUtil.formatFabricModuleName(name); - String trimmedName = name; - int maxNameWidth = rowWidth - 32 - 3; - FontRenderer font = this.fontRenderer; - trimmedName = ModListScreen.getString(fontRenderer, name, trimmedName, maxNameWidth); - this.drawStringNoShadow(font, trimmedName, x + 32 + 3, y + 1, 0xFFFFFF); - new BadgeRenderer(client, x + 32 + 3 + font.stringWidth(name) + 2, y, x + rowWidth, container, list.getParent()).draw(mouseX, mouseY); - String description = metadata.getDescription(); - if (description.isEmpty() && HardcodedUtil.getHardcodedDescriptions().containsKey(metadata.getId())) { - description = HardcodedUtil.getHardcodedDescription(metadata.getId()); - } - RenderUtils.INSTANCE.drawWrappedString(this, description, (x + 32 + 3 + 4), (y + 9 + 2), rowWidth - 32 - 7, 2, 0x808080); - - GLRenderer.popFrame(); - } - - static void internalRender(int y, int x) { - GLRenderer.pushFrame(); - GLRenderer.setShader(Shaders.INTERFACE); - GLRenderer.enableState(State.BLEND); - - TessellatorGeneral t = GLRenderer.getTessellator(); - t.startDrawingQuads(); - t.addVertexWithUV(x, y, 0, 0, 0); - t.addVertexWithUV(x, y + 32, 0, 0, 1); - t.addVertexWithUV(x + 32, y + 32, 0, 1, 1); - t.addVertexWithUV(x + 32, y, 0, 1, 0); - t.draw(); - - GLRenderer.popFrame(); - } - - private BufferedImage createIcon() { - try { - Path path = container.getPath(metadata.getIconPath(0).orElse("assets/" + metadata.getId() + "/icon.png")); - BufferedImage cached = this.list.getCachedModIcon(path); - if (cached != null) { - return cached; - } - if (!Files.exists(path)) { - ModContainer modMenu = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID).orElseThrow(IllegalAccessError::new); - if (HardcodedUtil.getFabricMods().contains(metadata.getId())) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/fabric_icon.png"); - } else if (metadata.getId().equals("minecraft")) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/mc_icon.png"); - } else if (metadata.getId().equals("java")) { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/java_icon.png"); - } else { - path = modMenu.getPath("assets/" + ModMenu.MOD_ID + "/grey_fabric_icon.png"); - } - } - cached = this.list.getCachedModIcon(path); - if (cached != null) { - return cached; - } - try (InputStream inputStream = Files.newInputStream(path)) { - BufferedImage image = ImageIO.read(Objects.requireNonNull(inputStream)); - if (image.getHeight() != image.getWidth()) - throw new IllegalStateException("Must be square icon"); - this.list.cacheModIcon(path, image); - return image; - } - - } catch (Throwable t) { - LOGGER.error("Invalid icon for mod {}", this.container.getMetadata().getName(), t); - return null; - } - } - - @Override - public void mouseClicked(int v, int v1, int i) { - list.select(this); - } - - public ModMetadata getMetadata() { - return metadata; - } - - public void bindIconTexture() { - if (this.iconLocation == null) { - BufferedImage icon = this.createIcon(); - if (icon != null) { - this.iconLocation = this.client.textureManager.loadBufferedTexture(icon).id(); - } else { - this.iconLocation = this.client.textureManager.loadTexture(UNKNOWN_ICON).id(); - } - } - this.client.textureManager.bindTexture(this.iconLocation); - } - - public void deleteTexture() { - if (iconLocation != null) { - this.client.textureManager.idToTextureMap.remove(iconLocation); - GL11.glDeleteTextures(iconLocation); - } - } - - public int getXOffset() { - return 0; - } -} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java b/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java deleted file mode 100644 index 29c7a7234..000000000 --- a/src/main/java/io/github/prospector/modmenu/gui/ModMenuButtonWidget.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.prospector.modmenu.gui; - - -import net.minecraft.client.gui.ButtonElement; - -public class ModMenuButtonWidget extends ButtonElement { - public ModMenuButtonWidget(int buttonId, int x, int y, int width, int height, String text) { - super(buttonId, x, y, width, height, text); - } -} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModMenuOptionsScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModMenuOptionsScreen.java new file mode 100644 index 000000000..11e9c0a82 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/gui/ModMenuOptionsScreen.java @@ -0,0 +1,233 @@ +package io.github.prospector.modmenu.gui; + +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.config.ModMenuConfigManager; +import io.github.prospector.modmenu.config.option.OptionConvertible; +import io.github.prospector.modmenu.util.ButtonUtil; +import io.github.prospector.modmenu.util.UpdateCheckerUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ButtonElement; +import net.minecraft.client.gui.Screen; +import net.minecraft.client.render.renderer.GLRenderer; +import net.minecraft.client.render.renderer.Shaders; +import net.minecraft.client.render.tessellator.TessellatorGeneral; +import net.minecraft.core.lang.I18n; +import net.minecraft.core.sound.SoundCategory; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +public class ModMenuOptionsScreen extends Screen { + private final Screen parent; + private final List options = new ArrayList<>(); + private final List optionButtons = new ArrayList<>(); + private static final int DONE_ID = 1000; + private static final int TOP = 32; + private static final int BOTTOM_MARGIN = 40; + + private float scrollPosition = 0.0F; + private int contentHeight = 0; + private boolean scrolling = false; + + public ModMenuOptionsScreen(Screen previous) { + this.parent = previous; + this.options.addAll(ModMenuConfig.asOptions()); + } + + @Override + public void init() { + I18n i18n = I18n.getInstance(); + + this.buttons.clear(); + this.optionButtons.clear(); + this.scrollPosition = 0.0F; + this.scrolling = false; + + int nextButtonId = 0; + int buttonWidth = 150; + int buttonHeight = 20; + int gap = 4; + int columns = 2; + int rowHeight = buttonHeight + gap; + + int totalWidth = columns * buttonWidth + (columns - 1) * gap; + int startX = this.width / 2 - totalWidth / 2; + int startY = 40; + + for (int i = 0; i < options.size(); i++) { + int col = i % columns; + int row = i / columns; + int x = startX + col * (buttonWidth + gap); + int y = startY + row * rowHeight; + + OptionConvertible option = options.get(i); + final int id = nextButtonId++; + + ButtonElement button = new ButtonElement(id, x, y, buttonWidth, buttonHeight, option.getButtonText()) { + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + int ay = this.yPosition - (int) ModMenuOptionsScreen.this.scrollPosition; + if (ay + this.height > TOP && ay < ModMenuOptionsScreen.this.height - BOTTOM_MARGIN) { + boolean hovered = mouseX >= this.xPosition && mouseY >= ay && mouseX < this.xPosition + this.width && mouseY < ay + this.height; + boolean inBounds = mouseY >= TOP && mouseY <= ModMenuOptionsScreen.this.height - BOTTOM_MARGIN; + int oy = this.yPosition; + this.yPosition = ay; + this.displayString = option.getButtonText(); + super.drawButton(mc, (hovered && !inBounds) ? -9999 : mouseX, (hovered && !inBounds) ? -9999 : mouseY); + this.yPosition = oy; + } + } + }; + this.buttons.add(button); + this.optionButtons.add(button); + } + + int lastRow = (options.size() - 1) / columns; + contentHeight = (startY - TOP) + lastRow * rowHeight + buttonHeight + 4; + + int doneY = this.height - 28; + this.buttons.add(ButtonUtil.createButton(DONE_ID, this.width / 2 - 75, doneY, 150, 20, i18n.translateKey("gui.options.button.done"))); + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + int top = TOP, bottom = this.height - BOTTOM_MARGIN, h = bottom - top; + int maxScroll = Math.max(0, contentHeight - h); + int sbX = (this.width + 310) / 2 + 6; + + if (maxScroll > 0 && mouseButton == 0 && mouseX >= sbX && mouseX <= sbX + 6 && mouseY >= top && mouseY <= bottom) { + this.scrolling = true; + return; + } + + for (Object obj : this.buttons) { + if (obj instanceof ButtonElement b) { + if (b.id == DONE_ID) { + if (mouseX >= b.xPosition && mouseY >= b.yPosition && mouseX < b.xPosition + b.width && mouseY < b.yPosition + b.height) { + this.clickedButton = b; + buttonClicked(b); + return; + } + } else if (mouseY >= top && mouseY <= bottom) { + int ay = b.yPosition - (int) this.scrollPosition; + if (mouseX >= b.xPosition && mouseY >= ay && mouseX < b.xPosition + b.width && mouseY < ay + b.height) { + int oy = b.yPosition; + b.yPosition = ay; + boolean clicked = b.mouseClicked(this.mc, mouseX, mouseY); + b.yPosition = oy; + if (clicked) { + this.clickedButton = b; + buttonClicked(b); + return; + } + } + } + } + } + + super.mouseClicked(-9999, -9999, mouseButton); + } + + @Override + public void mouseReleased(int mouseX, int mouseY, int mouseButton) { + if (mouseButton == 0) this.scrolling = false; + super.mouseReleased(mouseX, mouseY, mouseButton); + } + + @Override + protected void buttonClicked(ButtonElement button) { + this.mc.sndManager.playSound("random.click", SoundCategory.GUI_SOUNDS, 1.0F, 1.0F); + + if (button.id == DONE_ID) { + this.mc.displayScreen(this.parent); + return; + } + + if (button.id >= 0 && button.id < optionButtons.size()) { + OptionConvertible option = options.get(button.id); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + option.cycle(shift ? -1 : 1); + } + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + int top = TOP, bottom = this.height - BOTTOM_MARGIN, h = bottom - top; + int maxScroll = Math.max(0, contentHeight - h); + + if (this.scrolling && maxScroll > 0) { + if (!Mouse.isButtonDown(0)) { + this.scrolling = false; + } else { + float th = Math.max(32, (h * h) / (float) contentHeight); + float track = h - th; + if (track > 0) { + this.scrollPosition = Math.max(0, Math.min((mouseY - top - th / 2) * maxScroll / track, maxScroll)); + } + } + } + + int wheel = Mouse.getDWheel(); + if (wheel != 0 && maxScroll > 0) { + this.scrollPosition = Math.max(0, Math.min(this.scrollPosition + (wheel > 0 ? -24 : 24), maxScroll)); + } + + ((ModsScreen) this.parent).overlayBackground(0, 0, this.width, this.height, 64, 64, 64, 255, 255); + + int scale = this.mc.resolution.getScale(); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor(0, BOTTOM_MARGIN * scale, this.mc.gameWindow.getWidthPixels(), h * scale); + + for (Object obj : this.buttons) { + if (obj instanceof ButtonElement b && b.id != DONE_ID) b.drawButton(this.mc, mouseX, mouseY); + } + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + this.drawRect(0, 0, this.width, TOP, 0x40000000); + this.drawStringCenteredNoShadow(this.fontRenderer, I18n.getInstance().translateKey("modmenu.options"), this.width / 2, 12, 0xFFFFFF); + this.drawRect(0, bottom, this.width, this.height, 0x40000000); + + for (Object obj : this.buttons) { + if (obj instanceof ButtonElement b && b.id == DONE_ID) b.drawButton(this.mc, mouseX, mouseY); + } + + if (maxScroll > 0) { + int k = (this.width + 310) / 2 + 6, l = k + 6; + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.COLOR); + TessellatorGeneral t = GLRenderer.getTessellator(); + int th = Math.max(32, (h * h) / contentHeight); + int r = Math.max(top, (int) this.scrollPosition * (h - th) / maxScroll + top); + + t.startDrawingQuads(); + t.setColorOpaque3i(0, 0, 0); + t.addVertexWithUV(k, bottom, 0, 0, 1); t.addVertexWithUV(l, bottom, 0, 1, 1); + t.addVertexWithUV(l, top, 0, 1, 0); t.addVertexWithUV(k, top, 0, 0, 0); + t.draw(); + + t.startDrawingQuads(); + t.setColorOpaque3i(128, 128, 128); + t.addVertexWithUV(k, r + th, 0, 0, 1); t.addVertexWithUV(l, r + th, 0, 1, 1); + t.addVertexWithUV(l, r, 0, 1, 0); t.addVertexWithUV(k, r, 0, 0, 0); + t.draw(); + + t.startDrawingQuads(); + t.setColorOpaque3i(192, 192, 192); + t.addVertexWithUV(k, r + th - 1, 0, 0, 1); t.addVertexWithUV(l - 1, r + th - 1, 0, 1, 1); + t.addVertexWithUV(l - 1, r, 0, 1, 0); t.addVertexWithUV(k, r, 0, 0, 0); + t.draw(); + GLRenderer.popFrame(); + } + } + + @Override + public void removed() { + super.removed(); + ModMenuConfigManager.save(); + UpdateCheckerUtil.checkForUpdates(); + } +} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java b/src/main/java/io/github/prospector/modmenu/gui/ModsScreen.java similarity index 62% rename from src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java rename to src/main/java/io/github/prospector/modmenu/gui/ModsScreen.java index c5ce372de..53f8d3310 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListScreen.java +++ b/src/main/java/io/github/prospector/modmenu/gui/ModsScreen.java @@ -1,11 +1,14 @@ package io.github.prospector.modmenu.gui; import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.ModMenuConfig; import io.github.prospector.modmenu.config.ModMenuConfigManager; -import io.github.prospector.modmenu.util.BadgeRenderer; -import io.github.prospector.modmenu.util.ButtonUtil; -import io.github.prospector.modmenu.util.HardcodedUtil; -import io.github.prospector.modmenu.util.RenderUtils; +import io.github.prospector.modmenu.gui.widget.ModListWidget; +import io.github.prospector.modmenu.gui.widget.SmallModMenuButtonWidget; +import io.github.prospector.modmenu.gui.widget.TextFieldWidget; +import io.github.prospector.modmenu.gui.widget.DescriptionListWidget; +import io.github.prospector.modmenu.gui.widget.entries.ModListEntry; +import io.github.prospector.modmenu.util.*; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.metadata.ModMetadata; import net.fabricmc.loader.api.metadata.Person; @@ -17,9 +20,10 @@ import net.minecraft.client.render.renderer.Shaders; import net.minecraft.client.render.renderer.State; import net.minecraft.client.render.tessellator.TessellatorGeneral; +import net.minecraft.client.util.helper.FileOpener; +import net.minecraft.client.util.helper.UrlHelper; import net.minecraft.core.Global; import net.minecraft.core.lang.I18n; -import org.lwjgl.Sys; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL41; @@ -28,11 +32,10 @@ import org.spongepowered.include.com.google.common.base.Joiner; import java.io.File; -import java.net.MalformedURLException; -import java.text.NumberFormat; import java.util.*; +import java.util.stream.Collectors; -public class ModListScreen extends Screen { +public class ModsScreen extends Screen { private static final String FILTERS_BUTTON_LOCATION = "/assets/" + ModMenu.MOD_ID + "/textures/gui/filters_button.png"; private static final String CONFIGURE_BUTTON_LOCATION = "/assets/" + ModMenu.MOD_ID + "/textures/gui/configure_button.png"; private static final Logger LOGGER = LoggerFactory.getLogger("modlistscreen"); @@ -55,6 +58,9 @@ public class ModListScreen extends Screen { public Set showModChildren = new HashSet<>(); private String lastSearchString = null; + I18n i18n = I18n.getInstance(); + FabricLoader loader = FabricLoader.getInstance(); + private static final int CONFIGURE_BUTTON_ID = 0; private static final int WEBSITE_BUTTON_ID = 1; private static final int ISSUES_BUTTON_ID = 2; @@ -64,9 +70,9 @@ public class ModListScreen extends Screen { private static final int MODS_FOLDER_BUTTON_ID = 6; private static final int DONE_BUTTON_ID = 7; - public ModListScreen(Screen previousGui) { + public ModsScreen(Screen previousGui) { this.parent = previousGui; - this.textTitle = I18n.getInstance().translateKey("modmenu.title"); + this.textTitle = i18n.translateKey("modmenu.title"); } public void updateEvents() { @@ -93,9 +99,7 @@ public void tick() { @Override public void init() { - I18n i18n = I18n.getInstance(); Keyboard.enableRepeatEvents(true); - FontRenderer font = this.fontRenderer; paneY = 48; paneWidth = this.width / 2 - 8; rightPaneX = width - paneWidth; @@ -105,21 +109,23 @@ public void init() { String oldText = this.searchBox == null ? "" : this.searchBox.getText(); this.searchBox = new TextFieldWidget(searchBoxX, 22, searchBoxWidth, 20, i18n.translateKey("modmenu.search")); // field_6451_g this.searchBox.setText(oldText); - this.modList = new ModListWidget(this.mc, paneWidth, this.height, paneY + 19, this.height - 36, 36, this.searchBox.getText(), this.modList, this); + this.modList = new ModListWidget(this.mc, paneWidth, this.height, paneY + 19, this.height - 36, ModMenuConfig.COMPACT_LIST.getValue() ? 23 : 36, this.searchBox.getText(), this.modList, this); this.modList.setLeftPos(0); this.descriptionListWidget = new DescriptionListWidget(this.mc, paneWidth, this.height, paneY + 60, this.height - 36, 9 + 1, this); this.descriptionListWidget.setLeftPos(rightPaneX); - ButtonElement configureButton = new ModMenuTexturedButtonWidget(CONFIGURE_BUTTON_ID, width - 24, paneY, 20, 20, 0, 0, CONFIGURE_BUTTON_LOCATION, 32, 64) { - + ButtonElement configureButton = new SmallModMenuButtonWidget(CONFIGURE_BUTTON_ID, width - 24, paneY, 20, 20, 0, 0, CONFIGURE_BUTTON_LOCATION, 32, 64) { @Override public void render(Minecraft mc, int mouseX, int mouseY) { + if (isHovered(mouseX, mouseY)) { + setTooltip(i18n.translateKey("modmenu.configure")); + } if (selected != null) { - String modid = selected.getMetadata().getId(); - enabled = ModMenu.hasConfigScreenFactory(modid) || ModMenu.hasLegacyConfigScreenTask(modid); + String id = selected.getMetadata().getId(); + enabled = (ModMenu.hasConfigScreenFactory(id) || ModMenu.hasLegacyConfigScreenTask(id)); } else { enabled = false; } - visible = enabled; // visible = enabled + visible = enabled; int prevColor = GLRenderer.getColor(); GLRenderer.setColor4f(1, 1, 1, 1); @@ -132,20 +138,20 @@ public void render(Minecraft mc, int mouseX, int mouseY) { ButtonElement websiteButton = new ButtonElement(WEBSITE_BUTTON_ID, rightPaneX + (urlButtonWidths / 2) - (cappedButtonWidth / 2), paneY + 36, Math.min(urlButtonWidths, 200), 20, i18n.translateKey("modmenu.website")) { @Override public void drawButton(Minecraft mc, int var1, int var2) { - visible = selected != null; // visible = selected != null - enabled = visible && selected.getMetadata().getContact().get("homepage").isPresent(); + visible = selected != null; + enabled = visible && ("minecraft".equals(selected.getMetadata().getId()) || selected.getMetadata().getContact().get("homepage").isPresent()); super.drawButton(mc, var1, var2); } }; ButtonElement issuesButton = new ButtonElement(ISSUES_BUTTON_ID, rightPaneX + urlButtonWidths + 4 + (urlButtonWidths / 2) - (cappedButtonWidth / 2), paneY + 36, Math.min(urlButtonWidths, 200), 20, i18n.translateKey("modmenu.issues")) { @Override public void drawButton(Minecraft mc, int var1, int var2) { - visible = selected != null; // visible = selected != null - enabled = visible && selected.getMetadata().getContact().get("issues").isPresent(); + visible = selected != null; + enabled = visible && ("minecraft".equals(selected.getMetadata().getId()) || selected.getMetadata().getContact().get("issues").isPresent()); super.drawButton(mc, var1, var2); } }; - this.buttons.add(new ModMenuTexturedButtonWidget(TOGGLE_FILTER_OPTIONS_BUTTON_ID, paneWidth / 2 + searchBoxWidth / 2 - 20 / 2 + 2, 22, 20, 20, 0, 0, FILTERS_BUTTON_LOCATION, 32, 64) { + this.buttons.add(new SmallModMenuButtonWidget(TOGGLE_FILTER_OPTIONS_BUTTON_ID, paneWidth / 2 + searchBoxWidth / 2 - 20 / 2 + 2, 22, 20, 20, 0, 0, FILTERS_BUTTON_LOCATION, 32, 64) { @Override public void render(Minecraft mc, int int_1, int int_2) { super.render(mc, int_1, int_2); @@ -154,24 +160,19 @@ public void render(Minecraft mc, int int_1, int int_2) { } } }); - String showLibrariesText = i18n.translateKeyAndFormat("modmenu.showLibraries", i18n.translateKey("modmenu.showLibraries." + ModMenuConfigManager.getConfig().showLibraries())); - String sortingText = i18n.translateKeyAndFormat("modmenu.sorting", ModMenuConfigManager.getConfig().getSorting().getName()); - int showLibrariesWidth = fontRenderer.stringWidth(showLibrariesText) + 20; - int sortingWidth = fontRenderer.stringWidth(sortingText) + 20; - int filtersX; - int filtersWidth = showLibrariesWidth + sortingWidth + 2; - if ((filtersWidth + fontRenderer.stringWidth(i18n.translateKeyAndFormat("modmenu.showingMods", NumberFormat.getInstance().format(modList.getDisplayedCount()) + "/" + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size()))) + 20) >= searchBoxX + searchBoxWidth + 22) { - filtersX = paneWidth / 2 - filtersWidth / 2; - showModCount = false; - } else { - filtersX = searchBoxX + searchBoxWidth + 22 - filtersWidth + 1; - showModCount = true; - } + String showLibrariesText = ModMenuConfig.SHOW_LIBRARIES.getButtonText(); + String sortingText = ModMenuConfig.SORTING.getButtonText(); + + int filtersX = searchBoxX; + int sortingWidth = (searchBoxWidth - 2) / 2; + int showLibrariesWidth = searchBoxWidth - sortingWidth - 2; + showModCount = false; + this.buttons.add(new ButtonElement(TOGGLE_SORT_MODE_BUTTON_ID, filtersX, 45, sortingWidth, 20, sortingText) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = enabled = filterOptionsShown; - this.displayString = i18n.translateKeyAndFormat("modmenu.sorting", ModMenuConfigManager.getConfig().getSorting().getName()); + this.displayString = ModMenuConfig.SORTING.getButtonText(); super.drawButton(mc, mouseX, mouseY); } }); @@ -179,15 +180,17 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { @Override public void drawButton(Minecraft mc, int mouseX, int mouseY) { visible = enabled = filterOptionsShown; - this.displayString = i18n.translateKeyAndFormat("modmenu.showLibraries", i18n.translateKey("modmenu.showLibraries." + ModMenuConfigManager.getConfig().showLibraries())); + this.displayString = ModMenuConfig.SHOW_LIBRARIES.getButtonText(); super.drawButton(mc, mouseX, mouseY); } }); - this.buttons.add(configureButton); + if (!ModMenuConfig.HIDE_CONFIG_BUTTONS.getValue()) { + this.buttons.add(configureButton); + } this.buttons.add(websiteButton); this.buttons.add(issuesButton); - this.buttons.add(ButtonUtil.createButton(MODS_FOLDER_BUTTON_ID, this.width / 2 - 154, this.height - 28, 150, 20, "Open Mods Folder")); - this.buttons.add(ButtonUtil.createButton(DONE_BUTTON_ID, this.width / 2 + 4, this.height - 28, 150, 20, "Done")); + this.buttons.add(ButtonUtil.createButton(MODS_FOLDER_BUTTON_ID, this.width / 2 - 154, this.height - 28, 150, 20, i18n.translateKey("modmenu.modsFolder"))); + this.buttons.add(ButtonUtil.createButton(DONE_BUTTON_ID, this.width / 2 + 4, this.height - 28, 150, 20, i18n.translateKey("gui.options.button.done"))); this.searchBox.setFocused(true); init = true; @@ -197,23 +200,35 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY) { protected void buttonClicked(ButtonElement button) { switch (button.id) { case CONFIGURE_BUTTON_ID: { - final String modid = Objects.requireNonNull(selected).getMetadata().getId(); - final Screen screen = ModMenu.getConfigScreen(modid, this); + final String id = Objects.requireNonNull(selected).getMetadata().getId(); + final Screen screen = ModMenu.getConfigScreen(id, this); if (screen != null) { mc.displayScreen(screen); } else { - ModMenu.openConfigScreen(modid); + ModMenu.openConfigScreen(id); } break; } case WEBSITE_BUTTON_ID: { - final ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("homepage").ifPresent(Sys::openURL); + if (selected != null) { + String id = selected.getMetadata().getId(); + if ("minecraft".equals(id)) { + UrlHelper.openURL("https://www.betterthanadventure.net/"); + } else { + selected.getMetadata().getContact().get("homepage").ifPresent(UrlHelper::openURL); + } + } break; } case ISSUES_BUTTON_ID: { - final ModMetadata metadata = Objects.requireNonNull(selected).getMetadata(); - metadata.getContact().get("issues").ifPresent(Sys::openURL); + if (selected != null) { + String id = selected.getMetadata().getId(); + if ("minecraft".equals(id)) { + UrlHelper.openURL("https://support.betterthanadventure.net/"); + } else { + selected.getMetadata().getContact().get("issues").ifPresent(UrlHelper::openURL); + } + } break; } case TOGGLE_FILTER_OPTIONS_BUTTON_ID: { @@ -221,21 +236,26 @@ protected void buttonClicked(ButtonElement button) { break; } case TOGGLE_SORT_MODE_BUTTON_ID: { - ModMenuConfigManager.getConfig().toggleSortMode(); + ModMenuConfig.SORTING.cycleValue(Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT) ? -1 : 1); + ModMenuConfigManager.save(); modList.reloadFilters(); break; } case TOGGLE_SHOW_LIBRARIES_BUTTON_ID: { - ModMenuConfigManager.getConfig().toggleShowLibraries(); + ModMenuConfig.SHOW_LIBRARIES.cycleValue(Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT) ? -1 : 1); + ModMenuConfigManager.save(); modList.reloadFilters(); break; } case MODS_FOLDER_BUTTON_ID: { - File modsFolder = new File(FabricLoader.getInstance().getGameDir().toFile(), "mods"); + File modsFolder = new File(loader.getGameDir().toFile(), "mods"); + if (!modsFolder.exists()) { + modsFolder.mkdirs(); + } try { - Sys.openURL(modsFolder.toURI().toURL().toString()); - } catch (MalformedURLException e) { - LOGGER.error("Malformed mods folder URL", e); + FileOpener.open(modsFolder); + } catch (Exception e) { + LOGGER.error("Failed to open mods folder", e); } break; } @@ -253,9 +273,9 @@ public ModListWidget getModList() { @Override public void keyPressed(char char_1, int int_1, int mouseX, int mouseY) { this.searchBox.textboxKeyTyped(char_1, int_1); - if (int_1 == 1) { - this.mc.displayScreen(this.parent); - } + if (int_1 == 1) { + this.mc.displayScreen(this.parent); + } modList.keyPressed(int_1, 0, 0); descriptionListWidget.keyPressed(int_1, 0, 0); } @@ -275,12 +295,21 @@ public void mouseReleased(int mouseX, int mouseY, int mouseButton) { descriptionListWidget.mouseReleased(mouseX, mouseY, mouseButton); } } - + @Override public void render(int mouseX, int mouseY, float delta) { - I18n i18n = I18n.getInstance(); - int mouseDX = Mouse.getEventDX() * this.width / this.mc.resolution.getScaledWidthScreenCoords(); // field_6326_c - int mouseDY = this.height - Mouse.getEventDY() * this.height / this.mc.resolution.getScaledHeightScreenCoords() - 1; // field_6325_d + int dWheel = Mouse.getDWheel(); + if (dWheel != 0) { + double scrollDelta = dWheel > 0 ? 1.0 : -1.0; + if (this.modList != null && this.modList.isMouseOver(mouseX, mouseY)) { + this.modList.mouseScrolled(mouseX, mouseY, scrollDelta); + } else if (this.descriptionListWidget != null && this.descriptionListWidget.isMouseOver(mouseX, mouseY)) { + this.descriptionListWidget.mouseScrolled(mouseX, mouseY, scrollDelta); + } + } + + int mouseDX = Mouse.getDX() * this.width / this.mc.resolution.getScaledWidthScreenCoords(); // field_6326_c + int mouseDY = Mouse.getDY() * this.height / this.mc.resolution.getScaledHeightScreenCoords(); // field_6325_d for (int button = 0; button < Mouse.getButtonCount(); button++) { if (Mouse.isButtonDown(button)) { modList.mouseDragged(mouseX, mouseY, button, mouseDX, mouseDY); @@ -307,52 +336,64 @@ public void render(int mouseX, int mouseY, float delta) { this.drawStringCenteredNoShadow(font, this.textTitle, this.modList.getWidth() / 2, 8, 0xffffff); super.render(mouseX, mouseY, delta); if (showModCount || !filterOptionsShown) { - String showModCountString = i18n.translateKeyAndFormat("modmenu.showingMods", NumberFormat.getInstance().format(modList.getDisplayedCount()) + "/" + NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size())); + String showModCountString; + if (ModMenuConfig.SHOW_LIBRARIES.getValue().isTrue() && modList.getDisplayedCount() == 0) { + showModCountString = computeLibraryCountText(false); + } else { + showModCountString = computeModCountText(true, false); + } this.drawStringNoShadow(font, showModCountString, searchBoxX, 52, 0xFFFFFF); } if (selectedEntry != null) { ModMetadata metadata = selectedEntry.getMetadata(); + String id = metadata.getId(); + int x = rightPaneX; int prevColor = GLRenderer.getColor(); GLRenderer.setColor4f(1, 1, 1, 1); + if ("java".equals(id)) { + DrawingUtil.drawRandomVersionBackground(selectedEntry.getContainer(), x, paneY, 32, 32); + } + this.selected.bindIconTexture(); - ModListEntry.internalRender(paneY, x); - int lineSpacing = 9 + 1; + ModListEntry.internalRender(paneY, x, ModListEntry.FULL_ICON_SIZE); + int lineSpacing = 9 + 1; int imageOffset = 36; - String name = metadata.getName(); - if (name.equals("Minecraft")){ // BAD CODE - name = "Better than Adventure"; + String translationKey = "modmenu.nameTranslation." + id; + String translatedName = i18n.translateKey(translationKey); + String name; + if (!translatedName.equals(translationKey) && ModMenuConfig.TRANSLATE_NAMES.getValue()) { + name = translatedName; + } else { + name = metadata.getName(); } - name = HardcodedUtil.formatFabricModuleName(name); + String trimmedName = name; int maxNameWidth = this.width - (x + imageOffset); - trimmedName = getString(font, name, trimmedName, maxNameWidth); - this.drawStringNoShadow(font, trimmedName, x + imageOffset, paneY + 1, 0xFFFFFF); + trimmedName = getString(font, name, trimmedName, maxNameWidth); + this.drawStringNoShadow(font, trimmedName, x + imageOffset, paneY + 1, 0xFFFFFF); if (mouseX > x + imageOffset && mouseY > paneY + 1 && mouseY < paneY + 1 + 9 && mouseX < x + imageOffset + fontRenderer.stringWidth(trimmedName)) { setTooltip(i18n.translateKeyAndFormat("modmenu.modIdToolTip", metadata.getId())); } if (init || badgeRenderer == null || badgeRenderer.getMetadata() != metadata) { - badgeRenderer = new BadgeRenderer(mc, x + imageOffset + fontRenderer.stringWidth(trimmedName) + 2, paneY, width - 28, selectedEntry.container, this); + badgeRenderer = new BadgeRenderer(mc, x + imageOffset + fontRenderer.stringWidth(trimmedName) + 2, paneY, width - 28, selectedEntry.getContainer(), this); init = false; } badgeRenderer.draw(mouseX, mouseY); - String versionString; - if (metadata.getName().equals("Minecraft")){ // BAD CODE - versionString = Global.VERSION; - } else { - versionString = metadata.getVersion().getFriendlyString(); - } - this.drawStringNoShadow(font, "v" + versionString, x + imageOffset, paneY + 2 + lineSpacing, 0x808080); + String versionString = metadata.getId().equals("minecraft") + ? Global.VERSION + : metadata.getVersion().getFriendlyString(); + this.drawStringNoShadow(font, VersionUtil.getPrefixedVersion(versionString), x + imageOffset, paneY + 2 + lineSpacing, 0x808080); String authors; List names = new ArrayList<>(); metadata.getAuthors().stream() - .filter(Objects::nonNull) - .map(Person::getName) - .filter(Objects::nonNull) - .forEach(names::add); + .filter(Objects::nonNull) + .map(Person::getName) + .filter(Objects::nonNull) + .forEach(names::add); if (!names.isEmpty()) { if (names.size() > 1) { @@ -372,19 +413,19 @@ public void render(int mouseX, int mouseY, float delta) { GLRenderer.popFrame(); } - static String getString(FontRenderer fontRenderer, String name, String trimmedName, int maxNameWidth) { - if (fontRenderer.stringWidth(name) > maxNameWidth) { - int maxWidth = maxNameWidth - fontRenderer.stringWidth("..."); - trimmedName = ""; - while (fontRenderer.stringWidth(trimmedName) < maxWidth && trimmedName.length() < name.length()) { - trimmedName += name.charAt(trimmedName.length()); - } - trimmedName = trimmedName.isEmpty() ? "..." : trimmedName.substring(0, trimmedName.length() - 1) + "..."; - } - return trimmedName; - } + public static String getString(FontRenderer fontRenderer, String name, String trimmedName, int maxNameWidth) { + if (fontRenderer.stringWidth(name) > maxNameWidth) { + int maxWidth = maxNameWidth - fontRenderer.stringWidth("..."); + trimmedName = ""; + while (fontRenderer.stringWidth(trimmedName) < maxWidth && trimmedName.length() < name.length()) { + trimmedName += name.charAt(trimmedName.length()); + } + trimmedName = trimmedName.isEmpty() ? "..." : trimmedName.substring(0, trimmedName.length() - 1) + "..."; + } + return trimmedName; + } - public void overlayBackground(int x1, int y1, int x2, int y2, int red, int green, int blue, int startAlpha, int endAlpha) { + public void overlayBackground(int x1, int y1, int x2, int y2, int red, int green, int blue, int startAlpha, int endAlpha) { GLRenderer.pushFrame(); GLRenderer.setShader(Shaders.INTERFACE); @@ -425,6 +466,54 @@ public void updateSelectedEntry(ModListEntry entry) { } } + private int[] formatModCount(Set set, boolean allVisible) { + int visible = this.modList.getDisplayedCountFor(set); + int total = set.size(); + if (visible == total || allVisible) { + return new int[]{total}; + } else { + return new int[]{visible, total}; + } + } + + private String computeModCountText(boolean includeLibs, boolean onInit) { + Set rootModIds = loader.getAllMods().stream() + .map(container -> container.getMetadata().getId()) + .filter(id -> !ModMenu.LIBRARY_MODS.contains(id)) + .collect(Collectors.toSet()); + + int[] rootMods = formatModCount(rootModIds, onInit); + + if (includeLibs && !onInit && ModMenuConfig.SHOW_LIBRARIES.getValue().isTrue()) { + Set rootLibIds = loader.getAllMods().stream() + .map(container -> container.getMetadata().getId()) + .filter(ModMenu.LIBRARY_MODS::contains) + .collect(Collectors.toSet()); + + int[] rootLibs = formatModCount(rootLibIds, false); + + if (rootLibs.length == 1 && rootLibs[0] != 0 || rootLibs.length == 2 && rootLibs[1] != 0) { + return TranslationUtil.translateNumeric("modmenu.showingModsLibraries", rootMods, rootLibs); + } + } + + return TranslationUtil.translateNumeric("modmenu.showingMods", rootMods); + } + + private String computeLibraryCountText(boolean onInit) { + if (!onInit && ModMenuConfig.SHOW_LIBRARIES.getValue().isTrue()) { + Set rootLibIds = loader.getAllMods().stream() + .map(container -> container.getMetadata().getId()) + .filter(ModMenu.LIBRARY_MODS::contains) + .collect(Collectors.toSet()); + + int[] rootLibs = formatModCount(rootLibIds, false); + return TranslationUtil.translateNumeric("modmenu.showingLibraries", rootLibs); + } else { + return ""; + } + } + public double getScrollPercent() { return scrollPercent; } @@ -437,14 +526,8 @@ public String getSearchInput() { return searchBox.getText(); } - public boolean showingFilterOptions() { - return filterOptionsShown; - } - public void renderTooltip(List list, int i, int j) { if (!list.isEmpty()) { - FontRenderer font = this.fontRenderer; - GLRenderer.pushFrame(); GLRenderer.disableState(State.DEPTH_TEST); int k = 0; diff --git a/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/AlwaysSelectedEntryListWidget.java similarity index 87% rename from src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/AlwaysSelectedEntryListWidget.java index 0b285e5e6..0421660eb 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/AlwaysSelectedEntryListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/AlwaysSelectedEntryListWidget.java @@ -1,11 +1,6 @@ -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by Fernflower decompiler) -// - -package io.github.prospector.modmenu.gui; - +package io.github.prospector.modmenu.gui.widget; +import io.github.prospector.modmenu.gui.widget.entries.EntryListWidget; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; diff --git a/src/main/java/io/github/prospector/modmenu/gui/widget/DescriptionListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/DescriptionListWidget.java new file mode 100644 index 000000000..157375a70 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/DescriptionListWidget.java @@ -0,0 +1,330 @@ +package io.github.prospector.modmenu.gui.widget; + +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.gui.widget.entries.EntryListWidget; +import io.github.prospector.modmenu.gui.widget.entries.ModListEntry; +import io.github.prospector.modmenu.util.RenderUtils; +import io.github.prospector.modmenu.util.mod.Mod; +import net.fabricmc.loader.api.metadata.ContactInformation; +import net.fabricmc.loader.api.metadata.Person; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScreenCredits; +import net.minecraft.client.render.font.FontRenderer; +import net.minecraft.client.util.helper.UrlHelper; +import net.minecraft.core.lang.I18n; +import net.minecraft.core.net.command.TextFormatting; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class DescriptionListWidget extends EntryListWidget { + private final ModsScreen parent; + private final FontRenderer textRenderer; + private ModListEntry lastSelected = null; + + public DescriptionListWidget(Minecraft client, int width, int height, int top, int bottom, int entryHeight, ModsScreen parent) { + super(client, width, height, top, bottom, entryHeight); + this.parent = parent; + this.textRenderer = client.font; + } + + @Override + public DescriptionEntry getSelected() { + return null; + } + + @Override + public int getRowWidth() { + return this.width - 10; + } + + @Override + protected int getScrollbarPosition() { + return this.width - 6 + left; + } + + protected static class LinkEntry extends DescriptionEntry { + private final String link; + private final int indent; + private int lastRenderX; + private int lastRenderY; + + public LinkEntry(String text, String link, int indent) { + super(text); + this.link = link; + this.indent = indent; + } + + @Override + public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + this.lastRenderX = x + indent; + this.lastRenderY = y; + + String formattedText = TextFormatting.formatted(text, TextFormatting.BLUE, TextFormatting.UNDERLINE); + this.drawStringShadow(this.fontRenderer, formattedText, lastRenderX, lastRenderY, 0xFFFFFF); + } + + public void mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0) { + int textWidth = fontRenderer.stringWidth(text); + if (mouseX >= lastRenderX && mouseX <= lastRenderX + textWidth && mouseY >= lastRenderY && mouseY <= lastRenderY + 12) { + UrlHelper.openURL(link); + } + } + } + } + + protected static class BTACreditsEntry extends DescriptionEntry { + private final int indent; + private int lastRenderX; + private int lastRenderY; + + public BTACreditsEntry(String text, int indent) { + super(text); + this.indent = indent; + } + + @Override + public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + this.lastRenderX = x + indent; + this.lastRenderY = y; + + String formattedText = TextFormatting.formatted(text, TextFormatting.BLUE, TextFormatting.UNDERLINE); + this.drawStringShadow(this.fontRenderer, formattedText, lastRenderX, lastRenderY, 0xFFFFFF); + } + + public void mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0) { + int textWidth = fontRenderer.stringWidth(text); + if (mouseX >= lastRenderX && mouseX <= lastRenderX + textWidth && mouseY >= lastRenderY && mouseY <= lastRenderY + 12) { + mc.displayScreen(new ScreenCredits(mc.currentScreen)); + } + } + } + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + I18n i18n = I18n.getInstance(); + ModListEntry selectedEntry = parent.getSelectedEntry(); + if (selectedEntry != lastSelected) { + lastSelected = selectedEntry; + clearEntries(); + setScrollAmount(-Double.MAX_VALUE); + String id = selectedEntry.getMetadata().getId(); + String descKey = "modmenu.descriptionTranslation." + id; + String translatedDesc = i18n.translateKey(descKey); + + String description; + if (!translatedDesc.equals(descKey) && ModMenuConfig.TRANSLATE_DESCRIPTIONS.getValue()) { + description = translatedDesc; + } else { + description = selectedEntry.getMetadata().getDescription(); + } + + if ("java".equals(id)) { + String vendor = System.getProperty("java.vendor", "Unknown Vendor"); + String javaDist = i18n.translateKeyAndFormat("modmenu.javaDistributionName", vendor); + description = (description != null ? description : "") + "\n" + javaDist; + } + + if (lastSelected != null && description != null && !description.isEmpty()) { + String[] paragraphs = description.split("\r?\n"); + for (String paragraph : paragraphs) { + if (paragraph.isEmpty()) { + children().add(new DescriptionEntry("")); + continue; + } + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, paragraph, getRowWidth())) { + children().add(new DescriptionEntry(line)); + } + } + } + + Collection authors = selectedEntry.getMetadata().getAuthors(); + Collection contributors = selectedEntry.getMetadata().getContributors(); + Collection licenses = selectedEntry.getMetadata().getLicense(); + + ContactInformation contact = selectedEntry.getMetadata().getContact(); + Map links = new HashMap<>(contact.asMap()); + + links.remove("homepage"); + links.remove("issues"); + + if (ModMenuConfig.UPDATE_CHECKER.getValue()) { + Mod mod = ModMenu.MODS.get(id); + if (mod != null) { + UpdateInfo updateInfo = mod.getUpdateInfo(); + if (updateInfo != null && updateInfo.isUpdateAvailable()) { + + children().add(new DescriptionEntry("")); + + int index = 0; + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, i18n.translateKey("modmenu.hasUpdate"), getRowWidth() - 11)) { + DescriptionEntry entry = new DescriptionEntry(line); + if (index == 0) { + entry.setUpdateTextEntry(); + } + children().add(entry); + index++; + } + + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, i18n.translateKey("modmenu.experimental"), getRowWidth() - 16)) { + children().add(new DescriptionEntry(" " + TextFormatting.ORANGE + line)); + } + + String updateMessage = updateInfo.getUpdateMessage(); + String downloadLink = updateInfo.getDownloadLink(); + if (updateMessage == null) { + updateMessage = i18n.translateKey("modmenu.downloadLink" + TextFormatting.UNDERLINE + TextFormatting.BLUE); + } + + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, updateMessage, getRowWidth() - 16)) { + if (downloadLink != null) { + children().add(new LinkEntry(line, downloadLink, 8)); + } else { + children().add(new DescriptionEntry(" " + line)); + } + } + } + } + } + + if (!links.isEmpty() && !ModMenuConfig.HIDE_MOD_LINKS.getValue()) { + children().add(new DescriptionEntry("")); + + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, i18n.translateKey("modmenu.links"), getRowWidth())) { + children().add(new DescriptionEntry(line)); + } + + links.forEach((key, value) -> { + int indent = 8; + String translationKey = "modmenu." + key; + String translatedKey = i18n.translateKey(translationKey); + + if (translatedKey.equals(translationKey)) { + translatedKey = Character.toUpperCase(key.charAt(0)) + key.substring(1); + } + + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, translatedKey, getRowWidth() - 16)) { + children().add(new LinkEntry(line, value, indent)); + indent = 16; + } + }); + } + + if (!licenses.isEmpty() && !ModMenuConfig.HIDE_MOD_LICENSE.getValue()) { + if (!children().isEmpty()) children().add(new DescriptionEntry("")); + children().add(new DescriptionEntry(i18n.translateKey("modmenu.license"))); + for (String license : licenses) { + children().add(new DescriptionEntry(" " + license)); + } + } + + if ("minecraft".equals(id) && !ModMenuConfig.HIDE_MOD_CREDITS.getValue()) { + if (!children().isEmpty()) children().add(new DescriptionEntry("")); + String viewCreditsText = i18n.translateKey("modmenu.viewCredits"); + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, viewCreditsText, getRowWidth() - 16)) { + children().add(new BTACreditsEntry(line, 0)); + } + } + + if ((!authors.isEmpty() || !contributors.isEmpty()) && !ModMenuConfig.HIDE_MOD_CREDITS.getValue()) { + if (!children().isEmpty()) children().add(new DescriptionEntry("")); + + children().add(new DescriptionEntry(i18n.translateKey("modmenu.credits"))); + + if (!authors.isEmpty()) { + int indent = 8; + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, creditsRoleText("Author"), getRowWidth() - 16)) { + children().add(new DescriptionEntry(line, indent)); + indent = 16; + } + for (Person person : authors) { + indent = 16; + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, person.getName(), getRowWidth() - 24)) { + children().add(new DescriptionEntry(line, indent)); + indent = 24; + } + } + } + + if (!contributors.isEmpty()) { + if (!authors.isEmpty()) children().add(new DescriptionEntry("")); + + int indent = 8; + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, creditsRoleText("Contributor"), getRowWidth() - 16)) { + children().add(new DescriptionEntry(line, indent)); + indent = 16; + } + for (Person person : contributors) { + indent = 16; + for (String line : RenderUtils.INSTANCE.wrapStringToWidthAsList(textRenderer, person.getName(), getRowWidth() - 24)) { + children().add(new DescriptionEntry(line, indent)); + indent = 24; + } + } + } + } + } + super.render(mouseX, mouseY, delta); + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int button) { + super.mouseClicked(mouseX, mouseY, button); + for (DescriptionEntry entry : children()) { + if (entry instanceof LinkEntry || entry instanceof BTACreditsEntry) { + entry.mouseClicked(mouseX, mouseY, button); + } + } + } + + @Override + protected void renderHoleBackground(int y1, int y2, int startAlpha, int endAlpha) { + // Awful hack but it makes the background "seamless" + parent.overlayBackground(left, y1, right, y2, 64, 64, 64, startAlpha, endAlpha); + } + + private String creditsRoleText(String roleName) { + String translationKey = "modmenu.credits.role." + roleName.replaceAll("[ -]", "_").toLowerCase(); + String fallback = roleName.endsWith("r") ? roleName + "s" : roleName; + String translated = I18n.getInstance().translateKey(translationKey); + return (translated.equals(translationKey) ? fallback : translated) + ":"; + } + + protected static class DescriptionEntry extends Entry { + protected String text; + protected int indent; + public boolean updateTextEntry = false; + + public DescriptionEntry(String text, int indent) { + this.text = text; + this.indent = indent; + } + + public DescriptionEntry(String text) { + this(text, 0); + } + + public void setUpdateTextEntry() { + this.updateTextEntry = true; + } + + @Override + public void render(int index, int y, int x, int itemWidth, int itemHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + int renderX = x + indent; + + if (updateTextEntry) { + UpdateAvailableBadge.renderBadge(renderX, y + 1); + renderX += 11; + } + + this.drawStringShadow(this.fontRenderer, text, renderX, y, 0xAAAAAA); + } + } +} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/ModListWidget.java similarity index 56% rename from src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/ModListWidget.java index 0b2dbc52f..5a3977f53 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/ModListWidget.java @@ -1,11 +1,12 @@ -package io.github.prospector.modmenu.gui; - +package io.github.prospector.modmenu.gui.widget; import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.config.ModMenuConfigManager; -import io.github.prospector.modmenu.gui.entries.ChildEntry; -import io.github.prospector.modmenu.gui.entries.IndependentEntry; -import io.github.prospector.modmenu.gui.entries.ParentEntry; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.widget.entries.ModListEntry; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.gui.widget.entries.ChildEntry; +import io.github.prospector.modmenu.gui.widget.entries.IndependentEntry; +import io.github.prospector.modmenu.gui.widget.entries.ParentEntry; import io.github.prospector.modmenu.util.ModListSearch; import io.github.prospector.modmenu.util.TestModContainer; import net.fabricmc.loader.api.FabricLoader; @@ -16,26 +17,25 @@ import net.minecraft.client.render.renderer.Shaders; import net.minecraft.client.render.tessellator.TessellatorGeneral; import net.minecraft.core.util.helper.MathHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.image.BufferedImage; import java.nio.file.Path; import java.util.*; public class ModListWidget extends AlwaysSelectedEntryListWidget implements AutoCloseable { - private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); public static final boolean DEBUG = Boolean.getBoolean("modmenu.debug"); private final Map modIconsCache = new HashMap<>(); - private final ModListScreen parent; + private final ModsScreen parent; private List modContainerList = null; private Set addedMods = new HashSet<>(); private String selectedModId = null; - private boolean scrolling; - private boolean isFocused; - public ModListWidget(Minecraft client, int width, int height, int y1, int y2, int entryHeight, String searchTerm, ModListWidget list, ModListScreen parent) { + private boolean scrollBarDragging = false; + private double scrollBarGrabbedY = 0; + private double scrollBarGrabOffset = 0; + + public ModListWidget(Minecraft client, int width, int height, int y1, int y2, int entryHeight, String searchTerm, ModListWidget list, ModsScreen parent) { super(client, width, height, y1, y2, entryHeight); this.parent = parent; if (list != null) { @@ -49,18 +49,13 @@ public ModListWidget(Minecraft client, int width, int height, int y1, int y2, in public void setScrollAmount(double amount) { super.setScrollAmount(amount); int denominator = Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); - if (denominator <= 0) { + if (denominator == 0) { parent.updateScrollPercent(0); } else { parent.updateScrollPercent(getScrollAmount() / Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); } } - @Override - protected boolean isFocused() { - return isFocused; - } - public void select(ModListEntry entry) { this.setSelected(entry); } @@ -80,10 +75,10 @@ protected boolean isSelectedItem(int index) { @Override public int addEntry(ModListEntry entry) { - if (addedMods.contains(entry.container)) { + if (addedMods.contains(entry.getContainer())) { return 0; } - addedMods.add(entry.container); + addedMods.add(entry.getContainer()); int i = super.addEntry(entry); if (entry.getMetadata().getId().equals(selectedModId)) { setSelected(entry); @@ -93,13 +88,13 @@ public int addEntry(ModListEntry entry) { @Override protected boolean removeEntry(ModListEntry entry) { - addedMods.remove(entry.container); + addedMods.remove(entry.getContainer()); return super.removeEntry(entry); } @Override protected ModListEntry remove(int index) { - addedMods.remove(getEntry(index).container); + addedMods.remove(getEntry(index).getContainer()); return super.remove(index); } @@ -107,11 +102,44 @@ public void reloadFilters() { filter(parent.getSearchInput(), true, false); } - public void filter(String searchTerm, boolean refresh) { filter(searchTerm, refresh, true); } + private List getChildrenFor(String parentId) { + for (Map.Entry> e : ModMenu.PARENT_MAP.entrySet()) { + if (e.getKey().getMetadata().getId().equals(parentId)) { + return e.getValue(); + } + } + return null; + } + + private boolean isParent(String modId) { + for (ModContainer c : ModMenu.PARENT_MAP.keySet()) { + if (c.getMetadata().getId().equals(modId)) return true; + } + return false; + } + + private boolean isChild(String modId) { + for (List list : ModMenu.PARENT_MAP.values()) { + for (ModContainer c : list) { + if (c.getMetadata().getId().equals(modId)) return true; + } + } + return false; + } + + private boolean hasVisibleChildMods(ModContainer parent) { + List children = getChildrenFor(parent.getMetadata().getId()); + if (children == null) return false; + return !children.stream().allMatch(child -> { + String childId = child.getMetadata().getId(); + return ModMenu.LIBRARY_MODS.contains(childId) && !ModMenuConfig.SHOW_LIBRARIES.getValue().isTrue(); + }); + } + private void filter(String searchTerm, boolean refresh, boolean search) { this.clearEntries(); addedMods.clear(); @@ -125,31 +153,30 @@ private void filter(String searchTerm, boolean refresh, boolean search) { if (this.modContainerList == null || refresh) { this.modContainerList = new ArrayList<>(); modContainerList.addAll(mods); - this.modContainerList.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); + this.modContainerList.sort(ModMenuConfig.SORTING.getValue().getComparator()); } - boolean validSearch = ModListSearch.validSearchQuery(searchTerm); List matched = ModListSearch.search(parent, searchTerm, modContainerList); for (ModContainer container : matched) { ModMetadata metadata = container.getMetadata(); - String modId = metadata.getId(); - boolean library = ModMenu.LIBRARY_MODS.contains(modId); + String id = metadata.getId(); + boolean library = ModMenu.LIBRARY_MODS.contains(id); //Hide parent lib mods when the config is set to hide - if (library && !ModMenuConfigManager.getConfig().showLibraries()) { + if (library && !ModMenuConfig.SHOW_LIBRARIES.getValue().isTrue()) { continue; } - if (!ModMenu.PARENT_MAP.values().contains(container)) { - if (ModMenu.PARENT_MAP.keySet().contains(container)) { + if (!isChild(id)) { + if (isParent(id) && hasVisibleChildMods(container)) { //Add parent mods when not searching - List children = ModMenu.PARENT_MAP.get(container); - children.sort(ModMenuConfigManager.getConfig().getSorting().getComparator()); + List children = getChildrenFor(id); + children.sort(ModMenuConfig.SORTING.getValue().getComparator()); ParentEntry parent = new ParentEntry(minecraft, container, children, this); this.addEntry(parent); //Add children if they are meant to be shown - if (this.parent.showModChildren.contains(modId)) { + if (this.parent.showModChildren.contains(id)) { List validChildren = ModListSearch.search(this.parent, searchTerm, children); for (ModContainer child : validChildren) { this.addEntry(new ChildEntry(minecraft, child, parent, this, validChildren.indexOf(child) == validChildren.size() - 1)); @@ -178,6 +205,7 @@ private void filter(String searchTerm, boolean refresh, boolean search) { setScrollAmount(Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4))); } } + @Override protected void renderList(int x, int y, int mouseX, int mouseY, float delta) { int itemCount = this.getItemCount(); @@ -224,37 +252,111 @@ protected void renderList(int x, int y, int mouseX, int mouseY, float delta) { } GLRenderer.popFrame(); - } @Override - protected void updateScrollingState(double double_1, double double_2, int int_1) { - super.updateScrollingState(double_1, double_2, int_1); - this.scrolling = int_1 == 0 && double_1 >= (double) this.getScrollbarPosition() && double_1 < (double) (this.getScrollbarPosition() + 6); + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + this.updateScrollingState(mouseX, mouseY, mouseButton); + if (this.isMouseOver(mouseX, mouseY)) { + ModListEntry entry = this.getEntryAtPos(mouseX, mouseY); + if (entry != null) { + if (entry.getList().getFocused() == null || !entry.getList().getFocused().equals(entry)) { + this.setFocused(entry); + this.setSelected(entry); + this.setDragging(true); + } + entry.mouseClicked(mouseX, mouseY, mouseButton); + } else if (mouseButton == 0) { + this.clickedHeader( + (int) (mouseX - (double) (this.left + this.width / 2 - this.getRowWidth() / 2)), + (int) (mouseY - (double) this.top) + (int) this.getScrollAmount() - 4 + ); + } + } + } + + protected void updateScrollingState(double mouseX, double mouseY, int mouseButton) { + this.scrollBarDragging = false; + if (mouseButton == 0 + && mouseX >= (double) this.getScrollbarPosition() + && mouseX < (double) (this.getScrollbarPosition() + 6)) { + this.scrollBarDragging = true; + this.scrollBarGrabbedY = mouseY; + if (this.getMaxPosition() > 0) { + int trackHeight = this.bottom - this.top - 4; + int thumbHeight = Math.max(32, trackHeight * trackHeight / this.getMaxPosition()); + int thumbTop = (int) this.getScrollAmount() * (trackHeight - thumbHeight) / this.getMaxScroll() + this.top; + if (thumbTop < this.top) { + thumbTop = this.top; + } + if (thumbTop + thumbHeight > this.bottom) { + thumbTop = this.bottom - thumbHeight; + } + this.scrollBarGrabOffset = thumbTop - mouseY; + } else { + this.scrollBarGrabOffset = 0; + } + } + } + + protected void clickedHeader(int x, int y) { } @Override - public void mouseClicked(int double_1, int double_2, int int_1) { - this.updateScrollingState(double_1, double_2, int_1); - if (this.isMouseOver(double_1, double_2)) { - ModListEntry entry = this.getEntryAtPos(double_1, double_2); - if (entry != null) { - if (entry.list.getFocused() != null) { - if (!entry.list.getFocused().equals(entry)) { - this.setFocused(entry); - this.setSelected(entry); - this.setDragging(true); - super.mouseClicked(double_1, double_2, int_1); - } - } else { - this.setFocused(entry); - this.setSelected(entry); - this.setDragging(true); - super.mouseClicked(double_1, double_2, int_1); - } - } else if (int_1 == 0) { - this.clickedHeader((int) (double_1 - (double) (this.left + this.width / 2 - this.getRowWidth() / 2)), (int) (double_2 - (double) this.top) + (int) this.getScrollAmount() - 4); + public void mouseDragged(double mouseX, double mouseY, int mouseButton, double deltaX, double deltaY) { + if (this.scrollBarDragging && mouseButton == 0) { + int trackHeight = this.bottom - this.top - 4; + if (this.getMaxPosition() > 0 && trackHeight > 0) { + int thumbHeight = Math.max(32, trackHeight * trackHeight / this.getMaxPosition()); + double thumbTop = mouseY + this.scrollBarGrabOffset - this.top; + double scrollable = trackHeight - thumbHeight; + if (scrollable > 0) { + double amount = thumbTop / scrollable * this.getMaxScroll(); + this.setScrollAmount(amount); + } } + } else { + super.mouseDragged(mouseX, mouseY, mouseButton, deltaX, deltaY); + } + } + + @Override + public void mouseReleased(int mouseX, int mouseY, int mouseButton) { + this.scrollBarDragging = false; + super.mouseReleased(mouseX, mouseY, mouseButton); + } + + @Override + public void mouseScrolled(double mouseX, double mouseY, double scrollDelta) { + if (this.isMouseOver(mouseX, mouseY)) { + setScrollAmount(getScrollAmount() - scrollDelta * 20.0); + } + } + + public void ensureVisible(ModListEntry entry) { + int index = this.children().indexOf(entry); + if (index < 0) return; + int i = this.getRowTop(index); + int j = i - this.top - 4 - this.itemHeight; + if (j < 0) { + this.setScrollAmount(this.getScrollAmount() + j); + } + + int k = this.bottom - i - (this.itemHeight * 2); + if (k < 0) { + this.setScrollAmount(this.getScrollAmount() - k); + } + } + + public void keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == 264 || keyCode == 265) { + super.keyPressed(keyCode, scanCode, modifiers); + return; + } + + ModListEntry selected = getSelected(); + if (selected != null) { + selected.keyPressed(keyCode, scanCode, modifiers); } } @@ -275,7 +377,7 @@ public int getRowWidth() { } @Override - protected int getRowLeft() { + public int getRowLeft() { return left + 6; } @@ -287,7 +389,7 @@ public int getTop() { return this.top; } - public ModListScreen getParent() { + public ModsScreen getParent() { return parent; } @@ -305,15 +407,21 @@ public void close() { this.children().forEach(ModListEntry::deleteTexture); } - BufferedImage getCachedModIcon(Path path) { + public BufferedImage getCachedModIcon(Path path) { return this.modIconsCache.get(path); } - void cacheModIcon(Path path, BufferedImage tex) { + public void cacheModIcon(Path path, BufferedImage tex) { this.modIconsCache.put(path, tex); } - public Set getCurrentModSet() { - return addedMods; + public int getDisplayedCountFor(Set set) { + int count = 0; + for (ModListEntry c : children()) { + if (set.contains(c.getMetadata().getId())) { + count++; + } + } + return count; } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/widget/ModMenuButtonWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/ModMenuButtonWidget.java new file mode 100644 index 000000000..7dc182769 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/ModMenuButtonWidget.java @@ -0,0 +1,23 @@ +package io.github.prospector.modmenu.gui.widget; + +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.ModMenuConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ButtonElement; + +public class ModMenuButtonWidget extends ButtonElement { + public ModMenuButtonWidget(int id, int x, int y, int width, int height, String text) { + super(id, x, y, width, height, text); + } + + @Override + public void drawButton(Minecraft minecraft, int mouseX, int mouseY) { + super.drawButton(minecraft, mouseX, mouseY); + if (ModMenuConfig.BUTTON_UPDATE_BADGE.getValue() && ModMenu.areModUpdatesAvailable()) { + UpdateAvailableBadge.renderBadge( + this.xPosition + this.width - 13, + this.yPosition + (this.height / 2) - 4 + ); + } + } +} diff --git a/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/SmallModMenuButtonWidget.java similarity index 73% rename from src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/SmallModMenuButtonWidget.java index 244d9f575..0690355c7 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/ModMenuTexturedButtonWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/SmallModMenuButtonWidget.java @@ -1,29 +1,31 @@ -package io.github.prospector.modmenu.gui; - +package io.github.prospector.modmenu.gui.widget; +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.ModMenuConfig; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ButtonElement; import net.minecraft.client.render.font.FontRenderer; import net.minecraft.client.render.renderer.GLRenderer; import net.minecraft.client.render.renderer.Shaders; import net.minecraft.client.render.tessellator.TessellatorGeneral; +import net.minecraft.client.render.window.CursorShape; -public class ModMenuTexturedButtonWidget extends ButtonElement { +public class SmallModMenuButtonWidget extends ButtonElement { private final String texture; private final int u; private final int v; private final int uWidth; private final int vHeight; - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture) { + protected SmallModMenuButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture) { this(buttonId, x, y, width, height, u, v, texture, 256, 256); } - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight) { + protected SmallModMenuButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight) { this(buttonId, x, y, width, height, u, v, texture, uWidth, vHeight, ""); } - protected ModMenuTexturedButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight, String message) { + protected SmallModMenuButtonWidget(int buttonId, int x, int y, int width, int height, int u, int v, String texture, int uWidth, int vHeight, String message) { super(buttonId, x, y, width, height, message); this.uWidth = uWidth; this.vHeight = vHeight; @@ -51,6 +53,10 @@ public void render(Minecraft mc, int mouseX, int mouseY) { FontRenderer font = mc.font; boolean hovered = isHovered(mouseX, mouseY); + if (hovered && mc.currentScreen != null && this.enabled) { + mc.currentScreen.setDesiredCursor(CursorShape.HAND); + } + int adjustedV = this.v; if (!enabled) { adjustedV += this.height * 2; @@ -76,6 +82,15 @@ public void render(Minecraft mc, int mouseX, int mouseY) { GLRenderer.popFrame(); + if (this.id == 1000) { + if (ModMenuConfig.BUTTON_UPDATE_BADGE.getValue() && ModMenu.areModUpdatesAvailable()) { + UpdateAvailableBadge.renderBadge( + this.xPosition + this.width - 5, + this.yPosition - 3 + ); + } + } + this.mouseDragged(mc, mouseX, mouseY); if (!this.enabled) { this.drawStringCenteredNoShadow(font, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, 0xffa0a0a0); diff --git a/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/TextFieldWidget.java similarity index 99% rename from src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/TextFieldWidget.java index 0dd512c13..83c737e62 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/TextFieldWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/TextFieldWidget.java @@ -1,5 +1,4 @@ -package io.github.prospector.modmenu.gui; - +package io.github.prospector.modmenu.gui.widget; import io.github.prospector.modmenu.mixin.MinecraftAccessor; import io.github.prospector.modmenu.mixin.TextFieldEditorAccessor; @@ -70,7 +69,7 @@ public class TextFieldWidget extends Screen implements ITextField { private final @Nullable String emptyText; - public TextFieldWidget(int x, int y, int width, int height, String emptyText) { + public TextFieldWidget(int x, int y, int width, int height, @Nullable String emptyText) { this.x = x; this.y = y; this.width = width; @@ -432,9 +431,9 @@ public void mouseClicked(int mouseX, int mouseY, int mouseButton) { String s = trimStringToWidth(fontRenderer, text.substring(lineScrollOffset), getWidth()); setCursorPosition(trimStringToWidth(fontRenderer, s, i).length() + lineScrollOffset); //return true; - } else { - //return false; - } + } /*else { + return false; + }*/ } /** @@ -750,5 +749,4 @@ private static String trimStringToWidth(FontRenderer font, String text, int maxW width += font.stringWidth(Character.toString(text.charAt(reverse ? text.length() - 1 - length : length))); return reverse ? text.substring(text.length() - length) : text.substring(0, length); } - } diff --git a/src/main/java/io/github/prospector/modmenu/gui/widget/UpdateAvailableBadge.java b/src/main/java/io/github/prospector/modmenu/gui/widget/UpdateAvailableBadge.java new file mode 100644 index 000000000..839c29e91 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/UpdateAvailableBadge.java @@ -0,0 +1,40 @@ +package io.github.prospector.modmenu.gui.widget; + +import io.github.prospector.modmenu.ModMenu; +import net.minecraft.client.Minecraft; +import net.minecraft.client.render.tessellator.TessellatorGeneral; +import net.minecraft.client.render.renderer.GLRenderer; +import net.minecraft.client.render.renderer.Shaders; +import net.minecraft.client.render.renderer.State; +import org.lwjgl.opengl.GL11; + +public class UpdateAvailableBadge { + private static final String UPDATE_ICON = "/assets/" + ModMenu.MOD_ID + "/textures/gui/update_available.png"; + + public static void renderBadge(int x, int y) { + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.INTERFACE); + GLRenderer.enableState(State.BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + Minecraft.getMinecraft().textureManager.bindTexture(Minecraft.getMinecraft().textureManager.loadTexture(UPDATE_ICON)); + + int totalFrames = 6; + int frameHeight = 8; + int currentFrame = (int) (System.currentTimeMillis() / 200) % totalFrames; + + float vMin = (float) (currentFrame * frameHeight) / (totalFrames * frameHeight); + float vMax = (float) ((currentFrame + 1) * frameHeight) / (totalFrames * frameHeight); + + TessellatorGeneral t = GLRenderer.getTessellator(); + t.startDrawingQuads(); + t.addVertexWithUV(x, y + 8, 0, 0.0F, vMax); + t.addVertexWithUV(x + 8, y + 8, 0, 1.0F, vMax); + t.addVertexWithUV(x + 8, y, 0, 1.0F, vMin); + t.addVertexWithUV(x, y, 0, 0.0F, vMin); + t.draw(); + + GLRenderer.popFrame(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ChildEntry.java similarity index 55% rename from src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/entries/ChildEntry.java index 65831e077..985f8a2a7 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/ChildEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ChildEntry.java @@ -1,10 +1,9 @@ -package io.github.prospector.modmenu.gui.entries; +package io.github.prospector.modmenu.gui.widget.entries; - -import io.github.prospector.modmenu.gui.ModListEntry; -import io.github.prospector.modmenu.gui.ModListWidget; +import io.github.prospector.modmenu.gui.widget.ModListWidget; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.Minecraft; +import org.lwjgl.input.Keyboard; public class ChildEntry extends ModListEntry { private boolean bottomChild; @@ -19,10 +18,20 @@ public ChildEntry(Minecraft mc, ModContainer container, ParentEntry parent, ModL @Override public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { super.render(index, y, x, rowWidth, rowHeight, mouseX, mouseY, isSelected, delta); - x += 4; + x -= 2; int color = 0xFFA0A0A0; - drawRect(x, y - 2, x + 1, y + (bottomChild ? rowHeight / 2 : rowHeight + 2), color); - drawRect(x, y + rowHeight / 2, x + 7, y + rowHeight / 2 + 1, color); + drawRect(x, y - 2, x + 1, y + (bottomChild ? rowHeight / 2 : rowHeight + 2), color); + drawRect(x, y + rowHeight / 2, x + 7, y + rowHeight / 2 + 1, color); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == Keyboard.KEY_LEFT) { + list.select(parent); + list.ensureVisible(parent); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); } @Override diff --git a/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/EntryListWidget.java similarity index 91% rename from src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/entries/EntryListWidget.java index 4dfdb5788..3789550bd 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/EntryListWidget.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/EntryListWidget.java @@ -1,5 +1,4 @@ -package io.github.prospector.modmenu.gui; - +package io.github.prospector.modmenu.gui.widget.entries; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; @@ -20,7 +19,7 @@ import java.util.List; import java.util.Objects; -@SuppressWarnings({"unchecked", "unused"}) +@SuppressWarnings({"unused"}) @Environment(EnvType.CLIENT) public abstract class EntryListWidget> extends Screen { protected static final int DRAG_OUTSIDE = -2; @@ -39,7 +38,7 @@ public abstract class EntryListWidget> extend protected boolean renderSelection = true; protected boolean renderHeader; protected int headerHeight; - private boolean scrolling; + protected boolean scrolling; private E selected; private E focused; @@ -160,7 +159,6 @@ protected void clickedHeader(int i, int j) { protected void renderHeader(int i, int j) { } - public void renderBackground() { } @@ -168,7 +166,6 @@ protected void renderDecorations(int i, int j) { } public void render(int i, int j, float f) { - oldY = j; this.renderBackground(); int k = this.getScrollbarPosition(); int l = k + 6; @@ -274,7 +271,7 @@ protected void centerScrollOn(E entry) { this.setScrollAmount(this.children().indexOf(entry) * this.itemHeight + this.itemHeight / 2 - (this.bottom - this.top) / 2); } - protected void ensureVisible(E entry) { + public void ensureVisible(E entry) { int i = this.getRowTop(this.children().indexOf(entry)); int j = i - this.top - 4 - this.itemHeight; if (j < 0) { @@ -305,7 +302,7 @@ public void setScrollAmount(double d) { this.scrollAmount = d; } - private int getMaxScroll() { + protected int getMaxScroll() { return Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); } @@ -321,25 +318,25 @@ protected int getScrollbarPosition() { return this.width / 2 + 124; } - @Override + @Override public void mouseClicked(int d, int e, int i) { this.updateScrollingState(d, e, i); if (this.isMouseOver(d, e)) { E entry = this.getEntryAtPosition(d, e); if (entry != null) { - if (entry.list.getFocused() != null) { - if (!entry.list.getFocused().equals(entry)) { - this.focused = entry; - this.selected = entry; - this.dragging = true; - super.mouseClicked(d, e, i); - } - } else { - this.focused = entry; - this.selected = entry; - this.dragging = true; - super.mouseClicked(d, e, i); - } + if (entry.list.getFocused() != null) { + if (!entry.list.getFocused().equals(entry)) { + this.focused = entry; + this.selected = entry; + this.dragging = true; + super.mouseClicked(d, e, i); + } + } else { + this.focused = entry; + this.selected = entry; + this.dragging = true; + super.mouseClicked(d, e, i); + } } else if (i == 0) { this.clickedHeader((int)(d - (double)(this.left + this.width / 2 - this.getRowWidth() / 2)), (int)(e - (double)this.top) + (int)this.getScrollAmount() - 4); } @@ -353,32 +350,22 @@ public boolean mouseReleased(double d, double e, int i) { return false; } - double oldY = -1; - public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseDX, double mouseDY) { - if (mouseButton == 0 && isMouseOver(mouseX, mouseY)) { - setScrollAmount(getScrollAmount() - mouseY + oldY); - return true; - } else { - return false; + public void mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseDX, double mouseDY) { + if (this.scrolling) { + setScrollAmount(getScrollAmount() - mouseDY); } } - public boolean mouseScrolled(double d, double e, double f) { + public void mouseScrolled(double d, double e, double f) { this.setScrollAmount(this.getScrollAmount() - f * (double)this.itemHeight / 2.0D); - return true; } - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + public void keyPressed(int keyCode, int scanCode, int modifiers) { if (this.getFocused() != null && this.getFocused().keyPressed(keyCode, scanCode, modifiers)) { - return true; } else if (keyCode == 264) { this.moveSelection(1); - return true; } else if (keyCode == 265) { this.moveSelection(-1); - return true; - } else { - return false; } } @@ -555,8 +542,7 @@ public void mouseMoved(double d, double e) { public void mouseClicked(int d, int e, int f) { } - public boolean mouseReleased(double d, double e, int i) { - return false; + public void mouseReleased(double d, double e, int i) { } public boolean mouseScrolled(double d, double e, double f) { @@ -578,6 +564,5 @@ public boolean charTyped(char c, int i) { public boolean changeFocus(boolean bl) { return false; } - } } diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/IndependentEntry.java similarity index 61% rename from src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/entries/IndependentEntry.java index cecc1aaa8..082cacec7 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/IndependentEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/IndependentEntry.java @@ -1,8 +1,6 @@ -package io.github.prospector.modmenu.gui.entries; +package io.github.prospector.modmenu.gui.widget.entries; - -import io.github.prospector.modmenu.gui.ModListEntry; -import io.github.prospector.modmenu.gui.ModListWidget; +import io.github.prospector.modmenu.gui.widget.ModListWidget; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.Minecraft; diff --git a/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ModListEntry.java b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ModListEntry.java new file mode 100644 index 000000000..b0d46b369 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ModListEntry.java @@ -0,0 +1,324 @@ +package io.github.prospector.modmenu.gui.widget.entries; + +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.gui.widget.AlwaysSelectedEntryListWidget; +import io.github.prospector.modmenu.gui.widget.ModListWidget; +import io.github.prospector.modmenu.gui.widget.UpdateAvailableBadge; +import io.github.prospector.modmenu.util.*; +import io.github.prospector.modmenu.util.mod.Mod; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Screen; +import net.minecraft.client.render.font.FontRenderer; +import net.minecraft.client.render.renderer.GLRenderer; +import net.minecraft.client.render.renderer.Shaders; +import net.minecraft.client.render.renderer.State; +import net.minecraft.client.render.tessellator.TessellatorGeneral; +import net.minecraft.client.render.window.CursorShape; +import net.minecraft.core.Global; +import net.minecraft.core.lang.I18n; +import org.lwjgl.opengl.GL11; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class ModListEntry extends AlwaysSelectedEntryListWidget.Entry { + private static final Logger LOGGER = LoggerFactory.getLogger(ModMenu.MOD_ID); + public static final String UNKNOWN_ICON = "/assets/" + ModMenu.MOD_ID + "/unknown_icon.png"; + + protected final Minecraft client; + protected final ModContainer container; + protected final ModMetadata metadata; + protected final ModListWidget list; + public static final int FULL_ICON_SIZE = 32; + public static final int COMPACT_ICON_SIZE = 19; + protected Integer iconLocation; + + public ModListEntry(Minecraft mc, ModContainer container, ModListWidget list) { + this.container = container; + this.list = list; + this.metadata = container.getMetadata(); + this.client = mc; + } + + public ModContainer getContainer() { + return container; + } + + public ModListWidget getList() { + return list; + } + + @Override + public void render(int index, int y, int x, int rowWidth, int rowHeight, int mouseX, int mouseY, boolean isSelected, float delta) { + I18n i18n = I18n.getInstance(); + String id = metadata.getId(); + + x += getXOffset(); + rowWidth -= getXOffset(); + int iconSize = ModMenuConfig.COMPACT_LIST.getValue() ? COMPACT_ICON_SIZE : FULL_ICON_SIZE; + + GLRenderer.pushFrame(); + + if ("java".equals(id)) { + DrawingUtil.drawRandomVersionBackground(container, x, y, iconSize, iconSize); + } + + GLRenderer.setColor4f(1, 1, 1, 1); + + this.bindIconTexture(); + internalRender(y, x, iconSize); + + if (!(this instanceof ParentEntry) && (ModMenu.hasConfigScreenFactory(id) || ModMenu.hasLegacyConfigScreenTask(id)) && ModMenuConfig.QUICK_CONFIGURE.getValue()) { + boolean hovered = mouseX >= x && mouseY >= y && mouseX < x + rowWidth && mouseY < y + iconSize; + + if (hovered) { + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.COLOR); + GLRenderer.enableState(State.BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GLRenderer.setColor4f(1, 1, 1, 0.4f); + + TessellatorGeneral t = GLRenderer.getTessellator(); + t.startDrawingQuads(); + t.addVertexWithUV(x, y + iconSize, 0, 0, 0); + t.addVertexWithUV(x + iconSize, y + iconSize, 0, 1, 0); + t.addVertexWithUV(x + iconSize, y, 0, 1, 1); + t.addVertexWithUV(x, y, 0, 0, 1); + t.draw(); + GLRenderer.popFrame(); + + boolean hoveringIcon = mouseX < x + iconSize && mouseY < y + iconSize; + + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.INTERFACE); + GLRenderer.setColor4f(1, 1, 1, 1); + + client.textureManager.bindTexture(client.textureManager.loadTexture("/assets/modmenu/textures/gui/mod_configuration.png")); + + float maxUV = ModMenuConfig.COMPACT_LIST.getValue() ? (int) (256 / (FULL_ICON_SIZE / (double) COMPACT_ICON_SIZE)) : 256; + float vMin = hoveringIcon ? iconSize / maxUV : 0.0f; + float vMax = hoveringIcon ? (iconSize * 2) / maxUV : iconSize / maxUV; + float uMax = iconSize / maxUV; + + t.startDrawingQuads(); + t.addVertexWithUV(x, y + iconSize, 0, 0.0f, vMax); + t.addVertexWithUV(x + iconSize, y + iconSize, 0, uMax, vMax); + t.addVertexWithUV(x + iconSize, y, 0, uMax, vMin); + t.addVertexWithUV(x, y, 0, 0.0f, vMin); + t.draw(); + GLRenderer.popFrame(); + + if (hoveringIcon) { + list.getParent().setDesiredCursor(CursorShape.HAND); + } + } + } + + FontRenderer font = this.fontRenderer; + + String translationKey = "modmenu.nameTranslation." + id; + String translatedName = i18n.translateKey(translationKey); + String name; + if (!translatedName.equals(translationKey) && ModMenuConfig.TRANSLATE_NAMES.getValue()) { + name = translatedName; + } else { + name = metadata.getName(); + } + String trimmedName = name; + int maxNameWidth = rowWidth - iconSize - 3; + trimmedName = ModsScreen.getString(font, name, trimmedName, maxNameWidth); + this.drawStringNoShadow(font, trimmedName, x + iconSize + 3, y + 1, 0xFFFFFF); + + int badgeX = x + iconSize + 3 + font.stringWidth(trimmedName) + 2; + int updateBadgeOffset = 0; + + if (ModMenuConfig.UPDATE_CHECKER.getValue() && ModMenuConfig.BUTTON_UPDATE_BADGE.getValue()) { + Mod modInstance = ModMenu.MODS.get(id); + if (modInstance != null) { + UpdateInfo updateInfo = modInstance.getUpdateInfo(); + if (updateInfo != null && updateInfo.isUpdateAvailable()) { + UpdateAvailableBadge.renderBadge(badgeX, y); + updateBadgeOffset = 11; + } + } + } + + new BadgeRenderer(client, badgeX + updateBadgeOffset, y, x + rowWidth, container, list.getParent()).draw(mouseX, mouseY); + + int lineHeight = 9; + if (ModMenuConfig.COMPACT_LIST.getValue()) { + String versionString = "minecraft".equals(id) ? Global.VERSION : metadata.getVersion().getFriendlyString(); + String prefixedVersion = VersionUtil.getPrefixedVersion(versionString); + RenderUtils.INSTANCE.drawWrappedString(this, prefixedVersion, x + iconSize + 3, y + lineHeight + 2, rowWidth - iconSize - 7, 2, 0x808080); + } else { + String description; + + String descKey = "modmenu.descriptionTranslation." + id; + String translatedDesc = i18n.translateKey(descKey); + + String sumKey = "modmenu.summaryTranslation." + id; + String translatedSum = i18n.translateKey(sumKey); + + if (!translatedSum.equals(sumKey) && ModMenuConfig.TRANSLATE_DESCRIPTIONS.getValue()) { + description = translatedSum; + } else if (!translatedDesc.equals(descKey) && ModMenuConfig.TRANSLATE_DESCRIPTIONS.getValue()) { + description = translatedDesc; + } else { + description = metadata.getDescription(); + } + + if ("java".equals(id)) { + String vendor = System.getProperty("java.vendor", "Unknown Vendor"); + String javaDist = i18n.translateKeyAndFormat("modmenu.javaDistributionName", vendor); + description = (description != null ? description : "") + "\n" + javaDist; + } + + if (description != null) { + int textX = x + iconSize + 3 + 4; + int textY = y + lineHeight + 2; + int maxWidth = rowWidth - iconSize - 7; + int maxLines = 2; + + List allLines = new ArrayList<>(); + String[] paragraphs = description.split("\r?\n"); + for (String paragraph : paragraphs) { + allLines.addAll(RenderUtils.INSTANCE.wrapStringToWidthAsList(font, paragraph, maxWidth)); + } + + if (allLines.size() > maxLines) { + allLines = new ArrayList<>(allLines.subList(0, maxLines)); + String last = allLines.get(maxLines - 1); + String ellipsis = "..."; + int ellipsisWidth = font.stringWidth(ellipsis); + while (!last.isEmpty() && font.stringWidth(last) + ellipsisWidth > maxWidth) { + last = last.substring(0, last.length() - 1); + } + allLines.set(maxLines - 1, last + ellipsis); + } + + for (int i = 0; i < allLines.size(); i++) { + this.drawStringNoShadow(font, allLines.get(i), textX, textY + (i * lineHeight), 0x808080); + } + } + } + + GLRenderer.popFrame(); + } + + public static void internalRender(int y, int x, int iconSize) { + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.INTERFACE); + GLRenderer.enableState(State.BLEND); + + TessellatorGeneral t = GLRenderer.getTessellator(); + t.startDrawingQuads(); + t.addVertexWithUV(x, y, 0, 0, 0); + t.addVertexWithUV(x, y + iconSize, 0, 0, 1); + t.addVertexWithUV(x + iconSize, y + iconSize, 0, 1, 1); + t.addVertexWithUV(x + iconSize, y, 0, 1, 0); + t.draw(); + + GLRenderer.popFrame(); + } + + private BufferedImage createIcon() { + try { + Path path = container.findPath(metadata.getIconPath(0).orElse("assets/" + metadata.getId() + "/icon.png")).orElse(null); + BufferedImage cached = path != null ? this.list.getCachedModIcon(path) : null; + if (cached != null) { + return cached; + } + if (path == null || !Files.exists(path)) { + ModContainer modMenu = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID).orElseThrow(IllegalAccessError::new); + String icon; + if (metadata.getId().equals("minecraft")) { + icon = "assets/" + ModMenu.MOD_ID + "/mc_icon.png"; + } else if (metadata.getId().equals("java")) { + icon = "assets/" + ModMenu.MOD_ID + "/java_icon.png"; + } else { + icon = UNKNOWN_ICON; + } + path = modMenu.findPath(icon).orElse(null); + } + if (path != null) { + cached = this.list.getCachedModIcon(path); + if (cached != null) { + return cached; + } + try (InputStream inputStream = Files.newInputStream(path)) { + BufferedImage image = ImageIO.read(Objects.requireNonNull(inputStream)); + if (image.getHeight() != image.getWidth()) + throw new IllegalStateException("Must be square icon"); + this.list.cacheModIcon(path, image); + return image; + } + } + } catch (Throwable t) { + LOGGER.error("Invalid icon for mod {}", this.container.getMetadata().getName(), t); + return null; + } + return null; + } + + @Override + public void mouseClicked(int v, int v1, int i) { + list.select(this); + + if (i == 0 && !(this instanceof ParentEntry) && ModMenuConfig.QUICK_CONFIGURE.getValue()) { + String id = metadata.getId(); + if (ModMenu.hasConfigScreenFactory(id) || ModMenu.hasLegacyConfigScreenTask(id)) { + int iconSize = ModMenuConfig.COMPACT_LIST.getValue() ? COMPACT_ICON_SIZE : FULL_ICON_SIZE; + int iconLeft = list.getRowLeft() + getXOffset(); + if (v >= iconLeft && v < iconLeft + iconSize) { + final Screen screen = ModMenu.getConfigScreen(id, list.getParent()); + if (screen != null) { + client.displayScreen(screen); + } else { + ModMenu.openConfigScreen(id); + } + } + } + } + } + + public ModMetadata getMetadata() { + return metadata; + } + + public void bindIconTexture() { + if (this.iconLocation == null) { + BufferedImage icon = this.createIcon(); + if (icon != null) { + this.iconLocation = this.client.textureManager.loadBufferedTexture(icon).id(); + } else { + this.iconLocation = this.client.textureManager.loadTexture(UNKNOWN_ICON).id(); + } + } + this.client.textureManager.bindTexture(this.iconLocation); + } + + public void deleteTexture() { + if (iconLocation != null) { + this.client.textureManager.idToTextureMap.remove(iconLocation); + GL11.glDeleteTextures(iconLocation); + } + } + + public int getXOffset() { + return 0; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ParentEntry.java similarity index 54% rename from src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java rename to src/main/java/io/github/prospector/modmenu/gui/widget/entries/ParentEntry.java index bcfda4212..48f64b554 100644 --- a/src/main/java/io/github/prospector/modmenu/gui/entries/ParentEntry.java +++ b/src/main/java/io/github/prospector/modmenu/gui/widget/entries/ParentEntry.java @@ -1,9 +1,8 @@ -package io.github.prospector.modmenu.gui.entries; - +package io.github.prospector.modmenu.gui.widget.entries; import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.gui.ModListEntry; -import io.github.prospector.modmenu.gui.ModListWidget; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.widget.ModListWidget; import io.github.prospector.modmenu.util.ModListSearch; import net.fabricmc.loader.api.ModContainer; import net.minecraft.client.Minecraft; @@ -22,6 +21,7 @@ public class ParentEntry extends ModListEntry { protected List children; protected ModListWidget list; protected boolean hoveringIcon = false; + protected long sinceLastClick = 0; public ParentEntry(Minecraft mc, ModContainer parent, List children, ModListWidget list) { super(mc, parent, list); @@ -35,39 +35,52 @@ public void render(int index, int y, int x, int rowWidth, int rowHeight, int mou FontRenderer font = this.fontRenderer; int childrenBadgeHeight = 9; int childrenBadgeWidth = 9; - int children = ModListSearch.search(list.getParent(), list.getParent().getSearchInput(), getChildren()).size(); - int childrenWidth = font.stringWidth(Integer.toString(children)) - 1; + int shownChildren = ModListSearch.search(list.getParent(), list.getParent().getSearchInput(), getChildren()).size(); + String childrenStr = shownChildren == children.size() + ? Integer.toString(shownChildren) + : shownChildren + "/" + children.size(); + int childrenWidth = font.stringWidth(childrenStr) - 1; if (childrenBadgeWidth < childrenWidth + 4) { childrenBadgeWidth = childrenWidth + 4; } - int childrenBadgeX = x + 32 - childrenBadgeWidth; - int childrenBadgeY = y + 32 - childrenBadgeHeight; - int childrenOutlineColor = 0x8810d098; - int childrenFillColor = 0x88046146; + int iconSize = ModMenuConfig.COMPACT_LIST.getValue() ? COMPACT_ICON_SIZE : FULL_ICON_SIZE; + int childrenBadgeX = x + iconSize - childrenBadgeWidth; + int childrenBadgeY = y + iconSize - childrenBadgeHeight; + + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.INTERFACE); + GLRenderer.setColor4f(1, 1, 1, 1); + + int childrenOutlineColor = 0xff107454; + int childrenFillColor = 0xff093929; drawRect(childrenBadgeX + 1, childrenBadgeY, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, childrenOutlineColor); drawRect(childrenBadgeX, childrenBadgeY + 1, childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, childrenOutlineColor); drawRect(childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth, childrenBadgeY + childrenBadgeHeight - 1, childrenOutlineColor); drawRect(childrenBadgeX + 1, childrenBadgeY + 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight - 1, childrenFillColor); drawRect(childrenBadgeX + 1, childrenBadgeY + childrenBadgeHeight - 1, childrenBadgeX + childrenBadgeWidth - 1, childrenBadgeY + childrenBadgeHeight, childrenOutlineColor); - this.drawStringNoShadow(font, Integer.toString(children), childrenBadgeX + childrenBadgeWidth / 2 - childrenWidth / 2, childrenBadgeY + 1, 0xCACACA); - this.hoveringIcon = mouseX >= x - 1 && mouseX <= x - 1 + 32 && mouseY >= y - 1 && mouseY <= y - 1 + 32; + this.drawStringNoShadow(font, childrenStr, childrenBadgeX + childrenBadgeWidth / 2 - childrenWidth / 2, childrenBadgeY + 1, 0xFFCACACA); + + GLRenderer.popFrame(); + + this.hoveringIcon = mouseX >= x - 1 && mouseX <= x - 1 + iconSize && mouseY >= y - 1 && mouseY <= y - 1 + iconSize; if (isMouseOver(mouseX, mouseY)) { - drawRect(x, y, x + 32, y + 32, 0xA0909090); + drawRect(x, y, x + iconSize, y + iconSize, 0xA0909090); GLRenderer.pushFrame(); GLRenderer.setShader(Shaders.INTERFACE); GLRenderer.setColor4f(1, 1, 1, 1); - int xOffset = list.getParent().showModChildren.contains(getMetadata().getId()) ? 32 : 0; - int yOffset = hoveringIcon ? 32 : 0; + int xOffset = list.getParent().showModChildren.contains(getMetadata().getId()) ? iconSize : 0; + int yOffset = hoveringIcon ? iconSize : 0; + float maxUV = ModMenuConfig.COMPACT_LIST.getValue() ? (int) (256 / (FULL_ICON_SIZE / (double) COMPACT_ICON_SIZE)) : 256; this.client.textureManager.loadTexture(PARENT_MOD_TEXTURE).bind(); TessellatorGeneral t = GLRenderer.getTessellator(); t.startDrawingQuads(); - t.addVertexWithUV(x, y, 0, xOffset / 256f, yOffset / 256f); - t.addVertexWithUV(x, y + 32, 0, xOffset / 256f, (yOffset + 32) / 256f); - t.addVertexWithUV(x + 32, y + 32, 0, (xOffset + 32) / 256f, (yOffset + 32) / 256f); - t.addVertexWithUV(x + 32, y, 0, (xOffset + 32) / 256f, yOffset / 256f); + t.addVertexWithUV(x, y, 0, xOffset / maxUV, yOffset / maxUV); + t.addVertexWithUV(x, y + iconSize, 0, xOffset / maxUV, (yOffset + iconSize) / maxUV); + t.addVertexWithUV(x + iconSize, y + iconSize, 0, (xOffset + iconSize) / maxUV, (yOffset + iconSize) / maxUV); + t.addVertexWithUV(x + iconSize, y, 0, (xOffset + iconSize) / maxUV, yOffset / maxUV); t.draw(); GLRenderer.popFrame(); @@ -76,29 +89,50 @@ public void render(int index, int y, int x, int rowWidth, int rowHeight, int mou @Override public void mouseClicked(int mouseX, int mouseY, int i) { - if (hoveringIcon) { - String id = getMetadata().getId(); - if (list.getParent().showModChildren.contains(id)) { - list.getParent().showModChildren.remove(id); - } else { - list.getParent().showModChildren.add(id); - } - list.filter(list.getParent().getSearchInput(), false); + int iconSize = ModMenuConfig.COMPACT_LIST.getValue() ? COMPACT_ICON_SIZE : FULL_ICON_SIZE; + int iconLeft = list.getRowLeft() + getXOffset(); + if (mouseX >= iconLeft && mouseX < iconLeft + iconSize) { + toggleChildren(); + return; + } else if (!ModMenuConfig.QUICK_CONFIGURE.getValue() && System.currentTimeMillis() - sinceLastClick < 250) { + toggleChildren(); + return; } + this.sinceLastClick = System.currentTimeMillis(); super.mouseClicked(mouseX, mouseY, i); } + private void toggleChildren() { + String id = getMetadata().getId(); + if (list.getParent().showModChildren.contains(id)) { + list.getParent().showModChildren.remove(id); + } else { + list.getParent().showModChildren.add(id); + } + list.filter(list.getParent().getSearchInput(), false); + } + @Override public boolean keyPressed(int int_1, int int_2, int int_3) { + String id = getMetadata().getId(); if (int_1 == Keyboard.KEY_RETURN) { - String id = getMetadata().getId(); + toggleChildren(); + return true; + } else if (int_1 == Keyboard.KEY_LEFT) { if (list.getParent().showModChildren.contains(id)) { list.getParent().showModChildren.remove(id); - } else { - list.getParent().showModChildren.add(id); + list.filter(list.getParent().getSearchInput(), false); } - list.filter(list.getParent().getSearchInput(), false); return true; + } else if (int_1 == Keyboard.KEY_RIGHT) { + if (!list.getParent().showModChildren.contains(id)) { + list.getParent().showModChildren.add(id); + list.filter(list.getParent().getSearchInput(), false); + return true; + } else { + list.keyPressed(264, 0, 0); + return true; + } } return super.keyPressed(int_1, int_2, int_3); } diff --git a/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java b/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java index 4496da662..d57f10fbf 100644 --- a/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java +++ b/src/main/java/io/github/prospector/modmenu/impl/ModMenuApiImpl.java @@ -1,17 +1,12 @@ -package io.github.prospector.modmenu.impl; - - -import io.github.prospector.modmenu.api.ModMenuApi; -import io.github.prospector.modmenu.util.TriConsumer; - -public class ModMenuApiImpl implements ModMenuApi { - @Override - public String getModId() { - return "modmenu"; - } - - @Override - public void attachCustomBadges(TriConsumer consumer) { - consumer.accept("Mod Menu", 0xff7a2b7c, 0xff510d54); - } -} +package io.github.prospector.modmenu.impl; + +import io.github.prospector.modmenu.api.ModMenuApi; +import io.github.prospector.modmenu.api.ConfigScreenFactory; +import io.github.prospector.modmenu.gui.ModMenuOptionsScreen; + +public class ModMenuApiImpl implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return ModMenuOptionsScreen::new; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/ButtonElementAccessor.java similarity index 89% rename from src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java rename to src/main/java/io/github/prospector/modmenu/mixin/ButtonElementAccessor.java index f7fb5fe43..37f2f1980 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/GuiButtonAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/ButtonElementAccessor.java @@ -1,12 +1,11 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.gui.ButtonElement; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = ButtonElement.class, remap = false) -public interface GuiButtonAccessor { +public interface ButtonElementAccessor { @Accessor int getWidth(); diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java b/src/main/java/io/github/prospector/modmenu/mixin/I18nMixin.java similarity index 88% rename from src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java rename to src/main/java/io/github/prospector/modmenu/mixin/I18nMixin.java index f82548d3f..1734e2330 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinI18n.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/I18nMixin.java @@ -16,7 +16,7 @@ import java.util.Properties; @Mixin(value = I18n.class, remap = false) -public class MixinI18n { +public class I18nMixin { @Shadow private Language currentLanguage; @Shadow @@ -24,13 +24,10 @@ public static InputStream getResourceAsStream(String path) { throw new AssertionError(); } - @Inject( - method = "reload", - at = @At("TAIL") - ) + @Inject(method = "reload", at = @At("TAIL")) private void modmenu$addLangEntries(String languageCode, CallbackInfo ci) { Properties entries = ((LanguageAccessor) currentLanguage).getEntries(); - String lang = "/lang/modmenu/" + currentLanguage.getId() + ".lang"; + String lang = "/assets/modmenu/lang/" + currentLanguage.getId() + ".lang"; try (InputStream stream = getResourceAsStream(lang)) { if (stream != null) { InputStreamReader r = new InputStreamReader(stream, StandardCharsets.UTF_8); @@ -39,7 +36,7 @@ public static InputStream getResourceAsStream(String path) { } catch (IOException e) { ModMenu.LOGGER.error("Failed to load {} language.", currentLanguage.getId(), e); } - String defaultLang = "/lang/modmenu/en_US.lang"; + String defaultLang = "/assets/modmenu/lang/en_US.lang"; try (InputStream stream = getResourceAsStream(defaultLang)) { if (stream != null) { InputStreamReader r = new InputStreamReader(stream, StandardCharsets.UTF_8); diff --git a/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java index 6ea8a752f..83356ebba 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/LanguageAccessor.java @@ -6,10 +6,7 @@ import java.util.Properties; -@Mixin( - value = Language.class, - remap = false -) +@Mixin(value = Language.class, remap = false) public interface LanguageAccessor { @Accessor Properties getEntries(); diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java index fd5e6eb41..3dedb1ac1 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/MinecraftAccessor.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.Minecraft; import net.minecraft.core.enums.EnumOS; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java deleted file mode 100644 index 50678bb64..000000000 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiIngameMenu.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.prospector.modmenu.mixin; - - -import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.gui.ModListScreen; -import io.github.prospector.modmenu.gui.ModMenuButtonWidget; -import net.minecraft.client.gui.ButtonElement; -import net.minecraft.client.gui.Screen; -import net.minecraft.client.gui.ScreenPause; -import net.minecraft.core.lang.I18n; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(value = ScreenPause.class, remap = false) -public class MixinGuiIngameMenu extends Screen { - @SuppressWarnings("unchecked") - @Inject(at = @At("RETURN"), method = "init") - public void modmenu$drawMenuButton(CallbackInfo info) { - I18n i18n = I18n.getInstance(); - String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); - this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 - 100, this.height / 4 + 72 - 16, 200, 20, buttonText)); - } - - @Inject(method = "buttonClicked", at = @At("HEAD")) - private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { - if (button.id == 100) { - mc.displayScreen(new ModListScreen(this)); - } - } -} diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java b/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java deleted file mode 100644 index 6242108d3..000000000 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinGuiMainMenu.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.prospector.modmenu.mixin; - - -import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.gui.ModListScreen; -import io.github.prospector.modmenu.gui.ModMenuButtonWidget; -import net.minecraft.client.gui.ButtonElement; -import net.minecraft.client.gui.Screen; -import net.minecraft.client.gui.ScreenMainMenu; -import net.minecraft.core.lang.I18n; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.Random; - -@Mixin(value = ScreenMainMenu.class, remap = false) -public class MixinGuiMainMenu extends Screen { - @Inject(at = @At("RETURN"), method = "init") - public void modmenu$drawMenuButton(CallbackInfo info) { - I18n i18n = I18n.getInstance(); - ButtonElement texturePackButton = this.buttons.get(2); - texturePackButton.displayString = new Random().nextInt(1000) == 0 ? "Twin Peaks" : i18n.translateKey("gui.main_menu.button.asset_packs"); - int newWidth = ((GuiButtonAccessor) texturePackButton).getWidth() / 2 - 1; - ((GuiButtonAccessor) texturePackButton).setWidth(newWidth); - String buttonText = i18n.translateKey("modmenu.title") + " " + i18n.translateKeyAndFormat("modmenu.loaded", ModMenu.getFormattedModCount()); - this.buttons.add(new ModMenuButtonWidget(100, this.width / 2 + 2, texturePackButton.yPosition, newWidth, 20, buttonText)); - } - - @Inject(method = "buttonClicked", at = @At("HEAD")) - private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { - if (button.id == 100) { - mc.displayScreen(new ModListScreen(this)); - } - } -} diff --git a/src/main/java/io/github/prospector/modmenu/mixin/ScreenMainMenuMixin.java b/src/main/java/io/github/prospector/modmenu/mixin/ScreenMainMenuMixin.java new file mode 100644 index 000000000..6ef598e54 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/mixin/ScreenMainMenuMixin.java @@ -0,0 +1,75 @@ +package io.github.prospector.modmenu.mixin; + +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.gui.widget.ModMenuButtonWidget; +import io.github.prospector.modmenu.gui.widget.SmallModMenuButtonWidget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ButtonElement; +import net.minecraft.client.gui.ScreenMainMenu; +import net.minecraft.client.option.GameSettings; +import net.minecraft.core.lang.I18n; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Random; + +@Mixin(value = ScreenMainMenu.class, remap = false) +public class ScreenMainMenuMixin { + @Unique + I18n i18n = I18n.getInstance(); + @Inject(at = @At("RETURN"), method = "init") + public void modmenu$drawMenuButton(CallbackInfo info) { + ScreenMainMenu menu = (ScreenMainMenu) (Object) this; + ButtonElement texturePackButton = menu.buttons.get(2); + if (ModMenuConfig.EASTER_EGGS.getValue()) { + texturePackButton.displayString = new Random().nextInt(1000) == 0 ? "Twin Peaks" : i18n.translateKey("gui.main_menu.button.asset_packs"); + } + if (ModMenuConfig.MODS_BUTTON_STYLE.getValue() == ModMenuConfig.TitleMenuButtonStyle.INSERT) { + int index = menu.buttons.indexOf(texturePackButton); + if (index != -1) { + String buttonText = ModMenu.createModsButtonText(true); + ButtonElementAccessor accessor = (ButtonElementAccessor) texturePackButton; + ModMenuButtonWidget modButton = new ModMenuButtonWidget(1000, texturePackButton.xPosition, texturePackButton.yPosition, accessor.getWidth(), accessor.getHeight(), buttonText); + menu.buttons.set(index, modButton); + } + } else { + menu.buttons.removeIf(b -> b.id == 11); + menu.buttons.add(new SmallModMenuButtonWidget( + 1000, menu.width / 2 - 124, menu.height / 4 + 132, 20, 20, 0, 0, + "/assets/" + ModMenu.MOD_ID + "/textures/gui/open_button.png", 32, 64 + ) {}); + } + } + + @Inject(method = "buttonClicked", at = @At("HEAD")) + private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { + if (button.id == 1000) { + Minecraft.getMinecraft().displayScreen(new ModsScreen((ScreenMainMenu) (Object) this)); + } + } + + @Inject(method = "render", at = @At("RETURN")) + private void modmenu$renderModCount(int mx, int my, float partialTick, CallbackInfo ci) { + if (ModMenuConfig.MOD_COUNT_LOCATION.getValue().isOnTitleScreen()) { + ScreenMainMenu menu = (ScreenMainMenu) (Object) this; + String count = ModMenu.getDisplayedModCount(); + String specificKey = "modmenu.mods." + count; + String nKey = i18n.translateKey(specificKey); + String key = !nKey.equals(specificKey) ? specificKey : "modmenu.mods.n"; + String secretKey = specificKey + ".secret"; + if (ModMenuConfig.EASTER_EGGS.getValue() && !i18n.translateKey(secretKey).equals(secretKey)) { + key = secretKey; + } + String versionText = "Better than Adventure! " + Minecraft.VERSION; + String modCountText = " " + i18n.translateKeyAndFormat(key, count); + int versionWidth = menu.fontRenderer.stringWidth(versionText); + int versionTextColor = !(Boolean) GameSettings.ALPHA_MENU.value && menu.mc.mainMenuBackground.isEnabled() ? 16777215 : 5263440; + menu.drawStringShadow(menu.fontRenderer, modCountText, 2 + versionWidth, 2, versionTextColor); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/mixin/ScreenPauseMixin.java b/src/main/java/io/github/prospector/modmenu/mixin/ScreenPauseMixin.java new file mode 100644 index 000000000..c928a5b0d --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/mixin/ScreenPauseMixin.java @@ -0,0 +1,38 @@ +package io.github.prospector.modmenu.mixin; + +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; +import io.github.prospector.modmenu.gui.widget.ModMenuButtonWidget; +import io.github.prospector.modmenu.gui.widget.SmallModMenuButtonWidget; +import net.minecraft.client.gui.ButtonElement; +import net.minecraft.client.gui.Screen; +import net.minecraft.client.gui.ScreenPause; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = ScreenPause.class, remap = false) +public class ScreenPauseMixin extends Screen { + @Inject(at = @At("RETURN"), method = "init") + public void modmenu$drawMenuButton(CallbackInfo info) { + if (ModMenuConfig.GAME_MENU_BUTTON_STYLE.getValue() == ModMenuConfig.GameMenuButtonStyle.INSERT) { + String buttonText = ModMenu.createModsButtonText(false); + this.buttons.add(new ModMenuButtonWidget(1000, this.width / 2 - 100, this.height / 4 + 72 - 16, 200, 20, buttonText)); + } else { + this.buttons.removeIf(b -> b.id == 10); + this.buttons.add(new SmallModMenuButtonWidget( + 1000, this.width / 2 - 124, this.height / 4 + 104, 20, 20, 0, 0, + "/assets/" + ModMenu.MOD_ID + "/textures/gui/open_button.png", 32, 64 + ) {}); + } + } + + @Inject(method = "buttonClicked", at = @At("HEAD")) + private void modmenu$onActionPerformed(ButtonElement button, CallbackInfo ci) { + if (button.id == 1000) { + this.mc.displayScreen(new ModsScreen(this)); + } + } +} diff --git a/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java b/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java index ae9815373..501fc5fc3 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/TextFieldEditorAccessor.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import net.minecraft.client.gui.text.TextFieldEditor; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Invoker; diff --git a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java b/src/main/java/io/github/prospector/modmenu/mixin/TexturePackMixin.java similarity index 90% rename from src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java rename to src/main/java/io/github/prospector/modmenu/mixin/TexturePackMixin.java index 5ad18fdd3..59df01fb0 100644 --- a/src/main/java/io/github/prospector/modmenu/mixin/MixinTexturePacks.java +++ b/src/main/java/io/github/prospector/modmenu/mixin/TexturePackMixin.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.mixin; - import io.github.prospector.modmenu.ModMenu; import net.minecraft.client.render.texturepack.TexturePack; import org.spongepowered.asm.mixin.Mixin; @@ -10,8 +9,8 @@ import java.io.InputStream; -@Mixin(value = {TexturePack.class}, remap = false) -public class MixinTexturePacks { +@Mixin(value = TexturePack.class, remap = false) +public class TexturePackMixin { @Inject(method = "getResourceAsStream", at = @At(value = "INVOKE", target = "Ljava/lang/Class;getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;", remap = false), cancellable = true) private void modmenu$onGetResource(String resource, CallbackInfoReturnable ci) { InputStream in = ModMenu.class.getClassLoader().getResourceAsStream(resource); diff --git a/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java b/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java index a19b76738..f5a314909 100644 --- a/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java +++ b/src/main/java/io/github/prospector/modmenu/util/BadgeRenderer.java @@ -1,8 +1,8 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.gui.ModListScreen; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.ModMetadata; import net.minecraft.client.Minecraft; @@ -16,9 +16,9 @@ public class BadgeRenderer { protected ModContainer container; protected ModMetadata metadata; protected Minecraft client; - protected final ModListScreen screen; + protected final ModsScreen screen; - public BadgeRenderer(Minecraft client, int startX, int startY, int endX, ModContainer container, ModListScreen screen) { + public BadgeRenderer(Minecraft client, int startX, int startY, int endX, ModContainer container, ModsScreen screen) { this.startX = startX; this.startY = startY; this.badgeMax = endX; @@ -33,19 +33,19 @@ public void draw(int mouseX, int mouseY) { this.badgeX = startX; this.badgeY = startY; if (ModMenu.LIBRARY_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.library"), 0x8810d098, 0x88046146, mouseX, mouseY); + drawBadge(i18n.translateKey("modmenu.badge.library"), 0x8810d098, 0x88046146, mouseX, mouseY); } if (ModMenu.CLIENTSIDE_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.clientsideOnly"), 0x884383E3, 0x880E4699, mouseX, mouseY); + drawBadge(i18n.translateKey("modmenu.badge.clientsideOnly"), 0x884383E3, 0x880E4699, mouseX, mouseY); } if (ModMenu.DEPRECATED_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.deprecated"), 0xFF841426, 0xFF530C17, mouseX, mouseY); + drawBadge(i18n.translateKey("modmenu.badge.deprecated"), 0xFF841426, 0xFF530C17, mouseX, mouseY); } if (ModMenu.PATCHWORK_FORGE_MODS.contains(metadata.getId())) { - drawBadge(i18n.translateKey("modmenu.forge"), 0x887C89A3, 0x88202C43, mouseX, mouseY); + drawBadge(i18n.translateKey("modmenu.badge.forge"), 0x887C89A3, 0x88202C43, mouseX, mouseY); } if (metadata.getId().equals("minecraft")) { - drawBadge(i18n.translateKey("modmenu.minecraft"), 0x88BCBCBC, 0x88535353, mouseX, mouseY); + drawBadge(i18n.translateKey("modmenu.badge.minecraft"), 0x88BCBCBC, 0x88535353, mouseX, mouseY); } if (ModMenu.CUSTOM_BADGE_MODS.containsKey(metadata.getId())) { Map> map = ModMenu.CUSTOM_BADGE_MODS.get(metadata.getId()); @@ -53,27 +53,35 @@ public void draw(int mouseX, int mouseY) { drawBadge(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue(), mouseX, mouseY); } } - //noinspection MagicConstant - if (Calendar.getInstance().get(0b10) == 0b11 && Calendar.getInstance().get(0b101) == 0x1) { - if (metadata.getId().equals(new String(new byte[]{109, 111, 100, 109, 101, 110, 117}))) { - drawBadge(new String(new byte[]{-30, -100, -104, 32, 86, 105, 114, 117, 115, 32, 68, 101, 116, 101, 99, 116, 101, 100}), 0b10001000111111110010001000100010, 0b10001000011111110000100000001000, mouseX, mouseY); - } else if (metadata.getId().contains(new String(new byte[]{116, 97, 116, 101, 114}))) { - drawBadge(new String(new byte[]{116, 97, 116, 101, 114}), 0b10001000111010111011001100101011, 0b10001000100110010111000100010010, mouseX, mouseY); - } else { - drawBadge(new String(new byte[]{-30, -100, -108, 32, 98, 121, 32, 77, 99, 65, 102, 101, 101}), 0b10001000000111011111111101001000, 0b10001000000001110110100100001110, mouseX, mouseY); + if (ModMenuConfig.EASTER_EGGS.getValue()) { + //noinspection MagicConstant + if (Calendar.getInstance().get(0b10) == 0b11 && Calendar.getInstance().get(0b101) == 0x1) { + if (metadata.getId().equals(new String(new byte[]{109, 111, 100, 109, 101, 110, 117}))) { + drawBadge(new String(new byte[]{-30, -100, -104, 32, 86, 105, 114, 117, 115, 32, 68, 101, 116, 101, 99, 116, 101, 100}), 0b10001000111111110010001000100010, 0b10001000011111110000100000001000, mouseX, mouseY); + } else if (metadata.getId().contains(new String(new byte[]{116, 97, 116, 101, 114}))) { + drawBadge(new String(new byte[]{116, 97, 116, 101, 114}), 0b10001000111010111011001100101011, 0b10001000100110010111000100010010, mouseX, mouseY); + } else { + drawBadge(new String(new byte[]{-30, -100, -108, 32, 98, 121, 32, 77, 99, 65, 102, 101, 101}), 0b10001000000111011111111101001000, 0b10001000000001110110100100001110, mouseX, mouseY); + } } } } public void drawBadge(String text, int outlineColor, int fillColor, int mouseX, int mouseY) { - int width = client.font.stringWidth(text) + 6; - if (badgeX + width < badgeMax) { - RenderUtils.INSTANCE.drawBadge(screen, badgeX, badgeY, width, text, outlineColor, fillColor, 0xCACACA); - badgeX += width + 3; + if (!ModMenuConfig.HIDE_BADGES.getValue()) { + int width = client.font.stringWidth(text) + 6; + if (badgeX + width < badgeMax) { + RenderUtils.INSTANCE.drawBadge(screen, badgeX, badgeY, width, text, outlineColor, fillColor, 0xCACACA); + badgeX += width + 3; + } } } public ModMetadata getMetadata() { return metadata; } + + public int getWidth() { + return badgeX - startX; + } } diff --git a/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java b/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java index 29ba70825..b0b26ef9e 100644 --- a/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java +++ b/src/main/java/io/github/prospector/modmenu/util/ButtonUtil.java @@ -1,13 +1,12 @@ package io.github.prospector.modmenu.util; - -import io.github.prospector.modmenu.mixin.GuiButtonAccessor; +import io.github.prospector.modmenu.mixin.ButtonElementAccessor; import net.minecraft.client.gui.ButtonElement; public final class ButtonUtil { public static ButtonElement createButton(int buttonId, int x, int y, int width, int height, String text) { ButtonElement button = new ButtonElement(buttonId, x, y, text); - GuiButtonAccessor accessor = (GuiButtonAccessor) button; + ButtonElementAccessor accessor = (ButtonElementAccessor) button; accessor.setWidth(width); accessor.setHeight(height); return button; diff --git a/src/main/java/io/github/prospector/modmenu/util/DrawingUtil.java b/src/main/java/io/github/prospector/modmenu/util/DrawingUtil.java new file mode 100644 index 000000000..00b84c8fa --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/DrawingUtil.java @@ -0,0 +1,51 @@ +package io.github.prospector.modmenu.util; + +import io.github.prospector.modmenu.config.ModMenuConfig; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.minecraft.client.render.renderer.GLRenderer; +import net.minecraft.client.render.renderer.Shaders; +import net.minecraft.client.render.renderer.State; +import net.minecraft.client.render.tessellator.TessellatorGeneral; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; +import java.util.Random; + +@Environment(EnvType.CLIENT) +public class DrawingUtil { + public static void drawRandomVersionBackground(ModContainer container, int x, int y, int width, int height) { + ModMetadata meta = container.getMetadata(); + int seed = meta.getName().hashCode() + meta.getVersion().getFriendlyString().hashCode(); + + Random random = new Random(seed); + int color = 0xFF000000 | Color.HSBtoRGB(random.nextFloat(), random.nextFloat() * 0.1f + 0.7f, 0.9f); + if (!ModMenuConfig.RANDOM_JAVA_COLORS.getValue()) { + color = 0xFFDD5656; + } + + float red = (float) ((color >> 16) & 255) / 255.0f; + float green = (float) ((color >> 8) & 255) / 255.0f; + float blue = (float) (color & 255) / 255.0f; + float alpha = (float) ((color >> 24) & 255) / 255.0f; + + GLRenderer.pushFrame(); + GLRenderer.setShader(Shaders.COLOR); + GLRenderer.enableState(State.BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + GLRenderer.setColor4f(red, green, blue, alpha); + + TessellatorGeneral t = GLRenderer.getTessellator(); + t.startDrawingQuads(); + t.addVertexWithUV(x, y, 0, 0, 0); + t.addVertexWithUV(x, y + height, 0, 0, 1); + t.addVertexWithUV(x + width, y + height, 0, 1, 1); + t.addVertexWithUV(x + width, y, 0, 1, 0); + t.draw(); + + GLRenderer.popFrame(); + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java b/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java deleted file mode 100644 index ca10db64e..000000000 --- a/src/main/java/io/github/prospector/modmenu/util/HardcodedUtil.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.github.prospector.modmenu.util; - - -import io.github.prospector.modmenu.ModMenu; -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.metadata.ModMetadata; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class HardcodedUtil { - private static final Pattern FABRIC_PATTERN = Pattern.compile("^fabric-.*(-v\\d+)$"); - private static final Set FABRIC_MODS = new HashSet<>(); - private static final HashMap HARDCODED_DESCRIPTIONS = new HashMap<>(); - - public static void initializeHardcodings() { - HARDCODED_DESCRIPTIONS.put("minecraft", new Random().nextInt(1000) == 0 ? "The based game." : "The base game."); - } - - public static void hardcodeModuleMetadata(ModContainer mod, ModMetadata metadata, String id) { - Matcher matcher = FABRIC_PATTERN.matcher(id); - if (matcher.matches() || id.equals("fabric-api-base") || id.equals("fabric-renderer-indigo")) { - FABRIC_MODS.add(id); - if (FabricLoader.getInstance().isModLoaded("fabric")) { - Optional parent = FabricLoader.getInstance().getModContainer("fabric"); - parent.ifPresent(modContainer -> { - final List list = ModMenu.PARENT_MAP.computeIfAbsent(modContainer, k -> new ArrayList<>()); - list.add(mod); - });; - } - ModMenu.addLibraryMod(id); - if (id.equals("fabric-keybindings-v0") || id.equals("fabric-models-v0") || id.equals("fabric-renderer-api-v1") || id.equals("fabric-renderer-indigo") || id.equals("fabric-rendering-fluids-v1") || id.equals("fabric-rendering-v0") || id.equals("fabric-textures-v0")) { - ModMenu.CLIENTSIDE_MODS.add(id); - } - } - if (id.equals("fabricloader") || id.equals("fabric") || metadata.getName().endsWith(" API")) { - ModMenu.addLibraryMod(id); - } - } - - public static String formatFabricModuleName(String name) { - Matcher matcher = FABRIC_PATTERN.matcher(name); - if (matcher.matches() || name.equals("fabric-renderer-indigo") || name.equals("fabric-api-base")) { - if (matcher.matches()) { - String v = matcher.group(1); - name = capitalize(name.replace(v, "").replace("-", " ")); - name = name + " (" + v.replace("-", "") + ")"; - } else { - name = capitalize(name.replace("-", " ")); - } - name = name.replace("Api", "API"); - name = name.replace("Blockentity", "BlockEntity"); - } - return name; - } - - private static String capitalize(String str) { - StringBuilder sb = new StringBuilder(); - boolean capitalizeNext = true; - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (capitalizeNext) - c = Character.toTitleCase(c); - capitalizeNext = Character.isWhitespace(c); - sb.append(c); - } - return sb.toString(); - } - - public static String getHardcodedDescription(String id) { - return HARDCODED_DESCRIPTIONS.getOrDefault(id, ""); - } - - public static Set getFabricMods() { - return FABRIC_MODS; - } - - public static HashMap getHardcodedDescriptions() { - return HARDCODED_DESCRIPTIONS; - } -} diff --git a/src/main/java/io/github/prospector/modmenu/util/HttpUtil.java b/src/main/java/io/github/prospector/modmenu/util/HttpUtil.java new file mode 100644 index 000000000..72c01d0ea --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/HttpUtil.java @@ -0,0 +1,50 @@ +package io.github.prospector.modmenu.util; + +import io.github.prospector.modmenu.ModMenu; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +public class HttpUtil { + private static final Duration TIMEOUT = Duration.ofSeconds(30); + private static final String USER_AGENT = buildUserAgent(); + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + private HttpUtil() { + } + + public static HttpResponse request( + HttpRequest.Builder builder, + HttpResponse.BodyHandler handler + ) throws IOException, InterruptedException { + builder.setHeader("User-Agent", USER_AGENT); + builder.timeout(TIMEOUT); + return HTTP_CLIENT.send(builder.build(), handler); + } + + private static String buildUserAgent() { + String env = ModMenu.DEV_ENVIRONMENT ? "/development" : ""; + String loader = "bta-babric"; + + var modMenuVersion = getModMenuVersion(); + var minecraftVersion = "b1.7.3"; + + return "%s/%s (%s/%s%s)".formatted(ModMenu.GITHUB_REF, modMenuVersion, minecraftVersion, loader, env); + } + + private static String getModMenuVersion() { + var container = FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID); + if (container.isEmpty()) { + throw new RuntimeException("Unable to find Modmenu's own mod container!"); + } + + return VersionUtil.removeBuildMetadata(container.get().getMetadata().getVersion().getFriendlyString()); + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java b/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java index a21e9e567..ab8ec216a 100644 --- a/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java +++ b/src/main/java/io/github/prospector/modmenu/util/ModListSearch.java @@ -1,11 +1,12 @@ package io.github.prospector.modmenu.util; - import io.github.prospector.modmenu.ModMenu; -import io.github.prospector.modmenu.gui.ModListScreen; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.gui.ModsScreen; import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.ModMetadata; import net.fabricmc.loader.api.metadata.Person; +import net.minecraft.core.lang.I18n; import java.util.List; import java.util.Locale; @@ -18,28 +19,42 @@ public static boolean validSearchQuery(String query) { return query != null && !query.isEmpty(); } - public static List search(ModListScreen screen, String query, List candidates) { + public static List search(ModsScreen screen, String query, List candidates) { if (!validSearchQuery(query)) { return candidates; } return candidates.stream() - .filter(modContainer -> passesFilters(screen, modContainer, query.toLowerCase(Locale.ROOT))) - .collect(Collectors.toList()); + .filter(modContainer -> passesFilters(screen, modContainer, query.toLowerCase(Locale.ROOT))) + .collect(Collectors.toList()); } - private static boolean passesFilters(ModListScreen screen, ModContainer container, String query) { + private static boolean passesFilters(ModsScreen screen, ModContainer container, String query) { ModMetadata metadata = container.getMetadata(); - String modId = metadata.getId(); + String id = metadata.getId(); + I18n i18n = I18n.getInstance(); + + String nameTranslation = "modmenu.nameTranslation." + id; + String translatedName = i18n.translateKey(nameTranslation); + String modName = !translatedName.equals(nameTranslation) && ModMenuConfig.TRANSLATE_NAMES.getValue() ? translatedName : metadata.getName(); + String descriptionTranslation = "modmenu.descriptionTranslation." + id; + String translatedDescription = i18n.translateKey(descriptionTranslation); + String modDescription = !translatedDescription.equals(descriptionTranslation) && ModMenuConfig.TRANSLATE_DESCRIPTIONS.getValue() ? translatedDescription : metadata.getDescription(); - //Some basic search, could do with something more advanced but this will do for now - if (HardcodedUtil.formatFabricModuleName(metadata.getName()).toLowerCase(Locale.ROOT).contains(query) //Search mod name - || modId.toLowerCase(Locale.ROOT).contains(query) // Search mod name - || authorMatches(container, query) //Search via author - || (ModMenu.LIBRARY_MODS.contains(modId) && "api library".contains(query)) //Search for lib mods - || ("clientside".contains(query) && ModMenu.CLIENTSIDE_MODS.contains(modId)) //Search for clientside mods - || ("deprecated".contains(query) && ModMenu.DEPRECATED_MODS.contains(modId)) //Search for clientside mods - || ("configurations configs configures configurable".contains(query) && ModMenu.hasConfigScreenFactory(modId)) //Search for mods that can be configured + String summaryTranslation = "modmenu.summaryTranslation." + id; + String translatedSummary = i18n.translateKey(summaryTranslation); + String modSummary = !translatedSummary.equals(summaryTranslation) && ModMenuConfig.TRANSLATE_DESCRIPTIONS.getValue() ? translatedSummary : metadata.getDescription(); + + if (modName.toLowerCase(Locale.ROOT).contains(query) // Search mod name + || modDescription.toLowerCase(Locale.ROOT).contains(query) // Search mod description + || modSummary.toLowerCase(Locale.ROOT).contains(query) // Search mod summary + || id.toLowerCase(Locale.ROOT).contains(query) // Search mod id + || authorMatches(container, query) // Search via author + || (ModMenu.LIBRARY_MODS.contains(id) && i18n.translateKey("modmenu.searchTerms.library").toLowerCase(Locale.ROOT).contains(query)) // Search for lib mods + || (ModMenu.CLIENTSIDE_MODS.contains(id) && i18n.translateKey("modmenu.searchTerms.clientside").toLowerCase(Locale.ROOT).contains(query)) // Search for clientside mods + || (ModMenu.DEPRECATED_MODS.contains(id) && i18n.translateKey("modmenu.searchTerms.deprecated").toLowerCase(Locale.ROOT).contains(query)) // Search for deprecated mods + || (ModMenu.PATCHWORK_FORGE_MODS.contains(id) && i18n.translateKey("modmenu.searchTerms.patchwork").toLowerCase(Locale.ROOT).contains(query)) // Search for forge and patchwork + || (ModMenu.hasConfigScreenFactory(id) && i18n.translateKey("modmenu.searchTerms.configurable").toLowerCase(Locale.ROOT).contains(query)) // Search for mods that can be configured ) { return true; } @@ -57,11 +72,10 @@ private static boolean passesFilters(ModListScreen screen, ModContainer containe private static boolean authorMatches(ModContainer modContainer, String query) { return modContainer.getMetadata().getAuthors().stream() - .filter(Objects::nonNull) - .map(Person::getName) - .filter(Objects::nonNull) - .map(s -> s.toLowerCase(Locale.ROOT)) - .anyMatch(s -> s.contains(query.toLowerCase(Locale.ROOT))); + .filter(Objects::nonNull) + .map(Person::getName) + .filter(Objects::nonNull) + .map(s -> s.toLowerCase(Locale.ROOT)) + .anyMatch(s -> s.contains(query.toLowerCase(Locale.ROOT))); } - } diff --git a/src/main/java/io/github/prospector/modmenu/util/NullScreenFactory.java b/src/main/java/io/github/prospector/modmenu/util/NullScreenFactory.java new file mode 100644 index 000000000..7f6d0a4a3 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/NullScreenFactory.java @@ -0,0 +1,15 @@ +package io.github.prospector.modmenu.util; + +import io.github.prospector.modmenu.api.ConfigScreenFactory; +import net.minecraft.client.gui.Screen; + +/* + * This class is used to detect mods which do not override the default ModMenuApi.getModConfigScreenFactory() + * while still preserving the guarantee the above API method does not return null. + */ +public class NullScreenFactory implements ConfigScreenFactory { + @Override + public S create(Screen parent) { + return null; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java b/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java index 1db829870..89394cc94 100644 --- a/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java +++ b/src/main/java/io/github/prospector/modmenu/util/RenderUtils.java @@ -1,6 +1,5 @@ package io.github.prospector.modmenu.util; - import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.Screen; diff --git a/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java b/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java index 8657b19a8..2c3f50669 100644 --- a/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java +++ b/src/main/java/io/github/prospector/modmenu/util/TestModContainer.java @@ -1,10 +1,6 @@ package io.github.prospector.modmenu.util; - -import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.SemanticVersion; -import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.*; import net.fabricmc.loader.api.metadata.*; import net.fabricmc.loader.util.version.VersionParsingException; @@ -214,8 +210,7 @@ public Map getCustomValues() { return new HashMap<>(); } - @SuppressWarnings("UnstableApiUsage") - @Override + @Override public boolean containsCustomElement(String key) { return false; } diff --git a/src/main/java/io/github/prospector/modmenu/util/TranslationUtil.java b/src/main/java/io/github/prospector/modmenu/util/TranslationUtil.java new file mode 100644 index 000000000..409670b82 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/TranslationUtil.java @@ -0,0 +1,63 @@ +package io.github.prospector.modmenu.util; + +import io.github.prospector.modmenu.ModMenu; +import net.minecraft.core.lang.I18n; + +import java.text.NumberFormat; +import java.util.Arrays; + +public class TranslationUtil { + public static String translateNumeric(String key, int[]... args) { + Object[] realArgs = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + NumberFormat nf = NumberFormat.getInstance(); + if (args[i].length == 1) { + realArgs[i] = nf.format(args[i][0]); + } else { + assert args[i].length == 2; + realArgs[i] = nf.format(args[i][0]) + "/" + nf.format(args[i][1]); + } + } + + int[] override = new int[args.length]; + Arrays.fill(override, -1); + for (int i = 0; i < args.length; i++) { + int[] arg = args[i]; + if (arg == null) { + throw new NullPointerException("args[" + i + "]"); + } + if (arg.length == 1) { + if (arg[0] == 1) { + override[i] = 1; + } else { + override[i] = -1; + } + } + } + + String lastKey = key; + I18n i18n = I18n.getInstance(); + for (int flags = (1 << args.length) - 1; flags >= 0; flags--) { + StringBuilder fullKey = new StringBuilder(key); + for (int i = 0; i < args.length; i++) { + fullKey.append('.'); + if (((flags & (1 << i)) != 0) && override[i] != -1) { + fullKey.append(override[i]); + } else { + fullKey.append('n'); + } + } + lastKey = fullKey.toString(); + + String translated = i18n.translateKey(lastKey); + if (!translated.equals(lastKey)) { + return i18n.translateKeyAndFormat(lastKey, realArgs); + } + } + return i18n.translateKeyAndFormat(lastKey, realArgs); + } + + public static String translationKeyOf(String type, String id) { + return type + "." + ModMenu.MOD_ID + "." + id; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/UpdateCheckerUtil.java b/src/main/java/io/github/prospector/modmenu/util/UpdateCheckerUtil.java new file mode 100644 index 000000000..a06a117d7 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/UpdateCheckerUtil.java @@ -0,0 +1,419 @@ +package io.github.prospector.modmenu.util; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; +import io.github.prospector.modmenu.ModMenu; +import io.github.prospector.modmenu.api.*; +import io.github.prospector.modmenu.util.mod.*; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.*; +import java.util.concurrent.*; + +public class UpdateCheckerUtil { + public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker"); + + private static boolean modrinthApiV2Removed = false; + + private static final String BTA_VERSIONS_URL = "https://downloads.betterthanadventure.net/bta-client/release/versions.json"; + + private static boolean allowsUpdateChecks(Mod mod) { + return mod.allowsUpdateChecks(); + } + + public static void checkForUpdates() { + LOGGER.info("Checking mod updates..."); + new Thread(UpdateCheckerUtil::checkForUpdates0, "ModMenu/Update Checker Thread").start(); + } + + private static void checkForUpdates0() { + checkBtaUpdate(); + + List withoutUpdateChecker = new ArrayList<>(); + Map currentVersions = null; + Map updatedVersions = null; + Map> modHashes; + Future> currentVersionsFuture; + Future> updatedVersionsFuture; + + List updatableMods = ModMenu.MODS.values() + .stream() + .filter(UpdateCheckerUtil::allowsUpdateChecks) + .toList(); + + ExecutorService executor = Executors.newCachedThreadPool(); + + try { + for (Mod mod : updatableMods) { + UpdateChecker updateChecker = mod.getUpdateChecker(); + + if (updateChecker == null) { + withoutUpdateChecker.add(mod); + } else { + executor.submit(() -> { + Thread.currentThread().setName("ModMenu/Update Checker/%s".formatted(mod.getName())); + + var update = updateChecker.checkForUpdates(); + mod.setUpdateInfo(update); + + if (update != null && update.isUpdateAvailable()) { + LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion()); + } + }); + } + } + + if (modrinthApiV2Removed) { + return; + } + + modHashes = getModHashes(withoutUpdateChecker); + + currentVersionsFuture = executor.submit(() -> getCurrentVersions(modHashes.keySet())); + updatedVersionsFuture = executor.submit(() -> getUpdatedVersions(modHashes.keySet())); + + try { + currentVersions = currentVersionsFuture.get(); + updatedVersions = updatedVersionsFuture.get(); + } catch (ExecutionException e) { + LOGGER.error("Error checking for updates: ", e.getCause() != null ? e.getCause() : e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Error checking for updates: ", e); + } + } finally { + executor.shutdown(); + } + + if (currentVersions == null || updatedVersions == null) { + return; + } + + for (var hash : modHashes.keySet()) { + var date = currentVersions.get(hash); + var data = updatedVersions.get(hash); + + if (date == null || data == null) { + continue; + } + + if (Objects.equals(hash, data.hash)) { + continue; + } + + if (date.compareTo(data.releaseDate) >= 0) { + continue; + } + + for (var mod : modHashes.get(hash)) { + mod.setUpdateInfo(data.asUpdateInfo()); + LOGGER.info("Update available for '{}@{}', (-> {})", + mod.getId(), + mod.getVersion(), + data.versionNumber + ); + } + } + } + + private static void checkBtaUpdate() { + Mod btaMod = ModMenu.MODS.get("minecraft"); + if (btaMod == null) { + LOGGER.debug("BTA mod not found in MODS map, skipping BTA update check"); + return; + } + + if (!allowsUpdateChecks(btaMod)) { + LOGGER.debug("Update checks disabled for BTA, skipping"); + return; + } + + String currentVersion = btaMod.getVersion(); + LOGGER.debug("Current BTA version: {}", currentVersion); + + try { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .GET() + .uri(URI.create(BTA_VERSIONS_URL)); + + HttpResponse response = HttpUtil.request(requestBuilder, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + LOGGER.debug("BTA update check: HTTP {}", response.statusCode()); + return; + } + + JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); + if (!json.has("default")) { + LOGGER.debug("BTA update check: versions.json has no 'default' field"); + return; + } + + String latestVersion = json.get("default").getAsString(); + if (latestVersion.startsWith("v")) { + latestVersion = latestVersion.substring(1); + } + + LOGGER.debug("Latest BTA version from versions.json: {}", latestVersion); + + if (isNewer(latestVersion, currentVersion)) { + btaMod.setUpdateInfo(new BTAUpdateInfo( + latestVersion, + latestVersion, + latestVersion, + UpdateChannel.RELEASE + )); + LOGGER.info("Update available for BTA: {} -> {}", currentVersion, latestVersion); + } else { + LOGGER.debug("BTA is up to date ({} >= {})", currentVersion, latestVersion); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Error checking BTA update: ", e); + } catch (IOException | RuntimeException e) { + LOGGER.error("Error checking BTA update: ", e); + } + } + + private static boolean isNewer(String latest, String current) { + int[] a = parseVersion(latest); + int[] b = parseVersion(current); + int len = Math.max(a.length, b.length); + for (int i = 0; i < len; i++) { + int ai = i < a.length ? a[i] : 0; + int bi = i < b.length ? b[i] : 0; + if (ai != bi) { + return ai > bi; + } + } + return false; + } + + private static int[] parseVersion(String version) { + String cleaned = version.replaceAll("[^0-9._]", ""); + String[] parts = cleaned.split("[._]"); + int[] result = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + result[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + result[i] = 0; + } + } + return result; + } + + private static Map> getModHashes(Collection mods) { + Map> results = new HashMap<>(); + + for (var mod : mods) { + String modId = mod.getId(); + + try { + String hash = mod.getSha512Hash(); + + if (hash != null) { + LOGGER.debug("Hash for {} is {}", modId, hash); + results.putIfAbsent(hash, new HashSet<>()); + results.get(hash).add(mod); + } + } catch (IOException e) { + LOGGER.error("Error getting mod hash for mod {}: ", modId, e); + } + } + + return results; + } + + private static @Nullable Map getCurrentVersions(Collection modHashes) { + String body = ModMenu.GSON.toJson(new CurrentVersionsFromHashes(modHashes)); + + var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(body)) + .header("Content-Type", "application/json") + .uri(URI.create("https://api.modrinth.com/v2/version_files")); + + try { + var response = HttpUtil.request(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 410) { + modrinthApiV2Removed = true; + LOGGER.warn("Cannot check for updates because Modrinth's API v2 is no longer available."); + } else if (response.statusCode() == 200) { + Map results = new HashMap<>(); + JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject(); + + for (var entry : data.entrySet()) { + Instant date; + var version = entry.getValue().getAsJsonObject(); + + try { + date = Instant.parse(version.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + continue; + } + + results.put(entry.getKey(), date); + } + + return results; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Error checking for versions: ", e); + } catch (IOException | RuntimeException e) { + LOGGER.error("Error checking for versions: ", e); + } + + return null; + } + + public static class CurrentVersionsFromHashes { + public Collection hashes; + public String algorithm = "sha512"; + + public CurrentVersionsFromHashes(Collection hashes) { + this.hashes = hashes; + } + } + + private static UpdateChannel getUpdateChannel(String versionType) { + try { + return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException | NullPointerException e) { + return UpdateChannel.RELEASE; + } + } + + private static @Nullable Map getUpdatedVersions(Collection modHashes) { + String mcVer = "b1.7.3"; + List loaders = List.of("bta-babric"); + List updateChannels = getUpdateChannels(); + + String body = ModMenu.GSON.toJson(new LatestVersionsFromHashesBody(modHashes, + loaders, + mcVer, + updateChannels + )); + + LOGGER.debug("Body: {}", body); + var latestVersionsRequest = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(body)) + .header("Content-Type", "application/json") + .uri(URI.create("https://api.modrinth.com/v2/version_files/update")); + + try { + var latestVersionsResponse = HttpUtil.request(latestVersionsRequest, HttpResponse.BodyHandlers.ofString()); + + int status = latestVersionsResponse.statusCode(); + LOGGER.debug("Status: {}", status); + if (status == 410) { + modrinthApiV2Removed = true; + LOGGER.warn("Cannot check for updates because Modrinth's API v2 is no longer available."); + } else if (status == 200) { + Map results = new HashMap<>(); + JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject(); + LOGGER.debug(String.valueOf(responseObject)); + + for (var entry : responseObject.entrySet()) { + var versionObj = entry.getValue().getAsJsonObject(); + var projectId = versionObj.get("project_id").getAsString(); + var versionType = versionObj.get("version_type").getAsString(); + var versionNumber = versionObj.get("version_number").getAsString(); + var versionId = versionObj.get("id").getAsString(); + + var filesArray = versionObj.get("files").getAsJsonArray(); + com.google.gson.JsonElement primaryFile = null; + for (var fileElement : filesArray) { + if (fileElement.getAsJsonObject().get("primary").getAsBoolean()) { + primaryFile = fileElement; + break; + } + } + + if (primaryFile == null) { + continue; + } + + Instant date; + try { + date = Instant.parse(versionObj.get("date_published").getAsString()); + } catch (DateTimeParseException e) { + continue; + } + + var updateChannel = UpdateCheckerUtil.getUpdateChannel(versionType); + var versionHash = primaryFile.getAsJsonObject() + .get("hashes") + .getAsJsonObject() + .get("sha512") + .getAsString(); + + results.put(entry.getKey(), + new VersionUpdate(projectId, versionId, versionNumber, date, updateChannel, versionHash) + ); + } + + return results; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Error checking for updates: ", e); + } catch (IOException | RuntimeException e) { + LOGGER.error("Error checking for updates: ", e); + } + + return null; + } + + private static List getUpdateChannels() { + return switch(UpdateChannel.getUserPreference()) { + case RELEASE -> List.of(UpdateChannel.RELEASE); + case BETA -> List.of(UpdateChannel.BETA, UpdateChannel.RELEASE); + default -> List.of(UpdateChannel.ALPHA, UpdateChannel.BETA, UpdateChannel.RELEASE); + }; + } + + private record VersionUpdate( + String projectId, + String versionId, + String versionNumber, + Instant releaseDate, + UpdateChannel updateChannel, + String hash + ) { + private UpdateInfo asUpdateInfo() { + return new ModrinthUpdateInfo(this.projectId, this.versionId, this.versionNumber, this.updateChannel); + } + } + + public static class LatestVersionsFromHashesBody { + public Collection hashes; + public Collection loaders; + @SerializedName("game_versions") + public Collection gameVersions; + @SerializedName("version_types") + public Collection versionTypes; + + public LatestVersionsFromHashesBody( + Collection hashes, + Collection loaders, + String mcVersion, + Collection updateChannels + ) { + this.hashes = hashes; + this.loaders = loaders; + this.gameVersions = Set.of(mcVersion); + this.versionTypes = updateChannels.stream().map(value -> value.toString().toLowerCase()).toList(); + } + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/VersionUtil.java b/src/main/java/io/github/prospector/modmenu/util/VersionUtil.java new file mode 100644 index 000000000..5631db933 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/VersionUtil.java @@ -0,0 +1,30 @@ +package io.github.prospector.modmenu.util; + +import java.util.List; + +public final class VersionUtil { + private static final List PREFIXES = List.of("version", "ver", "v"); + + private VersionUtil() { + } + + public static String stripPrefix(String version) { + version = version.trim(); + + for (String prefix : PREFIXES) { + if (version.startsWith(prefix)) { + return version.substring(prefix.length()); + } + } + + return version; + } + + public static String getPrefixedVersion(String version) { + return "v" + stripPrefix(version); + } + + public static String removeBuildMetadata(String version) { + return version.split("\\+")[0]; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/prospector/modmenu/util/mod/BTAUpdateInfo.java b/src/main/java/io/github/prospector/modmenu/util/mod/BTAUpdateInfo.java new file mode 100644 index 000000000..9ee30c835 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/mod/BTAUpdateInfo.java @@ -0,0 +1,41 @@ +package io.github.prospector.modmenu.util.mod; + +import io.github.prospector.modmenu.api.UpdateChannel; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.util.VersionUtil; +import net.minecraft.core.lang.I18n; +import org.jetbrains.annotations.NotNull; + +public record BTAUpdateInfo(String projectId, String versionId, String versionNumber, + UpdateChannel updateChannel) implements UpdateInfo { + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public @NotNull UpdateChannel getUpdateChannel() { + return updateChannel; + } + + @Override + public @NotNull String getUpdateMessage() { + String versionNoPrefix = VersionUtil.stripPrefix(this.versionNumber); + String key = "modmenu.updateText"; + if (versionNoPrefix.isEmpty() || !Character.isDigit(versionNoPrefix.charAt(0))) { + key += ".nonNumerical"; + } + return I18n.getInstance().translateKeyAndFormat( + key, + versionNoPrefix, + I18n.getInstance().translateKey("modmenu.bta") + ); + } + + @Override + public String getDownloadLink() { + return "https://downloads.betterthanadventure.net/bta-client/release/v%s/bta.v%s.mmc.zip" + .formatted(projectId, versionId); + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/mod/FabricMod.java b/src/main/java/io/github/prospector/modmenu/util/mod/FabricMod.java new file mode 100644 index 000000000..8fa82101b --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/mod/FabricMod.java @@ -0,0 +1,115 @@ +package io.github.prospector.modmenu.util.mod; + +import io.github.prospector.modmenu.api.UpdateChecker; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.config.ModMenuConfig; +import io.github.prospector.modmenu.util.BadgeRenderer; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.ModMetadata; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class FabricMod implements Mod { + private final ModMetadata metadata; + private UpdateChecker updateChecker; + private UpdateInfo updateInfo; + private final boolean allowsUpdateChecks = true; + + public FabricMod(ModContainer container) { + this.metadata = container.getMetadata(); + } + + @Override + public @NotNull String getId() { + return this.metadata.getId(); + } + + @Override + public @NotNull String getName() { + return this.metadata.getName(); + } + + @Override + public @NotNull String getVersion() { + return this.metadata.getVersion().getFriendlyString(); + } + + @Override + public @Nullable String getParent() { + return null; + } + + @Override + public boolean isHidden() { + return ModMenuConfig.HIDDEN_MODS.getValue().contains(this.getId()); + } + + @Override + public @NotNull Set getBadges() { + return Collections.emptySet(); + } + + @Override + public boolean allowsUpdateChecks() { + if (ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(this.getId())) { + return false; + } else { + return this.allowsUpdateChecks; + } + } + + @Override + public @Nullable UpdateChecker getUpdateChecker() { + return this.updateChecker; + } + + @Override + public void setUpdateChecker(@Nullable UpdateChecker updateChecker) { + this.updateChecker = updateChecker; + } + + @Override + public @Nullable UpdateInfo getUpdateInfo() { + return this.updateInfo; + } + + @Override + public void setUpdateInfo(@Nullable UpdateInfo updateInfo) { + this.updateInfo = updateInfo; + } + + @Override + public @Nullable String getSha512Hash() throws IOException { + Optional containerOpt = FabricLoader.getInstance().getModContainer(getId()); + if (containerOpt.isPresent()) { + List paths = containerOpt.get().getOrigin().getPaths(); + if (!paths.isEmpty()) { + Path path = paths.get(0); + if (path != null && Files.exists(path) && !Files.isDirectory(path)) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + byte[] hashBytes = digest.digest(Files.readAllBytes(path)); + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + return null; + } + } + } + } + return null; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/mod/Mod.java b/src/main/java/io/github/prospector/modmenu/util/mod/Mod.java new file mode 100644 index 000000000..884682cf8 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/mod/Mod.java @@ -0,0 +1,45 @@ +package io.github.prospector.modmenu.util.mod; + +import io.github.prospector.modmenu.api.UpdateChecker; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.util.BadgeRenderer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +public interface Mod { + @NotNull String getId(); + + @NotNull String getName(); + + @NotNull String getVersion(); + + default @Nullable String getParent() { + return null; + } + + default boolean isHidden() { + return false; + } + + default @NotNull Set getBadges() { + return Collections.emptySet(); + } + + boolean allowsUpdateChecks(); + + @Nullable UpdateChecker getUpdateChecker(); + + void setUpdateChecker(@Nullable UpdateChecker updateChecker); + + @Nullable UpdateInfo getUpdateInfo(); + + void setUpdateInfo(@Nullable UpdateInfo updateInfo); + + default @Nullable String getSha512Hash() throws IOException { + return null; + } +} diff --git a/src/main/java/io/github/prospector/modmenu/util/mod/ModrinthUpdateInfo.java b/src/main/java/io/github/prospector/modmenu/util/mod/ModrinthUpdateInfo.java new file mode 100644 index 000000000..3f45f76c9 --- /dev/null +++ b/src/main/java/io/github/prospector/modmenu/util/mod/ModrinthUpdateInfo.java @@ -0,0 +1,31 @@ +package io.github.prospector.modmenu.util.mod; + +import io.github.prospector.modmenu.api.UpdateChannel; +import io.github.prospector.modmenu.api.UpdateInfo; +import io.github.prospector.modmenu.util.VersionUtil; +import net.minecraft.core.lang.I18n; +import org.jetbrains.annotations.NotNull; + +public record ModrinthUpdateInfo(String projectId, String versionId, String versionNumber, + UpdateChannel getUpdateChannel) implements UpdateInfo { + + @Override + public boolean isUpdateAvailable() { + return true; + } + + @Override + public @NotNull String getUpdateMessage() { + String versionNoPrefix = VersionUtil.stripPrefix(this.versionNumber); + String key = "modmenu.updateText"; + if (!Character.isDigit(versionNoPrefix.charAt(0))) { + key += ".nonNumerical"; + } + return I18n.getInstance().translateKeyAndFormat(key, versionNoPrefix, I18n.getInstance().translateKey("modmenu.modrinth")); + } + + @Override + public String getDownloadLink() { + return "https://modrinth.com/project/%s/version/%s".formatted(projectId, versionId); + } +} diff --git a/src/main/resources/assets/modmenu/grey_fabric_icon.png b/src/main/resources/assets/modmenu/grey_fabric_icon.png deleted file mode 100644 index e0b949d94..000000000 Binary files a/src/main/resources/assets/modmenu/grey_fabric_icon.png and /dev/null differ diff --git a/src/main/resources/assets/modmenu/lang/bg_BG/bg_BG.lang b/src/main/resources/assets/modmenu/lang/bg_BG.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/bg_BG/bg_BG.lang rename to src/main/resources/assets/modmenu/lang/bg_BG.lang diff --git a/src/main/resources/assets/modmenu/lang/bg_BG/bg_bg.json b/src/main/resources/assets/modmenu/lang/bg_BG/bg_bg.json deleted file mode 100644 index 1aaed5fb7..000000000 --- a/src/main/resources/assets/modmenu/lang/bg_BG/bg_bg.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "modmenu.title": "Модификации", - "modmenu.loaded": "(%s заредени)", - "modmenu.config": "Редактирай конфигурация", - "modmenu.modsFolder": "Отвори папка с модификации", - "modmenu.configsFolder": "Отвори папка с конфигурации", - "modmenu.configure": "Конфигурирай...", - "modmenu.modIdToolTip": "ID на модификацията: %s", - "modmenu.website": "Уебсайт", - "modmenu.issues": "Проблеми", - "modmenu.authorPrefix": "От %s" -} \ No newline at end of file diff --git a/src/main/resources/assets/modmenu/lang/de_DE/de_DE.lang b/src/main/resources/assets/modmenu/lang/de_DE.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/de_DE/de_DE.lang rename to src/main/resources/assets/modmenu/lang/de_DE.lang diff --git a/src/main/resources/assets/modmenu/lang/de_DE/de_de.json b/src/main/resources/assets/modmenu/lang/de_DE/de_de.json deleted file mode 100644 index 49652ebf4..000000000 --- a/src/main/resources/assets/modmenu/lang/de_DE/de_de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "modmenu.title": "Mods", - "modmenu.loaded": "(%s geladen)", - "modmenu.config": "Ändere Konfiguration", - "modmenu.modsFolder": "Öffne Mods-Ordner", - "modmenu.configsFolder": "Öffne Konfigurationen-Ordner", - "modmenu.configure": "Konfiguriere..." -} diff --git a/src/main/resources/assets/modmenu/lang/en_US.lang b/src/main/resources/assets/modmenu/lang/en_US.lang new file mode 100644 index 000000000..4df07407f --- /dev/null +++ b/src/main/resources/assets/modmenu/lang/en_US.lang @@ -0,0 +1,171 @@ +modmenu.title=Mods +modmenu.nameTranslation.modmenu=Mod Menu +modmenu.descriptionTranslation.modmenu=Adds a mod menu to view the list of mods you have installed. + +modmenu.loaded=(%s Loaded) +modmenu.loaded.short=(%s) +modmenu.loaded.69.secret=(%s Loaded...nice) +modmenu.loaded.420.secret=(%s Loaded...blaze it) +modmenu.mods.n= (%s Mods) +modmenu.mods.1= (%s Mod) +modmenu.mods.69.secret= (%s Mods...nice) +modmenu.mods.420.secret= (%s Mods...blaze it) + +modmenu.search=Search for mods +modmenu.searchTerms.library=api library +modmenu.searchTerms.clientside=clientside gameside +modmenu.searchTerms.deprecated=deprecated outdated old +modmenu.searchTerms.patchwork=patchwork forge fml +modmenu.searchTerms.configurable=configurations configs configures configurable options settings + +modmenu.toggleFilterOptions=Toggle Filter Options +modmenu.showingMods.n=Showing %s mods +modmenu.showingMods.1=Showing %s mod +modmenu.showingLibraries.n=Showing %s libraries +modmenu.showingLibraries.1=Showing %s library +modmenu.showingModsLibraries.n.n=Showing %s mods and %s libraries +modmenu.showingModsLibraries.n.1=Showing %s mods and %s library +modmenu.showingModsLibraries.1.n=Showing %s mod and %s libraries +modmenu.showingModsLibraries.1.1=Showing %s mod and %s library + +modmenu.badge.clientsideOnly=Client +modmenu.badge.library=Library +modmenu.badge.deprecated=Deprecated +modmenu.badge.minecraft=Minecraft +modmenu.badge.forge=Forge + +modmenu.modIdToolTip=Mod ID: %s +modmenu.authorPrefix=By %s +modmenu.config=Edit Config +modmenu.configure=Configure... +modmenu.website=Website +modmenu.issues=Issues + +modmenu.credits=Credits: +modmenu.viewCredits=View Credits +modmenu.license=License: +modmenu.links=Links: +modmenu.sources=Source +modmenu.hasUpdate=An update is available: +modmenu.experimental=(Mod Menu update checker is experimental!) +modmenu.childHasUpdate=A child of this mod has an update available. +modmenu.updateText=v%s on %s +modmenu.updateText.nonNumerical=%s on %s +modmenu.install_version=Install version %s +modmenu.downloadLink=Download + +modmenu.bluesky=Bluesky +modmenu.bta=Downloads BTA! +modmenu.buymeacoffee=Buy Me a Coffee +modmenu.coindrop=Coindrop +modmenu.crowdin=Crowdin +modmenu.curseforge=CurseForge +modmenu.discord=Discord +modmenu.donate=Donate +modmenu.flattr=Flattr +modmenu.github_releases=GitHub Releases +modmenu.github_sponsors=GitHub Sponsors +modmenu.kofi=Ko-fi +modmenu.liberapay=Liberapay +modmenu.mastodon=Mastodon +modmenu.modrinth=Modrinth +modmenu.opencollective=Open Collective +modmenu.patreon=Patreon +modmenu.paypal=PayPal +modmenu.reddit=Reddit +modmenu.twitch=Twitch +modmenu.wiki=Wiki +modmenu.twitter=X (Twitter) +modmenu.youtube=YouTube + +modmenu.credits.role.author=Authors +modmenu.credits.role.contributor=Contributors +modmenu.credits.role.translator=Translators +modmenu.credits.role.maintainer=Maintainers +modmenu.credits.role.playtester=Playtesters +modmenu.credits.role.illustrator=Illustrators +modmenu.credits.role.owner=Owners + +modmenu.modsFolder=Open Mods Folder +modmenu.configsFolder=Open Configs Folder + +modmenu.nameTranslation.minecraft=Better than Adventure! +modmenu.descriptionTranslation.minecraft=The base game. +modmenu.nameTranslation.fabricloader=Fabric Loader +modmenu.descriptionTranslation.fabricloader=The base mod loader. +modmenu.nameTranslation.java=Java +modmenu.descriptionTranslation.java=The Java runtime environment. +modmenu.javaDistributionName=Running: %s + +modmenu.options=Mod Menu Options +option.modmenu.sorting=Sort +option.modmenu.sorting.ascending=A-Z +option.modmenu.sorting.descending=Z-A +option.modmenu.sorting.has_update=Has Update +option.modmenu.show_libraries=Libraries +option.modmenu.show_libraries.true=Shown +option.modmenu.show_libraries.false=Hidden +option.modmenu.show_libraries.with_config=With Config +option.modmenu.hide_config_buttons=Config Buttons +option.modmenu.hide_config_buttons.true=Hidden +option.modmenu.hide_config_buttons.false=Shown +option.modmenu.hide_badges=Mod Badges +option.modmenu.hide_badges.true=Hidden +option.modmenu.hide_badges.false=Shown +option.modmenu.compact_list=List +option.modmenu.compact_list.true=Compact +option.modmenu.compact_list.false=Standard +option.modmenu.hide_mod_links=Mod Links +option.modmenu.hide_mod_links.true=Hidden +option.modmenu.hide_mod_links.false=Shown +option.modmenu.hide_mod_credits=Mod Credits +option.modmenu.hide_mod_credits.true=Hidden +option.modmenu.hide_mod_credits.false=Shown +option.modmenu.hide_mod_license=Mod License +option.modmenu.hide_mod_license.true=Hidden +option.modmenu.hide_mod_license.false=Shown +option.modmenu.count_children=Children +option.modmenu.count_children.true=Counted +option.modmenu.count_children.false=Not Counted +option.modmenu.count_libraries=Libraries +option.modmenu.count_libraries.true=Counted +option.modmenu.count_libraries.false=Not Counted +option.modmenu.count_hidden_mods=Hidden Mods +option.modmenu.count_hidden_mods.true=Counted +option.modmenu.count_hidden_mods.false=Not Counted +option.modmenu.mod_count_location=Mod Count +option.modmenu.mod_count_location.title_screen=Title Corner +option.modmenu.mod_count_location.mods_button=Mods Button +option.modmenu.mod_count_location.title_screen_and_mods_button=Both +option.modmenu.mod_count_location.none=Neither +option.modmenu.easter_eggs=Easter Eggs +option.modmenu.easter_eggs.true=Enabled +option.modmenu.easter_eggs.false=Disabled +option.modmenu.mods_button_style=Title Screen +option.modmenu.mods_button_style.insert=Insert +option.modmenu.mods_button_style.icon=Icon +option.modmenu.game_menu_button_style=Game Menu +option.modmenu.game_menu_button_style.insert=Insert +option.modmenu.game_menu_button_style.icon=Icon +option.modmenu.random_java_colors=Java Color +option.modmenu.random_java_colors.true=Vendor +option.modmenu.random_java_colors.false=Always Red +option.modmenu.translate_names=Names +option.modmenu.translate_names.true=Localized +option.modmenu.translate_names.false=Not Localized +option.modmenu.translate_descriptions=Descriptions +option.modmenu.translate_descriptions.true=Localized +option.modmenu.translate_descriptions.false=Not Localized +option.modmenu.update_checker=Update Checker +option.modmenu.update_checker.true=Enabled +option.modmenu.update_checker.false=Disabled +option.modmenu.button_update_badge=Update Indicator +option.modmenu.button_update_badge.true=Shown +option.modmenu.button_update_badge.false=Hidden +option.modmenu.quick_configure=Quick Configure +option.modmenu.quick_configure.true=Enabled +option.modmenu.quick_configure.false=Disabled +option.modmenu.update_channel=Update Channel +option.modmenu.update_channel.alpha=All +option.modmenu.update_channel.beta=Release/Beta +option.modmenu.update_channel.release=Release diff --git a/src/main/resources/assets/modmenu/lang/en_US/en_US.lang b/src/main/resources/assets/modmenu/lang/en_US/en_US.lang deleted file mode 100644 index 899411a92..000000000 --- a/src/main/resources/assets/modmenu/lang/en_US/en_US.lang +++ /dev/null @@ -1,28 +0,0 @@ -modmenu.title=Mods -modmenu.loaded=(%s Loaded) -modmenu.config=Edit Config -modmenu.modsFolder=Open Mods Folder -modmenu.done=Done -modmenu.configsFolder=Open Configs Folder -modmenu.configure=Configure... -modmenu.modIdToolTip=Mod ID: %s -modmenu.website=Website -modmenu.issues=Issues -modmenu.authorPrefix=By %s -modmenu.authors=Authors: -modmenu.contributors=Contributors: -modmenu.licenses=Licenses: -modmenu.search=Search for mods -modmenu.clientsideOnly=Client -modmenu.library=Library -modmenu.deprecated=Deprecated -modmenu.toggleFilterOptions=Toggle Filter Options -modmenu.showLibraries=Libraries: %s -modmenu.showLibraries.true=Shown -modmenu.showLibraries.false=Hidden -modmenu.showingMods=Showing %s Mods -modmenu.sorting=Sort: %s -modmenu.sorting.ascending=A-Z -modmenu.sorting.decending=Z-A -modmenu.minecraft=Minecraft -modmenu.forge=Forge diff --git a/src/main/resources/assets/modmenu/lang/en_US/en_us.json b/src/main/resources/assets/modmenu/lang/en_US/en_us.json deleted file mode 100644 index 3fce4052f..000000000 --- a/src/main/resources/assets/modmenu/lang/en_US/en_us.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "modmenu.title": "Mods", - "modmenu.loaded": "(%s Loaded)", - "modmenu.config": "Edit Config", - "modmenu.modsFolder": "Open Mods Folder", - "modmenu.configsFolder": "Open Configs Folder", - "modmenu.configure": "Configure...", - "modmenu.modIdToolTip": "Mod ID: %s", - "modmenu.website": "Website", - "modmenu.issues": "Issues", - "modmenu.authorPrefix": "By %s", - "modmenu.search": "Search for mods", - "modmenu.clientsideOnly": "Client", - "modmenu.library": "Library", - "modmenu.toggleFilterOptions": "Toggle Filter Options", - "modmenu.showLibraries": "Libraries: %s", - "modmenu.showLibraries.true": "Shown", - "modmenu.showLibraries.false": "Hidden", - "modmenu.showingMods": "Showing %s Mods", - "modmenu.sorting": "Sort: %s", - "modmenu.sorting.ascending": "A-Z", - "modmenu.sorting.decending": "Z-A", - "modmenu.minecraft": "Minecraft", - "modmenu.forge": "Forge" -} diff --git a/src/main/resources/assets/modmenu/lang/et_EE/et_EE.lang b/src/main/resources/assets/modmenu/lang/et_EE.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/et_EE/et_EE.lang rename to src/main/resources/assets/modmenu/lang/et_EE.lang diff --git a/src/main/resources/assets/modmenu/lang/et_EE/et_ee.json b/src/main/resources/assets/modmenu/lang/et_EE/et_ee.json deleted file mode 100644 index 46d6edf7d..000000000 --- a/src/main/resources/assets/modmenu/lang/et_EE/et_ee.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "modmenu.title": "Modid", - "modmenu.loaded": "(%s laaditud)", - "modmenu.config": "Muuda seadistust", - "modmenu.modsFolder": "Ava modide kaust", - "modmenu.configsFolder": "Ava seadistuste kaust", - "modmenu.configure": "Seadista...", - "modmenu.modIdToolTip": "Modi ID: %s", - "modmenu.website": "Veebileht", - "modmenu.issues": "Veahaldus", - "modmenu.authorPrefix": "Autorilt %s", - "modmenu.search": "Otsi mode", - "modmenu.clientsideOnly": "Klient", - "modmenu.library": "Teek", - "modmenu.toggleFilterOptions": "Lülita filtreerimisvalikuid", - "modmenu.showLibraries": "Teegid: %s", - "modmenu.showLibraries.true": "kuvatud", - "modmenu.showLibraries.false": "peidetud", - "modmenu.showingMods": "Kuvan %s modi", - "modmenu.sorting": "Jrk: %s", - "modmenu.sorting.ascending": "A-Z", - "modmenu.sorting.decending": "Z-A" -} diff --git a/src/main/resources/assets/modmenu/lang/fi_FI/fi_FI.lang b/src/main/resources/assets/modmenu/lang/fi_FI.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/fi_FI/fi_FI.lang rename to src/main/resources/assets/modmenu/lang/fi_FI.lang diff --git a/src/main/resources/assets/modmenu/lang/fi_FI/fi_fi.json b/src/main/resources/assets/modmenu/lang/fi_FI/fi_fi.json deleted file mode 100644 index d62cf4819..000000000 --- a/src/main/resources/assets/modmenu/lang/fi_FI/fi_fi.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "modmenu.title": "Modit", - "modmenu.loaded": "(%s ladattu)", - "modmenu.config": "Muuta asetuksia", - "modmenu.modsFolder": "Avaa modikansio", - "modmenu.configsFolder": "Avaa asetuskansio", - "modmenu.configure": "Asetukset...", - "modmenu.modIdToolTip": "Modi-ID: %s", - "modmenu.website": "Kotisivu", - "modmenu.issues": "Ongelmat", - "modmenu.authorPrefix": "Tehnyt %s", - "modmenu.search": "Etsi modeja" -} diff --git a/src/main/resources/assets/modmenu/lang/fr_FR/fr_FR.lang b/src/main/resources/assets/modmenu/lang/fr_FR.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/fr_FR/fr_FR.lang rename to src/main/resources/assets/modmenu/lang/fr_FR.lang diff --git a/src/main/resources/assets/modmenu/lang/fr_FR/fr_fr.json b/src/main/resources/assets/modmenu/lang/fr_FR/fr_fr.json deleted file mode 100644 index 48564175e..000000000 --- a/src/main/resources/assets/modmenu/lang/fr_FR/fr_fr.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "modmenu.title": "Mods", - "modmenu.loaded": "(%s chargés)", - "modmenu.config": "Modifier la config", - "modmenu.modsFolder": "Ouvrir le dossier des mods", - "modmenu.configsFolder": "Ouvrir le dossier des configs", - "modmenu.configure": "Configurer...", - "modmenu.modIdToolTip": "ID du mod : %s", - "modmenu.website": "Site web", - "modmenu.issues": "Problèmes", - "modmenu.authorPrefix": "Par %s", - "modmenu.search": "Rechercher des mods", - "modmenu.clientsideOnly": "Client", - "modmenu.library": "Librairie", - "modmenu.toggleFilterOptions": "Basculer options de filtre", - "modmenu.showLibraries": "Librairies : %s", - "modmenu.showLibraries.true": "montrées", - "modmenu.showLibraries.false": "cachées", - "modmenu.showingMods": "%s mods affichés" -} diff --git a/src/main/resources/assets/modmenu/lang/ja_JP/ja_JP.lang b/src/main/resources/assets/modmenu/lang/ja_JP.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/ja_JP/ja_JP.lang rename to src/main/resources/assets/modmenu/lang/ja_JP.lang diff --git a/src/main/resources/assets/modmenu/lang/ja_JP/ja_jp.json b/src/main/resources/assets/modmenu/lang/ja_JP/ja_jp.json deleted file mode 100644 index 5d2faf2f5..000000000 --- a/src/main/resources/assets/modmenu/lang/ja_JP/ja_jp.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "modmenu.title": "Mods", - "modmenu.loaded": "(%s個 読込済み)", - "modmenu.config": "設定の編集", - "modmenu.modsFolder": "modsフォルダーを開く", - "modmenu.configsFolder": "configフォルダーを開く", - "modmenu.configure": "設定…", - "modmenu.modIdToolTip": "Mod ID: %s", - "modmenu.website": "ウェブサイト", - "modmenu.issues": "バグを報告", - "modmenu.authorPrefix": "制作: %s", - "modmenu.search": "Modを検索", - "modmenu.clientsideOnly": "クライアント", - "modmenu.library": "前提mod", - "modmenu.toggleFilterOptions": "フィルター設定の切り替え", - "modmenu.showLibraries": "前提mod: %s", - "modmenu.showLibraries.true": "表示", - "modmenu.showLibraries.false": "非表示", - "modmenu.showingMods": "%s個のmodを表示中", - "modmenu.sorting": "表示順: %s", - "modmenu.sorting.ascending": "昇順(A-Z)", - "modmenu.sorting.decending": "降順(Z-A)", - "modmenu.minecraft": "Minecraft" -} diff --git a/src/main/resources/assets/modmenu/lang/pl_PL.lang b/src/main/resources/assets/modmenu/lang/pl_PL.lang new file mode 100644 index 000000000..5b3238112 --- /dev/null +++ b/src/main/resources/assets/modmenu/lang/pl_PL.lang @@ -0,0 +1,12 @@ +modmenu.title=Mody +modmenu.loaded=(%s Załadowanych) +modmenu.config=Edytuj konfigurację +modmenu.modsFolder=Otwórz folder z modami +modmenu.configsFolder=Otwórz folder z plikami kontiguracyjnymi +modmenu.configure=Konfiguruję... +modmenu.modIdToolTip=Mod ID: %s +modmenu.website=Strona +modmenu.issues=Problemy +modmenu.authorPrefix=Stworzone przez %s +modmenu.search=Szukam modów... +modmenu.clientsideOnly=Client diff --git a/src/main/resources/assets/modmenu/lang/pl_PL/pl_pl.json b/src/main/resources/assets/modmenu/lang/pl_PL/pl_pl.json deleted file mode 100644 index 8ce2ef341..000000000 --- a/src/main/resources/assets/modmenu/lang/pl_PL/pl_pl.json +++ /dev/null @@ -1,15 +0,0 @@ - -{ - "modmenu.title": "Mody", - "modmenu.loaded": "(%s Załadowanych)", - "modmenu.config": "Edytuj konfigurację", - "modmenu.modsFolder": "Otwórz folder z modami", - "modmenu.configsFolder": "Otwórz folder z plikami kontiguracyjnymi", - "modmenu.configure": "Konfiguruję...", - "modmenu.modIdToolTip": "Mod ID: %s", - "modmenu.website": "Strona", - "modmenu.issues": "Problemy", - "modmenu.authorPrefix": "Stworzone przez %s", - "modmenu.search": "Szukam modów...", - "modmenu.clientsideOnly": "Client" -} diff --git a/src/main/resources/assets/modmenu/lang/pt_BR/pt_BR.lang b/src/main/resources/assets/modmenu/lang/pt_BR.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/pt_BR/pt_BR.lang rename to src/main/resources/assets/modmenu/lang/pt_BR.lang diff --git a/src/main/resources/assets/modmenu/lang/pt_BR/pt_br.json b/src/main/resources/assets/modmenu/lang/pt_BR/pt_br.json deleted file mode 100644 index 9f5c719dd..000000000 --- a/src/main/resources/assets/modmenu/lang/pt_BR/pt_br.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "modmenu.title": "Mods", - "modmenu.loaded": "(%s Carregados)", - "modmenu.config": "Editar Config", - "modmenu.modsFolder": "Abrir pasta de mods", - "modmenu.configsFolder": "Abrir pasta de Config", - "modmenu.configure": "Configuração...", - "modmenu.modIdToolTip": "Mod ID: %s", - "modmenu.website": "Website", - "modmenu.issues": "Problemas", - "modmenu.authorPrefix": "Por %s" -} diff --git a/src/main/resources/assets/modmenu/lang/ru_RU.lang b/src/main/resources/assets/modmenu/lang/ru_RU.lang new file mode 100644 index 000000000..4e89d1bee --- /dev/null +++ b/src/main/resources/assets/modmenu/lang/ru_RU.lang @@ -0,0 +1,171 @@ +modmenu.title=Моды +modmenu.nameTranslation.modmenu=Mod Menu +modmenu.descriptionTranslation.modmenu=Удобное меню для просмотра списка установленных модов. + +modmenu.loaded=(загружено: %s) +modmenu.loaded.short=(%s) +modmenu.loaded.69.secret=(загружено: %s... идеально) +modmenu.loaded.420.secret=(загружено: %s... огоньку?) +modmenu.mods.n= (%s модов) +modmenu.mods.1= (%s мод) +modmenu.mods.69.secret=(%s модов... идеально) +modmenu.mods.420.secret= (%s модов... огоньку?) + +modmenu.search=Поиск модов +modmenu.searchTerms.library=апи библиотека +modmenu.searchTerms.clientside=клиент клиентский клиентские +modmenu.searchTerms.deprecated=устаревший доисторический старый +modmenu.searchTerms.patchwork=patchwork forge fml фордж +modmenu.searchTerms.configurable=настройки настраиваемый настраиваемые опции + +modmenu.toggleFilterOptions=Фильтры +modmenu.showingMods.n=Показано %s модов +modmenu.showingMods.1=Показан %s мод +modmenu.showingLibraries.n=Показано %s библиотек +modmenu.showingLibraries.1=Показана %s библиотека +modmenu.showingModsLibraries.n.n=Показано %s модов и %s библиотек +modmenu.showingModsLibraries.n.1=Показано %s модов и %s библиотека +modmenu.showingModsLibraries.1.n=Показан %s мод и %s библиотек +modmenu.showingModsLibraries.1.1=Показан %s мод и %s библиотека + +modmenu.badge.clientsideOnly=Клиент +modmenu.badge.library=Библиотека +modmenu.badge.deprecated=Устарел +modmenu.badge.minecraft=Minecraft +modmenu.badge.forge=Forge + +modmenu.modIdToolTip=ID мода: %s +modmenu.authorPrefix=Авторы: %s +modmenu.config=Изменить настройки +modmenu.configure=Настройки... +modmenu.website=Сайт +modmenu.issues=Проблемы + +modmenu.credits=Создатели: +modmenu.viewCredits=Показать авторов +modmenu.license=Лицензия: +modmenu.links=Ссылки: +modmenu.sources=Исходный код +modmenu.hasUpdate=Доступно обновление: +modmenu.experimental=(проверка обновлений ModMenu экспериментальна) +modmenu.childHasUpdate=Доступно обновление для дочернего мода. +modmenu.updateText=v%s на %s +modmenu.updateText.nonNumerical=%s на %s +modmenu.install_version=Установить версию %s +modmenu.downloadLink=Загрузить + +modmenu.bluesky=Bluesky +modmenu.bta=Загрузки BTA! +modmenu.buymeacoffee=Buy Me a Coffee +modmenu.coindrop=Coindrop +modmenu.crowdin=Crowdin +modmenu.curseforge=CurseForge +modmenu.discord=Discord +modmenu.donate=Поддержать +modmenu.flattr=Flattr +modmenu.github_releases=Релизы на GitHub +modmenu.github_sponsors=GitHub Sponsors +modmenu.kofi=Ko-fi +modmenu.liberapay=Liberapay +modmenu.mastodon=Mastodon +modmenu.modrinth=Modrinth +modmenu.opencollective=Open Collective +modmenu.patreon=Patreon +modmenu.paypal=PayPal +modmenu.reddit=Reddit +modmenu.twitch=Twitch +modmenu.wiki=Вики +modmenu.twitter=Твиттер +modmenu.youtube=YouTube + +modmenu.credits.role.author=Авторы +modmenu.credits.role.contributor=Участники +modmenu.credits.role.translator=Переводчики +modmenu.credits.role.maintainer=Основные разработчики +modmenu.credits.role.playtester=Тестировщики +modmenu.credits.role.illustrator=Иллюстраторы +modmenu.credits.role.owner=Владельцы + +modmenu.modsFolder=Папка с модами +modmenu.configsFolder=Папка с настройками + +modmenu.nameTranslation.minecraft=Minecraft +modmenu.descriptionTranslation.minecraft=Основная игра. +modmenu.nameTranslation.fabricloader=Загрузчик Fabric +modmenu.descriptionTranslation.fabricloader=Основной загрузчик модов. +modmenu.nameTranslation.java=Java +modmenu.descriptionTranslation.java=Исполняющая система Java. +modmenu.javaDistributionName=Запущено: %s + +modmenu.options=Настройки списка модов +option.modmenu.sorting=Сортировка +option.modmenu.sorting.ascending=A-Z +option.modmenu.sorting.descending=Z-A +option.modmenu.sorting.has_update=Обновление +option.modmenu.show_libraries=Библиотеки +option.modmenu.show_libraries.true=Показаны +option.modmenu.show_libraries.false=Скрыты +option.modmenu.show_libraries.with_config=Настройки +option.modmenu.hide_config_buttons=Кнопка настроек +option.modmenu.hide_config_buttons.true=Скрыта +option.modmenu.hide_config_buttons.false=Показана +option.modmenu.hide_badges=Метки модов +option.modmenu.hide_badges.true=Скрыты +option.modmenu.hide_badges.false=Показаны +option.modmenu.compact_list=Список +option.modmenu.compact_list.true=Компактный +option.modmenu.compact_list.false=Обычный +option.modmenu.hide_mod_links=Ссылки мода +option.modmenu.hide_mod_links.true=Скрыты +option.modmenu.hide_mod_links.false=Показаны +option.modmenu.hide_mod_credits=Авторы мода +option.modmenu.hide_mod_credits.true=Скрыты +option.modmenu.hide_mod_credits.false=Показаны +option.modmenu.hide_mod_license=Лицензия мода +option.modmenu.hide_mod_license.true=Скрыта +option.modmenu.hide_mod_license.false=Показана +option.modmenu.count_children=Дочерние +option.modmenu.count_children.true=Учитывать +option.modmenu.count_children.false=Не учитывать +option.modmenu.count_libraries=Библиотеки +option.modmenu.count_libraries.true=Учитывать +option.modmenu.count_libraries.false=Не учитывать +option.modmenu.count_hidden_mods=Скрытые +option.modmenu.count_hidden_mods.true=Учитывать +option.modmenu.count_hidden_mods.false=Не учитывать +option.modmenu.mod_count_location=Счётчик модов +option.modmenu.mod_count_location.title_screen=В углу меню +option.modmenu.mod_count_location.mods_button=На кнопке +option.modmenu.mod_count_location.title_screen_and_mods_button=Везде +option.modmenu.mod_count_location.none=Отключён +option.modmenu.easter_eggs=Пасхалки +option.modmenu.easter_eggs.true=Включены +option.modmenu.easter_eggs.false=Выключены +option.modmenu.mods_button_style=Кнопка +option.modmenu.mods_button_style.insert=Посередине +option.modmenu.mods_button_style.icon=Отдельный значок +option.modmenu.game_menu_button_style=В игре +option.modmenu.game_menu_button_style.insert=Посередине +option.modmenu.game_menu_button_style.icon=Отдельный значок +option.modmenu.random_java_colors=Цвет Java +option.modmenu.random_java_colors.true=От поставщика +option.modmenu.random_java_colors.false=Всегда красный +option.modmenu.translate_names=Локализация названий +option.modmenu.translate_names.true=Вкл +option.modmenu.translate_names.false=Выкл +option.modmenu.translate_descriptions=Локализация описаний +option.modmenu.translate_descriptions.true=Вкл +option.modmenu.translate_descriptions.false=Выкл +option.modmenu.update_checker=Проверка обновлений +option.modmenu.update_checker.true=Вкл +option.modmenu.update_checker.false=Выкл +option.modmenu.button_update_badge=Индикатор обновлений +option.modmenu.button_update_badge.true=Вкл +option.modmenu.button_update_badge.false=Выкл +option.modmenu.quick_configure=Быстрые настройки +option.modmenu.quick_configure.true=Вкл +option.modmenu.quick_configure.false=Выкл +option.modmenu.update_channel=Канал обнов. +option.modmenu.update_channel.alpha=Все +option.modmenu.update_channel.beta=Релиз и Бета +option.modmenu.update_channel.release=Релиз diff --git a/src/main/resources/assets/modmenu/lang/ru_RU/ru_RU.lang b/src/main/resources/assets/modmenu/lang/ru_RU/ru_RU.lang deleted file mode 100644 index fbb6a3936..000000000 --- a/src/main/resources/assets/modmenu/lang/ru_RU/ru_RU.lang +++ /dev/null @@ -1,23 +0,0 @@ -modmenu.title=Моды -modmenu.loaded=(загружено: %s) -modmenu.config=Изменить настройки -modmenu.modsFolder=Открыть папку модов -modmenu.configsFolder=Открыть папку настроек -modmenu.configure=Настроить... -modmenu.modIdToolTip=ID мода: %s -modmenu.website=Сайт -modmenu.issues=Проблемы/Вопросы -modmenu.authorPrefix=От %s -modmenu.search=Поиск модов -modmenu.clientsideOnly=Клиент -modmenu.library=Библиотека -modmenu.toggleFilterOptions=Переключить параметры фильтра -modmenu.showLibraries=Библиотеки: %s -modmenu.showLibraries.true=Показано -modmenu.showLibraries.false=Скрыто -modmenu.showingMods=Показано модов: %s -modmenu.sorting=Сортировка: %s -modmenu.sorting.ascending=A-Z -modmenu.sorting.decending=Z-A -modmenu.minecraft=Minecraft -modmenu.forge=Forge diff --git a/src/main/resources/assets/modmenu/lang/ru_RU/ru_ru.json b/src/main/resources/assets/modmenu/lang/ru_RU/ru_ru.json deleted file mode 100644 index 5621d5894..000000000 --- a/src/main/resources/assets/modmenu/lang/ru_RU/ru_ru.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "modmenu.title": "Моды", - "modmenu.loaded": "(загружено: %s)", - "modmenu.config": "Изменить настройки", - "modmenu.modsFolder": "Открыть папку модов", - "modmenu.configsFolder": "Открыть папку настроек", - "modmenu.configure": "Настроить...", - "modmenu.modIdToolTip": "ID мода: %s", - "modmenu.website": "Сайт", - "modmenu.issues": "Проблемы/Вопросы", - "modmenu.authorPrefix": "От %s", - "modmenu.search": "Поиск модов", - "modmenu.clientsideOnly": "Клиент", - "modmenu.library": "Библиотека", - "modmenu.toggleFilterOptions": "Переключить параметры фильтра", - "modmenu.showLibraries": "Библиотеки: %s", - "modmenu.showLibraries.true": "Показано", - "modmenu.showLibraries.false": "Скрыто", - "modmenu.showingMods": "Показано модов: %s", - "modmenu.sorting": "Сортировка: %s", - "modmenu.sorting.ascending": "A-Z", - "modmenu.sorting.decending": "Z-A", - "modmenu.minecraft": "Minecraft", - "modmenu.forge": "Forge" -} diff --git a/src/main/resources/assets/modmenu/lang/uk_UA.lang b/src/main/resources/assets/modmenu/lang/uk_UA.lang new file mode 100644 index 000000000..8a619bd4e --- /dev/null +++ b/src/main/resources/assets/modmenu/lang/uk_UA.lang @@ -0,0 +1,171 @@ +modmenu.title=Моди +modmenu.nameTranslation.modmenu=Mod Menu +modmenu.descriptionTranslation.modmenu=Додає меню для перегляду встановлених модів. + +modmenu.loaded=(Завантажено %s) +modmenu.loaded.short=(%s) +modmenu.loaded.69.secret=(Завантажено %s… Славно) +modmenu.loaded.420.secret=(Завантажено %s… Спали їх) +modmenu.mods.n= (%s модів) +modmenu.mods.1= (%s мод) +modmenu.mods.69.secret= (%s модів… Славно) +modmenu.mods.420.secret= (%s модів… Спали їх) + +modmenu.search=Пошук +modmenu.searchTerms.library=api бібліотека library +modmenu.searchTerms.clientside=сторонаклієнта клієт client +modmenu.searchTerms.deprecated=застарілий віджилий старий old outdated deprecated +modmenu.searchTerms.patchwork=роботапатча forge fml patch +modmenu.searchTerms.configurable=конфігурація налаштування налаштувань параметри настройки config settings options + +modmenu.toggleFilterOptions=Фільтри +modmenu.showingMods.n=Показано %s модів +modmenu.showingMods.1=Показано %s мод +modmenu.showingLibraries.n=Показано %s бібліотек +modmenu.showingLibraries.1=Показано %s бібліотеку +modmenu.showingModsLibraries.n.n=Показано %s модів і %s бібліотек +modmenu.showingModsLibraries.n.1=Показано %s модів і %s бібліотеку +modmenu.showingModsLibraries.1.n=Показано %s мод і %s бібліотек +modmenu.showingModsLibraries.1.1=Показано %s мод і %s бібліотеку + +modmenu.badge.clientsideOnly=Клієнт +modmenu.badge.library=Бібліотека +modmenu.badge.deprecated=Застарілий +modmenu.badge.minecraft=Minecraft +modmenu.badge.forge=Forge + +modmenu.modIdToolTip=ID мода: %s +modmenu.authorPrefix=Від %s +modmenu.config=Редагувати налаштування +modmenu.configure=Налаштування… +modmenu.website=Сайт мода +modmenu.issues=Звітувати помилку + +modmenu.credits=Згадки: +modmenu.viewCredits=Показати авторів +modmenu.license=Ліцензія: +modmenu.links=Посилання: +modmenu.sources=Початковий код +modmenu.hasUpdate=Оновлення доступне: +modmenu.experimental=(Перевірка оновлень є експериментальною!) +modmenu.childHasUpdate=Дочірній мод цього мода має доступне оновлення +modmenu.updateText=v%s на %s +modmenu.updateText.nonNumerical=%s на %s +modmenu.install_version=Установити версію %s +modmenu.downloadLink=Завантажити + +modmenu.bluesky=Bluesky +modmenu.bta=Завантаження BTA! +modmenu.buymeacoffee=Buy Me a Coffee +modmenu.coindrop=Coindrop +modmenu.crowdin=Crowdin +modmenu.curseforge=CurseForge +modmenu.discord=Discord +modmenu.donate=Підтримати +modmenu.flattr=Flattr +modmenu.github_releases=Випуски на GitHub +modmenu.github_sponsors=Спонсори GitHub +modmenu.kofi=Ko-fi +modmenu.liberapay=Liberapay +modmenu.mastodon=Mastodon +modmenu.modrinth=Modrinth +modmenu.opencollective=Open Collective +modmenu.patreon=Patreon +modmenu.paypal=PayPal +modmenu.reddit=Reddit +modmenu.twitch=Twitch +modmenu.wiki=Вікі +modmenu.twitter=Х (Twitter) +modmenu.youtube=YouTube + +modmenu.credits.role.author=Автори +modmenu.credits.role.contributor=Дописувачі +modmenu.credits.role.translator=Перекладачі +modmenu.credits.role.maintainer=Супроводжувачі +modmenu.credits.role.playtester=Тестери +modmenu.credits.role.illustrator=Ілюстратори +modmenu.credits.role.owner=Власники + +modmenu.modsFolder=Відкрити теку модів +modmenu.configsFolder=Відкрити теку налаштувань + +modmenu.nameTranslation.minecraft=Better than Adventure! +modmenu.descriptionTranslation.minecraft=Усталена гра. +modmenu.nameTranslation.fabricloader=Завантажувач Fabric +modmenu.descriptionTranslation.fabricloader=Усталений завантажувач модів. +modmenu.nameTranslation.java=Java +modmenu.descriptionTranslation.java=Середовище виконання Java. +modmenu.javaDistributionName=Запуск: %s + +modmenu.options=Налаштування Mod Menu +option.modmenu.sorting=Сортування +option.modmenu.sorting.ascending=А-Я +option.modmenu.sorting.descending=Я-А +option.modmenu.sorting.has_update=Оновлення +option.modmenu.show_libraries=Бібліотеки +option.modmenu.show_libraries.true=Показано +option.modmenu.show_libraries.false=Приховано +option.modmenu.show_libraries.with_config=Налаштув. +option.modmenu.hide_config_buttons=Кнопки налашт. +option.modmenu.hide_config_buttons.true=Приховано +option.modmenu.hide_config_buttons.false=Показано +option.modmenu.hide_badges=Значки модів +option.modmenu.hide_badges.true=Приховано +option.modmenu.hide_badges.false=Показано +option.modmenu.compact_list=Перелік +option.modmenu.compact_list.true=Стислий +option.modmenu.compact_list.false=Звичайний +option.modmenu.hide_mod_links=Посилання +option.modmenu.hide_mod_links.true=Приховано +option.modmenu.hide_mod_links.false=Показано +option.modmenu.hide_mod_credits=Згадки +option.modmenu.hide_mod_credits.true=Приховано +option.modmenu.hide_mod_credits.false=Показано +option.modmenu.hide_mod_license=Ліцензія +option.modmenu.hide_mod_license.true=Приховано +option.modmenu.hide_mod_license.false=Показано +option.modmenu.count_children=Дочірні моди +option.modmenu.count_children.true=Ураховано +option.modmenu.count_children.false=Не враховано +option.modmenu.count_libraries=Бібліотеки +option.modmenu.count_libraries.true=Ураховано +option.modmenu.count_libraries.false=Не враховано +option.modmenu.count_hidden_mods=Сховані моди +option.modmenu.count_hidden_mods.true=Ураховано +option.modmenu.count_hidden_mods.false=Не враховано +option.modmenu.mod_count_location=Кількість модів +option.modmenu.mod_count_location.title_screen=У кутку +option.modmenu.mod_count_location.mods_button=Кнопка +option.modmenu.mod_count_location.title_screen_and_mods_button=Усюди +option.modmenu.mod_count_location.none=Приховано +option.modmenu.easter_eggs=Прихованки +option.modmenu.easter_eggs.true=УВІМК. +option.modmenu.easter_eggs.false=ВИМК. +option.modmenu.mods_button_style=Головне меню +option.modmenu.mods_button_style.insert=Окремо +option.modmenu.mods_button_style.icon=Значок +option.modmenu.game_menu_button_style=Ігрове меню +option.modmenu.game_menu_button_style.insert=Окремо +option.modmenu.game_menu_button_style.icon=Значок +option.modmenu.random_java_colors=Колір Java +option.modmenu.random_java_colors.true=Постачальник +option.modmenu.random_java_colors.false=Червоний +option.modmenu.translate_names=Назви +option.modmenu.translate_names.true=Перекладено +option.modmenu.translate_names.false=Не перекладено +option.modmenu.translate_descriptions=Описи +option.modmenu.translate_descriptions.true=Перекладено +option.modmenu.translate_descriptions.false=Не перекладено +option.modmenu.update_checker=Перевірка оновлень +option.modmenu.update_checker.true=УВІМК. +option.modmenu.update_checker.false=ВИМК. +option.modmenu.button_update_badge=Індикатор онов. +option.modmenu.button_update_badge.true=Показано +option.modmenu.button_update_badge.false=Приховано +option.modmenu.quick_configure=Швидке налаштування +option.modmenu.quick_configure.true=УВІМК. +option.modmenu.quick_configure.false=ВИМК. +option.modmenu.update_channel=Тип оновлень +option.modmenu.update_channel.alpha=Усі +option.modmenu.update_channel.beta=Випуск/бета +option.modmenu.update_channel.release=Випуск diff --git a/src/main/resources/assets/modmenu/lang/zh_CN/zh_CN.lang b/src/main/resources/assets/modmenu/lang/zh_CN.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/zh_CN/zh_CN.lang rename to src/main/resources/assets/modmenu/lang/zh_CN.lang diff --git a/src/main/resources/assets/modmenu/lang/zh_CN/zh_cn.json b/src/main/resources/assets/modmenu/lang/zh_CN/zh_cn.json deleted file mode 100644 index a3b774aaa..000000000 --- a/src/main/resources/assets/modmenu/lang/zh_CN/zh_cn.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "modmenu.title": "模组", - "modmenu.loaded": "(已载入%s个)", - "modmenu.config": "编辑配置", - "modmenu.modsFolder": "开启模组文件夹", - "modmenu.configsFolder": "开启配置文件夹", - "modmenu.configure": "配置...", - "modmenu.modIdToolTip": "模组 ID: %s", - "modmenu.website": "网站", - "modmenu.issues": "反馈", - "modmenu.authorPrefix": "作者: %s", - "modmenu.search": "搜索模组", - "modmenu.clientsideOnly": "客户端", - "modmenu.library": "运行库", - "modmenu.toggleFilterOptions": "切换过滤选项", - "modmenu.showLibraries": "运行库: %s", - "modmenu.showLibraries.true": "显示", - "modmenu.showLibraries.false": "隐藏", - "modmenu.showingMods": "显示 %s 个模组", - "modmenu.sorting": "排序方式: %s" -} diff --git a/src/main/resources/assets/modmenu/lang/zh_TW/zh_TW.lang b/src/main/resources/assets/modmenu/lang/zh_TW.lang similarity index 100% rename from src/main/resources/assets/modmenu/lang/zh_TW/zh_TW.lang rename to src/main/resources/assets/modmenu/lang/zh_TW.lang diff --git a/src/main/resources/assets/modmenu/lang/zh_TW/zh_tw.json b/src/main/resources/assets/modmenu/lang/zh_TW/zh_tw.json deleted file mode 100644 index 9eb629814..000000000 --- a/src/main/resources/assets/modmenu/lang/zh_TW/zh_tw.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "modmenu.title": "模組", - "modmenu.loaded": "(已載入%s個)", - "modmenu.config": "編輯設置", - "modmenu.modsFolder": "開啟模組資料夾", - "modmenu.configsFolder": "開啟設置資料夾", - "modmenu.configure": "設置...", - "modmenu.modIdToolTip": "模組 ID: %s", - "modmenu.website": "網站", - "modmenu.issues": "反饋", - "modmenu.authorPrefix": "作者: %s", - "modmenu.search": "搜索模组", - "modmenu.clientsideOnly": "玩家端", - "modmenu.toggleFilterOptions": "切換過濾選項", - "modmenu.showLibraries.true": "顯示", - "modmenu.showLibraries.false": "隱藏", - "modmenu.showingMods": "隱藏 %s 個模組", - "modmenu.sorting": "排序方式: %s" -} diff --git a/src/main/resources/assets/modmenu/textures/gui/configure_button.png b/src/main/resources/assets/modmenu/textures/gui/configure_button.png index e84c234a4..49e8429c1 100644 Binary files a/src/main/resources/assets/modmenu/textures/gui/configure_button.png and b/src/main/resources/assets/modmenu/textures/gui/configure_button.png differ diff --git a/src/main/resources/assets/modmenu/textures/gui/filters_button.png b/src/main/resources/assets/modmenu/textures/gui/filters_button.png index 9d8be05b1..de8495a3f 100644 Binary files a/src/main/resources/assets/modmenu/textures/gui/filters_button.png and b/src/main/resources/assets/modmenu/textures/gui/filters_button.png differ diff --git a/src/main/resources/assets/modmenu/textures/gui/mod_configuration.png b/src/main/resources/assets/modmenu/textures/gui/mod_configuration.png new file mode 100644 index 000000000..fb1ca1a77 Binary files /dev/null and b/src/main/resources/assets/modmenu/textures/gui/mod_configuration.png differ diff --git a/src/main/resources/assets/modmenu/textures/gui/open_button.png b/src/main/resources/assets/modmenu/textures/gui/open_button.png new file mode 100644 index 000000000..0bfb10171 Binary files /dev/null and b/src/main/resources/assets/modmenu/textures/gui/open_button.png differ diff --git a/src/main/resources/assets/modmenu/textures/gui/update_available.png b/src/main/resources/assets/modmenu/textures/gui/update_available.png new file mode 100644 index 000000000..a5c19b6f6 Binary files /dev/null and b/src/main/resources/assets/modmenu/textures/gui/update_available.png differ diff --git a/src/main/resources/assets/modmenu/unknown_icon.png b/src/main/resources/assets/modmenu/unknown_icon.png new file mode 100644 index 000000000..e1f16e099 Binary files /dev/null and b/src/main/resources/assets/modmenu/unknown_icon.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index db1082c74..610a2363b 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -7,7 +7,7 @@ "license": "MIT", "icon": "assets/modmenu/icon.png", "entrypoints": { - "main": [ + "client": [ "io.github.prospector.modmenu.ModMenu" ], "modmenu": [ diff --git a/src/main/resources/modmenu.mixins.json b/src/main/resources/modmenu.mixins.json index bbd0bd053..53a476703 100644 --- a/src/main/resources/modmenu.mixins.json +++ b/src/main/resources/modmenu.mixins.json @@ -4,14 +4,14 @@ "compatibilityLevel": "JAVA_${java}", "plugin": "io.github.prospector.modmenu.mixin.ModMenuMixinConfigPlugin", "client": [ - "MixinGuiIngameMenu", - "MixinGuiMainMenu", - "MixinTexturePacks", - "GuiButtonAccessor", + "ScreenPauseMixin", + "ScreenMainMenuMixin", + "TexturePackMixin", + "ButtonElementAccessor", "MinecraftAccessor", "LanguageAccessor", "TextFieldEditorAccessor", - "MixinI18n" + "I18nMixin" ], "injectors": { "defaultRequire": 1