mirror of
https://github.com/alexcrea/CustomAnvil.git
synced 2026-06-23 16:16:17 +02:00
new treat anvil event
This commit is contained in:
parent
d679cd73f9
commit
2efb6e55e2
10 changed files with 244 additions and 31 deletions
|
|
@ -17,7 +17,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
* Most of the time you would likely need {@link CAPreAnvilBypassEvent} or {@link CAEarlyPreAnvilBypassEvent}
|
||||
* for this event to be useful.
|
||||
* <p>
|
||||
* There is also {@link CATreatAnvilResultEvent} that may be better for some use case.
|
||||
* There is also {@link CATreatAnvilResult2Event} that may be better for some use case.
|
||||
*/
|
||||
public class CAClickResultBypassEvent extends Event implements Cancellable {
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
* <p>
|
||||
* You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
|
||||
* <p>
|
||||
* It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResultEvent}
|
||||
* It is also recommended that you read about {@link CAPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
|
||||
* as your use case may be more prone to use theses.
|
||||
*/
|
||||
public class CAEarlyPreAnvilBypassEvent extends Event implements Cancellable {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
* <p>
|
||||
* You should also use {@link CAClickResultBypassEvent} if you want to use this event for something useful.
|
||||
* <p>
|
||||
* It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResultEvent}
|
||||
* It is also recommended that you read about {@link CAEarlyPreAnvilBypassEvent} and {@link CATreatAnvilResult2Event}
|
||||
* as your use case may be more prone to use theses.
|
||||
*/
|
||||
public class CAPreAnvilBypassEvent extends Event implements Cancellable {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
package xyz.alexcrea.cuanvil.api.event.listener;
|
||||
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.alexcrea.cuanvil.anvil.AnvilUseType;
|
||||
import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil;
|
||||
|
||||
/**
|
||||
* Called after custom anvil processed the click on the result on the anvil inventory.
|
||||
* This event should be used to modify the result of an anvil use.
|
||||
* <p>
|
||||
* You may also want to check {@link CAClickResultBypassEvent},
|
||||
* {@link CAPreAnvilBypassEvent}
|
||||
* and {@link CAEarlyPreAnvilBypassEvent} for your use case
|
||||
* <p>
|
||||
* A null result will cancel this event
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class CATreatAnvilResult2Event extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private final InventoryView view;
|
||||
|
||||
private final AnvilUseType useType;
|
||||
|
||||
@Nullable
|
||||
private final ItemStack left;
|
||||
@Nullable
|
||||
private final ItemStack right;
|
||||
|
||||
@Nullable
|
||||
private ItemStack result;
|
||||
|
||||
private final AnvilXpUtil.AnvilCost cost;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public CATreatAnvilResult2Event(
|
||||
@NotNull InventoryView view,
|
||||
Inventory inv,
|
||||
AnvilUseType useType,
|
||||
@Nullable ItemStack result,
|
||||
AnvilXpUtil.AnvilCost cost) {
|
||||
this.view = view;
|
||||
this.useType = useType;
|
||||
|
||||
this.left = inv.getItem(0); // TODO use view here
|
||||
this.right = inv.getItem(1);
|
||||
this.result = result;
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bukkit inventory view.
|
||||
* <p>
|
||||
* Temporarily marked as internal as it will get changed to anvil view on legacy removal
|
||||
* so signature will change
|
||||
*
|
||||
* @return The inventory view of this event.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public @NotNull InventoryView getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the type of use source of the result.
|
||||
*
|
||||
* @return The craft use type.
|
||||
*/
|
||||
public AnvilUseType getUseType() {
|
||||
return useType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the left item of the anvil use
|
||||
*
|
||||
* @return the left item
|
||||
*/
|
||||
public @Nullable ItemStack getLeftItem() {
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the right item of the anvil use
|
||||
*
|
||||
* @return the right item
|
||||
*/
|
||||
public @Nullable ItemStack getRightItem() {
|
||||
return right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current result
|
||||
* <p>
|
||||
* note that it will not be null unless another listener previously set it to null.
|
||||
*
|
||||
* @return The current result.
|
||||
*/
|
||||
public @Nullable ItemStack getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current result
|
||||
* <p>
|
||||
* note that a null result will cancel this anvil use.
|
||||
*
|
||||
* @param result The new result
|
||||
*/
|
||||
public void setResult(@Nullable ItemStack result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the level cost displayed on the anvil.
|
||||
* <h3>Important note:</h3>
|
||||
* the final price are re calculated on click for the following use case:
|
||||
* <ul>
|
||||
* <li>Custom craft</li>
|
||||
* <li>Unit repair</li>
|
||||
* <li>Lore edit</li>
|
||||
* </ul>
|
||||
* This value will be used as final price for:
|
||||
* <li>Item merge</li>
|
||||
* <li>Item rename</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return The current cost.
|
||||
* @deprecated use #{@link #getCost()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.17.0")
|
||||
public int getLevelCost() {
|
||||
return cost.asXpCost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the level cost displayed on the anvil.
|
||||
* <h3>Important note:</h3>
|
||||
* the final price are re calculated on click for the following use case:
|
||||
* <ul>
|
||||
* <li>Custom craft</li>
|
||||
* <li>Unit repair</li>
|
||||
* <li>Lore edit</li>
|
||||
* </ul>
|
||||
* This value will be used as final price for:
|
||||
* <li>Item merge</li>
|
||||
* <li>Item rename</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param levelCost The new cost.
|
||||
* @deprecated use #{@link #getCost()} and set value on this instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.17.0")
|
||||
public void setLevelCost(int levelCost) {
|
||||
cost.setGeneric(levelCost - cost.getGeneric() - cost.asXpCost());
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow access to the current cost of the event
|
||||
* Note that modifying this object will change the event resulting cost
|
||||
*
|
||||
* <h3>Important note:</h3>
|
||||
* the final price are re calculated on click for the following use case:
|
||||
* <ul>
|
||||
* <li>Custom craft</li>
|
||||
* <li>Unit repair</li>
|
||||
* <li>Lore edit</li>
|
||||
* </ul>
|
||||
* This value will be used as final price for:
|
||||
* <li>Item merge</li>
|
||||
* <li>Item rename</li>
|
||||
*
|
||||
* @return the current anvil cost
|
||||
*/
|
||||
public AnvilXpUtil.AnvilCost getCost() {
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,8 @@ import xyz.alexcrea.cuanvil.util.anvil.AnvilXpUtil.AnvilCost;
|
|||
* <p>
|
||||
* A null result will cancel this pre anvil event
|
||||
*
|
||||
* @deprecated Prepare anvil Event should not be provided as it can be called on result and therefor not have prepare anvil event
|
||||
* TODO a replacement is necessary but not yet made
|
||||
* @deprecated Prepare anvil Event cannot be provided as it can be called on result and therefore not have prepared anvil event
|
||||
* use {@link CATreatAnvilResult2Event} instead
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Deprecated(forRemoval = true, since = "1.17.0")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import org.bukkit.Material
|
|||
import org.bukkit.entity.HumanEntity
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.AnvilInventory
|
||||
import org.bukkit.inventory.InventoryView
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.ItemMeta
|
||||
import org.bukkit.persistence.PersistentDataType
|
||||
|
|
@ -99,6 +100,7 @@ object AnvilMergeLogic {
|
|||
}
|
||||
|
||||
fun doRenaming(
|
||||
view: InventoryView, //TODO use anvil view
|
||||
inventory: AnvilInventory,
|
||||
player: Player, first: ItemStack
|
||||
): AnvilResult {
|
||||
|
|
@ -113,7 +115,7 @@ object AnvilMergeLogic {
|
|||
}
|
||||
|
||||
cost.workPenalty = AnvilXpUtil.calculatePenalty(first, null, resultItem, AnvilUseType.RENAME_ONLY)
|
||||
val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.RENAME_ONLY, cost)
|
||||
val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.RENAME_ONLY, cost)
|
||||
|
||||
return AnvilResult(result, cost)
|
||||
}
|
||||
|
|
@ -183,6 +185,7 @@ object AnvilMergeLogic {
|
|||
}
|
||||
|
||||
fun doMerge(
|
||||
view: InventoryView, //TODO use anvil view instead
|
||||
inventory: AnvilInventory,
|
||||
player: Player,
|
||||
first: ItemStack, second: ItemStack
|
||||
|
|
@ -217,7 +220,7 @@ object AnvilMergeLogic {
|
|||
// Calculate rename cost
|
||||
cost.rename = handleRename(resultItem, inventory, player)
|
||||
|
||||
val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.MERGE, cost)
|
||||
val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.MERGE, cost)
|
||||
|
||||
return AnvilResult(result, cost)
|
||||
}
|
||||
|
|
@ -236,6 +239,8 @@ object AnvilMergeLogic {
|
|||
|
||||
// return true if a custom recipe exist with these ingredients
|
||||
fun testCustomRecipe(
|
||||
view: InventoryView, //TODO use anvil view instead
|
||||
inventory: AnvilInventory,
|
||||
player: Player,
|
||||
first: ItemStack, second: ItemStack?
|
||||
): CustomCraftResult {
|
||||
|
|
@ -256,21 +261,23 @@ object AnvilMergeLogic {
|
|||
cost.recipe = if (recipe.removeExactLinearXp) AnvilXpUtil.calculateMinimumLevelForXp(xpCost)
|
||||
else AnvilXpUtil.calculateLevelForXp(xpCost)
|
||||
|
||||
val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.CUSTOM_CRAFT, cost)
|
||||
val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.CUSTOM_CRAFT, cost)
|
||||
return CustomCraftResult(result, cost, amount, recipe)
|
||||
}
|
||||
|
||||
fun testUnitRepair(
|
||||
view: InventoryView, //TODO use anvil view
|
||||
inventory: AnvilInventory,
|
||||
player: Player,
|
||||
first: ItemStack, second: ItemStack
|
||||
): UnitRepairResult {
|
||||
val unitRepairAmount = first.getRepair(second) ?: return UnitRepairResult.EMPTY
|
||||
|
||||
return testUnitRepair(inventory, player, first, second, unitRepairAmount)
|
||||
return testUnitRepair(view, inventory, player, first, second, unitRepairAmount)
|
||||
}
|
||||
|
||||
fun testUnitRepair(
|
||||
view: InventoryView, //TODO use anvil view instead
|
||||
inventory: AnvilInventory,
|
||||
player: Player,
|
||||
first: ItemStack, second: ItemStack,
|
||||
|
|
@ -293,7 +300,7 @@ object AnvilMergeLogic {
|
|||
return UnitRepairResult.EMPTY
|
||||
}
|
||||
|
||||
val result = DependencyManager.tryTreatAnvilResult(resultItem, AnvilUseType.UNIT_REPAIR, cost)
|
||||
val result = DependencyManager.tryTreatAnvilResult(view, inventory, player, resultItem, AnvilUseType.UNIT_REPAIR, cost)
|
||||
return UnitRepairResult(result, cost, repairAmount)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ import org.bukkit.event.inventory.InventoryClickEvent
|
|||
import org.bukkit.event.inventory.PrepareAnvilEvent
|
||||
import org.bukkit.inventory.AnvilInventory
|
||||
import org.bukkit.inventory.Inventory
|
||||
import org.bukkit.inventory.InventoryView
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import xyz.alexcrea.cuanvil.anvil.AnvilUseType
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CAClickResultBypassEvent
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CAEarlyPreAnvilBypassEvent
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CAPreAnvilBypassEvent
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event
|
||||
import xyz.alexcrea.cuanvil.config.ConfigHolder
|
||||
import xyz.alexcrea.cuanvil.dependency.datapack.DataPackDependency
|
||||
import xyz.alexcrea.cuanvil.dependency.gui.GenericExternGuiTester
|
||||
|
|
@ -234,23 +235,24 @@ object DependencyManager {
|
|||
|
||||
// Return null if there was an issue
|
||||
fun tryTreatAnvilResult(
|
||||
view: InventoryView,
|
||||
inventory: Inventory, // TODO REMOVE, use view instead on legacy removal
|
||||
player: HumanEntity,
|
||||
result: ItemStack,
|
||||
useType: AnvilUseType,
|
||||
cost: AnvilXpUtil.AnvilCost
|
||||
): ItemStack? {
|
||||
//TODO
|
||||
/*val treatEvent = CATreatAnvilResultEvent(event, useType, result, cost)
|
||||
val treatEvent = CATreatAnvilResult2Event(view, inventory, useType, result, cost)
|
||||
try {
|
||||
unsafeTryTreatAnvilResult(treatEvent)
|
||||
return treatEvent.result
|
||||
} catch (e: Exception) {
|
||||
logExceptionAndClear(event.view.player, event.inventory, e)
|
||||
logExceptionAndClear(player, inventory, e)
|
||||
return null
|
||||
}*/
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResultEvent) {
|
||||
private fun unsafeTryTreatAnvilResult(event: CATreatAnvilResult2Event) {
|
||||
Bukkit.getPluginManager().callEvent(event)
|
||||
|
||||
excellentEnchantsCompatibility?.treatAnvilResult(event)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import org.bukkit.event.inventory.PrepareAnvilEvent
|
|||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.plugin.RegisteredListener
|
||||
import xyz.alexcrea.cuanvil.api.EnchantmentApi
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResultEvent
|
||||
import xyz.alexcrea.cuanvil.api.event.listener.CATreatAnvilResult2Event
|
||||
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEPreV5Enchantment
|
||||
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5Enchantment
|
||||
import xyz.alexcrea.cuanvil.enchant.wrapped.CAEEV5_4Enchantment
|
||||
|
|
@ -218,14 +218,17 @@ class ExcellentEnchantsDependency {
|
|||
return handleRechargeMethod.invoke(this.usedAnvilListener, event, first, second) as Boolean
|
||||
}
|
||||
|
||||
fun treatAnvilResult(event: CATreatAnvilResultEvent) {
|
||||
fun treatAnvilResult(event: CATreatAnvilResult2Event) {
|
||||
val result = event.result
|
||||
if (result == null) return
|
||||
|
||||
val first: ItemStack = treatInput(event.event.inventory.getItem(0))
|
||||
val second: ItemStack = treatInput(event.event.inventory.getItem(1))
|
||||
val first: ItemStack = treatInput(event.leftItem)
|
||||
val second: ItemStack = treatInput(event.rightItem)
|
||||
|
||||
handleCombineMethod.invoke(this.usedAnvilListener, event.event, first, second, result)
|
||||
val fakeEvent = PrepareAnvilEvent(event.view, result)
|
||||
handleCombineMethod.invoke(this.usedAnvilListener, fakeEvent, first, second, result)
|
||||
|
||||
event.result = fakeEvent.result
|
||||
}
|
||||
|
||||
fun testAnvilResult(event: InventoryClickEvent): Any {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class AnvilResultListener : Listener {
|
|||
fun anvilExtractionCheck(event: InventoryClickEvent) {
|
||||
val player = event.whoClicked as? Player ?: return
|
||||
val inventory = event.inventory as? AnvilInventory ?: return
|
||||
val view = event.view
|
||||
|
||||
if (event.rawSlot != ANVIL_OUTPUT_SLOT) {
|
||||
return
|
||||
|
|
@ -74,7 +75,7 @@ class AnvilResultListener : Listener {
|
|||
}
|
||||
|
||||
// Test custom recipe
|
||||
val customRecipeResult = AnvilMergeLogic.testCustomRecipe(player, leftItem, rightItem)
|
||||
val customRecipeResult = AnvilMergeLogic.testCustomRecipe(view, inventory, player, leftItem, rightItem)
|
||||
if (!customRecipeResult.isEmpty()) {
|
||||
onCustomCraft(
|
||||
event, player, inventory,
|
||||
|
|
@ -90,7 +91,7 @@ class AnvilResultListener : Listener {
|
|||
|
||||
// Rename
|
||||
if (rightItem == null) {
|
||||
val result = AnvilMergeLogic.doRenaming(inventory, player, leftItem)
|
||||
val result = AnvilMergeLogic.doRenaming(view, inventory, player, leftItem)
|
||||
if (result.isEmpty()) return
|
||||
|
||||
extractAnvilResult(
|
||||
|
|
@ -105,7 +106,7 @@ class AnvilResultListener : Listener {
|
|||
// Merge
|
||||
val canMerge = leftItem.canMergeWith(rightItem)
|
||||
if (canMerge) {
|
||||
val result = AnvilMergeLogic.doMerge(inventory, player, leftItem, rightItem)
|
||||
val result = AnvilMergeLogic.doMerge(view, inventory, player, leftItem, rightItem)
|
||||
|
||||
extractAnvilResult(
|
||||
event, player, inventory,
|
||||
|
|
@ -118,7 +119,7 @@ class AnvilResultListener : Listener {
|
|||
|
||||
// Unit repair
|
||||
val unitRepairResult = AnvilMergeLogic.testUnitRepair(
|
||||
inventory, player,
|
||||
view, inventory, player,
|
||||
leftItem, rightItem
|
||||
)
|
||||
if (unitRepairResult.isEmpty()) {
|
||||
|
|
@ -481,6 +482,8 @@ class AnvilResultListener : Listener {
|
|||
) {
|
||||
val paperMeta = rightItem.itemMeta ?: return
|
||||
|
||||
|
||||
|
||||
val paperCopy: ItemStack?
|
||||
if (LoreEditType.APPEND_PAPER.doConsume) {
|
||||
paperCopy = null
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import org.bukkit.event.EventPriority
|
|||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.inventory.PrepareAnvilEvent
|
||||
import org.bukkit.inventory.AnvilInventory
|
||||
import org.bukkit.inventory.InventoryView
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.EnchantmentStorageMeta
|
||||
import org.bukkit.inventory.meta.ItemMeta
|
||||
|
|
@ -91,11 +92,12 @@ class PrepareAnvilListener : Listener {
|
|||
|
||||
if (!player.hasPermission(CustomAnvil.affectedByPluginPermission)) return
|
||||
|
||||
val result = getResult(inventory, player, first, second)
|
||||
val result = getResult(view, inventory, player, first, second)
|
||||
applyResult(event, player, result)
|
||||
}
|
||||
|
||||
fun getResult(
|
||||
view: InventoryView, //TODO use anvil view
|
||||
inventory: AnvilInventory,
|
||||
player: Player,
|
||||
first: ItemStack?, second: ItemStack?) : AnvilResult
|
||||
|
|
@ -104,7 +106,7 @@ class PrepareAnvilListener : Listener {
|
|||
return AnvilResult.EMPTY
|
||||
|
||||
// Test custom recipe
|
||||
var result: AnvilResult = testCustomRecipe(player, first, second)
|
||||
var result: AnvilResult = testCustomRecipe(view, inventory, player, first, second)
|
||||
if (!result.isEmpty())
|
||||
return result
|
||||
|
||||
|
|
@ -112,14 +114,14 @@ class PrepareAnvilListener : Listener {
|
|||
val shouldTryRename = second.isAir
|
||||
CustomAnvil.verboseLog("checking air in main logic: $shouldTryRename")
|
||||
if (shouldTryRename)
|
||||
return doRenaming(inventory, player, first)
|
||||
return doRenaming(view, inventory, player, first)
|
||||
|
||||
// Test for merge
|
||||
if (first.canMergeWith(second!!))
|
||||
return doMerge(inventory, player, first, second)
|
||||
return doMerge(view, inventory, player, first, second)
|
||||
|
||||
// Test for unit repair
|
||||
result = testUnitRepair(inventory, player, first, second)
|
||||
result = testUnitRepair(view, inventory, player, first, second)
|
||||
if (!result.isEmpty())
|
||||
return result
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue