Entry.java 11 KB

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