Entry.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package me.lortseam.completeconfig.data;
  2. import com.google.common.collect.Lists;
  3. import lombok.EqualsAndHashCode;
  4. import lombok.Getter;
  5. import lombok.NonNull;
  6. import lombok.experimental.Accessors;
  7. import lombok.extern.log4j.Log4j2;
  8. import me.lortseam.completeconfig.CompleteConfig;
  9. import me.lortseam.completeconfig.api.ConfigContainer;
  10. import me.lortseam.completeconfig.api.ConfigEntry;
  11. import me.lortseam.completeconfig.data.entry.EntryOrigin;
  12. import me.lortseam.completeconfig.data.entry.Transformation;
  13. import me.lortseam.completeconfig.data.entry.Transformer;
  14. import me.lortseam.completeconfig.data.structure.DataPart;
  15. import me.lortseam.completeconfig.data.structure.Identifiable;
  16. import me.lortseam.completeconfig.data.text.TranslationKey;
  17. import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
  18. import me.lortseam.completeconfig.extensions.CompleteConfigExtension;
  19. import me.lortseam.completeconfig.util.ReflectionUtils;
  20. import net.minecraft.text.Text;
  21. import net.minecraft.text.TextColor;
  22. import org.apache.commons.lang3.StringUtils;
  23. import org.spongepowered.configurate.CommentedConfigurationNode;
  24. import org.spongepowered.configurate.serialize.SerializationException;
  25. import java.beans.IntrospectionException;
  26. import java.lang.reflect.Field;
  27. import java.lang.reflect.InvocationTargetException;
  28. import java.lang.reflect.Method;
  29. import java.lang.reflect.Type;
  30. import java.util.Collection;
  31. import java.util.*;
  32. import java.util.function.UnaryOperator;
  33. @Log4j2(topic = "CompleteConfig")
  34. @EqualsAndHashCode(onlyExplicitlyIncluded = true)
  35. public class Entry<T> implements DataPart, Identifiable {
  36. private static final Transformer DEFAULT_TRANSFORMER = Entry::new;
  37. private static final List<Transformation> transformations = Lists.newArrayList(
  38. Transformation.builder().byType(boolean.class, Boolean.class).byAnnotation(ConfigEntry.Boolean.class, true).transforms(BooleanEntry::new),
  39. Transformation.builder().byType(int.class, Integer.class).byAnnotation(ConfigEntry.BoundedInteger.class).transforms(origin -> {
  40. ConfigEntry.BoundedInteger bounds = origin.getAnnotation(ConfigEntry.BoundedInteger.class);
  41. return new BoundedEntry<>(origin, bounds.min(), bounds.max());
  42. }),
  43. Transformation.builder().byType(int.class, Integer.class).byAnnotation(Arrays.asList(ConfigEntry.BoundedInteger.class, ConfigEntry.Slider.class)).transforms(origin -> {
  44. ConfigEntry.BoundedInteger bounds = origin.getAnnotation(ConfigEntry.BoundedInteger.class);
  45. return new SliderEntry<>(origin, bounds.min(), bounds.max());
  46. }),
  47. Transformation.builder().byType(long.class, Long.class).byAnnotation(ConfigEntry.BoundedLong.class).transforms(origin -> {
  48. ConfigEntry.BoundedLong bounds = origin.getAnnotation(ConfigEntry.BoundedLong.class);
  49. return new BoundedEntry<>(origin, bounds.min(), bounds.max());
  50. }),
  51. Transformation.builder().byType(long.class, Long.class).byAnnotation(Arrays.asList(ConfigEntry.BoundedLong.class, ConfigEntry.Slider.class)).transforms(origin -> {
  52. ConfigEntry.BoundedLong bounds = origin.getAnnotation(ConfigEntry.BoundedLong.class);
  53. return new SliderEntry<>(origin, bounds.min(), bounds.max());
  54. }),
  55. Transformation.builder().byType(float.class, Float.class).byAnnotation(ConfigEntry.BoundedFloat.class).transforms(origin -> {
  56. ConfigEntry.BoundedFloat bounds = origin.getAnnotation(ConfigEntry.BoundedFloat.class);
  57. return new BoundedEntry<>(origin, bounds.min(), bounds.max());
  58. }),
  59. Transformation.builder().byType(double.class, Double.class).byAnnotation(ConfigEntry.BoundedDouble.class).transforms(origin -> {
  60. ConfigEntry.BoundedDouble bounds = origin.getAnnotation(ConfigEntry.BoundedDouble.class);
  61. return new BoundedEntry<>(origin, bounds.min(), bounds.max());
  62. }),
  63. Transformation.builder().byType(type -> Enum.class.isAssignableFrom(ReflectionUtils.getTypeClass(type))).transforms(EnumEntry::new),
  64. Transformation.builder().byType(type -> Enum.class.isAssignableFrom(ReflectionUtils.getTypeClass(type))).byAnnotation(ConfigEntry.Dropdown.class).transforms(DropdownEntry::new),
  65. Transformation.builder().byAnnotation(ConfigEntry.Color.class).transforms(ColorEntry::new),
  66. Transformation.builder().byType(TextColor.class).transforms(origin -> new ColorEntry<>(origin, false))
  67. );
  68. private static final Set<Entry> entries = new HashSet<>();
  69. static {
  70. for (Collection<Transformation> transformations : CompleteConfig.collectExtensions(CompleteConfigExtension.class, CompleteConfigExtension::getTransformations)) {
  71. Entry.transformations.addAll(transformations);
  72. }
  73. }
  74. static Entry<?> of(Field field, ConfigContainer parentObject, TranslationKey parentTranslation) {
  75. EntryOrigin origin = new EntryOrigin(field, parentObject, parentTranslation);
  76. Entry<?> entry = transformations.stream().filter(transformation -> transformation.test(origin)).findFirst().map(Transformation::getTransformer).orElse(DEFAULT_TRANSFORMER).transform(origin);
  77. if (!entries.add(entry)) {
  78. throw new UnsupportedOperationException(entry + " was already resolved");
  79. }
  80. return entry;
  81. }
  82. @EqualsAndHashCode.Include
  83. private final Field field;
  84. @Getter
  85. private final Type type;
  86. @Getter
  87. private final Class<T> typeClass;
  88. @EqualsAndHashCode.Include
  89. private final ConfigContainer parentObject;
  90. @Getter
  91. private final String id;
  92. @Getter
  93. private final T defaultValue;
  94. protected final TranslationKey translation;
  95. private final TranslationKey[] tooltipTranslation;
  96. @Accessors(fluent = true)
  97. @Getter
  98. private final boolean requiresRestart;
  99. private final String comment;
  100. private final UnaryOperator<T> valueModifier;
  101. protected Entry(EntryOrigin origin, UnaryOperator<T> valueModifier) {
  102. field = origin.getField();
  103. if (!field.isAccessible()) {
  104. field.setAccessible(true);
  105. }
  106. type = origin.getType();
  107. typeClass = (Class<T>) ReflectionUtils.getTypeClass(type);
  108. parentObject = origin.getParentObject();
  109. this.valueModifier = valueModifier;
  110. defaultValue = getValue();
  111. ConfigEntry annotation = field.getDeclaredAnnotation(ConfigEntry.class);
  112. id = annotation != null && !StringUtils.isBlank(annotation.value()) ? annotation.value() : field.getName();
  113. if (annotation != null && !StringUtils.isBlank(annotation.translationKey())) {
  114. translation = origin.getParentTranslation().append(annotation.translationKey());
  115. } else {
  116. translation = origin.getParentTranslation().append(id);
  117. }
  118. if (annotation != null && annotation.tooltipTranslationKeys().length > 0) {
  119. tooltipTranslation = Arrays.stream(annotation.tooltipTranslationKeys()).map(key -> {
  120. if (StringUtils.isBlank(key)) {
  121. throw new IllegalAnnotationParameterException("Tooltip translation key of entry " + this + " may not be blank");
  122. }
  123. return translation.root().append(key);
  124. }).toArray(TranslationKey[]::new);
  125. } else {
  126. tooltipTranslation = translation.appendTooltip().orElse(null);
  127. }
  128. requiresRestart = annotation != null && annotation.requiresRestart();
  129. comment = annotation != null && !StringUtils.isBlank(annotation.comment()) ? annotation.comment() : null;
  130. }
  131. protected Entry(EntryOrigin origin) {
  132. this(origin, null);
  133. }
  134. public T getValue() {
  135. if (update()) {
  136. return getValue();
  137. }
  138. return getFieldValue();
  139. }
  140. private T getFieldValue() {
  141. try {
  142. return (T) Objects.requireNonNull(field.get(parentObject), field.toString());
  143. } catch (IllegalAccessException e) {
  144. throw new RuntimeException("Failed to get entry value", e);
  145. }
  146. }
  147. public void setValue(@NonNull T value) {
  148. update(value);
  149. }
  150. private boolean update() {
  151. return update(getFieldValue());
  152. }
  153. private boolean update(T value) {
  154. if (valueModifier != null) {
  155. value = valueModifier.apply(value);
  156. }
  157. if (value.equals(getFieldValue())) {
  158. return false;
  159. }
  160. set(value);
  161. return true;
  162. }
  163. private void set(T value) {
  164. try {
  165. Optional<Method> writeMethod = ReflectionUtils.getWriteMethod(field);
  166. if (writeMethod.isPresent()) {
  167. writeMethod.get().invoke(parentObject, value);
  168. } else {
  169. field.set(parentObject, value);
  170. }
  171. } catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) {
  172. throw new RuntimeException("Failed to set entry value", e);
  173. }
  174. }
  175. public Text getText() {
  176. return translation.toText();
  177. }
  178. public Optional<Text[]> getTooltip() {
  179. return Optional.ofNullable(tooltipTranslation).map(lines -> {
  180. return Arrays.stream(lines).map(TranslationKey::toText).toArray(Text[]::new);
  181. });
  182. }
  183. @Override
  184. public void apply(CommentedConfigurationNode node) {
  185. try {
  186. T value = (T) node.get(type);
  187. // value could be null despite the virtual() check
  188. // see https://github.com/SpongePowered/Configurate/issues/187
  189. if(value == null) return;
  190. setValue(value);
  191. } catch (SerializationException e) {
  192. logger.error("Failed to apply value to entry", e);
  193. }
  194. }
  195. @Override
  196. public void fetch(CommentedConfigurationNode node) {
  197. try {
  198. node.set(type, getValue());
  199. if (comment != null) {
  200. node.comment(comment);
  201. }
  202. } catch (SerializationException e) {
  203. logger.error("Failed to fetch value from entry", e);
  204. }
  205. }
  206. @Override
  207. public String toString() {
  208. return field.toString();
  209. }
  210. }