From 7be2adc73b1f75983a0746cfc3fd661498273f61 Mon Sep 17 00:00:00 2001 From: Jamalam Date: Mon, 5 Aug 2024 18:30:06 +0100 Subject: [PATCH] V1.0.11 - (feat) config options with long names will now render in the screen as scrolling strings. - (chore) various code cleanups - (chore) use parchment mappings --- CHANGELOG.md | 6 +- build.gradle | 10 +- .../io/github/jamalam360/jamlib/JamLib.java | 2 +- .../jamlib/config/ConfigExtensions.java | 305 ++++++------- .../jamlib/config/ConfigManager.java | 432 +++++++++--------- .../jamlib/config/gui/ConfigScreen.java | 20 +- .../jamlib/config/gui/SelectionList.java | 13 - .../jamlib/config/gui/SelectionListEntry.java | 25 +- gradle.properties | 2 +- libs.versions.toml | 3 + .../resources/assets/testmod/lang/en_us.json | 2 +- 11 files changed, 417 insertions(+), 403 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7ba27..eeb93f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -- Update versions. -- Modify how config screens are registered on Neoforge. - +- (feat) config options with long names will now render in the screen as scrolling strings. +- (chore) various code cleanups +- (chore) use parchment mappings diff --git a/build.gradle b/build.gradle index 7af5fe3..29ef12e 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,10 @@ subprojects { dependencies { minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" - mappings loom.officialMojangMappings() + mappings loom.layered() { + officialMojangMappings() + parchment "org.parchmentmc.data:parchment-${rootProject.minecraft_version}:${libs.versions.parchment.get()}@zip" + } } } @@ -67,6 +70,11 @@ allprojects { name = "Modrinth" url = "https://api.modrinth.com/maven" } + + maven { + name = "ParchmentMC" + url = "https://maven.parchmentmc.org" + } } tasks.withType(JavaCompile) { diff --git a/common/src/main/java/io/github/jamalam360/jamlib/JamLib.java b/common/src/main/java/io/github/jamalam360/jamlib/JamLib.java index 39e2e58..8cc4b6b 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/JamLib.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/JamLib.java @@ -18,7 +18,7 @@ public class JamLib { @ApiStatus.Internal public static void init() { - LOGGER.info("Initializing JamLib on " + JamLibPlatform.getPlatform()); + LOGGER.info("Initializing JamLib on {}", JamLibPlatform.getPlatform()); checkForJarRenaming(JamLib.class); EnvExecutor.runInEnv(EnvType.CLIENT, () -> () -> ClientPlayerEvent.CLIENT_PLAYER_JOIN.register(JamLibClient::onPlayerJoin)); diff --git a/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigExtensions.java b/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigExtensions.java index f0542b6..767d8c2 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigExtensions.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigExtensions.java @@ -1,13 +1,14 @@ package io.github.jamalam360.jamlib.config; import io.github.jamalam360.jamlib.JamLib; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + import java.lang.reflect.Field; -import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; /** * Implement this interface on your mod's config class for additional functionality. @@ -16,155 +17,151 @@ */ public interface ConfigExtensions { - /** - * @return The links to display on your config screen. - * - * @implNote Be careful, returning too many links may cause overflow. - * @see Link - */ - default List getLinks() { - return List.of(); - } - - /** - * Can be used to validate your config fields after they have been edited in the config screen. Remember that this function is only called when the user is using a - * config screen - if they are editing the file directly they are on their own. - * - * @return A list of the errors and warnings that occurred during validation. - * - * @implNote It is worth calling {@code super.getValidationErrors()} and adding your own errors to the list, since by default this method performs validation such as - * adding warnings when the field is marked with {@link RequiresRestart}. - */ - default List getValidationErrors(ConfigManager manager, FieldValidationInfo info) { - List result = new ArrayList<>(); - - if (info.backingField().isAnnotationPresent(MatchesRegex.class)) { - MatchesRegex annotation = info.backingField().getAnnotation(MatchesRegex.class); - - if (!info.value().toString().matches(annotation.value())) { - result.add(new ValidationError(ValidationError.Type.ERROR, info, Component.translatable("config.jamlib.matches_regex_tooltip", annotation.value()))); - } - } - - if (info.backingField().isAnnotationPresent(RequiresRestart.class) && !info.initialValue().equals(info.value())) { - result.add(new ValidationError(ValidationError.Type.WARNING, info, Component.translatable("config.jamlib.requires_restart_tooltip"))); - } - - if (info.backingField().isAnnotationPresent(WithinRange.class)) { - if (info.backingField().getType() != double.class && info.backingField().getType() != Double.class - && info.backingField().getType() != float.class && info.backingField().getType() != Float.class - && info.backingField().getType() != int.class && info.backingField().getType() != Integer.class - && info.backingField().getType() != long.class && info.backingField().getType() != Long.class - ) { - JamLib.LOGGER.error("Field " + info.backingField().getName() + " is annotated with @WithinRange but is not a number!"); - } else { - WithinRange annotation = info.backingField().getAnnotation(WithinRange.class); - Number value = (Number) info.value(); - - if (value.doubleValue() < annotation.min() || value.doubleValue() > annotation.max()) { - result.add(new ValidationError(ValidationError.Type.ERROR, info, Component.translatable("config.jamlib.within_range_tooltip", annotation.min(), annotation.max()))); - } - } - } - - return result; - } - - /** - * A link within your config screen. Displayed in the top right corner. Icons are displayed as 16x16 textures in 20x20 buttons. - * - * @see ConfigExtensions#getLinks() - */ - class Link { - - // I am not an artist but these are recognizable at least. - public static final ResourceLocation DISCORD = JamLib.id("textures/gui/link_discord.png"); - public static final ResourceLocation GENERIC_LINK = JamLib.id("textures/gui/link_generic.png"); - public static final ResourceLocation GITHUB = JamLib.id("textures/gui/link_github.png"); - - private final ResourceLocation texture; - private final URL url; - private final Component tooltip; - - public Link(ResourceLocation texture, String url, Component tooltip) { - this.texture = texture; - - try { - this.url = new URL(url); - } catch (MalformedURLException e) { - JamLib.LOGGER.error("Malformed URL for config screen link: " + url); - throw new RuntimeException(e); - } - - this.tooltip = tooltip; - } - - public Link(ResourceLocation texture, URL url, Component tooltip) { - this.texture = texture; - this.url = url; - this.tooltip = tooltip; - } - - public ResourceLocation getTexture() { - return this.texture; - } - - public URL getUrl() { - return this.url; - } - - public Component getTooltip() { - return this.tooltip; - } - } - - /** - * A validation error within your config screen. Displayed next to the field. - * - * @param type The type of error - * @param field The field that caused the error - * @param message The message to display in the tooltip - * - * @see FieldValidationInfo - * @see ConfigExtensions#getValidationErrors(ConfigManager, FieldValidationInfo) - */ - record ValidationError(Type type, FieldValidationInfo field, Component message) { - - /** - * The type of error. - */ - public enum Type { - /** - * A warning - the user may choose to ignore this. - */ - WARNING, - /** - * An error - the user must fix this before they can save through the config file. - */ - ERROR; - - private final ResourceLocation texture; - - Type() { - this.texture = JamLib.id("textures/gui/validation_" + this.name().toLowerCase() + ".png"); - } - - public ResourceLocation getTexture() { - return this.texture; - } - } - } - - /** - * Used to provide information about config fields, for the validator. - * - * @param name The Java name of the field - * @param value The new value of the field - * @param initialValue The value of the field before the config screen was opened - * @param backingField The Java field that corresponds to this config field - * - * @see ConfigExtensions#getValidationErrors(ConfigManager, FieldValidationInfo) - */ - record FieldValidationInfo(String name, Object value, Object initialValue, Field backingField) { - } + /** + * @return The links to display on your config screen. + * @implNote Be careful, returning too many links may cause overflow. + * @see Link + */ + default List getLinks() { + return List.of(); + } + + /** + * Can be used to validate your config fields after they have been edited in the config screen. Remember that this function is only called when the user is using a + * config screen - if they are editing the file directly they are on their own. + * + * @return A list of the errors and warnings that occurred during validation. + * @implNote It is worth calling {@code super.getValidationErrors()} and adding your own errors to the list, since by default this method performs validation such as + * adding warnings when the field is marked with {@link RequiresRestart}. + */ + default List getValidationErrors(ConfigManager manager, FieldValidationInfo info) { + List result = new ArrayList<>(); + + if (info.backingField().isAnnotationPresent(MatchesRegex.class)) { + MatchesRegex annotation = info.backingField().getAnnotation(MatchesRegex.class); + + if (!info.value().toString().matches(annotation.value())) { + result.add(new ValidationError(ValidationError.Type.ERROR, info, Component.translatable("config.jamlib.matches_regex_tooltip", annotation.value()))); + } + } + + if (info.backingField().isAnnotationPresent(RequiresRestart.class) && !info.initialValue().equals(info.value())) { + result.add(new ValidationError(ValidationError.Type.WARNING, info, Component.translatable("config.jamlib.requires_restart_tooltip"))); + } + + if (info.backingField().isAnnotationPresent(WithinRange.class)) { + if (info.backingField().getType() != double.class && info.backingField().getType() != Double.class + && info.backingField().getType() != float.class && info.backingField().getType() != Float.class + && info.backingField().getType() != int.class && info.backingField().getType() != Integer.class + && info.backingField().getType() != long.class && info.backingField().getType() != Long.class + ) { + JamLib.LOGGER.error("Field {} is annotated with @WithinRange but is not a number!", info.backingField().getName()); + } else { + WithinRange annotation = info.backingField().getAnnotation(WithinRange.class); + Number value = (Number) info.value(); + + if (value.doubleValue() < annotation.min() || value.doubleValue() > annotation.max()) { + result.add(new ValidationError(ValidationError.Type.ERROR, info, Component.translatable("config.jamlib.within_range_tooltip", annotation.min(), annotation.max()))); + } + } + } + + return result; + } + + /** + * A link within your config screen. Displayed in the top right corner. Icons are displayed as 16x16 textures in 20x20 buttons. + * + * @see ConfigExtensions#getLinks() + */ + class Link { + + // I am not an artist but these are recognizable at least. + public static final ResourceLocation DISCORD = JamLib.id("textures/gui/link_discord.png"); + public static final ResourceLocation GENERIC_LINK = JamLib.id("textures/gui/link_generic.png"); + public static final ResourceLocation GITHUB = JamLib.id("textures/gui/link_github.png"); + + private final ResourceLocation texture; + private final URL url; + private final Component tooltip; + + public Link(ResourceLocation texture, String url, Component tooltip) { + this.texture = texture; + + try { + this.url = new URI(url).toURL(); + } catch (Exception e) { + JamLib.LOGGER.error("Malformed URL for config screen link: {}", url); + throw new RuntimeException(e); + } + + this.tooltip = tooltip; + } + + public Link(ResourceLocation texture, URL url, Component tooltip) { + this.texture = texture; + this.url = url; + this.tooltip = tooltip; + } + + public ResourceLocation getTexture() { + return this.texture; + } + + public URL getUrl() { + return this.url; + } + + public Component getTooltip() { + return this.tooltip; + } + } + + /** + * A validation error within your config screen. Displayed next to the field. + * + * @param type The type of error + * @param field The field that caused the error + * @param message The message to display in the tooltip + * @see FieldValidationInfo + * @see ConfigExtensions#getValidationErrors(ConfigManager, FieldValidationInfo) + */ + record ValidationError(Type type, FieldValidationInfo field, Component message) { + + /** + * The type of error. + */ + public enum Type { + /** + * A warning - the user may choose to ignore this. + */ + WARNING, + /** + * An error - the user must fix this before they can save through the config file. + */ + ERROR; + + private final ResourceLocation texture; + + Type() { + this.texture = JamLib.id("textures/gui/validation_" + this.name().toLowerCase() + ".png"); + } + + public ResourceLocation getTexture() { + return this.texture; + } + } + } + + /** + * Used to provide information about config fields, for the validator. + * + * @param name The Java name of the field + * @param value The new value of the field + * @param initialValue The value of the field before the config screen was opened + * @param backingField The Java field that corresponds to this config field + * @see ConfigExtensions#getValidationErrors(ConfigManager, FieldValidationInfo) + */ + record FieldValidationInfo(String name, Object value, Object initialValue, Field backingField) { + } } diff --git a/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigManager.java b/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigManager.java index 6fa3e0f..db2d8c5 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigManager.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/config/ConfigManager.java @@ -6,7 +6,8 @@ import blue.endless.jankson.JsonObject; import dev.architectury.platform.Platform; import io.github.jamalam360.jamlib.JamLib; -import java.io.File; +import org.jetbrains.annotations.ApiStatus; + import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; @@ -15,230 +16,225 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import org.jetbrains.annotations.ApiStatus; /** * Manages config files. * * @param The config class. - * * @see RequiresRestart */ public class ConfigManager { - @ApiStatus.Internal - public static final Map> MANAGERS = new HashMap<>(); - private static final Jankson JANKSON = Jankson.builder().build(); - private final Path configPath; - private final String modId; - private final String configName; - private final Class configClass; - private T config; - - /** - * Creates a new config manager, either saving a default config or loading an existing one if one exists. This construct assumes the config name is the same as the - * mod ID. - * - * @param modId The mod ID - * @param configClass The config class - */ - public ConfigManager(String modId, Class configClass) { - this(modId, modId, configClass); - } - - /** - * Creates a new config manager, either saving a default config or loading an existing one if one exists. - * - * @param modId The mod ID - * @param configName The name of the config, usually the mod ID - * @param configClass The config class - */ - public ConfigManager(String modId, String configName, Class configClass) { - MANAGERS.put(configName, this); - this.configPath = Platform.getConfigFolder().resolve(configName + ".json5"); - this.configName = configName; - this.modId = modId; - this.configClass = configClass; - - this.validateConfigClass(); - - if (!Files.exists(this.configPath)) { - this.config = this.createDefaultConfig(); - this.save(); - } - - this.reloadFromDisk(); - // There is an extra save here in-case the config schema was updated. - this.save(); - } - - /** - * @return The current config object. - * - * @apiNote Do not store this object, as it will not update when the config is reloaded. - */ - public T get() { - return this.config; - } - - /** - * @return The name of the current config, as passed to the constructor. - */ - public String getConfigName() { - return this.configName; - } - - /** - * @return The class of the current config, as passed to the constructor. - */ - public Class getConfigClass() { - return this.configClass; - } - - /** - * @return The mod ID of the current config, as passed to the constructor. - */ - public String getModId() { - return this.modId; - } - - /** - * Saves the current config to the config file. - */ - public void save() { - File f = this.configPath.toFile(); - JsonElement json = JANKSON.toJson(this.config); - transformJsonBeforeSave(json); - JsonGrammar grammar = JsonGrammar.builder().bareRootObject(false).bareSpecialNumerics(false).printCommas(true).printWhitespace(true).printUnquotedKeys(true).withComments(true).build(); - String stringifiedJson = json.toJson(grammar); - - try { - f.createNewFile(); - Files.writeString(this.configPath, stringifiedJson); - JamLib.LOGGER.info("Updated config file at " + this.configPath); - } catch (IOException e) { - JamLib.LOGGER.error("Failed to write config file at " + this.configPath, e); - } - } - - /** - * Reloads the config from the config file. - */ - public void reloadFromDisk() { - try { - JsonObject json = JANKSON.load(Files.readAllLines(this.configPath).stream().reduce((a, b) -> a + "\n" + b).orElse("")); - this.config = JANKSON.fromJsonCarefully(json, this.configClass); - } catch (Exception e) { - JamLib.LOGGER.error("Failed to read config file at " + configPath, e); - JamLib.LOGGER.error("Resetting to defaults; a backup will be written to " + configPath + ".broken"); - - try { - Files.move(configPath, configPath.resolveSibling(configPath.getFileName() + ".broken"), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e2) { - JamLib.LOGGER.error("Failed to write backup config file at " + configPath + ".broken", e2); - } - - this.config = this.createDefaultConfig(); - } - } - - private void validateConfigClass() { - T defaultConfig = this.createDefaultConfig(); - - try { - for (Field field : this.configClass.getFields()) { - if (field.get(defaultConfig) == null) { - throw new RuntimeException("Config field " + field.getName() + " is null by default. Config fields cannot be null by default."); - } - } - } catch (IllegalArgumentException | IllegalAccessException e) { - JamLib.LOGGER.error("Failed to validate config class " + this.configClass.getName(), e); - } - } - - private T createDefaultConfig() { - try { - return this.configClass.getConstructor().newInstance(); - } catch (Exception e) { - JamLib.LOGGER.error("Failed to create default config for " + this.configClass.getName(), e); - } - - return null; - } - - private void transformJsonBeforeSave(JsonElement e) { - if (!(e instanceof JsonObject root)) { - throw new IllegalArgumentException("Config must be a JSON object"); - } - - T defaultConfig = this.createDefaultConfig(); - if (defaultConfig != null) { - attachDefaultComments(this.configClass, defaultConfig, root); - } - } - - private void attachDefaultComments(Class clazz, Object defaults, JsonObject obj) { - for (String key : obj.keySet()) { - JsonElement e = obj.get(key); - if (e instanceof JsonObject) { - attachDefaultComments(clazz, defaults, (JsonObject) e); - } else { - try { - Field field = clazz.getField(key); - String currentComment = obj.getComment(key); - StringBuilder comment = new StringBuilder(); - - Object defaultValue = field.get(defaults); - if (defaultValue != null) { - if (defaultValue instanceof String s) { - defaultValue = "\\\"" + s + "\\\""; - } - - comment.append("- default: ").append(defaultValue); - } - - if (field.isAnnotationPresent(RequiresRestart.class)) { - if (!comment.isEmpty()) { - comment.append("\n"); - } - - comment.append("- requires game restart"); - } - - if (field.isAnnotationPresent(MatchesRegex.class)) { - MatchesRegex annotation = field.getAnnotation(MatchesRegex.class); - - if (!comment.isEmpty()) { - comment.append("\n"); - } - - comment.append("- must match regex: ").append(annotation.value()); - } - - if (field.isAnnotationPresent(WithinRange.class)) { - WithinRange annotation = field.getAnnotation(WithinRange.class); - - if (!comment.isEmpty()) { - comment.append("\n"); - } - - comment.append("- must be between ").append(annotation.min()).append(" and ").append(annotation.max()); - } - - if (Enum.class.isAssignableFrom(field.getType())) { - if (!comment.isEmpty()) { - comment.append("\n"); - } - - comment.append("- must be one of: ").append(Arrays.stream(field.getType().getEnumConstants()).map(Object::toString).reduce((a, b) -> a + ", " + b).orElse("")); - } - - String newComment = (currentComment == null ? "" : currentComment) + (currentComment == null ? "" : "\n") + comment; - obj.setComment(key, newComment); - } catch (NoSuchFieldException | IllegalAccessException ignored) { - // This isn't critical functionality, so these exceptions doesn't need logging - } - } - } - } + @ApiStatus.Internal + public static final Map> MANAGERS = new HashMap<>(); + private static final Jankson JANKSON = Jankson.builder().build(); + private final Path configPath; + private final String modId; + private final String configName; + private final Class configClass; + private T config; + + /** + * Creates a new config manager, either saving a default config or loading an existing one if one exists. This construct assumes the config name is the same as the + * mod ID. + * + * @param modId The mod ID + * @param configClass The config class + */ + public ConfigManager(String modId, Class configClass) { + this(modId, modId, configClass); + } + + /** + * Creates a new config manager, either saving a default config or loading an existing one if one exists. + * + * @param modId The mod ID + * @param configName The name of the config, usually the mod ID + * @param configClass The config class + */ + public ConfigManager(String modId, String configName, Class configClass) { + MANAGERS.put(configName, this); + this.configPath = Platform.getConfigFolder().resolve(configName + ".json5"); + this.configName = configName; + this.modId = modId; + this.configClass = configClass; + + this.validateConfigClass(); + + if (!Files.exists(this.configPath)) { + this.config = this.createDefaultConfig(); + this.save(); + } + + this.reloadFromDisk(); + // There is an extra save here in-case the config schema was updated. + this.save(); + } + + /** + * @return The current config object. + * @apiNote Do not store this object, as it will not update when the config is reloaded. + */ + public T get() { + return this.config; + } + + /** + * @return The name of the current config, as passed to the constructor. + */ + public String getConfigName() { + return this.configName; + } + + /** + * @return The class of the current config, as passed to the constructor. + */ + public Class getConfigClass() { + return this.configClass; + } + + /** + * @return The mod ID of the current config, as passed to the constructor. + */ + public String getModId() { + return this.modId; + } + + /** + * Saves the current config to the config file. + */ + public void save() { + JsonElement json = JANKSON.toJson(this.config); + transformJsonBeforeSave(json); + JsonGrammar grammar = JsonGrammar.builder().bareRootObject(false).bareSpecialNumerics(false).printCommas(true).printWhitespace(true).printUnquotedKeys(true).withComments(true).build(); + String stringifiedJson = json.toJson(grammar); + + try { + Files.writeString(this.configPath, stringifiedJson); + JamLib.LOGGER.info("Updated config file at {}", this.configPath); + } catch (IOException e) { + JamLib.LOGGER.error("Failed to write config file at {}", this.configPath, e); + } + } + + /** + * Reloads the config from the config file. + */ + public void reloadFromDisk() { + try { + JsonObject json = JANKSON.load(Files.readAllLines(this.configPath).stream().reduce((a, b) -> a + "\n" + b).orElse("")); + this.config = JANKSON.fromJsonCarefully(json, this.configClass); + } catch (Exception e) { + JamLib.LOGGER.error("Failed to read config file at {}", configPath, e); + JamLib.LOGGER.error("Resetting to defaults; a backup will be written to {}.broken", configPath); + + try { + Files.move(configPath, configPath.resolveSibling(configPath.getFileName() + ".broken"), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e2) { + JamLib.LOGGER.error("Failed to write backup config file at {}.broken", configPath, e2); + } + + this.config = this.createDefaultConfig(); + } + } + + private void validateConfigClass() { + T defaultConfig = this.createDefaultConfig(); + + try { + for (Field field : this.configClass.getFields()) { + if (field.get(defaultConfig) == null) { + throw new RuntimeException("Config field " + field.getName() + " is null by default. Config fields cannot be null by default."); + } + } + } catch (IllegalArgumentException | IllegalAccessException e) { + JamLib.LOGGER.error("Failed to validate config class " + this.configClass.getName(), e); + } + } + + private T createDefaultConfig() { + try { + return this.configClass.getConstructor().newInstance(); + } catch (Exception e) { + JamLib.LOGGER.error("Failed to create default config for " + this.configClass.getName(), e); + } + + return null; + } + + private void transformJsonBeforeSave(JsonElement e) { + if (!(e instanceof JsonObject root)) { + throw new IllegalArgumentException("Config must be a JSON object"); + } + + T defaultConfig = this.createDefaultConfig(); + if (defaultConfig != null) { + attachDefaultComments(this.configClass, defaultConfig, root); + } + } + + private void attachDefaultComments(Class clazz, Object defaults, JsonObject obj) { + for (String key : obj.keySet()) { + JsonElement e = obj.get(key); + if (e instanceof JsonObject) { + attachDefaultComments(clazz, defaults, (JsonObject) e); + } else { + try { + Field field = clazz.getField(key); + String currentComment = obj.getComment(key); + StringBuilder comment = new StringBuilder(); + + Object defaultValue = field.get(defaults); + if (defaultValue != null) { + if (defaultValue instanceof String s) { + defaultValue = "\\\"" + s + "\\\""; + } + + comment.append("- default: ").append(defaultValue); + } + + if (field.isAnnotationPresent(RequiresRestart.class)) { + if (!comment.isEmpty()) { + comment.append("\n"); + } + + comment.append("- requires game restart"); + } + + if (field.isAnnotationPresent(MatchesRegex.class)) { + MatchesRegex annotation = field.getAnnotation(MatchesRegex.class); + + if (!comment.isEmpty()) { + comment.append("\n"); + } + + comment.append("- must match regex: ").append(annotation.value()); + } + + if (field.isAnnotationPresent(WithinRange.class)) { + WithinRange annotation = field.getAnnotation(WithinRange.class); + + if (!comment.isEmpty()) { + comment.append("\n"); + } + + comment.append("- must be between ").append(annotation.min()).append(" and ").append(annotation.max()); + } + + if (Enum.class.isAssignableFrom(field.getType())) { + if (!comment.isEmpty()) { + comment.append("\n"); + } + + comment.append("- must be one of: ").append(Arrays.stream(field.getType().getEnumConstants()).map(Object::toString).reduce((a, b) -> a + ", " + b).orElse("")); + } + + String newComment = (currentComment == null ? "" : currentComment) + (currentComment == null ? "" : "\n") + comment; + obj.setComment(key, newComment); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + // This isn't critical functionality, so these exceptions doesn't need logging + } + } + } + } } diff --git a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/ConfigScreen.java b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/ConfigScreen.java index c8d7897..0dc23d1 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/ConfigScreen.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/ConfigScreen.java @@ -93,7 +93,7 @@ protected void init() { }).pos(this.width / 2 - 154, this.height - 28).size(150, 20).build()); this.doneButton = this.addRenderableWidget(Button.builder(CommonComponents.GUI_DONE, button -> { - if (this.changedFields.size() > 0) { + if (!this.changedFields.isEmpty()) { this.manager.save(); } @@ -104,7 +104,7 @@ protected void init() { new ButtonWithTextureWidget( 7, 7, 20, 20, Component.translatable("config.jamlib.edit_manually"), ResourceLocation.withDefaultNamespace("textures/item/writable_book.png"), 16, 16, button -> { - if (this.changedFields.size() > 0) { + if (!this.changedFields.isEmpty()) { this.manager.save(); } @@ -116,7 +116,7 @@ protected void init() { ConfigEntryList list = new ConfigEntryList(this.minecraft, this.width, this.height - 64, 32, 25); - if (this.entries.size() == 0) { + if (this.entries.isEmpty()) { for (Field field : this.manager.getConfigClass().getDeclaredFields()) { if (field.isAnnotationPresent(HiddenInGui.class)) { continue; @@ -610,12 +610,12 @@ private void validate(ConfigManager manager, List widgets) { List errors = ((ConfigExtensions) ext).getValidationErrors(manager, new ConfigExtensions.FieldValidationInfo(this.field.getName(), newValue, this.initialValue, this.field)); errors.sort((o1, o2) -> o2.type().ordinal() - o1.type().ordinal()); - TextureWidget validationIcon = (TextureWidget) widgets.get(0); - if (errors.size() > 0) { - this.isValid = errors.get(0).type() != ConfigExtensions.ValidationError.Type.ERROR; + TextureWidget validationIcon = (TextureWidget) widgets.getFirst(); + if (!errors.isEmpty()) { + this.isValid = errors.getFirst().type() != ConfigExtensions.ValidationError.Type.ERROR; validationIcon.visible = true; - validationIcon.setTexture(errors.get(0).type().getTexture()); - validationIcon.setTooltip(Tooltip.create(errors.get(0).message())); + validationIcon.setTexture(errors.getFirst().type().getTexture()); + validationIcon.setTooltip(Tooltip.create(errors.getFirst().message())); } else { this.isValid = true; validationIcon.visible = false; @@ -641,7 +641,7 @@ private V getFieldValue(ConfigManager manager) { try { return (V) this.field.get(manager.get()); } catch (IllegalAccessException e) { - JamLib.LOGGER.error("Failed to access field for config " + manager.getConfigName(), e); + JamLib.LOGGER.error("Failed to access field for config {}", manager.getConfigName(), e); return null; } } @@ -666,7 +666,7 @@ private void setFieldValue(ConfigManager manager, V v) { try { this.field.set(manager.get(), realValue); } catch (IllegalAccessException e) { - JamLib.LOGGER.error("Failed to access field for config " + manager.getConfigName(), e); + JamLib.LOGGER.error("Failed to access field for config {}", manager.getConfigName(), e); } } diff --git a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionList.java b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionList.java index 4aecd6a..866fb92 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionList.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionList.java @@ -15,19 +15,6 @@ public SelectionList(Minecraft minecraft, int width, int height, int y, int item super(minecraft, width, height, y, itemHeight); this.centerListVertically = false; } -// -// @Override -// public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { -// super.render(graphics, mouseX, mouseY, delta); -// SelectionListEntry hovered = this.getHoveredEntry(mouseX, mouseY); -// -// if (hovered != null) { -// if (hovered.getTooltip() != null) { -// graphics.renderTooltip(Minecraft.getInstance().font, hovered.getTooltip(), mouseX, mouseY); -// } -// } -// } - @Override public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { diff --git a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionListEntry.java b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionListEntry.java index 62ac662..cccf1d7 100644 --- a/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionListEntry.java +++ b/common/src/main/java/io/github/jamalam360/jamlib/config/gui/SelectionListEntry.java @@ -1,6 +1,8 @@ package io.github.jamalam360.jamlib.config.gui; import java.util.List; + +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; @@ -9,6 +11,7 @@ import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.network.chat.Component; import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -32,7 +35,27 @@ public void render(GuiGraphics graphics, int i, int y, int x, int width, int hei widget.render(graphics, mouseX, mouseY, delta); } - graphics.drawString(Minecraft.getInstance().font, this.title, 12, y + 5, 0xFFFFFF); + this.renderTitle(graphics, y, 12, x + width / 2 - 10); + } + + // Mainly taken from AbstractWidget + private void renderTitle(GuiGraphics graphics, int minY, int minX, int maxX) { + int textWidth = Minecraft.getInstance().font.width(this.title); + int y = minY + Minecraft.getInstance().font.lineHeight / 2 + 1; + int width = maxX - minX; + + if (textWidth > width) { + int difference = textWidth - width; + double nanos = (double)Util.getMillis() / 1000.0; + double e = Math.max((double) difference * 0.5, 3.0); + double f = Math.sin((Math.PI / 2) * Math.cos((Math.PI * 2) * nanos / e)) / 2.0 + 0.5; + double g = Mth.lerp(f, 0.0, difference); + graphics.enableScissor(minX, minY, maxX, minY + Minecraft.getInstance().font.lineHeight * 2); + graphics.drawString(Minecraft.getInstance().font, this.title, minX - (int)g, y, 0xFFFFFF); + graphics.disableScissor(); + } else { + graphics.drawString(Minecraft.getInstance().font, this.title, minX, y, 0xFFFFFF); + } } @Override diff --git a/gradle.properties b/gradle.properties index aeb3688..07c9701 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false org.gradle.parallel=true -version=1.0.10+1.21 +version=1.0.11+1.21 minecraft_version=1.21 branch=main group=io.github.jamalam360 diff --git a/libs.versions.toml b/libs.versions.toml index 347aac0..e532d33 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -2,6 +2,9 @@ # https://modrinth.com/mod/architectury-api/versions architectury = "13.0.6" +# https://parchmentmc.org/docs/getting-started +parchment = "2024.07.28" + # https://projects.neoforged.net/neoforged/neoforge neoforge = "21.0.144" diff --git a/testmod-common/src/main/resources/assets/testmod/lang/en_us.json b/testmod-common/src/main/resources/assets/testmod/lang/en_us.json index 05dc7aa..86976db 100644 --- a/testmod-common/src/main/resources/assets/testmod/lang/en_us.json +++ b/testmod-common/src/main/resources/assets/testmod/lang/en_us.json @@ -3,7 +3,7 @@ "config.testmod.second_config.title": "Second config", "config.testmod.first_config.tooltip": "Tooltip for the first config. It goes onto multiple lines as it is quite a long tooltip.", "config.testmod.second_config.tooltip": "Tooltip for the second config", - "config.testmod.first_config.testBoolean": "Test boolean", + "config.testmod.first_config.testBoolean": "Test boolean (it has quite a long name, so the text should scroll)", "config.testmod.first_config.testBoolean.tooltip": "Tooltip for the test config boolean", "config.testmod.first_config.testEnum.first": "Translated (First)", "config.testmod.i_dont_like_4": "4 is NOT allowed"