Entry.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. package me.lortseam.completeconfig.data;
  2. import lombok.Getter;
  3. import me.lortseam.completeconfig.api.ConfigEntry;
  4. import me.lortseam.completeconfig.api.ConfigEntryContainer;
  5. import me.lortseam.completeconfig.data.gui.TranslationIdentifier;
  6. import me.lortseam.completeconfig.data.part.DataPart;
  7. import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
  8. import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
  9. import net.minecraft.text.Text;
  10. import org.apache.commons.lang3.StringUtils;
  11. import org.apache.logging.log4j.LogManager;
  12. import org.apache.logging.log4j.Logger;
  13. import org.spongepowered.configurate.CommentedConfigurationNode;
  14. import org.spongepowered.configurate.serialize.SerializationException;
  15. import java.lang.reflect.Field;
  16. import java.lang.reflect.Method;
  17. import java.math.BigDecimal;
  18. import java.util.*;
  19. import java.util.function.Consumer;
  20. public class Entry<T> extends EntryBase<T> implements DataPart {
  21. private static final Logger LOGGER = LogManager.getLogger();
  22. private static final Map<Field, EntryBase> ENTRIES = new HashMap<>();
  23. static EntryBase<?> of(String fieldName, Class<? extends ConfigEntryContainer> parentClass) {
  24. try {
  25. return of(parentClass.getDeclaredField(fieldName));
  26. } catch (NoSuchFieldException e) {
  27. throw new RuntimeException(e);
  28. }
  29. }
  30. static EntryBase<?> of(Field field) {
  31. return ENTRIES.computeIfAbsent(field, absentField -> new Draft<>(field));
  32. }
  33. private final ConfigEntryContainer parentObject;
  34. private String customID;
  35. @Getter
  36. private final T defaultValue;
  37. private final TranslationIdentifier parentTranslation;
  38. private TranslationIdentifier customTranslation;
  39. private TranslationIdentifier[] customTooltipTranslations;
  40. private boolean forceUpdate;
  41. private boolean requiresRestart;
  42. @Getter
  43. private final Extras<T> extras = new Extras<>(this);
  44. private String comment;
  45. private final List<Listener<T>> listeners = new ArrayList<>();
  46. private Entry(Field field, ConfigEntryContainer parentObject, TranslationIdentifier parentTranslation) {
  47. super(field);
  48. this.parentObject = parentObject;
  49. defaultValue = getValue();
  50. this.parentTranslation = parentTranslation;
  51. }
  52. public T getValue() {
  53. if (update()) {
  54. return getValue();
  55. }
  56. return getFieldValue();
  57. }
  58. private T getFieldValue() {
  59. try {
  60. return (T) Objects.requireNonNull(field.get(parentObject));
  61. } catch (IllegalAccessException e) {
  62. throw new RuntimeException(e);
  63. }
  64. }
  65. public void setValue(T value) {
  66. update(value);
  67. }
  68. private boolean update() {
  69. return update(getFieldValue());
  70. }
  71. private boolean update(T value) {
  72. if (extras.getBounds() != null) {
  73. if (new BigDecimal(value.toString()).compareTo(new BigDecimal(extras.getBounds().getMin().toString())) < 0) {
  74. LOGGER.warn("[CompleteConfig] Tried to set value of field " + field + " to a value less than minimum bound, setting to minimum now!");
  75. value = extras.getBounds().getMin();
  76. } else if (new BigDecimal(value.toString()).compareTo(new BigDecimal(extras.getBounds().getMax().toString())) > 0) {
  77. LOGGER.warn("[CompleteConfig] Tried to set value of field " + field + " to a value greater than maximum bound, setting to maximum now!");
  78. value = extras.getBounds().getMax();
  79. }
  80. }
  81. if (value.equals(getFieldValue())) {
  82. return false;
  83. }
  84. set(value);
  85. return true;
  86. }
  87. private void set(T value) {
  88. if (listeners.stream().noneMatch(listener -> listener.getParentObject() == parentObject) || forceUpdate) {
  89. try {
  90. field.set(parentObject, value);
  91. } catch (IllegalAccessException e) {
  92. throw new RuntimeException(e);
  93. }
  94. }
  95. for (Listener<T> listener : listeners) {
  96. listener.invoke(value);
  97. }
  98. }
  99. void addListener(Method method, ConfigEntryContainer parentObject) {
  100. listeners.add(new Listener<>(method, parentObject));
  101. }
  102. String getID() {
  103. return customID != null ? customID : field.getName();
  104. }
  105. TranslationIdentifier getTranslation() {
  106. return customTranslation != null ? customTranslation : parentTranslation.append(getID());
  107. }
  108. public Text getText() {
  109. return getTranslation().translate();
  110. }
  111. public Optional<Text[]> getTooltip() {
  112. TranslationIdentifier[] translations = null;
  113. if (customTooltipTranslations != null) {
  114. translations = customTooltipTranslations;
  115. } else {
  116. TranslationIdentifier defaultTooltipTranslation = getTranslation().append("tooltip");
  117. if (defaultTooltipTranslation.exists()) {
  118. translations = new TranslationIdentifier[] {defaultTooltipTranslation};
  119. } else {
  120. List<TranslationIdentifier> defaultTooltipTranslations = new ArrayList<>();
  121. for(int i = 0;; i++) {
  122. TranslationIdentifier key = defaultTooltipTranslation.append(Integer.toString(i));
  123. if(key.exists()) {
  124. defaultTooltipTranslations.add(key);
  125. } else {
  126. if (!defaultTooltipTranslations.isEmpty()) {
  127. translations = defaultTooltipTranslations.toArray(new TranslationIdentifier[0]);
  128. }
  129. break;
  130. }
  131. }
  132. }
  133. }
  134. return translations != null ? Optional.of(Arrays.stream(translations).map(TranslationIdentifier::translate).toArray(Text[]::new)) : Optional.empty();
  135. }
  136. public boolean requiresRestart() {
  137. return requiresRestart;
  138. }
  139. void resolve(Field field) {
  140. if (field.isAnnotationPresent(ConfigEntry.class)) {
  141. ConfigEntry annotation = field.getDeclaredAnnotation(ConfigEntry.class);
  142. String id = annotation.value();
  143. if (!StringUtils.isBlank(id)) {
  144. customID = id;
  145. }
  146. String customTranslationKey = annotation.customTranslationKey();
  147. if (!StringUtils.isBlank(customTranslationKey)) {
  148. customTranslation = parentTranslation.getModTranslation().appendKey(customTranslationKey);
  149. }
  150. String[] customTooltipKeys = annotation.customTooltipKeys();
  151. if (customTooltipKeys.length > 0) {
  152. if (Arrays.stream(customTooltipKeys).anyMatch(StringUtils::isBlank)) {
  153. throw new IllegalAnnotationParameterException("Entry tooltip key(s) must not be blank");
  154. }
  155. customTooltipTranslations = Arrays.stream(customTooltipKeys).map(key -> parentTranslation.getModTranslation().appendKey(key)).toArray(TranslationIdentifier[]::new);
  156. }
  157. forceUpdate = annotation.forceUpdate();
  158. requiresRestart = annotation.requiresRestart();
  159. String comment = annotation.comment();
  160. if (!StringUtils.isBlank(comment)) {
  161. this.comment = comment;
  162. }
  163. }
  164. if (field.isAnnotationPresent(ConfigEntry.Bounded.Integer.class)) {
  165. if (field.getType() != int.class && field.getType() != Integer.class) {
  166. throw new IllegalAnnotationTargetException("Cannot apply Integer bound to non Integer field " + field);
  167. }
  168. ConfigEntry.Bounded.Integer bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Integer.class);
  169. extras.setBounds(bounds.min(), bounds.max(), bounds.slider());
  170. }
  171. if (field.isAnnotationPresent(ConfigEntry.Bounded.Long.class)) {
  172. if (field.getType() != long.class && field.getType() != Long.class) {
  173. throw new IllegalAnnotationTargetException("Cannot apply Long bound to non Long field " + field);
  174. }
  175. ConfigEntry.Bounded.Long bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Long.class);
  176. extras.setBounds(bounds.min(), bounds.max(), bounds.slider());
  177. }
  178. if (field.isAnnotationPresent(ConfigEntry.Bounded.Float.class)) {
  179. if (field.getType() != float.class && field.getType() != Float.class) {
  180. throw new IllegalAnnotationTargetException("Cannot apply Float bound to non Float field " + field);
  181. }
  182. ConfigEntry.Bounded.Float bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Float.class);
  183. extras.setBounds(bounds.min(), bounds.max(), false);
  184. }
  185. if (field.isAnnotationPresent(ConfigEntry.Bounded.Double.class)) {
  186. if (field.getType() != double.class && field.getType() != Double.class) {
  187. throw new IllegalAnnotationTargetException("Cannot apply Double bound to non Double field " + field);
  188. }
  189. ConfigEntry.Bounded.Double bounds = field.getDeclaredAnnotation(ConfigEntry.Bounded.Double.class);
  190. extras.setBounds(bounds.min(), bounds.max(), false);
  191. }
  192. if (Enum.class.isAssignableFrom(field.getType())) {
  193. if (field.isAnnotationPresent(ConfigEntry.EnumOptions.class)) {
  194. extras.setEnumOptions(field.getDeclaredAnnotation(ConfigEntry.EnumOptions.class).displayType());
  195. } else {
  196. extras.setEnumOptions(EnumOptions.DisplayType.DEFAULT);
  197. }
  198. } else if (field.isAnnotationPresent(ConfigEntry.EnumOptions.class)) {
  199. throw new IllegalAnnotationTargetException("Cannot apply enum options to non enum field " + field);
  200. }
  201. }
  202. @Override
  203. public void apply(CommentedConfigurationNode node) {
  204. try {
  205. setValue((T) node.get(type));
  206. } catch (SerializationException e) {
  207. LOGGER.error("[CompleteConfig] Failed to apply value to entry!", e);
  208. }
  209. }
  210. @Override
  211. public void fetch(CommentedConfigurationNode node) {
  212. try {
  213. node.set(type, getValue());
  214. if (comment != null) {
  215. node.comment(comment);
  216. }
  217. } catch (SerializationException e) {
  218. LOGGER.error("[CompleteConfig] Failed to fetch value from entry!", e);
  219. }
  220. }
  221. @Override
  222. void interact(Consumer<Entry<T>> interaction) {
  223. interaction.accept(this);
  224. }
  225. static class Draft<T> extends EntryBase<T> {
  226. static <T> Draft<T> of(Field field) {
  227. EntryBase<T> accessor = (EntryBase<T>) Entry.of(field);
  228. if (!(accessor instanceof Draft)) {
  229. throw new UnsupportedOperationException("Entry draft of field " + field + " was already built");
  230. }
  231. return (Draft<T>) accessor;
  232. }
  233. private final List<Consumer<Entry<T>>> interactions = new ArrayList<>();
  234. private Draft(Field field) {
  235. super(field);
  236. }
  237. @Override
  238. void interact(Consumer<Entry<T>> interaction) {
  239. interactions.add(interaction);
  240. }
  241. Entry<T> build(ConfigEntryContainer parentObject, TranslationIdentifier parentTranslation) {
  242. Entry<T> entry = new Entry<>(field, parentObject, parentTranslation);
  243. for (Consumer<Entry<T>> interaction : interactions) {
  244. interaction.accept(entry);
  245. }
  246. ENTRIES.put(field, entry);
  247. return entry;
  248. }
  249. }
  250. }