ConfigManager.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package me.lortseam.completeconfig;
  2. import com.google.gson.Gson;
  3. import com.google.gson.GsonBuilder;
  4. import com.google.gson.JsonElement;
  5. import com.google.gson.JsonNull;
  6. import lombok.AccessLevel;
  7. import lombok.Getter;
  8. import me.lortseam.completeconfig.api.ConfigCategory;
  9. import me.lortseam.completeconfig.api.ConfigEntry;
  10. import me.lortseam.completeconfig.api.ConfigEntryContainer;
  11. import me.lortseam.completeconfig.api.ConfigEntryListener;
  12. import me.lortseam.completeconfig.collection.Collection;
  13. import me.lortseam.completeconfig.entry.Entry;
  14. import me.lortseam.completeconfig.exception.IllegalModifierException;
  15. import me.lortseam.completeconfig.gui.GuiRegistry;
  16. import me.lortseam.completeconfig.exception.IllegalAnnotationParameterException;
  17. import me.lortseam.completeconfig.exception.IllegalAnnotationTargetException;
  18. import me.lortseam.completeconfig.exception.IllegalReturnValueException;
  19. import me.lortseam.completeconfig.listener.Listener;
  20. import me.lortseam.completeconfig.serialization.CollectionsDeserializer;
  21. import me.lortseam.completeconfig.serialization.EntrySerializer;
  22. import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
  23. import me.shedaniel.clothconfig2.api.ConfigBuilder;
  24. import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
  25. import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
  26. import net.fabricmc.loader.api.FabricLoader;
  27. import net.minecraft.client.gui.screen.Screen;
  28. import net.minecraft.text.TranslatableText;
  29. import org.apache.commons.lang3.StringUtils;
  30. import java.io.*;
  31. import java.lang.reflect.Modifier;
  32. import java.nio.file.Files;
  33. import java.nio.file.Path;
  34. import java.nio.file.Paths;
  35. import java.util.*;
  36. import java.util.function.Supplier;
  37. import java.util.stream.Collectors;
  38. public class ConfigManager {
  39. @Getter(AccessLevel.PACKAGE)
  40. private final String modID;
  41. private final Path jsonPath;
  42. private final LinkedHashMap<String, Collection> config = new LinkedHashMap<>();
  43. private final JsonElement json;
  44. private final Set<Listener> pendingListeners = new HashSet<>();
  45. @Getter
  46. private final GuiRegistry guiRegistry = new GuiRegistry();
  47. private Supplier<ConfigBuilder> guiBuilder = ConfigBuilder::create;
  48. ConfigManager(String modID) {
  49. this.modID = modID;
  50. jsonPath = Paths.get(FabricLoader.getInstance().getConfigDirectory().toPath().toString(), modID + ".json");
  51. json = load();
  52. }
  53. private JsonElement load() {
  54. if(Files.exists(jsonPath)) {
  55. try {
  56. return new Gson().fromJson(new FileReader(jsonPath.toString()), JsonElement.class);
  57. } catch (FileNotFoundException e) {
  58. throw new RuntimeException(e);
  59. }
  60. }
  61. return JsonNull.INSTANCE;
  62. }
  63. private LinkedHashMap<String, Entry> getContainerEntries(ConfigEntryContainer container) {
  64. LinkedHashMap<String, Entry> entries = new LinkedHashMap<>();
  65. Class clazz = container.getClass();
  66. while (clazz != null) {
  67. List<Listener> listeners = new ArrayList<>();
  68. Iterator<Listener> iter = pendingListeners.iterator();
  69. while (iter.hasNext()) {
  70. Listener listener = iter.next();
  71. if (listener.getFieldClass() == clazz) {
  72. listeners.add(listener);
  73. pendingListeners.remove(listener);
  74. }
  75. }
  76. Arrays.stream(clazz.getDeclaredMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()) && method.isAnnotationPresent(ConfigEntryListener.class)).forEach(method -> {
  77. ConfigEntryListener listener = method.getDeclaredAnnotation(ConfigEntryListener.class);
  78. String fieldName = listener.value();
  79. Class<? extends ConfigEntryContainer> fieldClass = listener.container();
  80. if (fieldClass == ConfigEntryContainer.class) {
  81. listeners.add(new Listener(method, container, fieldName));
  82. } else {
  83. Map<String, Entry> fieldClassEntries = findEntries(config, fieldClass);
  84. if (fieldClassEntries.isEmpty()) {
  85. pendingListeners.add(new Listener(method, container, fieldName, fieldClass));
  86. } else {
  87. Entry entry = fieldClassEntries.get(fieldName);
  88. if (entry == null) {
  89. throw new IllegalAnnotationParameterException("Could not find field " + fieldName + " in " + fieldClass + " requested by listener method " + method);
  90. }
  91. entry.addListener(method, container);
  92. }
  93. }
  94. });
  95. LinkedHashMap<String, Entry> clazzEntries = new LinkedHashMap<>();
  96. Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
  97. if (Modifier.isStatic(field.getModifiers())) {
  98. return false;
  99. }
  100. if (container.isConfigPOJO()) {
  101. return !ConfigEntryContainer.class.isAssignableFrom(field.getType()) && !field.isAnnotationPresent(ConfigEntry.Ignore.class);
  102. }
  103. return field.isAnnotationPresent(ConfigEntry.class);
  104. }).forEach(field -> {
  105. if (Modifier.isFinal(field.getModifiers())) {
  106. throw new IllegalModifierException("Entry field " + field + " must not be final");
  107. }
  108. if (!field.isAccessible()) {
  109. field.setAccessible(true);
  110. }
  111. Entry.Builder builder = Entry.Builder.create(field, container);
  112. if (field.isAnnotationPresent(ConfigEntry.class)) {
  113. ConfigEntry entryAnnotation = field.getDeclaredAnnotation(ConfigEntry.class);
  114. String customTranslationKey = entryAnnotation.customTranslationKey();
  115. if (!StringUtils.isBlank(customTranslationKey)) {
  116. builder.setCustomTranslationKey(customTranslationKey);
  117. }
  118. builder.setForceUpdate(entryAnnotation.forceUpdate());
  119. }
  120. if (field.isAnnotationPresent(ConfigEntry.Integer.Bounded.class)) {
  121. if (field.getType() != int.class && field.getType() != Integer.class) {
  122. throw new IllegalAnnotationTargetException("Cannot apply Integer bound to non Integer field " + field);
  123. }
  124. ConfigEntry.Integer.Bounded bounds = field.getDeclaredAnnotation(ConfigEntry.Integer.Bounded.class);
  125. builder.setBounds(bounds.min(), bounds.max());
  126. } else if (field.isAnnotationPresent(ConfigEntry.Long.Bounded.class)) {
  127. if (field.getType() != long.class && field.getType() != Long.class) {
  128. throw new IllegalAnnotationTargetException("Cannot apply Long bound to non Long field " + field);
  129. }
  130. ConfigEntry.Long.Bounded bounds = field.getDeclaredAnnotation(ConfigEntry.Long.Bounded.class);
  131. builder.setBounds(bounds.min(), bounds.max());
  132. } else if (field.isAnnotationPresent(ConfigEntry.Float.Bounded.class)) {
  133. if (field.getType() != float.class && field.getType() != Float.class) {
  134. throw new IllegalAnnotationTargetException("Cannot apply Float bound to non Float field " + field);
  135. }
  136. ConfigEntry.Float.Bounded bounds = field.getDeclaredAnnotation(ConfigEntry.Float.Bounded.class);
  137. builder.setBounds(bounds.min(), bounds.max());
  138. } else if (field.isAnnotationPresent(ConfigEntry.Double.Bounded.class)) {
  139. if (field.getType() != double.class && field.getType() != Double.class) {
  140. throw new IllegalAnnotationTargetException("Cannot apply Double bound to non Double field " + field);
  141. }
  142. ConfigEntry.Double.Bounded bounds = field.getDeclaredAnnotation(ConfigEntry.Double.Bounded.class);
  143. builder.setBounds(bounds.min(), bounds.max());
  144. }
  145. Entry<?> entry = builder.build();
  146. if (guiRegistry.getProvider(entry) == null) {
  147. throw new UnsupportedOperationException("Could not find gui provider for field type " + entry.getType());
  148. }
  149. String fieldName = field.getName();
  150. listeners.removeIf(listener -> {
  151. if (!listener.getFieldName().equals(fieldName)) {
  152. return false;
  153. }
  154. entry.addListener(listener.getMethod(), listener.getParentObject());
  155. return true;
  156. });
  157. clazzEntries.put(fieldName, entry);
  158. });
  159. if (!listeners.isEmpty()) {
  160. Listener listener = listeners.iterator().next();
  161. throw new IllegalAnnotationParameterException("Could not find field " + listener.getFieldName() + " in " + clazz + " requested by listener method " + listener.getMethod());
  162. }
  163. clazzEntries.putAll(entries);
  164. entries = clazzEntries;
  165. clazz = clazz.getSuperclass();
  166. }
  167. return entries;
  168. }
  169. private Map<String, Entry> findEntries(LinkedHashMap<String, Collection> collections, Class<? extends ConfigEntryContainer> parentClass) {
  170. Map<String, Entry> entries = new HashMap<>();
  171. for (Collection collection : collections.values()) {
  172. entries.putAll(collection.getEntries().entrySet().stream().filter(entry -> entry.getValue().getParentObject().getClass() == parentClass).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
  173. entries.putAll(findEntries(collection.getCollections(), parentClass));
  174. }
  175. return entries;
  176. }
  177. public void register(ConfigCategory... categories) {
  178. Arrays.stream(categories).forEach(category -> registerCategory(config, category, true));
  179. }
  180. private void registerCategory(LinkedHashMap<String, Collection> configMap, ConfigCategory category, boolean applyJson) {
  181. String categoryID = category.getConfigCategoryID();
  182. if (StringUtils.isBlank(categoryID)) {
  183. throw new IllegalReturnValueException("Category ID of " + category.getClass() + " must not be null or blank");
  184. }
  185. if (configMap.containsKey(categoryID)) {
  186. throw new IllegalStateException("Duplicate category ID found: " + categoryID);
  187. }
  188. Collection collection = new me.lortseam.completeconfig.collection.Collection();
  189. configMap.put(categoryID, collection);
  190. registerContainer(collection, category);
  191. if (collection.getEntries().isEmpty() && collection.getCollections().isEmpty()) {
  192. configMap.remove(categoryID);
  193. return;
  194. }
  195. if (applyJson) {
  196. new GsonBuilder()
  197. .registerTypeAdapter(CollectionsDeserializer.TYPE, new CollectionsDeserializer(configMap, categoryID))
  198. .create()
  199. .fromJson(json, CollectionsDeserializer.TYPE);
  200. }
  201. }
  202. private void registerContainer(Collection collection, ConfigEntryContainer container) {
  203. if (!findEntries(config, container.getClass()).isEmpty()) {
  204. throw new UnsupportedOperationException("An instance of " + container.getClass() + " is already registered");
  205. }
  206. collection.getEntries().putAll(getContainerEntries(container));
  207. List<ConfigEntryContainer> containers = new ArrayList<>();
  208. Class clazz = container.getClass();
  209. while (clazz != null) {
  210. containers.addAll(Arrays.stream(clazz.getDeclaredFields()).filter(field -> {
  211. if (Modifier.isStatic(field.getModifiers())) {
  212. return false;
  213. }
  214. if (container.isConfigPOJO()) {
  215. return ConfigEntryContainer.class.isAssignableFrom(field.getType());
  216. }
  217. if (field.isAnnotationPresent(ConfigEntryContainer.Transitive.class)) {
  218. if (!ConfigEntryContainer.class.isAssignableFrom(field.getType())) {
  219. throw new IllegalAnnotationTargetException("Transitive entry " + field + " must implement ConfigEntryContainer");
  220. }
  221. return true;
  222. }
  223. return false;
  224. }).map(field -> {
  225. if (!field.isAccessible()) {
  226. field.setAccessible(true);
  227. }
  228. try {
  229. return (ConfigEntryContainer) field.get(container);
  230. } catch (IllegalAccessException e) {
  231. throw new RuntimeException(e);
  232. }
  233. }).collect(Collectors.toList()));
  234. clazz = clazz.getSuperclass();
  235. }
  236. containers.addAll(Arrays.asList(container.getTransitiveConfigEntryContainers()));
  237. for (ConfigEntryContainer c : containers) {
  238. if (c instanceof ConfigCategory) {
  239. registerCategory(collection.getCollections(), (ConfigCategory) c, false);
  240. } else {
  241. registerContainer(collection, c);
  242. collection.getEntries().putAll(getContainerEntries(c));
  243. }
  244. }
  245. }
  246. private String joinIDs(String... ids) {
  247. return String.join(".", ids);
  248. }
  249. private String buildTranslationKey(String... ids) {
  250. return joinIDs("config", modID, joinIDs(ids));
  251. }
  252. public void setCustomGuiBuilder(Supplier<ConfigBuilder> guiBuilder) {
  253. this.guiBuilder = guiBuilder;
  254. }
  255. public Screen getConfigScreen(Screen parentScreen) {
  256. ConfigBuilder builder = guiBuilder.get();
  257. builder.setParentScreen(parentScreen)
  258. .setTitle(new TranslatableText(buildTranslationKey("title")))
  259. .setSavingRunnable(this::save);
  260. config.forEach((categoryID, category) -> {
  261. me.shedaniel.clothconfig2.api.ConfigCategory configCategory = builder.getOrCreateCategory(new TranslatableText(buildTranslationKey(categoryID)));
  262. for (AbstractConfigListEntry entry : buildCollection(categoryID, category)) {
  263. configCategory.addEntry(entry);
  264. }
  265. });
  266. return builder.build();
  267. }
  268. private List<AbstractConfigListEntry> buildCollection(String parentID, Collection collection) {
  269. List<AbstractConfigListEntry> list = new ArrayList<>();
  270. collection.getEntries().forEach((entryID, entry) -> {
  271. String translationKey = entry.getCustomTranslationKey() != null ? buildTranslationKey(entry.getCustomTranslationKey()) : buildTranslationKey(parentID, entryID);
  272. list.add(guiRegistry.getProvider(entry).build(new TranslatableText(translationKey), entry.getField(), entry.getValue(), entry.getDefaultValue(), entry.getExtras(), entry::setValue));
  273. });
  274. collection.getCollections().forEach((subcategoryID, c) -> {
  275. String id = joinIDs(parentID, subcategoryID);
  276. SubCategoryBuilder subBuilder = ConfigEntryBuilder.create().startSubCategory(new TranslatableText(buildTranslationKey(id)));
  277. subBuilder.addAll(buildCollection(id, c));
  278. list.add(subBuilder.build());
  279. });
  280. return list;
  281. }
  282. public void save() {
  283. if (!Files.exists(jsonPath)) {
  284. try {
  285. Files.createDirectories(jsonPath.getParent());
  286. Files.createFile(jsonPath);
  287. } catch (IOException e) {
  288. throw new RuntimeException(e);
  289. }
  290. }
  291. try(Writer writer = new FileWriter(jsonPath.toString())) {
  292. new GsonBuilder()
  293. .registerTypeAdapter(EntrySerializer.TYPE, new EntrySerializer())
  294. .setPrettyPrinting()
  295. .create()
  296. .toJson(config, writer);
  297. } catch (IOException e) {
  298. throw new RuntimeException(e);
  299. }
  300. }
  301. }