Added ReflectionStorage (mostly version-independent)
(Started on 2019.08.29.) This means it supports 1.14.4 It automatically falls back to the previous storage if the newer version is not found
This commit is contained in:
parent
14bdf0ebe0
commit
6d5f42b2a5
3 changed files with 355 additions and 142 deletions
4
pom.xml
4
pom.xml
|
@ -159,8 +159,8 @@
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>2.3.2</version>
|
<version>2.3.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.6</source>
|
<source>1.8</source>
|
||||||
<target>1.6</target>
|
<target>1.8</target>
|
||||||
<annotationProcessors>
|
<annotationProcessors>
|
||||||
<!-- Needed to fetch DocComments from Source -->
|
<!-- Needed to fetch DocComments from Source -->
|
||||||
<annotationProcessor>de.jaschastarke.maven.AnnotationProcessor</annotationProcessor>
|
<annotationProcessor>de.jaschastarke.maven.AnnotationProcessor</annotationProcessor>
|
||||||
|
|
|
@ -1,140 +1,136 @@
|
||||||
package de.jaschastarke.minecraft.limitedcreative;
|
package de.jaschastarke.minecraft.limitedcreative;
|
||||||
|
|
||||||
import java.io.File;
|
import de.jaschastarke.bukkit.lib.CoreModule;
|
||||||
import java.util.Map;
|
import de.jaschastarke.minecraft.limitedcreative.inventories.*;
|
||||||
import java.util.WeakHashMap;
|
import de.jaschastarke.minecraft.limitedcreative.inventories.store.PlayerInventoryStorage;
|
||||||
|
import de.jaschastarke.minecraft.limitedcreative.inventories.store.ReflectionStorage;
|
||||||
import org.bukkit.GameMode;
|
import de.jaschastarke.modularize.IModule;
|
||||||
import org.bukkit.entity.Player;
|
import de.jaschastarke.modularize.ModuleEntry;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.GameMode;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
import de.jaschastarke.bukkit.lib.CoreModule;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.ArmoryConfig;
|
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.Inventory;
|
import java.io.File;
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.InventoryConfig;
|
import java.util.Map;
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.InventoryPermissions;
|
import java.util.WeakHashMap;
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.PlayerListener;
|
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.store.InvYamlStorage;
|
public class ModInventories extends CoreModule<LimitedCreative> {
|
||||||
import de.jaschastarke.minecraft.limitedcreative.inventories.store.PlayerInventoryStorage;
|
protected PlayerInventoryStorage storage;
|
||||||
import de.jaschastarke.modularize.IModule;
|
protected Map<Player, Inventory> inventories;
|
||||||
import de.jaschastarke.modularize.ModuleEntry;
|
protected InventoryConfig config;
|
||||||
|
protected ArmoryConfig armor_config;
|
||||||
public class ModInventories extends CoreModule<LimitedCreative> {
|
|
||||||
protected PlayerInventoryStorage storage;
|
public ModInventories(LimitedCreative plugin) {
|
||||||
protected Map<Player, Inventory> inventories;
|
super(plugin);
|
||||||
protected InventoryConfig config;
|
}
|
||||||
protected ArmoryConfig armor_config;
|
@Override
|
||||||
|
public String getName() {
|
||||||
public ModInventories(LimitedCreative plugin) {
|
return "Inventory";
|
||||||
super(plugin);
|
}
|
||||||
}
|
|
||||||
@Override
|
@SuppressWarnings("deprecation")
|
||||||
public String getName() {
|
@Override
|
||||||
return "Inventory";
|
public void initialize(ModuleEntry<IModule> entry) {
|
||||||
}
|
super.initialize(entry);
|
||||||
|
listeners.addListener(new PlayerListener(this));
|
||||||
@SuppressWarnings("deprecation")
|
config = plugin.getPluginConfig().registerSection(new InventoryConfig(this, entry));
|
||||||
@Override
|
armor_config = config.registerSection(new ArmoryConfig(this));
|
||||||
public void initialize(ModuleEntry<IModule> entry) {
|
|
||||||
super.initialize(entry);
|
if (Hooks.isAuthMePresent()) {
|
||||||
listeners.addListener(new PlayerListener(this));
|
addModule(new de.jaschastarke.minecraft.limitedcreative.inventories.AuthMeInventories(plugin, this));
|
||||||
config = plugin.getPluginConfig().registerSection(new InventoryConfig(this, entry));
|
}
|
||||||
armor_config = config.registerSection(new ArmoryConfig(this));
|
String incomp = Hooks.InventoryIncompatible.test();
|
||||||
|
if (config.getEnabled() && incomp != null) {
|
||||||
if (Hooks.isAuthMePresent()) {
|
getLog().warn(plugin.getLocale().trans("inventory.warning.conflict", incomp, this.getName()));
|
||||||
addModule(new de.jaschastarke.minecraft.limitedcreative.inventories.AuthMeInventories(plugin, this));
|
entry.deactivateUsage();
|
||||||
}
|
}
|
||||||
String incomp = Hooks.InventoryIncompatible.test();
|
}
|
||||||
if (config.getEnabled() && incomp != null) {
|
@Override
|
||||||
getLog().warn(plugin.getLocale().trans("inventory.warning.conflict", incomp, this.getName()));
|
public void onEnable() {
|
||||||
entry.deactivateUsage();
|
String incomp = Hooks.InventoryIncompatible.test();
|
||||||
}
|
if (incomp != null) {
|
||||||
}
|
throw new IllegalAccessError(plugin.getLocale().trans("inventory.warning.conflict", incomp, this.getName()));
|
||||||
@Override
|
}
|
||||||
public void onEnable() {
|
super.onEnable();
|
||||||
String incomp = Hooks.InventoryIncompatible.test();
|
//storage = new InvYamlStorage(this, new File(plugin.getDataFolder(), config.getFolder()));
|
||||||
if (incomp != null) {
|
storage = new ReflectionStorage(this, new File(plugin.getDataFolder(), config.getFolder()));
|
||||||
throw new IllegalAccessError(plugin.getLocale().trans("inventory.warning.conflict", incomp, this.getName()));
|
inventories = new WeakHashMap<Player, Inventory>();
|
||||||
}
|
getLog().info(plugin.getLocale().trans("basic.loaded.module"));
|
||||||
super.onEnable();
|
}
|
||||||
storage = new InvYamlStorage(this, new File(plugin.getDataFolder(), config.getFolder()));
|
public InventoryConfig getConfig() {
|
||||||
inventories = new WeakHashMap<Player, Inventory>();
|
return config;
|
||||||
getLog().info(plugin.getLocale().trans("basic.loaded.module"));
|
}
|
||||||
}
|
public ArmoryConfig getArmorConfig() {
|
||||||
public InventoryConfig getConfig() {
|
return armor_config;
|
||||||
return config;
|
}
|
||||||
}
|
|
||||||
public ArmoryConfig getArmorConfig() {
|
public PlayerInventoryStorage getStorage() {
|
||||||
return armor_config;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerInventoryStorage getStorage() {
|
public Inventory getInventory(Player player) {
|
||||||
return storage;
|
if (inventories.containsKey(player)) {
|
||||||
}
|
return inventories.get(player);
|
||||||
|
} else {
|
||||||
public Inventory getInventory(Player player) {
|
Inventory inv = new Inventory(storage, player);
|
||||||
if (inventories.containsKey(player)) {
|
inventories.put(player, inv);
|
||||||
return inventories.get(player);
|
return inv;
|
||||||
} else {
|
}
|
||||||
Inventory inv = new Inventory(storage, player);
|
}
|
||||||
inventories.put(player, inv);
|
|
||||||
return inv;
|
public void onSetGameMode(Player player, GameMode gm) {
|
||||||
}
|
if (plugin.getPermManager().hasPermission(player, InventoryPermissions.KEEP_INVENTORY))
|
||||||
}
|
return;
|
||||||
|
player.closeInventory();
|
||||||
public void onSetGameMode(Player player, GameMode gm) {
|
|
||||||
if (plugin.getPermManager().hasPermission(player, InventoryPermissions.KEEP_INVENTORY))
|
GameMode cgm = player.getGameMode();
|
||||||
return;
|
if (gm == GameMode.ADVENTURE && !config.getSeparateAdventure())
|
||||||
player.closeInventory();
|
gm = GameMode.SURVIVAL;
|
||||||
|
else if (gm == GameMode.SPECTATOR)
|
||||||
GameMode cgm = player.getGameMode();
|
gm = GameMode.CREATIVE;
|
||||||
if (gm == GameMode.ADVENTURE && !config.getSeparateAdventure())
|
if (cgm == GameMode.ADVENTURE && !config.getSeparateAdventure())
|
||||||
gm = GameMode.SURVIVAL;
|
cgm = GameMode.SURVIVAL;
|
||||||
else if (gm == GameMode.SPECTATOR)
|
else if (cgm == GameMode.SPECTATOR)
|
||||||
gm = GameMode.CREATIVE;
|
cgm = GameMode.CREATIVE;
|
||||||
if (cgm == GameMode.ADVENTURE && !config.getSeparateAdventure())
|
|
||||||
cgm = GameMode.SURVIVAL;
|
if (gm != cgm) {
|
||||||
else if (cgm == GameMode.SPECTATOR)
|
if (gm != GameMode.CREATIVE || config.getStoreCreative()) {
|
||||||
cgm = GameMode.CREATIVE;
|
getInventory(player).save(cgm);
|
||||||
|
}
|
||||||
if (gm != cgm) {
|
if (gm == GameMode.CREATIVE) {
|
||||||
if (gm != GameMode.CREATIVE || config.getStoreCreative()) {
|
if (config.getStoreCreative() && getInventory(player).isStored(GameMode.CREATIVE)) {
|
||||||
getInventory(player).save(cgm);
|
getInventory(player).load(GameMode.CREATIVE);
|
||||||
}
|
} else {
|
||||||
if (gm == GameMode.CREATIVE) {
|
getInventory(player).clear();
|
||||||
if (config.getStoreCreative() && getInventory(player).isStored(GameMode.CREATIVE)) {
|
}
|
||||||
getInventory(player).load(GameMode.CREATIVE);
|
setCreativeArmor(player);
|
||||||
} else {
|
} else if (gm == GameMode.SURVIVAL) {
|
||||||
getInventory(player).clear();
|
if (getInventory(player).isStored(GameMode.SURVIVAL))
|
||||||
}
|
getInventory(player).load(GameMode.SURVIVAL);
|
||||||
setCreativeArmor(player);
|
} else if (gm == GameMode.ADVENTURE) {
|
||||||
} else if (gm == GameMode.SURVIVAL) {
|
if (getInventory(player).isStored(GameMode.ADVENTURE))
|
||||||
if (getInventory(player).isStored(GameMode.SURVIVAL))
|
getInventory(player).load(GameMode.ADVENTURE);
|
||||||
getInventory(player).load(GameMode.SURVIVAL);
|
else
|
||||||
} else if (gm == GameMode.ADVENTURE) {
|
getInventory(player).clear();
|
||||||
if (getInventory(player).isStored(GameMode.ADVENTURE))
|
}
|
||||||
getInventory(player).load(GameMode.ADVENTURE);
|
}
|
||||||
else
|
}
|
||||||
getInventory(player).clear();
|
|
||||||
}
|
public void setCreativeArmor(Player player) {
|
||||||
}
|
if (!getPlugin().getPermManager().hasPermission(player, InventoryPermissions.BYPASS_CREATIVE_ARMOR)) {
|
||||||
}
|
Map<String, ItemStack> armor = armor_config.getCreativeArmor();
|
||||||
|
if (armor != null) {
|
||||||
public void setCreativeArmor(Player player) {
|
ItemStack[] is = new ItemStack[4];
|
||||||
if (!getPlugin().getPermManager().hasPermission(player, InventoryPermissions.BYPASS_CREATIVE_ARMOR)) {
|
if (armor.containsKey("feet"))
|
||||||
Map<String, ItemStack> armor = armor_config.getCreativeArmor();
|
is[0] = armor.get("feet");
|
||||||
if (armor != null) {
|
if (armor.containsKey("legs"))
|
||||||
ItemStack[] is = new ItemStack[4];
|
is[1] = armor.get("legs");
|
||||||
if (armor.containsKey("feet"))
|
if (armor.containsKey("chest"))
|
||||||
is[0] = armor.get("feet");
|
is[2] = armor.get("chest");
|
||||||
if (armor.containsKey("legs"))
|
if (armor.containsKey("head"))
|
||||||
is[1] = armor.get("legs");
|
is[3] = armor.get("head");
|
||||||
if (armor.containsKey("chest"))
|
player.getInventory().setArmorContents(is);
|
||||||
is[2] = armor.get("chest");
|
}
|
||||||
if (armor.containsKey("head"))
|
}
|
||||||
is[3] = armor.get("head");
|
}
|
||||||
player.getInventory().setArmorContents(is);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
package de.jaschastarke.minecraft.limitedcreative.inventories.store;
|
||||||
|
|
||||||
|
import de.jaschastarke.bukkit.lib.CoreModule;
|
||||||
|
import de.jaschastarke.bukkit.lib.ModuleLogger;
|
||||||
|
import de.jaschastarke.minecraft.limitedcreative.inventories.Inventory;
|
||||||
|
import org.bukkit.configuration.Configuration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class ReflectionStorage extends PlayerInventoryStorage {
|
||||||
|
private CoreModule<?> mod;
|
||||||
|
private File dir;
|
||||||
|
private String nms;
|
||||||
|
private InvYamlStorage yamlStorage;
|
||||||
|
|
||||||
|
public ReflectionStorage(CoreModule<?> mod, File file) {
|
||||||
|
this.mod = mod;
|
||||||
|
dir = file;
|
||||||
|
yamlStorage = new InvYamlStorage(mod, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleLogger getLog() {
|
||||||
|
return mod.getLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getFile(UUID uuid) {
|
||||||
|
return new File(dir, uuid.toString() + "_ref.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getInventory(Player player) throws Exception {
|
||||||
|
org.bukkit.inventory.Inventory inv = player.getInventory();
|
||||||
|
if (getInventory == null)
|
||||||
|
getInventory = inv.getClass().getMethod("getInventory");
|
||||||
|
Object handle = getInventory.invoke(inv);
|
||||||
|
if (nms == null)
|
||||||
|
nms = handle.getClass().getPackage().getName();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void store(Inventory pinv, Inventory.Target target) {
|
||||||
|
try {
|
||||||
|
File f = getFile(pinv.getPlayer().getUniqueId());
|
||||||
|
YamlConfiguration config = YamlConfiguration.loadConfiguration(f);
|
||||||
|
config.set(target.name(), serialize(getInventory(pinv.getPlayer())));
|
||||||
|
config.save(f);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getInventory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load(Inventory pinv, Inventory.Target target) {
|
||||||
|
Player player = pinv.getPlayer();
|
||||||
|
try {
|
||||||
|
File f = getFile(player.getUniqueId());
|
||||||
|
if (!f.exists()) { //If not found use the older file(s)
|
||||||
|
yamlStorage.load(pinv, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//String content = new String(Files.readAllBytes(f.toPath()));
|
||||||
|
Configuration config = YamlConfiguration.loadConfiguration(f);
|
||||||
|
String content = config.getString(target.name());
|
||||||
|
if (content == null) {
|
||||||
|
yamlStorage.load(pinv, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFromSerialized(getInventory(player), content);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(Inventory pinv, Inventory.Target target) {
|
||||||
|
File f = getFile(pinv.getPlayer().getUniqueId());
|
||||||
|
if (!f.exists()) return;
|
||||||
|
Configuration config = YamlConfiguration.loadConfiguration(f);
|
||||||
|
config.set(target.name(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(Inventory pinv, Inventory.Target target) {
|
||||||
|
File f = getFile(pinv.getPlayer().getUniqueId());
|
||||||
|
if (!f.exists()) return yamlStorage.contains(pinv, target);
|
||||||
|
Configuration config = YamlConfiguration.loadConfiguration(f);
|
||||||
|
return config.contains(target.name()) || yamlStorage.contains(pinv, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Based on iie's per-world inventory
|
||||||
|
//https://github.com/TBMCPlugins/iiePerWorldInventory/blob/master/src/buttondevteam/perworld/serializers/inventory.java
|
||||||
|
|
||||||
|
private Method save;
|
||||||
|
private Class<?> nbtcl;
|
||||||
|
private Method nbtcsta;
|
||||||
|
private Class<?> nbtcstcl;
|
||||||
|
|
||||||
|
//SERIALIZE ITEMSTACK
|
||||||
|
private String serializeItemStack(Object itemStack) throws Exception {
|
||||||
|
if (nbtcl == null)
|
||||||
|
nbtcl = Class.forName(nms + ".NBTTagCompound");
|
||||||
|
if (save == null)
|
||||||
|
save = itemStack.getClass().getMethod("save", nbtcl);
|
||||||
|
if (nbtcstcl == null)
|
||||||
|
nbtcstcl = Class.forName(nms + ".NBTCompressedStreamTools");
|
||||||
|
if (nbtcsta == null)
|
||||||
|
nbtcsta = nbtcstcl.getMethod("a", nbtcl, OutputStream.class);
|
||||||
|
Object tag = save.invoke(itemStack, nbtcl.newInstance());
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
nbtcsta.invoke(null, tag, outputStream);
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Method nbtcstaa;
|
||||||
|
private Function<Object, Object> createStack;
|
||||||
|
|
||||||
|
//DESERIALIZE ITEMSTACK
|
||||||
|
private Object deserializeItemStack(String itemStackString) throws Exception {
|
||||||
|
if (nbtcstcl == null)
|
||||||
|
nbtcstcl = Class.forName(nms + ".NBTCompressedStreamTools");
|
||||||
|
if (nbtcstaa == null)
|
||||||
|
nbtcstaa = nbtcstcl.getMethod("a", InputStream.class);
|
||||||
|
if (nbtcl == null)
|
||||||
|
nbtcl = Class.forName(nms + ".NBTTagCompound");
|
||||||
|
try {
|
||||||
|
if (createStack == null) {
|
||||||
|
final Method a = iscl.getMethod("a", nbtcl);
|
||||||
|
createStack = nbt -> {
|
||||||
|
try {
|
||||||
|
return a.invoke(null, nbt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (NoSuchMethodException ex) { //It can only get here inside the if
|
||||||
|
final Constructor<?> constructor = iscl.getConstructor(nbtcl);
|
||||||
|
createStack = nbt -> {
|
||||||
|
try {
|
||||||
|
return constructor.newInstance(nbt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(itemStackString));
|
||||||
|
|
||||||
|
Object nbtTagCompound = nbtcstaa.invoke(null, inputStream);
|
||||||
|
return createStack.apply(nbtTagCompound);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getSize;
|
||||||
|
private Method getItem;
|
||||||
|
|
||||||
|
//SERIALIZE INVENTORY
|
||||||
|
private String serialize(Object invInventory) throws Exception {
|
||||||
|
if (getSize == null)
|
||||||
|
getSize = invInventory.getClass().getMethod("getSize");
|
||||||
|
if (getItem == null)
|
||||||
|
getItem = invInventory.getClass().getMethod("getItem", int.class);
|
||||||
|
return IntStream.range(0, (int) getSize.invoke(invInventory))
|
||||||
|
.mapToObj(s -> {
|
||||||
|
try {
|
||||||
|
//nms ItemStack
|
||||||
|
Object i = getItem.invoke(invInventory, s);
|
||||||
|
return Objects.isNull(i) ? null : s + "#" + serializeItemStack(i);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.joining(";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method clear;
|
||||||
|
private Method setItem;
|
||||||
|
private Class<?> iscl;
|
||||||
|
|
||||||
|
//SET INVENTORY FROM SERIALIZED
|
||||||
|
private void setFromSerialized(Object invInventory, String invString) throws Exception {
|
||||||
|
if (clear == null)
|
||||||
|
clear = invInventory.getClass().getMethod("clear");
|
||||||
|
if (iscl == null)
|
||||||
|
iscl = Class.forName(nms + ".ItemStack");
|
||||||
|
if (setItem == null)
|
||||||
|
setItem = invInventory.getClass().getMethod("setItem", int.class, iscl);
|
||||||
|
clear.invoke(invInventory); //clear inventory
|
||||||
|
if (invString != null && !invString.isEmpty())
|
||||||
|
Arrays.asList(invString.split(";"))
|
||||||
|
.parallelStream()
|
||||||
|
.forEach(s -> {
|
||||||
|
String[] e = s.split("#");
|
||||||
|
try {
|
||||||
|
setItem.invoke(invInventory, Integer.parseInt(e[0]), deserializeItemStack(e[1]));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue