just a copy of petroleum right now
This commit is contained in:
commit
183c81258a
88 changed files with 7405 additions and 0 deletions
59
remappedSrc/pm/j4/petroleum/util/config/Config.java
Normal file
59
remappedSrc/pm/j4/petroleum/util/config/Config.java
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import pm.j4.petroleum.PetroleumMod;
|
||||
import pm.j4.petroleum.util.module.ModuleBase;
|
||||
|
||||
/**
|
||||
* The type Config.
|
||||
*/
|
||||
public abstract class Config {
|
||||
/**
|
||||
* The Enabled modules.
|
||||
*/
|
||||
public List<String> enabledModules = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Is enabled boolean.
|
||||
*
|
||||
* @param mod the mod
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isEnabled(String mod) {
|
||||
return enabledModules.contains(mod);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable module.
|
||||
*
|
||||
* @param mod the mod
|
||||
*/
|
||||
public void disableModule(String mod) {
|
||||
if (isEnabled(mod) && PetroleumMod.isActive(mod) && PetroleumMod.getMod(mod).isPresent()) {
|
||||
ModuleBase moduleInfo = PetroleumMod.getMod(mod).get();
|
||||
if (moduleInfo.isActivatable()) {
|
||||
enabledModules.remove(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle module.
|
||||
*
|
||||
* @param mod the mod
|
||||
*/
|
||||
public void toggleModule(String mod) {
|
||||
if (PetroleumMod.isActive(mod) && PetroleumMod.getMod(mod).isPresent()) {
|
||||
ModuleBase moduleInfo = PetroleumMod.getMod(mod).get();
|
||||
if (moduleInfo.isActivatable()) {
|
||||
if (isEnabled(mod)) {
|
||||
enabledModules.remove(mod);
|
||||
} else {
|
||||
enabledModules.add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
remappedSrc/pm/j4/petroleum/util/config/ConfigHolder.java
Normal file
102
remappedSrc/pm/j4/petroleum/util/config/ConfigHolder.java
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import pm.j4.petroleum.PetroleumMod;
|
||||
import pm.j4.petroleum.util.module.ModuleBase;
|
||||
|
||||
/**
|
||||
* The type Config holder.
|
||||
*/
|
||||
public class ConfigHolder {
|
||||
/**
|
||||
* The Global config.
|
||||
*/
|
||||
public GlobalConfig globalConfig;
|
||||
/**
|
||||
* The Server configs.
|
||||
*/
|
||||
public Map<String, ServerConfig> serverConfigs;
|
||||
|
||||
/**
|
||||
* Is module enabled boolean.
|
||||
*
|
||||
* @param module the module
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isModuleEnabled(String module) {
|
||||
|
||||
if (!PetroleumMod.isActive(module)) {
|
||||
return false;
|
||||
}
|
||||
if (globalConfig.isEnabled(module)) {
|
||||
return true;
|
||||
}
|
||||
String server = this.getServer();
|
||||
if (serverConfigs.containsKey(server)) {
|
||||
return serverConfigs.get(server).isEnabled(module);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets enabled modules.
|
||||
*
|
||||
* @return the enabled modules
|
||||
*/
|
||||
public List<ModuleBase> getEnabledModules() {
|
||||
List<ModuleBase> modules = PetroleumMod.getActiveMods();
|
||||
return modules.stream().filter(module ->
|
||||
isModuleEnabled(module.getModuleName())
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets server.
|
||||
*
|
||||
* @return the server
|
||||
*/
|
||||
public String getServer() {
|
||||
return PetroleumMod.getServerAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle module.
|
||||
*
|
||||
* @param module the module
|
||||
*/
|
||||
public void toggleModule(String module) {
|
||||
String server = this.getServer();
|
||||
if (serverConfigs.containsKey(server)) {
|
||||
System.out.println("Toggling module " + module + " on server " + server);
|
||||
serverConfigs.get(server).toggleModule(module);
|
||||
} else {
|
||||
globalConfig.toggleModule(module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable module.
|
||||
*
|
||||
* @param module the module
|
||||
*/
|
||||
public void disableModule(String module) {
|
||||
String server = this.getServer();
|
||||
if (serverConfigs.containsKey(server)) {
|
||||
System.out.println("disabling module " + module + " on server " + server);
|
||||
serverConfigs.get(server).disableModule(module);
|
||||
} else {
|
||||
globalConfig.disableModule(module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save module.
|
||||
*
|
||||
* @param module the module
|
||||
*/
|
||||
public static void saveModule(ModuleBase module) {
|
||||
|
||||
}
|
||||
}
|
||||
270
remappedSrc/pm/j4/petroleum/util/config/ConfigManager.java
Normal file
270
remappedSrc/pm/j4/petroleum/util/config/ConfigManager.java
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.google.gson.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import pm.j4.petroleum.PetroleumMod;
|
||||
import pm.j4.petroleum.modules.bindings.BindingInfo;
|
||||
import pm.j4.petroleum.modules.menu.ModMenu;
|
||||
import pm.j4.petroleum.util.data.ButtonInformation;
|
||||
import pm.j4.petroleum.util.data.ModuleConfig;
|
||||
import pm.j4.petroleum.util.data.OptionSerializiable;
|
||||
import pm.j4.petroleum.util.module.ModuleBase;
|
||||
|
||||
/**
|
||||
* The type Config manager.
|
||||
*/
|
||||
public class ConfigManager {
|
||||
/**
|
||||
* The constant GSON.
|
||||
*/
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(GlobalConfig.class, SerializationHelper.getGlobalSerializer())
|
||||
.registerTypeAdapter(GlobalConfig.class, SerializationHelper.getGlobalDeserializer())
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().create();
|
||||
/**
|
||||
* The constant config.
|
||||
*/
|
||||
private static ConfigHolder config;
|
||||
|
||||
/**
|
||||
* Prepare config file.
|
||||
*
|
||||
* @param path the path
|
||||
* @param filename the filename
|
||||
* @return the file
|
||||
*/
|
||||
private static File prepareConfigFile(String path, String filename) {
|
||||
if (path != "") {
|
||||
File directory = new File(FabricLoader.getInstance().getConfigDir().toString(), path);
|
||||
if (!directory.exists()) directory.mkdir();
|
||||
}
|
||||
return new File(FabricLoader.getInstance().getConfigDir().toString(), path + filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init config.
|
||||
*/
|
||||
public static void initConfig() {
|
||||
if (config != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
config = new ConfigHolder();
|
||||
config.globalConfig = new DefaultConfig();
|
||||
config.serverConfigs = new HashMap<>();
|
||||
|
||||
config = load("petroleum/", "petroleum.json", ConfigHolder.class);
|
||||
initModules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init modules.
|
||||
*/
|
||||
public static void initModules() {
|
||||
PetroleumMod.getActiveMods().forEach(module -> {
|
||||
ModuleConfig options = load("petroleum/modules/", module.getModuleName() + ".json", ModuleConfig.class);
|
||||
if (options != null && options.options != null) {
|
||||
options.options.forEach((key, option) -> {
|
||||
if (module.hasOption(option.key)) {
|
||||
module.setConfigOption(option.key, option.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load.
|
||||
*
|
||||
* @param <T> the type parameter
|
||||
* @param path the path
|
||||
* @param filename the filename
|
||||
* @param tClass the t class
|
||||
* @return the t
|
||||
*/
|
||||
private static <T> T load(String path, String filename, Class<T> tClass) {
|
||||
File file = prepareConfigFile(path, filename);
|
||||
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
save(path, filename, tClass.newInstance());
|
||||
}
|
||||
if (file.exists()) {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
T parsedConfig = null;
|
||||
try {
|
||||
parsedConfig = GSON.fromJson(reader, tClass);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Couldn't parse config file");
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (parsedConfig != null) {
|
||||
return parsedConfig;
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
System.out.println("Couldn't load configuration file at " + path);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize element t.
|
||||
*
|
||||
* @param <T> the type parameter
|
||||
* @param element the element
|
||||
* @param tClass the t class
|
||||
* @return the t
|
||||
*/
|
||||
public static <T> T deserializeElement(JsonElement element, Class<T> tClass) {
|
||||
return GSON.fromJson(element, tClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save.
|
||||
*
|
||||
* @param <T> the type parameter
|
||||
* @param path the path
|
||||
* @param filename the filename
|
||||
* @param data the data
|
||||
*/
|
||||
private static <T> void save(String path, String filename, T data) {
|
||||
File file = prepareConfigFile(path, filename);
|
||||
|
||||
String json = GSON.toJson(data);
|
||||
try (FileWriter fileWriter = new FileWriter(file)) {
|
||||
fileWriter.write(json);
|
||||
} catch (IOException e) {
|
||||
System.out.println("Couldn't save configuration file at " + path);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save module.
|
||||
*
|
||||
* @param b the b
|
||||
*/
|
||||
public static void saveModule(ModuleBase b) {
|
||||
ModuleConfig c = new ModuleConfig();
|
||||
c.options = GlobalConfig.serializeModuleConfiguration(b);
|
||||
save("petroleum/modules/", b.getModuleName() + ".json", c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all modules.
|
||||
*/
|
||||
public static void saveAllModules() {
|
||||
List<ModuleBase> mods = PetroleumMod.getActiveMods();
|
||||
mods.forEach(ConfigManager::saveModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save global config.
|
||||
*/
|
||||
public static void saveGlobalConfig() {
|
||||
save("petroleum/", "petroleum.json", config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets config.
|
||||
*
|
||||
* @return the config
|
||||
*/
|
||||
public static Optional<ConfigHolder> getConfig() {
|
||||
if (config == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type Serialization helper.
|
||||
*/
|
||||
class SerializationHelper {
|
||||
|
||||
/**
|
||||
* The constant s.
|
||||
*/
|
||||
private static final JsonSerializer<GlobalConfig> GLOBAL_CONFIG_JSON_SERIALIZER = (src, typeOfSrc, ctx) -> {
|
||||
JsonObject jsonConfig = new JsonObject();
|
||||
|
||||
JsonArray bindings = ctx.serialize(src.serializeBindings()).getAsJsonArray();
|
||||
jsonConfig.add("bindings", bindings);
|
||||
|
||||
JsonArray modules = ctx.serialize(src.enabledModules).getAsJsonArray();
|
||||
jsonConfig.add("enabled_modules", modules);
|
||||
|
||||
JsonObject tabCoordinates = new JsonObject();
|
||||
ModMenu.getButtons().forEach((category, coordinates) -> {
|
||||
tabCoordinates.add(category, ctx.serialize(coordinates));
|
||||
});
|
||||
jsonConfig.add("button_coordinates", tabCoordinates);
|
||||
|
||||
return jsonConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* The constant ds.
|
||||
*/
|
||||
private static final JsonDeserializer<GlobalConfig> GLOBAL_CONFIG_JSON_DESERIALIZER = ((json, typeOfT, ctx) -> {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
|
||||
List<BindingInfo> bindings = new ArrayList<>();
|
||||
if (obj.has("bindings")) {
|
||||
obj.get("bindings").getAsJsonArray().forEach(b -> bindings.add(ctx.deserialize(b, BindingInfo.class)));
|
||||
}
|
||||
List<String> modules = new ArrayList<>();
|
||||
if (obj.has("enabled_modules")) {
|
||||
obj.get("enabled_modules").getAsJsonArray().forEach(m -> modules.add(m.getAsString()));
|
||||
}
|
||||
GlobalConfig cfg = new GlobalConfig();
|
||||
Map<String, List<OptionSerializiable>> options;
|
||||
Type type = new TypeToken<Map<String, List<OptionSerializiable>>>() {
|
||||
}.getType();
|
||||
if (obj.has("module_configuration")) {
|
||||
options = ctx.deserialize(obj.get("module_configuration"), type);
|
||||
} else {
|
||||
options = new HashMap<>();
|
||||
}
|
||||
if (obj.has("button_coordinates")) {
|
||||
obj.get("button_coordinates").getAsJsonObject().entrySet().forEach(
|
||||
value -> {
|
||||
cfg.setButton(value.getKey(), ctx.deserialize(value.getValue(), ButtonInformation.class));
|
||||
}
|
||||
);
|
||||
}
|
||||
PetroleumMod.getActiveMods().forEach(module -> {
|
||||
if (options.containsKey(module.getModuleName())) {
|
||||
cfg.deserializeModuleConfiguration(options.get(module.getModuleName()), module);
|
||||
}
|
||||
});
|
||||
cfg.deserializeBindings(bindings);
|
||||
cfg.enabledModules = modules;
|
||||
return cfg;
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets serializer.
|
||||
*
|
||||
* @return the serializer
|
||||
*/
|
||||
public static JsonSerializer<GlobalConfig> getGlobalSerializer() {
|
||||
return GLOBAL_CONFIG_JSON_SERIALIZER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets deserializer.
|
||||
*
|
||||
* @return the deserializer
|
||||
*/
|
||||
public static JsonDeserializer<GlobalConfig> getGlobalDeserializer() {
|
||||
return GLOBAL_CONFIG_JSON_DESERIALIZER;
|
||||
}
|
||||
}
|
||||
15
remappedSrc/pm/j4/petroleum/util/config/DefaultConfig.java
Normal file
15
remappedSrc/pm/j4/petroleum/util/config/DefaultConfig.java
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* The type Default config.
|
||||
*/
|
||||
public class DefaultConfig extends GlobalConfig {
|
||||
/**
|
||||
* Instantiates a new Default config.
|
||||
*/
|
||||
public DefaultConfig() {
|
||||
this.enabledModules = Collections.singletonList("petroleum.splashtext");
|
||||
}
|
||||
}
|
||||
189
remappedSrc/pm/j4/petroleum/util/config/GlobalConfig.java
Normal file
189
remappedSrc/pm/j4/petroleum/util/config/GlobalConfig.java
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.minecraft.client.options.KeyBinding;
|
||||
import net.minecraft.client.util.InputUtil;
|
||||
import pm.j4.petroleum.PetroleumMod;
|
||||
import pm.j4.petroleum.modules.bindings.BindingInfo;
|
||||
import pm.j4.petroleum.util.data.ButtonInformation;
|
||||
import pm.j4.petroleum.util.data.OptionSerializiable;
|
||||
import pm.j4.petroleum.util.module.ModuleBase;
|
||||
import pm.j4.petroleum.util.module.option.ConfigurationOption;
|
||||
|
||||
/**
|
||||
* The type Global config.
|
||||
*/
|
||||
public class GlobalConfig extends Config {
|
||||
/**
|
||||
* The Bindings.
|
||||
*/
|
||||
public final Map<KeyBinding, ModuleBase> bindings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Is bound boolean.
|
||||
*
|
||||
* @param func the func
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isBound(ModuleBase func) {
|
||||
AtomicBoolean found = new AtomicBoolean(false);
|
||||
bindings.forEach((key, binding) -> {
|
||||
if (binding.equals(func)) {
|
||||
found.set(true);
|
||||
}
|
||||
});
|
||||
return found.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets binding.
|
||||
*
|
||||
* @param bind the bind
|
||||
* @param func the func
|
||||
*/
|
||||
public void setBinding(KeyBinding bind, ModuleBase func) {
|
||||
if (bindings.containsValue(func)) {
|
||||
bindings.forEach((key, binding) -> {
|
||||
if (binding.equals(func)) {
|
||||
PetroleumMod.removeBind(key);
|
||||
bindings.remove(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (PetroleumMod.isActive(func.getModuleName())) {
|
||||
PetroleumMod.addBind(bind);
|
||||
bindings.put(bind, func);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binding.
|
||||
*
|
||||
* @param info the info
|
||||
*/
|
||||
private void convertBinding(BindingInfo info) {
|
||||
Optional<ModuleBase> match = PetroleumMod.getMod(info.attachedModuleName);
|
||||
match.ifPresent(moduleBase -> setBinding(reconstructBinding(info),
|
||||
moduleBase));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct binding key binding.
|
||||
*
|
||||
* @param info the info
|
||||
* @return the key binding
|
||||
*/
|
||||
public static KeyBinding reconstructBinding(BindingInfo info) {
|
||||
return new KeyBinding(
|
||||
info.translationKey,
|
||||
info.type,
|
||||
info.key,
|
||||
info.category
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract binding info.
|
||||
*
|
||||
* @param b the b
|
||||
* @param f the f
|
||||
* @return the binding info
|
||||
*/
|
||||
public static BindingInfo extractBinding(KeyBinding b, ModuleBase f) {
|
||||
BindingInfo res = new BindingInfo();
|
||||
res.attachedModuleName = f.getModuleName();
|
||||
|
||||
res.translationKey = b.getTranslationKey();
|
||||
InputUtil.Key k = b.getDefaultKey();
|
||||
res.type = k.getCategory();
|
||||
res.key = k.getCode();
|
||||
res.category = b.getCategory();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize bindings list.
|
||||
*
|
||||
* @return the list
|
||||
*/
|
||||
public List<BindingInfo> serializeBindings() {
|
||||
List<BindingInfo> b = new ArrayList<>();
|
||||
bindings.forEach((k, f) -> b.add(extractBinding(k, f)));
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize bindings.
|
||||
*
|
||||
* @param info the info
|
||||
*/
|
||||
public void deserializeBindings(List<BindingInfo> info) {
|
||||
info.forEach(this::convertBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize module configuration list.
|
||||
*
|
||||
* @param module the module
|
||||
* @return the list
|
||||
*/
|
||||
public static Map<String, OptionSerializiable> serializeModuleConfiguration(ModuleBase module) {
|
||||
Map<String, OptionSerializiable> opts = new HashMap<>();
|
||||
Map<String, ConfigurationOption> configuration = module.getModuleConfiguration();
|
||||
configuration.forEach((key, value) -> {
|
||||
opts.put(key, new OptionSerializiable(key, value.toJson()));
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize module configuration.
|
||||
*
|
||||
* @param opts the opts
|
||||
* @param module the module
|
||||
*/
|
||||
public void deserializeModuleConfiguration(List<OptionSerializiable> opts, ModuleBase module) {
|
||||
opts.forEach(option -> {
|
||||
if (module.hasOption(option.key)) {
|
||||
module.setConfigOption(option.key, option.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The Button locations.
|
||||
*/
|
||||
private final Map<String, ButtonInformation> buttonLocations = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Sets button.
|
||||
*
|
||||
* @param category the category
|
||||
* @param buttonInformation the button information
|
||||
*/
|
||||
public void setButton(String category, ButtonInformation buttonInformation) {
|
||||
if (buttonLocations.containsKey(category)) {
|
||||
buttonLocations.replace(category, buttonInformation);
|
||||
} else {
|
||||
buttonLocations.put(category, buttonInformation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets button.
|
||||
*
|
||||
* @param category the category
|
||||
* @return the button
|
||||
*/
|
||||
public ButtonInformation getButton(String category) {
|
||||
if (buttonLocations.containsKey(category)) {
|
||||
return buttonLocations.get(category);
|
||||
}
|
||||
System.out.println("Could not find button of category " + category);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
11
remappedSrc/pm/j4/petroleum/util/config/ServerConfig.java
Normal file
11
remappedSrc/pm/j4/petroleum/util/config/ServerConfig.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package pm.j4.petroleum.util.config;
|
||||
|
||||
/**
|
||||
* The type Server config.
|
||||
*/
|
||||
public class ServerConfig extends Config {
|
||||
/**
|
||||
* The Address.
|
||||
*/
|
||||
public String address = "";
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue