Integration migration (WIP)
This commit is contained in:
parent
a63fc3c8ed
commit
d8fe6cd766
11 changed files with 86 additions and 284 deletions
|
@ -4,16 +4,15 @@ import net.fabricmc.api.EnvType;
|
|||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import ru.bclib.api.ModIntegrationAPI;
|
||||
import ru.bclib.api.WorldDataAPI;
|
||||
import ru.bclib.util.Logger;
|
||||
import ru.betterend.api.BetterEndPlugin;
|
||||
import ru.betterend.config.Configs;
|
||||
import ru.betterend.effects.EndEnchantments;
|
||||
import ru.betterend.effects.EndPotions;
|
||||
import ru.betterend.events.PlayerAdvancementsCallback;
|
||||
import ru.betterend.integration.EndBiomeIntegration;
|
||||
import ru.betterend.integration.Integrations;
|
||||
import ru.betterend.item.GuideBookItem;
|
||||
import ru.betterend.recipe.AlloyingRecipes;
|
||||
import ru.betterend.recipe.AnvilRecipes;
|
||||
import ru.betterend.recipe.CraftingRecipes;
|
||||
|
@ -38,7 +37,6 @@ import ru.betterend.world.surface.SurfaceBuilders;
|
|||
public class BetterEnd implements ModInitializer {
|
||||
public static final String MOD_ID = "betterend";
|
||||
public static final Logger LOGGER = new Logger(MOD_ID);
|
||||
private static boolean hasHydrogen;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
|
@ -61,36 +59,22 @@ public class BetterEnd implements ModInitializer {
|
|||
SmithingRecipes.register();
|
||||
InfusionRecipes.register();
|
||||
EndStructures.register();
|
||||
Integrations.register();
|
||||
BonemealPlants.init();
|
||||
GeneratorOptions.init();
|
||||
DataFixerUtil.init();
|
||||
LootTableUtil.init();
|
||||
|
||||
if (hasGuideBook()) {
|
||||
GuideBookItem.register();
|
||||
}
|
||||
hasHydrogen = FabricLoader.getInstance().isModLoaded("hydrogen");
|
||||
|
||||
Integrations.init();
|
||||
initIntegrationBiomes();
|
||||
FabricLoader.getInstance().getEntrypoints("betterend", BetterEndPlugin.class).forEach(BetterEndPlugin::register);
|
||||
Configs.saveConfigs();
|
||||
|
||||
if (hasGuideBook()) {
|
||||
PlayerAdvancementsCallback.PLAYER_ADVANCEMENT_COMPLETE.register((player, advancement, criterionName) -> {
|
||||
ResourceLocation advId = new ResourceLocation("minecraft:end/enter_end_gateway");
|
||||
if (advId.equals(advancement.getId())) {
|
||||
player.addItem(new ItemStack(GuideBookItem.GUIDE_BOOK));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasGuideBook() {
|
||||
return FabricLoader.getInstance().isModLoaded("patchouli");
|
||||
}
|
||||
|
||||
public static boolean hasHydrogen() {
|
||||
return hasHydrogen;
|
||||
private void initIntegrationBiomes() {
|
||||
ModIntegrationAPI.getIntegrations().forEach(integration -> {
|
||||
if (integration instanceof EndBiomeIntegration && integration.modIsInstalled()) {
|
||||
((EndBiomeIntegration) integration).biomeRegister();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ResourceLocation makeID(String path) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package ru.betterend.integration;
|
||||
|
||||
public interface EndBiomeIntegration {
|
||||
void biomeRegister();
|
||||
|
||||
void addBiomes();
|
||||
}
|
|
@ -6,6 +6,7 @@ import java.util.Map;
|
|||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
import ru.bclib.integration.ModIntegration;
|
||||
import ru.bclib.util.ColorUtil;
|
||||
import ru.betterend.blocks.HydraluxPetalColoredBlock;
|
||||
import ru.betterend.blocks.complex.ColoredMaterial;
|
||||
|
@ -17,7 +18,7 @@ public class FlamboyantRefabricatedIntegration extends ModIntegration {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void register() {
|
||||
public void init() {
|
||||
Map<Integer, String> colors = Maps.newHashMap();
|
||||
Map<Integer, ItemLike> dyes = Maps.newHashMap();
|
||||
|
||||
|
|
|
@ -1,37 +1,53 @@
|
|||
package ru.betterend.integration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import ru.bclib.api.ModIntegrationAPI;
|
||||
import ru.bclib.integration.ModIntegration;
|
||||
import ru.bclib.recipes.GridRecipe;
|
||||
import ru.betterend.BetterEnd;
|
||||
import ru.betterend.config.Configs;
|
||||
import ru.betterend.events.PlayerAdvancementsCallback;
|
||||
import ru.betterend.integration.byg.BYGIntegration;
|
||||
import ru.betterend.item.GuideBookItem;
|
||||
import ru.betterend.registry.EndItems;
|
||||
|
||||
public class Integrations {
|
||||
public static final List<ModIntegration> INTEGRATIONS = Lists.newArrayList();
|
||||
public static final ModIntegration BYG = register(new BYGIntegration());
|
||||
public static final ModIntegration NOURISH = register(new NourishIntegration());
|
||||
//public static final ModIntegration EXTRA_PIECES = register(new ExtraPiecesIntegration());
|
||||
//public static final ModIntegration ADORN = register(new AdornIntegration());
|
||||
public static final ModIntegration FLAMBOYANT_REFABRICATED = register(new FlamboyantRefabricatedIntegration());
|
||||
public static final ModIntegration BYG = ModIntegrationAPI.register(new BYGIntegration());
|
||||
public static final ModIntegration NOURISH = ModIntegrationAPI.register(new NourishIntegration());
|
||||
public static final ModIntegration FLAMBOYANT_REFABRICATED = ModIntegrationAPI.register(new FlamboyantRefabricatedIntegration());
|
||||
|
||||
public static void register() {
|
||||
INTEGRATIONS.forEach((integration) -> {
|
||||
if (integration.modIsInstalled()) {
|
||||
integration.register();
|
||||
}
|
||||
});
|
||||
private static boolean hasHydrogen;
|
||||
|
||||
public static void init() {
|
||||
if (hasGuideBook()) {
|
||||
GuideBookItem.register();
|
||||
|
||||
PlayerAdvancementsCallback.PLAYER_ADVANCEMENT_COMPLETE.register((player, advancement, criterionName) -> {
|
||||
ResourceLocation advId = new ResourceLocation("minecraft:end/enter_end_gateway");
|
||||
if (advId.equals(advancement.getId())) {
|
||||
player.addItem(new ItemStack(GuideBookItem.GUIDE_BOOK));
|
||||
}
|
||||
});
|
||||
|
||||
GridRecipe.make(BetterEnd.MOD_ID, "guide_book", GuideBookItem.GUIDE_BOOK)
|
||||
.checkConfig(Configs.RECIPE_CONFIG)
|
||||
.setShape("D", "B", "C")
|
||||
.addMaterial('D', EndItems.ENDER_DUST)
|
||||
.addMaterial('B', Items.BOOK)
|
||||
.addMaterial('C', EndItems.CRYSTAL_SHARDS)
|
||||
.build();
|
||||
}
|
||||
hasHydrogen = FabricLoader.getInstance().isModLoaded("hydrogen");
|
||||
}
|
||||
|
||||
public static void addBiomes() {
|
||||
INTEGRATIONS.forEach((integration) -> {
|
||||
if (integration.modIsInstalled()) {
|
||||
integration.addBiomes();
|
||||
}
|
||||
});
|
||||
public static boolean hasGuideBook() {
|
||||
return FabricLoader.getInstance().isModLoaded("patchouli");
|
||||
}
|
||||
|
||||
private static ModIntegration register(ModIntegration integration) {
|
||||
INTEGRATIONS.add(integration);
|
||||
return integration;
|
||||
public static boolean hasHydrogen() {
|
||||
return hasHydrogen;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
package ru.betterend.integration;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import net.fabricmc.fabric.api.tag.TagRegistry;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.data.BuiltinRegistries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.tags.Tag;
|
||||
import net.minecraft.tags.Tag.Named;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import ru.bclib.world.features.BCLFeature;
|
||||
import ru.betterend.BetterEnd;
|
||||
|
||||
public abstract class ModIntegration {
|
||||
private final String modID;
|
||||
|
||||
public void register() {}
|
||||
|
||||
public void addBiomes() {}
|
||||
|
||||
public ModIntegration(String modID) {
|
||||
this.modID = modID;
|
||||
}
|
||||
|
||||
public ResourceLocation getID(String name) {
|
||||
return new ResourceLocation(modID, name);
|
||||
}
|
||||
|
||||
public Block getBlock(String name) {
|
||||
return Registry.BLOCK.get(getID(name));
|
||||
}
|
||||
|
||||
public Item getItem(String name) {
|
||||
return Registry.ITEM.get(getID(name));
|
||||
}
|
||||
|
||||
public BlockState getDefaultState(String name) {
|
||||
return getBlock(name).defaultBlockState();
|
||||
}
|
||||
|
||||
public ResourceKey<Biome> getKey(String name) {
|
||||
return ResourceKey.create(Registry.BIOME_REGISTRY, getID(name));
|
||||
}
|
||||
|
||||
public boolean modIsInstalled() {
|
||||
return FabricLoader.getInstance().isModLoaded(modID);
|
||||
}
|
||||
|
||||
public BCLFeature getFeature(String featureID, String configuredFeatureID, GenerationStep.Decoration featureStep) {
|
||||
Feature<?> feature = Registry.FEATURE.get(getID(featureID));
|
||||
ConfiguredFeature<?, ?> featureConfigured = BuiltinRegistries.CONFIGURED_FEATURE.get(getID(configuredFeatureID));
|
||||
return new BCLFeature(feature, featureConfigured, featureStep);
|
||||
}
|
||||
|
||||
public BCLFeature getFeature(String name, GenerationStep.Decoration featureStep) {
|
||||
return getFeature(name, name, featureStep);
|
||||
}
|
||||
|
||||
public ConfiguredFeature<?, ?> getConfiguredFeature(String name) {
|
||||
return BuiltinRegistries.CONFIGURED_FEATURE.get(getID(name));
|
||||
}
|
||||
|
||||
public Biome getBiome(String name) {
|
||||
return BuiltinRegistries.BIOME.get(getID(name));
|
||||
}
|
||||
|
||||
public Class<?> getClass(String path) {
|
||||
Class<?> cl = null;
|
||||
try {
|
||||
cl = Class.forName(path);
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
BetterEnd.LOGGER.error(e.getMessage());
|
||||
if (BetterEnd.isDevEnvironment()) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return cl;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Object> T getStaticFieldValue(Class<?> cl, String name) {
|
||||
if (cl != null) {
|
||||
try {
|
||||
Field field = cl.getDeclaredField(name);
|
||||
if (field != null) {
|
||||
return (T) field.get(null);
|
||||
}
|
||||
}
|
||||
catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object getFieldValue(Class<?> cl, String name, Object classInstance) {
|
||||
if (cl != null) {
|
||||
try {
|
||||
Field field = cl.getDeclaredField(name);
|
||||
if (field != null) {
|
||||
return field.get(classInstance);
|
||||
}
|
||||
}
|
||||
catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Method getMethod(Class<?> cl, String functionName, Class<?>... args) {
|
||||
if (cl != null) {
|
||||
try {
|
||||
return cl.getMethod(functionName, args);
|
||||
}
|
||||
catch (NoSuchMethodException | SecurityException e) {
|
||||
BetterEnd.LOGGER.error(e.getMessage());
|
||||
if (BetterEnd.isDevEnvironment()) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object executeMethod(Object instance, Method method, Object... args) {
|
||||
if (method != null) {
|
||||
try {
|
||||
return method.invoke(instance, args);
|
||||
}
|
||||
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
BetterEnd.LOGGER.error(e.getMessage());
|
||||
if (BetterEnd.isDevEnvironment()) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object getAndExecuteStatic(Class<?> cl, String functionName, Object... args) {
|
||||
if (cl != null) {
|
||||
Class<?>[] classes = new Class<?>[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
classes[i] = args[i].getClass();
|
||||
}
|
||||
Method method = getMethod(cl, functionName, classes);
|
||||
return executeMethod(null, method, args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Object> T getAndExecuteRuntime(Class<?> cl, Object instance, String functionName, Object... args) {
|
||||
if (instance != null) {
|
||||
Class<?>[] classes = new Class<?>[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
classes[i] = args[i].getClass();
|
||||
}
|
||||
Method method = getMethod(cl, functionName, classes);
|
||||
return (T) executeMethod(instance, method, args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object newInstance(Class<?> cl, Object... args) {
|
||||
if (cl != null) {
|
||||
for (Constructor<?> constructor: cl.getConstructors()) {
|
||||
if (constructor.getParameterCount() == args.length) {
|
||||
try {
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
BetterEnd.LOGGER.error(e.getMessage());
|
||||
if (BetterEnd.isDevEnvironment()) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Tag.Named<Item> getItemTag(String name) {
|
||||
ResourceLocation id = getID(name);
|
||||
Tag<Item> tag = ItemTags.getAllTags().getTag(id);
|
||||
return tag == null ? (Named<Item>) TagRegistry.item(id) : (Named<Item>) tag;
|
||||
}
|
||||
|
||||
public Tag.Named<Block> getBlockTag(String name) {
|
||||
ResourceLocation id = getID(name);
|
||||
Tag<Block> tag = BlockTags.getAllTags().getTag(id);
|
||||
return tag == null ? (Named<Block>) TagRegistry.block(id) : (Named<Block>) tag;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package ru.betterend.integration;
|
|||
|
||||
import net.minecraft.tags.Tag;
|
||||
import net.minecraft.world.item.Item;
|
||||
import ru.bclib.integration.ModIntegration;
|
||||
import ru.bclib.util.TagHelper;
|
||||
import ru.betterend.registry.EndItems;
|
||||
|
||||
|
@ -11,7 +12,7 @@ public class NourishIntegration extends ModIntegration {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void register() {
|
||||
public void init() {
|
||||
Tag.Named<Item> fats = getItemTag("fats");
|
||||
Tag.Named<Item> fruit = getItemTag("fruit");
|
||||
Tag.Named<Item> protein = getItemTag("protein");
|
||||
|
|
|
@ -7,27 +7,31 @@ import net.minecraft.data.BuiltinRegistries;
|
|||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.ai.behavior.WeightedList;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import ru.bclib.api.BiomeAPI;
|
||||
import ru.bclib.api.TagAPI;
|
||||
import ru.bclib.integration.ModIntegration;
|
||||
import ru.bclib.util.TagHelper;
|
||||
import ru.bclib.world.biomes.BCLBiome;
|
||||
import ru.betterend.integration.EndBiomeIntegration;
|
||||
import ru.betterend.integration.Integrations;
|
||||
import ru.betterend.integration.ModIntegration;
|
||||
import ru.betterend.integration.byg.biomes.BYGBiomes;
|
||||
import ru.betterend.integration.byg.features.BYGFeatures;
|
||||
import ru.betterend.registry.EndBiomes;
|
||||
|
||||
public class BYGIntegration extends ModIntegration {
|
||||
public class BYGIntegration extends ModIntegration implements EndBiomeIntegration {
|
||||
public BYGIntegration() {
|
||||
super("byg");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register() {
|
||||
TagHelper.addTags(Integrations.BYG.getBlock("ivis_phylium"), TagAPI.END_GROUND, TagAPI.GEN_TERRAIN);
|
||||
public void init() {
|
||||
Block block = Integrations.BYG.getBlock("ivis_phylium");
|
||||
if (block != null) {
|
||||
TagHelper.addTags(block, TagAPI.END_GROUND, TagAPI.GEN_TERRAIN);
|
||||
}
|
||||
BYGBlocks.register();
|
||||
BYGFeatures.register();
|
||||
BYGBiomes.register();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -74,4 +78,9 @@ public class BYGIntegration extends ModIntegration {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void biomeRegister() {
|
||||
BYGBiomes.register();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import net.minecraft.util.Mth;
|
|||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.chunk.ChunkBiomeContainer;
|
||||
import ru.betterend.BetterEnd;
|
||||
import ru.betterend.integration.Integrations;
|
||||
import ru.betterend.interfaces.IBiomeArray;
|
||||
|
||||
@Mixin(ChunkBiomeContainer.class)
|
||||
|
@ -39,7 +40,7 @@ public class ChunkBiomeContainerMixin implements IBiomeArray {
|
|||
int biomeZ = pos.getZ() >> 2;
|
||||
int index = be_getArrayIndex(biomeX, biomeY, biomeZ);
|
||||
|
||||
if (BetterEnd.hasHydrogen()) {
|
||||
if (Integrations.hasHydrogen()) {
|
||||
try {
|
||||
ChunkBiomeContainer self = (ChunkBiomeContainer) (Object) this;
|
||||
BitStorage storage = be_getHydrogenStorage(self);
|
||||
|
|
|
@ -11,23 +11,12 @@ import ru.bclib.api.TagAPI;
|
|||
import ru.bclib.recipes.GridRecipe;
|
||||
import ru.betterend.BetterEnd;
|
||||
import ru.betterend.config.Configs;
|
||||
import ru.betterend.item.GuideBookItem;
|
||||
import ru.betterend.registry.EndBlocks;
|
||||
import ru.betterend.registry.EndItems;
|
||||
|
||||
public class CraftingRecipes {
|
||||
|
||||
public static void register() {
|
||||
if (BetterEnd.hasGuideBook()) {
|
||||
GridRecipe.make(BetterEnd.MOD_ID, "guide_book", GuideBookItem.GUIDE_BOOK)
|
||||
.checkConfig(Configs.RECIPE_CONFIG)
|
||||
.setShape("D", "B", "C")
|
||||
.addMaterial('D', EndItems.ENDER_DUST)
|
||||
.addMaterial('B', Items.BOOK)
|
||||
.addMaterial('C', EndItems.CRYSTAL_SHARDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
GridRecipe.make(BetterEnd.MOD_ID, "ender_perl_to_block", EndBlocks.ENDER_BLOCK)
|
||||
.checkConfig(Configs.RECIPE_CONFIG)
|
||||
.setShape("OO", "OO")
|
||||
|
|
|
@ -23,13 +23,14 @@ import net.minecraft.world.level.biome.Biome;
|
|||
import net.minecraft.world.level.biome.Biome.BiomeCategory;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import ru.bclib.api.BiomeAPI;
|
||||
import ru.bclib.api.ModIntegrationAPI;
|
||||
import ru.bclib.util.JsonFactory;
|
||||
import ru.bclib.world.biomes.BCLBiome;
|
||||
import ru.bclib.world.generator.BiomeMap;
|
||||
import ru.bclib.world.generator.BiomePicker;
|
||||
import ru.betterend.BetterEnd;
|
||||
import ru.betterend.config.Configs;
|
||||
import ru.betterend.integration.Integrations;
|
||||
import ru.betterend.integration.EndBiomeIntegration;
|
||||
import ru.betterend.interfaces.IBiomeList;
|
||||
import ru.betterend.world.biome.EndBiome;
|
||||
import ru.betterend.world.biome.air.BiomeIceStarfield;
|
||||
|
@ -167,7 +168,11 @@ public class EndBiomes {
|
|||
}
|
||||
}
|
||||
});
|
||||
Integrations.addBiomes();
|
||||
ModIntegrationAPI.getIntegrations().forEach(integration -> {
|
||||
if (integration instanceof EndBiomeIntegration && integration.modIsInstalled()) {
|
||||
((EndBiomeIntegration) integration).addBiomes();
|
||||
}
|
||||
});
|
||||
Configs.BIOME_CONFIG.saveChanges();
|
||||
|
||||
rebuildPicker(LAND_BIOMES, biomeRegistry);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue