Add Vault support

I created a EconomyComponent that extends a QuartzComponent but I'm
unfortunately not fully across the QuartzLib pattern so I will probably
need some guidance there.

The base functionality should be included to withdraw money from
player's accounts if they enable economy and set costs.

What I would like to include next is notifications to the player so they
know how much has been removed or if an error occured. I would also like
to include a confirm/deny option which tells the player how much the
operation is going to cost them before they do it.

I'm uncertain of the best way to go about telling the player how much
a copy of a poster is going to cost before they take it.
This commit is contained in:
Reldeam 2022-02-24 16:06:33 +11:00
parent 243cfc2402
commit de406f5f9d
17 changed files with 356 additions and 54 deletions

13
pom.xml
View File

@ -123,6 +123,13 @@
<id>CodeMC</id>
<url>https://repo.codemc.org/repository/maven-public</url>
</repository>
<!-- VaultAPI -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
@ -142,5 +149,11 @@
<version>1.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -46,6 +46,7 @@ import fr.moribus.imageonmap.commands.maptool.ListCommand;
import fr.moribus.imageonmap.commands.maptool.NewCommand;
import fr.moribus.imageonmap.commands.maptool.RenameCommand;
import fr.moribus.imageonmap.commands.maptool.UpdateCommand;
import fr.moribus.imageonmap.economy.EconomyComponent;
import fr.moribus.imageonmap.image.ImageIOExecutor;
import fr.moribus.imageonmap.image.ImageRendererExecutor;
import fr.moribus.imageonmap.image.MapInitEvent;
@ -113,8 +114,15 @@ public final class ImageOnMap extends QuartzPlugin {
saveDefaultConfig();
commandWorker = loadComponent(CommandWorkers.class);
loadComponents(I18n.class, Gui.class, Commands.class, PluginConfiguration.class, ImageIOExecutor.class,
ImageRendererExecutor.class);
loadComponents(
I18n.class,
Gui.class,
Commands.class,
PluginConfiguration.class,
ImageIOExecutor.class,
ImageRendererExecutor.class,
EconomyComponent.class
);
//Init all the things !
I18n.setPrimaryLocale(PluginConfiguration.LANG.get());
@ -122,6 +130,7 @@ public final class ImageOnMap extends QuartzPlugin {
MapManager.init();
MapInitEvent.init();
MapItemManager.init();
EconomyComponent.init();
Commands.register(

View File

@ -64,4 +64,9 @@ public final class PluginConfiguration extends Configuration {
public static ConfigurationList<String> IMAGES_HOSTNAMES_WHITELIST =
list("images-hostnames-whitelist", String.class);
}
public static ConfigurationItem<Boolean> ENABLE_ECONOMY = item("enable-economy", false);
public static ConfigurationItem<Boolean> SCALE_MAP_COST = item("scale-map-cost", true);
public static ConfigurationItem<Integer> COST_PER_MAP = item("cost-per-map", 100);
public static ConfigurationItem<Boolean> SCALE_COPY_COST = item("scale-copy-cost", false);
public static ConfigurationItem<Integer> COST_PER_COPY = item("cost-per-copy", 50);
}

View File

@ -39,6 +39,8 @@ package fr.moribus.imageonmap.commands.maptool;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.commands.IoMCommand;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.map.MapManager;
import fr.zcraft.quartzlib.components.commands.CommandException;
@ -96,10 +98,19 @@ public class GetCommand extends IoMCommand {
return;
}
if (map.give(sender)) {
info(I.t("The requested map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
try {
if (map.give(sender)) {
info(I.t("The requested map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
}
} catch (EconomyNotEnabledException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InsufficientFundsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}

View File

@ -38,6 +38,8 @@ package fr.moribus.imageonmap.commands.maptool;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.commands.IoMCommand;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.ui.MapItemManager;
import fr.zcraft.quartzlib.components.commands.CommandException;
import fr.zcraft.quartzlib.components.commands.CommandInfo;
@ -56,14 +58,25 @@ public class GetRemainingCommand extends IoMCommand {
return;
}
int givenMaps = MapItemManager.giveCache(player);
if (givenMaps == 0) {
error(I.t("Your inventory is full! Make some space before requesting the remaining maps."));
} else {
info(I.tn("There is {0} map remaining.", "There are {0} maps remaining.",
MapItemManager.getCacheSize(player)));
try {
int givenMaps = MapItemManager.giveCache(player);
if (givenMaps == 0) {
error(I.t("Your inventory is full! Make some space before requesting the remaining maps."));
} else {
info(I.tn(
"There is {0} map remaining.", "There are {0} maps remaining.",
MapItemManager.getCacheSize(player)
));
}
} catch (EconomyNotEnabledException exception) {
// TODO Auto-generated catch block
exception.printStackTrace();
} catch (InsufficientFundsException exception) {
// TODO Auto-generated catch block
exception.printStackTrace();
}
}
@Override

View File

@ -38,6 +38,8 @@ package fr.moribus.imageonmap.commands.maptool;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.commands.IoMCommand;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.map.MapManager;
import fr.zcraft.quartzlib.components.commands.CommandException;
@ -111,11 +113,21 @@ public class GiveCommand extends IoMCommand {
}
retrieveUUID(playerName, uuid2 -> {
if (Bukkit.getPlayer((uuid2)) != null && Bukkit.getPlayer((uuid2)).isOnline()
&& map.give(Bukkit.getPlayer(uuid2))) {
info(I.t("The requested map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
try {
if (Bukkit.getPlayer((uuid2)) != null && Bukkit.getPlayer((uuid2)).isOnline()
&& map.give(Bukkit.getPlayer(uuid2))) {
info(I.t("The requested map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
}
} catch (EconomyNotEnabledException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InsufficientFundsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
});

View File

@ -37,7 +37,10 @@
package fr.moribus.imageonmap.commands.maptool;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.moribus.imageonmap.commands.IoMCommand;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.image.ImageRendererExecutor;
import fr.moribus.imageonmap.image.ImageUtils;
import fr.moribus.imageonmap.map.ImageMap;
@ -162,11 +165,20 @@ public class NewCommand extends IoMCommand {
MessageSender
.sendActionBarMessage(player, ChatColor.DARK_GREEN + I.t("Rendering finished!"));
if (result.give(player)
&& (result instanceof PosterMap && !((PosterMap) result).hasColumnData())) {
info(I.t("The rendered map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
try {
if (result.give(player)
&& (result instanceof PosterMap && !((PosterMap) result).hasColumnData())) {
info(I.t("The rendered map was too big to fit in your inventory."));
info(I.t("Use '/maptool getremaining' to get the remaining maps."));
}
} catch (EconomyNotEnabledException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InsufficientFundsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override

View File

@ -0,0 +1,34 @@
package fr.moribus.imageonmap.economy;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.zcraft.quartzlib.core.QuartzComponent;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class EconomyComponent extends QuartzComponent {
private static Economy economy = null;
public static void init() {
System.out.println("HELLO!");
if (PluginConfiguration.ENABLE_ECONOMY.get()
&& Bukkit.getPluginManager().getPlugin("Vault") != null) {
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
if (rsp != null) {
economy = rsp.getProvider();
}
}
}
public static Economy getEconomy() throws EconomyNotEnabledException {
if (economy != null) {
return economy;
} else {
throw new EconomyNotEnabledException();
}
}
}

View File

@ -0,0 +1,9 @@
package fr.moribus.imageonmap.economy;
public class EconomyNotEnabledException extends Exception {
public EconomyNotEnabledException() {
super("The economy is disabled");
}
}

View File

@ -0,0 +1,24 @@
package fr.moribus.imageonmap.economy;
import org.bukkit.OfflinePlayer;
public class InsufficientFundsException extends Exception {
private final OfflinePlayer player;
private final double cost;
public InsufficientFundsException(OfflinePlayer player, double cost) {
super("Insufficient funds");
this.player = player;
this.cost = cost;
}
public OfflinePlayer getPlayer() {
return player;
}
public double getCost() {
return cost;
}
}

View File

@ -38,6 +38,8 @@ package fr.moribus.imageonmap.gui;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.map.MapManager;
import fr.moribus.imageonmap.map.PosterMap;
@ -143,25 +145,37 @@ public class MapListGui extends ExplorerGui<ImageMap> {
@Override
protected ItemStack getPickedUpItem(ImageMap map) {
if (!Permissions.GET.grantedTo(getPlayer())) {
return null;
}
if (map instanceof SingleMap) {
return MapItemManager.createMapItem(map.getMapsIDs()[0], map.getName(), false, true);
} else if (map instanceof PosterMap) {
PosterMap poster = (PosterMap) map;
try {
if (poster.hasColumnData()) {
return SplatterMapManager.makeSplatterMap((PosterMap) map);
if (!Permissions.GET.grantedTo(getPlayer())) {
return null;
}
MapItemManager.giveParts(getPlayer(), poster);
return null;
}
if (map instanceof SingleMap) {
return MapItemManager.createMapItem(map.getMapsIDs()[0], map.getName(), false, true);
} else if (map instanceof PosterMap) {
PosterMap poster = (PosterMap) map;
MapItemManager.give(getPlayer(), map);
if (poster.hasColumnData()) {
return SplatterMapManager.makeSplatterMap((PosterMap) map);
}
MapItemManager.giveParts(getPlayer(), poster);
return null;
}
MapItemManager.give(getPlayer(), map);
} catch (EconomyNotEnabledException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InsufficientFundsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
@Override

View File

@ -37,6 +37,8 @@
package fr.moribus.imageonmap.map;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.ui.MapItemManager;
import fr.zcraft.quartzlib.components.i18n.I;
import java.io.File;
@ -168,8 +170,20 @@ public abstract class ImageMap implements ConfigurationSerializable {
//
public boolean give(Player player) {
return MapItemManager.give(player, this);
public boolean give(Player player, boolean ignoreCost) {
try {
return MapItemManager.give(player, this, ignoreCost);
} catch (EconomyNotEnabledException | InsufficientFundsException exception) {
if (ignoreCost) {
//TODO it shouldn't get to this state
exception.printStackTrace();
}
return false;
}
}
public boolean give(Player player) throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, false);
}
protected abstract void postSerialize(Map<String, Object> map);

View File

@ -38,6 +38,8 @@ package fr.moribus.imageonmap.map;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.image.ImageIOExecutor;
import fr.moribus.imageonmap.image.PosterImage;
import fr.moribus.imageonmap.map.MapManagerException.Reason;
@ -100,13 +102,15 @@ public abstract class MapManager {
return false;
}
public static ImageMap createMap(UUID playerUUID, int mapID) throws MapManagerException {
public static ImageMap createMap(UUID playerUUID, int mapID)
throws MapManagerException, EconomyNotEnabledException, InsufficientFundsException {
ImageMap newMap = new SingleMap(playerUUID, mapID);
addMap(newMap);
return newMap;
}
public static ImageMap createMap(PosterImage image, UUID playerUUID, int[] mapsIDs) throws MapManagerException {
public static ImageMap createMap(PosterImage image, UUID playerUUID, int[] mapsIDs)
throws MapManagerException, EconomyNotEnabledException, InsufficientFundsException {
ImageMap newMap;
if (image.getImagesCount() == 1) {
@ -141,7 +145,8 @@ public abstract class MapManager {
return ((MapMeta) meta).hasMapId() ? ((MapMeta) meta).getMapId() : 0;
}
public static void addMap(ImageMap map) throws MapManagerException {
public static void addMap(ImageMap map)
throws MapManagerException, EconomyNotEnabledException, InsufficientFundsException {
getPlayerMapStore(map.getUserUUID()).addMap(map);
}

View File

@ -38,6 +38,9 @@ package fr.moribus.imageonmap.map;
import fr.moribus.imageonmap.ImageOnMap;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.moribus.imageonmap.economy.EconomyComponent;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.map.MapManagerException.Reason;
import fr.zcraft.quartzlib.tools.PluginLogger;
import java.io.File;
@ -47,7 +50,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.milkbowl.vault.economy.EconomyResponse;
import net.milkbowl.vault.economy.EconomyResponse.ResponseType;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
@ -92,9 +99,42 @@ public class PlayerMapStore implements ConfigurationSerializable {
return false;
}
public synchronized void addMap(ImageMap map) throws MapManagerException {
checkMapLimit(map);
insertMap(map);
public synchronized void addMap(ImageMap map)
throws MapManagerException, EconomyNotEnabledException, InsufficientFundsException {
double cost = 0;
// withdraw money from account if economy is enabled
if (PluginConfiguration.ENABLE_ECONOMY.get()) {
// calculate the cost of the new map(s)
cost = PluginConfiguration.COST_PER_MAP.get();
if (PluginConfiguration.SCALE_MAP_COST.get()) {
cost *= map.getMapCount();
}
EconomyResponse response = EconomyComponent.getEconomy().withdrawPlayer(getPlayer(), cost);
if (response.type == ResponseType.FAILURE) {
throw new InsufficientFundsException(getPlayer(), cost);
}
}
try {
checkMapLimit(map);
insertMap(map);
} catch (MapManagerException exception) {
// give back the money if player fails to get the map(s)
if (PluginConfiguration.ENABLE_ECONOMY.get()) {
EconomyComponent.getEconomy().depositPlayer(getPlayer(), cost);
}
throw exception;
}
}
public OfflinePlayer getPlayer() {
return Bukkit.getOfflinePlayer(getUUID());
}
public synchronized void insertMap(ImageMap map) {

View File

@ -37,13 +37,16 @@
package fr.moribus.imageonmap.ui;
import fr.moribus.imageonmap.Permissions;
import fr.moribus.imageonmap.PluginConfiguration;
import fr.moribus.imageonmap.economy.EconomyComponent;
import fr.moribus.imageonmap.economy.EconomyNotEnabledException;
import fr.moribus.imageonmap.economy.InsufficientFundsException;
import fr.moribus.imageonmap.map.ImageMap;
import fr.moribus.imageonmap.map.MapManager;
import fr.moribus.imageonmap.map.PosterMap;
import fr.moribus.imageonmap.map.SingleMap;
import fr.zcraft.quartzlib.components.i18n.I;
import fr.zcraft.quartzlib.core.QuartzLib;
import fr.zcraft.quartzlib.tools.PluginLogger;
import fr.zcraft.quartzlib.tools.items.ItemStackBuilder;
import fr.zcraft.quartzlib.tools.items.ItemUtils;
import fr.zcraft.quartzlib.tools.runners.RunTask;
@ -51,6 +54,8 @@ import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Queue;
import java.util.UUID;
import net.milkbowl.vault.economy.EconomyResponse;
import net.milkbowl.vault.economy.EconomyResponse.ResponseType;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.GameMode;
@ -86,37 +91,86 @@ public class MapItemManager implements Listener {
mapItemCache = null;
}
public static boolean give(Player player, ImageMap map) {
public static boolean give(Player player, ImageMap map, boolean ignoreCost)
throws EconomyNotEnabledException, InsufficientFundsException {
if (map instanceof PosterMap) {
return give(player, (PosterMap) map);
return give(player, (PosterMap) map, ignoreCost);
} else if (map instanceof SingleMap) {
return give(player, (SingleMap) map);
return give(player, (SingleMap) map, ignoreCost);
}
return false;
}
public static boolean give(Player player, SingleMap map) {
return give(player, createMapItem(map, true));
public static boolean give(Player player, ImageMap map)
throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, map, false);
}
public static boolean give(Player player, PosterMap map) {
public static boolean give(Player player, SingleMap map, boolean ignoreCost)
throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, createMapItem(map, true), 1, ignoreCost);
}
public static boolean give(Player player, SingleMap map)
throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, createMapItem(map, true), 1, false);
}
public static boolean give(Player player, PosterMap map, boolean ignoreCost)
throws EconomyNotEnabledException, InsufficientFundsException {
if (!map.hasColumnData()) {
return giveParts(player, map);
}
return give(player, SplatterMapManager.makeSplatterMap(map));
return give(player, SplatterMapManager.makeSplatterMap(map), map.getMapCount(), ignoreCost);
}
private static boolean give(final Player player, final ItemStack item) {
public static boolean give(Player player, PosterMap map)
throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, SplatterMapManager.makeSplatterMap(map), map.getMapCount(), false);
}
private static boolean give(final Player player, final ItemStack item, Integer mapCount, boolean ignoreCost)
throws EconomyNotEnabledException, InsufficientFundsException {
double cost = 0;
// withdraw money from account if economy is enabled
if (PluginConfiguration.ENABLE_ECONOMY.get()
&& !ignoreCost) {
// calculate the cost of the new map(s)
cost = PluginConfiguration.COST_PER_COPY.get();
if (PluginConfiguration.SCALE_COPY_COST.get()) {
cost *= mapCount;
}
EconomyResponse response = EconomyComponent.getEconomy().withdrawPlayer(player, cost);
if (response.type == ResponseType.FAILURE) {
throw new InsufficientFundsException(player, cost);
}
}
boolean given = ItemUtils.give(player, item);
if (given) {
player.playSound(player.getLocation(), Sound.ENTITY_ITEM_PICKUP, SoundCategory.PLAYERS, 1, 1);
} else if (PluginConfiguration.ENABLE_ECONOMY.get()
&& cost > 0) {
EconomyComponent.getEconomy().depositPlayer(player, cost);
}
return !given;
}
public static boolean giveParts(Player player, PosterMap map) {
private static boolean give(final Player player, final ItemStack item)
throws EconomyNotEnabledException, InsufficientFundsException {
return give(player, item, 1, false);
}
public static boolean giveParts(Player player, PosterMap map)
throws EconomyNotEnabledException, InsufficientFundsException {
boolean inventoryFull = false;
ItemStack mapPartItem;
@ -129,7 +183,7 @@ public class MapItemManager implements Listener {
return inventoryFull;
}
public static int giveCache(Player player) {
public static int giveCache(Player player) throws EconomyNotEnabledException, InsufficientFundsException {
Queue<ItemStack> cache = getCache(player);
Inventory inventory = player.getInventory();
int givenItemsCount = 0;
@ -305,7 +359,7 @@ public class MapItemManager implements Listener {
if (player.getGameMode() != GameMode.CREATIVE
|| !SplatterMapManager.hasSplatterMap(player, poster)) {
poster.give(player);
poster.give(player, true);
}
return;
}

View File

@ -40,3 +40,25 @@ save-full-image: false
# - cdn.discordapp.com
images-hostnames-whitelist:
#-------------------------------------------------------------------------------
# Economy
# Enables Vault-compatible economy plugin (requires Vault and a Vault
# compatible economy plugin to work) if set to true
enable-economy: false
# Should the cost to create a splatter map scale with the size of the image?
# i.e. cost-per-map * width * height
# If set to false then the cost to create a splatter map is just the
# cost-per-map no matter what the dimensions of the splatter map are.
scale-map-cost: true
cost-per-map: 100
# Should the cost to copy a splatter map scale with the size of the image?
# This is equivalent to scale-map-cost but is applied whenever the player
# wishes to get a copy of a pre-existing map.
scale-copy-cost: false
# The cost for each copy of the
cost-per-copy: 50

View File

@ -3,6 +3,7 @@ main: fr.moribus.imageonmap.ImageOnMap
version: "4.2.2"
api-version: "1.13"
softdepend: [Vault]
commands:
tomap:
@ -42,6 +43,8 @@ permissions:
imageonmap.bypassimagelimit: false
imageonmap.bypasswhitelist: true
imageonmap.placeinvisiblesplattermap: true
imageonmap.ignoremapcost: true
imageonmap.ignorecopycost: true
imageonmap.userender:
description: "Allows you to use /tomap and related commands (/maptool getremaining). Alias of imageonmap.new."
@ -130,3 +133,11 @@ permissions:
imageonmap.placeinvisiblesplattermap:
description: "Allows you to make the item frame on which you placed your splatter map invisible."
default: true
imageonmap.ignoremapcost:
description: "Makes making maps free (if economy is enabled)"
default: op
imageonmap.ignorecopycost:
description: "Makes making copies of splatter maps free (if economy is enabled)"
default: op