-
Notifications
You must be signed in to change notification settings - Fork 5
/
OmegaConfig.java
244 lines (208 loc) · 9.36 KB
/
OmegaConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package draylar.omegaconfig;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import draylar.omegaconfig.api.Comment;
import draylar.omegaconfig.api.Config;
import draylar.omegaconfig.gson.SyncableExclusionStrategy;
import io.netty.buffer.Unpooled;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtList;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.util.Identifier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class OmegaConfig implements ModInitializer {
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static final Identifier CONFIG_SYNC_PACKET = new Identifier("omegaconfig", "sync");
public static final Gson SYNC_ONLY_GSON = new GsonBuilder().addSerializationExclusionStrategy(new SyncableExclusionStrategy()).setPrettyPrinting().create();
public static final Logger LOGGER = LogManager.getLogger();
private static final List<Config> REGISTERED_CONFIGURATIONS = new ArrayList<>();
@Override
public void onInitialize() {
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> server.execute(() -> {
PacketByteBuf packet = new PacketByteBuf(Unpooled.buffer());
// list of configurations that are synced
NbtCompound root = new NbtCompound();
NbtList configurations = new NbtList();
// Iterate over each configuration.
// Find values that should be synced and send the value over.
OmegaConfig.getRegisteredConfigurations().forEach(config -> {
if (config.hasAnySyncable()) {
configurations.add(config.writeSyncingTag());
}
});
// save to packet and send to user
root.put("Configurations", configurations);
packet.writeNbt(root);
handler.sendPacket(ServerPlayNetworking.createS2CPacket(CONFIG_SYNC_PACKET, packet));
}));
}
public static <T extends Config> T register(Class<T> configClass) {
try {
// Attempt to instantiate a new instance of this class.
T config = configClass.getDeclaredConstructor().newInstance();
// Exceptions will have been thrown at this point.
// We want to provide access to the config as soon as it is created, so we:
// 1. serialize to disk if the config does not already exist
// 2. read from disk if it does exist
if (!configExists(config)) {
config.save();
REGISTERED_CONFIGURATIONS.add(config);
} else {
try {
// Read from the disk config file to populate the correct values into our config object.
List<String> lines = Files.readAllLines(getConfigPath(config));
lines.removeIf(line -> line.trim().startsWith("//"));
StringBuilder res = new StringBuilder();
lines.forEach(res::append);
T object = GSON.fromJson(res.toString(), configClass);
// re-write the config to add new values
object.save();
REGISTERED_CONFIGURATIONS.add(object);
return object;
} catch (Exception e) {
LOGGER.error(e);
LOGGER.info(String.format("Encountered an error while reading %s config, falling back to default values.", config.getName()));
LOGGER.info(String.format("If this problem persists, delete the config file %s and try again.", config.getName() + "." + config.getExtension()));
}
}
return config;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
exception.printStackTrace();
throw new RuntimeException("No valid constructor found for: " + configClass.getName());
}
}
public static <T extends Config> void writeConfig(Class<T> configClass, T instance) {
// Write the config to disk with the default values.
String json = GSON.toJson(instance);
// Cursed time.
List<String> lines = new ArrayList<>(Arrays.asList(json.split("\n")));
Map<Integer, String> insertions = new TreeMap<>();
Map<String, String> keyToComments = new HashMap<>();
// populate key -> comments map
for (Field field : configClass.getDeclaredFields()) {
addFieldComments(field, keyToComments);
}
// "flattens" all the inner classes of the Config class into a single list
for (Class<?> clazz : flatten(configClass.getDeclaredClasses())) {
for (Field field : clazz.getDeclaredFields()) {
addFieldComments(field, keyToComments);
}
}
// Find areas we should insert comments into...
for (int i = 0; i < lines.size(); i++) {
String at = lines.get(i);
String startingWhitespace = getStartingWhitespace(at);
for (Map.Entry<String, String> entry : keyToComments.entrySet()) {
String comment = entry.getValue();
// Check if we should insert comment
if (at.trim().startsWith(String.format("\"%s\"", entry.getKey()))) {
if (comment.contains("\n")) {
comment = startingWhitespace + "//" + String.join(String.format("\n%s//", startingWhitespace), comment.split("\n"));
} else {
comment = String.format("%s//%s", startingWhitespace, comment);
}
insertions.put(i + insertions.size(), comment);
break;
}
}
}
// insertions -> list
for (Map.Entry<Integer, String> entry : insertions.entrySet()) {
Integer key = entry.getKey();
String value = entry.getValue();
lines.add(key, value);
}
// list -> string
StringBuilder res = new StringBuilder();
lines.forEach(str -> res.append(String.format("%s%n", str)));
try {
Path configPath = getConfigPath(instance);
configPath.toFile().getParentFile().mkdirs();
Files.write(configPath, res.toString().getBytes());
} catch (IOException ioException) {
LOGGER.error(ioException);
LOGGER.info(String.format("Write error, using default values for config %s.", configClass));
}
}
public static List<Class<?>> flatten(Class<?>[] array) {
List<Class<?>> list = new ArrayList<>();
for (Class<?> clazz : array) {
populateRecursively(list, clazz);
}
return list;
}
private static void populateRecursively(List<Class<?>> list, Class<?> aClass) {
list.add(aClass);
Class<?>[] classes = aClass.getDeclaredClasses();
if (classes.length != 0) {
for (Class<?> clazz : classes) {
populateRecursively(list, clazz);
}
}
}
private static void addFieldComments(Field field, Map<String, String> keyToComments) {
String fieldName = field.getName();
Annotation[] annotations = field.getDeclaredAnnotations();
// Find comment
for (Annotation annotation : annotations) {
if (annotation instanceof Comment) {
keyToComments.put(fieldName, ((Comment) annotation).value());
break;
}
}
}
/**
* Returns a string with the left-side whitespace characters of the given input, up till the first non-whitespace character.
*
* <p>
* " hello" -> " "
* "p" -> ""
* " p" -> " "
*
* @param input input to retrieve whitespaces from
* @return starting whitespaces from the given input
*/
private static String getStartingWhitespace(String input) {
int index = -1;
char[] chars = input.toCharArray();
for (int i = 0; i < chars.length; i++) {
char at = chars[i];
if (at != ' ') {
index = i;
break;
}
}
if (index != -1) {
return input.substring(0, index);
} else {
return "";
}
}
public static Path getConfigPath(Config config) {
return Paths.get(FabricLoader.getInstance().getConfigDir().toString(), config.getDirectory(), String.format("%s.%s", config.getName(), config.getExtension()));
}
public static boolean configExists(Config config) {
return Files.exists(getConfigPath(config));
}
public static List<Config> getRegisteredConfigurations() {
return REGISTERED_CONFIGURATIONS;
}
}