Power system re-write, recipe fixes.
Please note that this version breaks all wires placed in worlds before.
This commit is contained in:
parent
25c581e129
commit
d982cc4028
33 changed files with 507 additions and 859 deletions
241
src/main/java/techreborn/power/PowerNet.java
Normal file
241
src/main/java/techreborn/power/PowerNet.java
Normal file
|
@ -0,0 +1,241 @@
|
|||
package techreborn.power;
|
||||
|
||||
import cofh.api.energy.IEnergyReceiver;
|
||||
import com.google.common.collect.Lists;
|
||||
import ic2.api.energy.tile.IEnergySink;
|
||||
import net.darkhax.tesla.api.ITeslaConsumer;
|
||||
import net.darkhax.tesla.capability.TeslaCapabilities;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.fml.common.Loader;
|
||||
import reborncore.api.power.IEnergyInterfaceTile;
|
||||
import reborncore.common.RebornCoreConfig;
|
||||
import reborncore.common.powerSystem.TilePowerAcceptor;
|
||||
import reborncore.mcmultipart.block.TileMultipartContainer;
|
||||
import reborncore.mcmultipart.multipart.ISlottedPart;
|
||||
import reborncore.mcmultipart.multipart.PartSlot;
|
||||
import techreborn.parts.powerCables.CableMultipart;
|
||||
import techreborn.parts.powerCables.EnumCableType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class PowerNet {
|
||||
|
||||
|
||||
/**
|
||||
* Same as {@link #dispatchEnergyPacketRecursively(World, ArrayList, BlockPos, boolean, double)}
|
||||
* But not simulated and excluding source block
|
||||
* @param world the world to dispatch in
|
||||
* @param energyAmount energy units to dispatch
|
||||
* @return dispatched energy units
|
||||
*/
|
||||
public static double dispatchEnergyPacket(World world, BlockPos source, double energyAmount) {
|
||||
return dispatchEnergyPacketRecursively(world, Lists.newArrayList(source), source, false, energyAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches energy packet recursively through wires to all connected decives
|
||||
* @param world the world to dispatch in
|
||||
* @param excludes blocks to exclude from checking. as example, start block
|
||||
* @param blockPos block pos to start checking
|
||||
* @param maxEnergy energy units to dispatch
|
||||
* @param simulate true if dispatch is only simulated
|
||||
* @return dispatched energy units
|
||||
*/
|
||||
public static double dispatchEnergyPacketRecursively(World world, ArrayList<BlockPos> excludes, BlockPos blockPos, boolean simulate, final double maxEnergy) {
|
||||
double energyLeft = maxEnergy;
|
||||
Collection<EnumFacing> connections = Lists.newArrayList(EnumFacing.VALUES);
|
||||
|
||||
//Workaround for multipart cables
|
||||
CableMultipart selfCable = getWire(world, blockPos);
|
||||
|
||||
if(selfCable != null) {
|
||||
connections = selfCable.connectedSides.keySet();
|
||||
}
|
||||
|
||||
TileEntity tileEntity = world.getTileEntity(blockPos);
|
||||
if(tileEntity instanceof TilePowerAcceptor) {
|
||||
for(EnumFacing facing : EnumFacing.VALUES) {
|
||||
if(!((TilePowerAcceptor) tileEntity).canProvideEnergy(facing))
|
||||
connections.remove(facing);
|
||||
}
|
||||
}
|
||||
|
||||
for(EnumFacing facing : connections) {
|
||||
BlockPos offsetPos = blockPos.offset(facing);
|
||||
if(!excludes.contains(offsetPos)) {
|
||||
excludes.add(offsetPos);
|
||||
if(isWire(world, offsetPos)) {
|
||||
CableMultipart cable = getWire(world, offsetPos);
|
||||
EnumCableType cableType = cable.getCableType();
|
||||
double transferRate = cableType.transferRate;
|
||||
double maxPacket = transferRate > maxEnergy ? maxEnergy : transferRate;
|
||||
energyLeft -= dispatchEnergyPacketRecursively(world, excludes, offsetPos, simulate, maxPacket);
|
||||
if(energyLeft == 0) {
|
||||
if(selfCable != null)
|
||||
selfCable.lastEnergyPacket = maxEnergy * 1.3F;
|
||||
return maxEnergy;
|
||||
}
|
||||
} else {
|
||||
PowerNetReceiver receiver = getReceiver(world, facing.getOpposite(), offsetPos);
|
||||
if(receiver != null) {
|
||||
energyLeft -= receiver.receiveEnergy(energyLeft, simulate);
|
||||
if(energyLeft == 0) {
|
||||
if(selfCable != null)
|
||||
selfCable.lastEnergyPacket = maxEnergy * 1.3F;
|
||||
return maxEnergy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double energyDispatched = maxEnergy - energyLeft;
|
||||
if(selfCable != null)
|
||||
selfCable.lastEnergyPacket = energyDispatched * 1.3F;
|
||||
return energyDispatched;
|
||||
}
|
||||
|
||||
|
||||
//////////////////// Wire utilities ////////////////////
|
||||
|
||||
public static boolean isWire(World world, BlockPos blockPos) {
|
||||
return getWire(world, blockPos) != null;
|
||||
}
|
||||
|
||||
public static CableMultipart getWire(World world, BlockPos pos) {
|
||||
TileEntity tileEntity = world.getTileEntity(pos);
|
||||
if(tileEntity instanceof TileMultipartContainer) {
|
||||
TileMultipartContainer multipart = (TileMultipartContainer) tileEntity;
|
||||
ISlottedPart centerPart = multipart.getPartInSlot(PartSlot.CENTER);
|
||||
if(centerPart instanceof CableMultipart)
|
||||
return (CableMultipart) centerPart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//////////////////// Power Net Sources functionality ////////////////////
|
||||
//TODO
|
||||
|
||||
|
||||
|
||||
|
||||
//////////////////// Power Net Receivers functionality ////////////////////
|
||||
|
||||
public static PowerNetReceiver getReceiver(World world, EnumFacing side, BlockPos pos) {
|
||||
TileEntity tileEntity = world.getTileEntity(pos);
|
||||
if(Loader.isModLoaded("IC2") && RebornCoreConfig.getRebornPower().eu()) {
|
||||
PowerNetReceiver ic2receiver = getIC2Receiver(tileEntity, side);
|
||||
if(ic2receiver != null) return ic2receiver;
|
||||
}
|
||||
if(Loader.isModLoaded("Tesla") && RebornCoreConfig.getRebornPower().tesla()) {
|
||||
PowerNetReceiver teslaReceiver = getTeslaReceiver(tileEntity, side);
|
||||
if(teslaReceiver != null) return teslaReceiver;
|
||||
}
|
||||
if(RebornCoreConfig.getRebornPower().rf()) {
|
||||
PowerNetReceiver rfReceiver = getRFReceiver(tileEntity, side);
|
||||
if(rfReceiver != null) return rfReceiver;
|
||||
}
|
||||
if(RebornCoreConfig.getRebornPower().internal()) {
|
||||
PowerNetReceiver internalReceiver = getInternalReceiver(tileEntity, side);
|
||||
if(internalReceiver != null) return internalReceiver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PowerNetReceiver getInternalReceiver(TileEntity tileEntity, EnumFacing side) {
|
||||
if(tileEntity instanceof IEnergyInterfaceTile) {
|
||||
IEnergyInterfaceTile energyInterface = (IEnergyInterfaceTile) tileEntity;
|
||||
return new PowerNetReceiver(
|
||||
(energy, simulated) -> {
|
||||
if(energyInterface.canAcceptEnergy(side))
|
||||
return energyInterface.addEnergy(energy, simulated);
|
||||
return 0.0;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PowerNetReceiver getIC2Receiver(TileEntity tileEntity, EnumFacing side) {
|
||||
if(tileEntity instanceof IEnergySink) {
|
||||
IEnergySink acceptor = (IEnergySink) tileEntity;
|
||||
return new PowerNetReceiver(
|
||||
(energy, simulated) -> {
|
||||
if(simulated) {
|
||||
double max = acceptor.getDemandedEnergy();
|
||||
return max > energy ? energy : max;
|
||||
}
|
||||
return energy - acceptor.injectEnergy(side, energy, acceptor.getSinkTier());
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PowerNetReceiver getTeslaReceiver(TileEntity tileEntity, EnumFacing side) {
|
||||
if(tileEntity.hasCapability(TeslaCapabilities.CAPABILITY_CONSUMER, side)) {
|
||||
ITeslaConsumer consumer = tileEntity.getCapability(TeslaCapabilities.CAPABILITY_CONSUMER, side);
|
||||
return new PowerNetReceiver(
|
||||
(energy, simulated) -> {
|
||||
long teslaEnergy = eu2tesla(energy);
|
||||
return tesla2eu(consumer.givePower(teslaEnergy, simulated));
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PowerNetReceiver getRFReceiver(TileEntity tileEntity, EnumFacing side) {
|
||||
if(tileEntity instanceof IEnergyReceiver) {
|
||||
IEnergyReceiver receiver = (IEnergyReceiver) tileEntity;
|
||||
return new PowerNetReceiver(
|
||||
(energy, simulated) -> {
|
||||
int rfEnergy = eu2rf(energy);
|
||||
return rf2eu(receiver.receiveEnergy(side, rfEnergy, simulated));
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
//////////////////// Energy Conversion utils ////////////////////
|
||||
|
||||
public static double rf2eu(int rf) {
|
||||
return rf / 4F;
|
||||
}
|
||||
|
||||
public static int eu2rf(double eu) {
|
||||
return (int) Math.floor(eu * 4F);
|
||||
}
|
||||
|
||||
public static long eu2tesla(double eu) {
|
||||
return (long) Math.floor(eu / 3.75F);
|
||||
}
|
||||
|
||||
public static double tesla2eu(long tesla) {
|
||||
return tesla * 3.75F;
|
||||
}
|
||||
|
||||
|
||||
public static class PowerNetReceiver {
|
||||
|
||||
private final BiFunction<Double, Boolean, Double> receiveEnergy;
|
||||
|
||||
public PowerNetReceiver(BiFunction<Double, Boolean, Double> receiveEnergy) {
|
||||
this.receiveEnergy = receiveEnergy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends given amount fo EU to receiver
|
||||
* @param max max eu to receive
|
||||
* @return eu received
|
||||
*/
|
||||
public double receiveEnergy(double max, boolean simulated) {
|
||||
return receiveEnergy.apply(max, simulated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package techreborn.power;
|
||||
|
||||
import net.minecraftforge.fml.common.eventhandler.Event;
|
||||
|
||||
public class PowerTickEvent extends Event
|
||||
{
|
||||
public PowerTickEvent()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,358 +0,0 @@
|
|||
package techreborn.power;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.entity.item.EntityTNTPrimed;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import reborncore.api.power.IEnergyInterfaceTile;
|
||||
import techreborn.parts.powerCables.CableMultipart;
|
||||
import techreborn.parts.powerCables.EnumCableType;
|
||||
|
||||
public class TRPowerNet
|
||||
{
|
||||
public ArrayList<EnergyHandler> endpoints = new ArrayList();
|
||||
int tick = 0;
|
||||
EnumCableType cableType;
|
||||
private ArrayList<CableMultipart> cables = new ArrayList();
|
||||
private int energy = 0;
|
||||
|
||||
public TRPowerNet(EnumCableType cableType)
|
||||
{
|
||||
this.cableType = cableType;
|
||||
MinecraftForge.EVENT_BUS.register(this);
|
||||
}
|
||||
|
||||
public static void buildEndpoint(TRPowerNet net)
|
||||
{
|
||||
ArrayList<CableMultipart> parts = new ArrayList<>();
|
||||
ArrayList<CableMultipart> partsToMerge = new ArrayList<>();
|
||||
parts.addAll(net.cables);
|
||||
for (CableMultipart cable : parts)
|
||||
{
|
||||
for (EnumFacing facing : EnumFacing.VALUES)
|
||||
{
|
||||
BlockPos pos = cable.getPos().offset(facing);
|
||||
TileEntity tile = cable.getWorld().getTileEntity(pos);
|
||||
if (tile != null && tile instanceof IEnergyInterfaceTile)
|
||||
{
|
||||
IEnergyInterfaceTile eit = (IEnergyInterfaceTile) tile;
|
||||
net.addConnection(eit, facing);
|
||||
}
|
||||
CableMultipart cableMultipart = CableMultipart.getPartFromWorld(cable.getWorld(), pos, null);
|
||||
if (cableMultipart != null)
|
||||
{
|
||||
if (cableMultipart.getNetwork() != net)
|
||||
{
|
||||
partsToMerge.add(cableMultipart);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (CableMultipart cableMultipart : partsToMerge)
|
||||
{
|
||||
cableMultipart.mergeWith = net;
|
||||
}
|
||||
net.checkAndRemoveOldEndpoints();
|
||||
}
|
||||
|
||||
public int getIOLimit()
|
||||
{
|
||||
return cableType.transferRate;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void tick(PowerTickEvent evt)
|
||||
{
|
||||
if (tick < 20)
|
||||
{
|
||||
tick++;
|
||||
return;
|
||||
}
|
||||
if (tick % 80 == 0)
|
||||
{
|
||||
List<CableMultipart> oldCables = new ArrayList<>();
|
||||
for (CableMultipart cableMultipart : cables)
|
||||
{
|
||||
if (cableMultipart.getWorld() == null || cableMultipart.getPos() == null)
|
||||
{
|
||||
oldCables.add(cableMultipart);
|
||||
}
|
||||
CableMultipart mp = cableMultipart.getPartFromWorld(cableMultipart.getWorld(), cableMultipart.getPos(),
|
||||
null);
|
||||
if (mp == null)
|
||||
{
|
||||
oldCables.add(cableMultipart);
|
||||
}
|
||||
}
|
||||
cables.removeAll(oldCables);
|
||||
}
|
||||
if (!cables.isEmpty())
|
||||
{
|
||||
ArrayList<EnergyHandler> collectibles = new ArrayList();
|
||||
ArrayList<EnergyHandler> insertibles = new ArrayList();
|
||||
for (EnergyHandler ei : endpoints)
|
||||
{
|
||||
if (ei.isCollectible())
|
||||
{
|
||||
collectibles.add(ei);
|
||||
}
|
||||
if (ei.isInsertible())
|
||||
{
|
||||
insertibles.add(ei);
|
||||
}
|
||||
}
|
||||
|
||||
if (energy < cableType.transferRate * cables.size())
|
||||
{
|
||||
for (EnergyHandler handler : collectibles)
|
||||
{
|
||||
energy += handler.collectEnergy(cableType.transferRate);
|
||||
}
|
||||
}
|
||||
|
||||
for (EnergyHandler handler : insertibles)
|
||||
{
|
||||
energy -= handler.addEnergy(Math.min(energy, cableType.transferRate));
|
||||
}
|
||||
} else
|
||||
{
|
||||
MinecraftForge.EVENT_BUS.unregister(this);
|
||||
}
|
||||
tick++;
|
||||
}
|
||||
|
||||
public void addElement(CableMultipart te)
|
||||
{
|
||||
if (!cables.contains(te))
|
||||
{
|
||||
cables.add(te);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeElement(CableMultipart te)
|
||||
{
|
||||
cables.remove(te);
|
||||
this.rebuild();
|
||||
this.checkAndRemoveOldEndpoints();
|
||||
}
|
||||
|
||||
public void checkAndRemoveOldEndpoints()
|
||||
{
|
||||
List<EnergyHandler> deadHandlers = new ArrayList<>();
|
||||
for (EnergyHandler energyHandler : endpoints)
|
||||
{
|
||||
TileEntity tile = (TileEntity) energyHandler.tile;
|
||||
if (tile.getWorld().getTileEntity(tile.getPos()) == null)
|
||||
{
|
||||
deadHandlers.add(energyHandler);
|
||||
} else
|
||||
{
|
||||
boolean hasNet = false;
|
||||
for (EnumFacing facing : EnumFacing.VALUES)
|
||||
{
|
||||
BlockPos pos = tile.getPos().offset(facing);
|
||||
CableMultipart multipart = CableMultipart.getPartFromWorld(tile.getWorld(), pos, facing);
|
||||
if (multipart != null && multipart.getNetwork() == this)
|
||||
{
|
||||
hasNet = true;
|
||||
}
|
||||
}
|
||||
if (!hasNet)
|
||||
{
|
||||
deadHandlers.add(energyHandler);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
endpoints.removeAll(deadHandlers);
|
||||
}
|
||||
|
||||
public void rebuild()
|
||||
{
|
||||
for (int i = 0; i < cables.size(); i++)
|
||||
{
|
||||
CableMultipart te = cables.get(i);
|
||||
te.setNetwork(null);
|
||||
te.findAndJoinNetwork(te.getWorld(), te.getPos());
|
||||
}
|
||||
this.clear(true);
|
||||
MinecraftForge.EVENT_BUS.unregister(this);
|
||||
}
|
||||
|
||||
public int getEnergy()
|
||||
{
|
||||
return energy;
|
||||
}
|
||||
|
||||
public void setEnergy(int energy)
|
||||
{
|
||||
energy += energy;
|
||||
if (energy < 0)
|
||||
{
|
||||
energy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void addConnection(IEnergyInterfaceTile ih, EnumFacing dir)
|
||||
{
|
||||
if (ih instanceof CableMultipart)
|
||||
return;
|
||||
EnergyHandler has = this.getHandleFrom(ih);
|
||||
if (has == null)
|
||||
{
|
||||
endpoints.add(new EnergyHandler(ih, cableType, dir));
|
||||
} else
|
||||
{
|
||||
has.side = dir;
|
||||
}
|
||||
}
|
||||
|
||||
public void merge(TRPowerNet n)
|
||||
{
|
||||
if (n != this)
|
||||
{
|
||||
ArrayList<CableMultipart> li = new ArrayList();
|
||||
for (int i = 0; i < n.cables.size(); i++)
|
||||
{
|
||||
CableMultipart wire = n.cables.get(i);
|
||||
li.add(wire);
|
||||
}
|
||||
for (EnergyHandler ei : n.endpoints)
|
||||
{
|
||||
endpoints.add(ei);
|
||||
}
|
||||
n.clear(false);
|
||||
for (int i = 0; i < li.size(); i++)
|
||||
{
|
||||
CableMultipart wire = li.get(i);
|
||||
wire.setNetwork(this);
|
||||
}
|
||||
checkAndRemoveOldEndpoints();
|
||||
MinecraftForge.EVENT_BUS.unregister(n);
|
||||
}
|
||||
}
|
||||
|
||||
private EnergyHandler getHandleFrom(IEnergyInterfaceTile tile)
|
||||
{
|
||||
for (EnergyHandler ei : endpoints)
|
||||
{
|
||||
if (ei.tile == tile)
|
||||
return ei;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void clear(boolean clearTiles)
|
||||
{
|
||||
if (clearTiles)
|
||||
{
|
||||
for (int i = 0; i < cables.size(); i++)
|
||||
{
|
||||
cables.get(i).resetNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
cables.clear();
|
||||
endpoints.clear();
|
||||
energy = 0;
|
||||
|
||||
MinecraftForge.EVENT_BUS.unregister(this);
|
||||
}
|
||||
|
||||
public int addEnergy(int maxAdd, boolean simulate)
|
||||
{
|
||||
if (energy >= this.getIOLimit())
|
||||
return 0;
|
||||
maxAdd = Math.min(this.getIOLimit(), maxAdd);
|
||||
if (!simulate)
|
||||
energy += maxAdd;
|
||||
return maxAdd;
|
||||
}
|
||||
|
||||
private static class EnergyHandler
|
||||
{
|
||||
private final IEnergyInterfaceTile tile;
|
||||
private final EnumCableType type;
|
||||
private EnumFacing side;
|
||||
|
||||
private EnergyHandler(IEnergyInterfaceTile ih, EnumCableType type, EnumFacing dir)
|
||||
{
|
||||
tile = ih;
|
||||
this.type = type;
|
||||
this.side = dir;
|
||||
}
|
||||
|
||||
public boolean isInsertible()
|
||||
{
|
||||
return this.getTotalInsertible() > 0;
|
||||
}
|
||||
|
||||
public boolean isCollectible()
|
||||
{
|
||||
return this.getTotalCollectible() > 0;
|
||||
}
|
||||
|
||||
public boolean contains(IEnergyInterfaceTile tile)
|
||||
{
|
||||
return tile == this.tile;
|
||||
}
|
||||
|
||||
public int collectEnergy(int max)
|
||||
{
|
||||
int total = 0;
|
||||
if (tile.canProvideEnergy(side.getOpposite()))
|
||||
{
|
||||
int collect = (int) Math.min(max, Math.min(tile.getMaxOutput(), tile.getEnergy()));
|
||||
total = (int) tile.useEnergy(collect, false);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public int addEnergy(int max)
|
||||
{
|
||||
int total = 0;
|
||||
if (tile.canAcceptEnergy(side.getOpposite()) && max > 0)
|
||||
{
|
||||
if (type.tier.ordinal() > tile.getTier().ordinal() && max > tile.getMaxInput())
|
||||
{
|
||||
if (tile instanceof TileEntity)
|
||||
{
|
||||
((TileEntity) tile).getWorld().createExplosion(
|
||||
new EntityTNTPrimed(((TileEntity) tile).getWorld()),
|
||||
((TileEntity) tile).getPos().getX(), ((TileEntity) tile).getPos().getY(),
|
||||
((TileEntity) tile).getPos().getZ(), 2.5F, true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int add = max - total;
|
||||
total += tile.addEnergy(add, false);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public int getTotalCollectible()
|
||||
{
|
||||
if (tile.canProvideEnergy(side.getOpposite()) && tile.getEnergy() != 0)
|
||||
{
|
||||
return (int) Math.min(tile.getMaxOutput(), tile.getEnergy());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getTotalInsertible()
|
||||
{
|
||||
int total = 0;
|
||||
if (tile.canAcceptEnergy(side.getOpposite()) && tile.getMaxPower() - tile.getEnergy() != 0)
|
||||
{
|
||||
total += tile.addEnergy(type.transferRate, true);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue