Shopkeepers/src/main/java/com/nisovin/shopkeepers/Settings.java

643 lines
22 KiB
Java

package com.nisovin.shopkeepers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.Configuration;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import com.nisovin.shopkeepers.api.ShopkeepersPlugin;
import com.nisovin.shopkeepers.config.Config;
import com.nisovin.shopkeepers.config.ConfigLoadException;
import com.nisovin.shopkeepers.config.migration.ConfigMigrations;
import com.nisovin.shopkeepers.util.ConversionUtils;
import com.nisovin.shopkeepers.util.EntityUtils;
import com.nisovin.shopkeepers.util.ItemData;
import com.nisovin.shopkeepers.util.ItemUtils;
import com.nisovin.shopkeepers.util.Log;
import com.nisovin.shopkeepers.util.PermissionUtils;
public class Settings extends Config {
/*
* General Settings
*/
public static int configVersion = 4;
public static boolean debug = false;
// See DebugOptions for all available options.
public static List<String> debugOptions = new ArrayList<>(0);
public static boolean enableMetrics = true;
/*
* Messages
*/
public static String language = "en-default";
/*
* Shopkeeper Data
*/
public static String fileEncoding = "UTF-8";
public static boolean saveInstantly = true;
public static boolean convertPlayerItems = false;
public static boolean convertAllPlayerItems = true;
public static List<ItemData> convertPlayerItemsExceptions = new ArrayList<>();
/*
* Plugin Compatibility
*/
public static boolean enableSpawnVerifier = false;
public static boolean bypassSpawnBlocking = true;
public static boolean checkShopInteractionResult = false;
public static boolean enableWorldGuardRestrictions = false;
public static boolean requireWorldGuardAllowShopFlag = false;
public static boolean registerWorldGuardAllowShopFlag = true;
public static boolean enableTownyRestrictions = false;
/*
* Shop Creation (and removal)
*/
public static ItemData shopCreationItem = new ItemData(Material.VILLAGER_SPAWN_EGG, "&aShopkeeper", null);
public static boolean preventShopCreationItemRegularUsage = true;
public static boolean deletingPlayerShopReturnsCreationItem = false;
public static boolean createPlayerShopWithCommand = false;
public static boolean requireContainerRecentlyPlaced = true;
public static int maxContainerDistance = 15;
public static int maxShopsPerPlayer = -1;
public static String maxShopsPermOptions = "10,15,25";
public static boolean protectContainers = true;
public static boolean preventItemMovement = true;
public static boolean deleteShopkeeperOnBreakContainer = false;
public static int playerShopkeeperInactiveDays = 0;
/*
* Shop (Object) Types
*/
public static List<String> enabledLivingShops = Arrays.asList(
EntityType.VILLAGER.name(),
EntityType.COW.name(),
EntityType.MUSHROOM_COW.name(),
EntityType.SHEEP.name(),
EntityType.PIG.name(),
EntityType.CHICKEN.name(),
EntityType.OCELOT.name(),
EntityType.RABBIT.name(),
EntityType.WOLF.name(),
EntityType.SNOWMAN.name(),
EntityType.IRON_GOLEM.name(),
EntityType.BLAZE.name(),
EntityType.SILVERFISH.name(),
EntityType.POLAR_BEAR.name(), // MC 1.10
EntityType.SKELETON.name(),
EntityType.STRAY.name(), // MC 1.11
EntityType.WITHER_SKELETON.name(), // MC 1.11
EntityType.SPIDER.name(),
EntityType.CAVE_SPIDER.name(),
EntityType.CREEPER.name(),
EntityType.WITCH.name(),
EntityType.ENDERMAN.name(),
EntityType.ZOMBIE.name(),
EntityType.ZOMBIE_VILLAGER.name(), // MC 1.11
EntityType.HUSK.name(), // MC 1.11
EntityType.GIANT.name(),
EntityType.GHAST.name(),
EntityType.SLIME.name(),
EntityType.MAGMA_CUBE.name(),
EntityType.SQUID.name(),
EntityType.HORSE.name(),
EntityType.MULE.name(),
EntityType.DONKEY.name(),
EntityType.SKELETON_HORSE.name(),
EntityType.ZOMBIE_HORSE.name(),
EntityType.EVOKER.name(), // MC 1.11
EntityType.VEX.name(), // MC 1.11
EntityType.VINDICATOR.name(), // MC 1.11
EntityType.ILLUSIONER.name(), // MC 1.12
EntityType.PARROT.name(), // MC 1.12
EntityType.TURTLE.name(), // MC 1.13
EntityType.PHANTOM.name(), // MC 1.13
EntityType.COD.name(), // MC 1.13
EntityType.SALMON.name(), // MC 1.13
EntityType.PUFFERFISH.name(), // MC 1.13
EntityType.TROPICAL_FISH.name(), // MC 1.13
EntityType.DROWNED.name(), // MC 1.13
EntityType.DOLPHIN.name(), // MC 1.13
EntityType.CAT.name(), // MC 1.14
EntityType.PANDA.name(), // MC 1.14
EntityType.PILLAGER.name(), // MC 1.14
EntityType.RAVAGER.name(), // MC 1.14
EntityType.LLAMA.name(), // MC 1.11
EntityType.TRADER_LLAMA.name(), // MC 1.14
EntityType.WANDERING_TRADER.name(), // MC 1.14
EntityType.FOX.name(), // MC 1.14
"BEE", // MC 1.15
"ZOMBIFIED_PIGLIN", // MC 1.16, replaced PIG_ZOMBIE
"PIGLIN", // MC 1.16
"HOGLIN", // MC 1.16
"ZOGLIN", // MC 1.16
"STRIDER", // MC 1.16
"PIGLIN_BRUTE" // MC 1.16.2
);
public static boolean disableGravity = false;
public static int gravityChunkRange = 4;
public static boolean silenceLivingShopEntities = true;
public static boolean showNameplates = true;
public static boolean alwaysShowNameplates = false;
public static boolean enableCitizenShops = true;
public static boolean enableSignShops = true;
public static boolean enableSignPostShops = true;
/*
* Naming
*/
public static String nameRegex = "[A-Za-z0-9 ]{3,32}";
public static boolean namingOfPlayerShopsViaItem = false;
public static boolean allowRenamingOfPlayerNpcShops = false;
/*
* Editor Menu
*/
public static int maxTradesPages = 5;
public static ItemData previousPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData nextPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData currentPageItem = new ItemData(Material.WRITABLE_BOOK);
public static ItemData tradeSetupItem = new ItemData(Material.PAPER);
public static ItemData nameItem = new ItemData(Material.NAME_TAG);
public static boolean enableContainerOptionOnPlayerShop = true;
public static ItemData containerItem = new ItemData(Material.CHEST);
public static ItemData deleteItem = new ItemData(Material.BONE);
/*
* Non-shopkeeper villagers
*/
public static boolean disableOtherVillagers = false;
public static boolean blockVillagerSpawns = false;
public static boolean disableZombieVillagerCuring = false;
public static boolean hireOtherVillagers = false;
public static boolean disableWanderingTraders = false;
public static boolean blockWanderingTraderSpawns = false;
public static boolean hireWanderingTraders = false;
public static boolean editRegularVillagers = true;
public static boolean editRegularWanderingTraders = true;
/*
* Hiring
*/
public static ItemData hireItem = new ItemData(Material.EMERALD);
public static int hireOtherVillagersCosts = 1;
public static boolean hireRequireCreationPermission = true;
/*
* Trading
*/
public static boolean preventTradingWithOwnShop = true;
public static boolean preventTradingWhileOwnerIsOnline = false;
public static boolean useStrictItemComparison = false;
public static boolean enablePurchaseLogging = false;
public static boolean incrementVillagerStatistics = false;
public static int taxRate = 0;
public static boolean taxRoundUp = false;
/*
* Currencies
*/
public static ItemData currencyItem = new ItemData(Material.EMERALD);
public static ItemData zeroCurrencyItem = new ItemData(Material.BARRIER);
public static ItemData highCurrencyItem = new ItemData(Material.EMERALD_BLOCK);
public static ItemData zeroHighCurrencyItem = new ItemData(Material.BARRIER);
// Note: This can in general be larger than 64!
public static int highCurrencyValue = 9;
public static int highCurrencyMinCost = 20;
///// DEBUGGING
public static final class DebugOptions {
private DebugOptions() {
}
// Logs details of the server version dependent capabilities.
public static final String capabilities = "capabilities";
// Logs all events (spams!). Starts slightly delayed. Subsequent calls of the same event get combined into a
// single logging entry to slightly reduce spam.
public static final String logAllEvents = "log-all-events";
// Prints the registered listeners for the first call of each event.
public static final String printListeners = "print-listeners";
// Enables debugging output related to shopkeeper activation.
public static final String shopkeeperActivation = "shopkeeper-activation";
// Enables additional commands related debugging output.
public static final String commands = "commands";
// Logs information when updating stored shop owner names.
public static final String ownerNameUpdates = "owner-name-updates";
// Logs whenever a shopkeeper performs item migrations (eg. for trading offers).
public static final String itemMigrations = "item-migrations";
// Logs whenever we explicitly convert items to Spigot's data format. Note that this does not log when items get
// implicitly converted, which may happen under various circumstances.
public static final String itemConversions = "item-conversions";
}
public static boolean isDebugging() {
return isDebugging(null);
}
public static boolean isDebugging(String option) {
if (Bukkit.isPrimaryThread()) {
return Settings.debug && (option == null || Settings.debugOptions.contains(option));
} else {
AsyncSettings async = Settings.async();
return async.debug && (option == null || async.debugOptions.contains(option));
}
}
///// DERIVED SETTINGS
public static class MaxShopsPermission implements Comparable<MaxShopsPermission> {
// Integer.MAX_VALUE indicates no limit.
public final int maxShops;
public final String permission;
private MaxShopsPermission(int maxShops, String permission) {
this.maxShops = maxShops;
this.permission = permission;
}
@Override
public int compareTo(MaxShopsPermission other) {
return Integer.compare(this.maxShops, other.maxShops);
}
}
// Stores derived settings which get setup after loading the config.
public static class DerivedSettings {
public static ItemData namingItemData;
// Button items:
public static ItemData nameButtonItem;
public static ItemData containerButtonItem;
public static ItemData deleteButtonItem;
public static ItemData hireButtonItem;
public static ItemData deleteVillagerButtonItem;
public static ItemData villagerInventoryButtonItem;
public static Pattern shopNamePattern;
// Sorted in descending order:
public static final List<MaxShopsPermission> maxShopsPermissions = new ArrayList<>();
public static final Set<EntityType> enabledLivingShops = new LinkedHashSet<>();
static {
// Initial setup of default values:
setup();
}
// Gets called after setting values have changed (eg. after the config has been loaded):
private static void setup() {
// Ignore display name (which is used for specifying the new shopkeeper name):
namingItemData = new ItemData(ItemUtils.setItemStackName(nameItem.createItemStack(), null));
// Button items:
nameButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(nameItem.createItemStack(), Messages.buttonName, Messages.buttonNameLore));
containerButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(containerItem.createItemStack(), Messages.buttonContainer, Messages.buttonContainerLore));
deleteButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(deleteItem.createItemStack(), Messages.buttonDelete, Messages.buttonDeleteLore));
hireButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(hireItem.createItemStack(), Messages.buttonHire, Messages.buttonHireLore));
// Note: These use the same item types as the corresponding shopkeeper buttons.
deleteVillagerButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(deleteItem.createItemStack(), Messages.buttonDeleteVillager, Messages.buttonDeleteVillagerLore));
villagerInventoryButtonItem = new ItemData(ItemUtils.setItemStackNameAndLore(containerItem.createItemStack(), Messages.buttonVillagerInventory, Messages.buttonVillagerInventoryLore));
// Shop name pattern:
try {
shopNamePattern = Pattern.compile("^" + Settings.nameRegex + "$");
} catch (PatternSyntaxException e) {
Log.warning(INSTANCE.getLogPrefix() + "'name-regex' is not a valid regular expression ('" + Settings.nameRegex + "'). Reverting to default.");
Settings.nameRegex = "[A-Za-z0-9 ]{3,32}";
shopNamePattern = Pattern.compile("^" + Settings.nameRegex + "$");
}
// Maximum shops permissions:
maxShopsPermissions.clear();
// Add permission for an unlimited number of shops:
maxShopsPermissions.add(new MaxShopsPermission(Integer.MAX_VALUE, ShopkeepersPlugin.MAXSHOPS_UNLIMITED_PERMISSION));
String[] maxShopsPermOptions = Settings.maxShopsPermOptions.replace(" ", "").split(",");
for (String permOption : maxShopsPermOptions) {
// Validate:
Integer maxShops = ConversionUtils.parseInt(permOption);
if (maxShops == null || maxShops <= 0) {
Log.warning(INSTANCE.getLogPrefix() + "Ignoring invalid entry in 'max-shops-perm-options': " + permOption);
continue;
}
String permission = "shopkeeper.maxshops." + permOption;
maxShopsPermissions.add(new MaxShopsPermission(maxShops, permission));
}
Collections.sort(maxShopsPermissions, Collections.reverseOrder()); // Descending order
// Enabled living shop types:
enabledLivingShops.clear();
boolean foundInvalidEntityType = false;
for (String entityTypeId : Settings.enabledLivingShops) {
EntityType entityType = EntityUtils.matchEntityType(entityTypeId);
if (entityType == null || !entityType.isAlive() || !entityType.isSpawnable()) {
foundInvalidEntityType = true;
if ("PIG_ZOMBIE".equals(entityTypeId)) {
// Migration note for MC 1.16 TODO Remove this again at some point?
Log.warning(INSTANCE.getLogPrefix() + "Ignoring mob type 'PIG_ZOMBIE' in setting 'enabled-living-shops'. This mob no longer exist since MC 1.16. Consider replacing it with 'ZOMBIFIED_PIGLIN'.");
} else {
Log.warning(INSTANCE.getLogPrefix() + "Invalid living entity type name in 'enabled-living-shops': " + entityTypeId);
}
} else {
enabledLivingShops.add(entityType);
}
}
if (foundInvalidEntityType) {
Log.warning(INSTANCE.getLogPrefix() + "All existing entity type names can be found here: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html");
}
}
private DerivedSettings() {
}
}
// Cached values for settings which are used asynchronously.
public static final class AsyncSettings {
private static volatile AsyncSettings INSTANCE = new AsyncSettings();
private static void refresh() {
INSTANCE = new AsyncSettings();
}
public final boolean debug;
public final List<String> debugOptions;
public final String fileEncoding;
private AsyncSettings() {
this.debug = Settings.debug;
this.debugOptions = new ArrayList<>(Settings.debugOptions);
this.fileEncoding = Settings.fileEncoding;
}
}
public static AsyncSettings async() {
return AsyncSettings.INSTANCE;
}
// Can also be called externally, when settings are changed externally.
public static void onSettingsChanged() {
// Update derived settings:
DerivedSettings.setup();
// Refresh async settings cache:
AsyncSettings.refresh();
}
// ITEMS
// Shop creation item:
public static ItemStack createShopCreationItem() {
return shopCreationItem.createItemStack();
}
public static boolean isShopCreationItem(ItemStack item) {
return shopCreationItem.matches(item);
}
// Naming item:
public static boolean isNamingItem(ItemStack item) {
return DerivedSettings.namingItemData.matches(item);
}
public static ItemStack createNameButtonItem() {
return DerivedSettings.nameButtonItem.createItemStack();
}
// Container button:
public static ItemStack createContainerButtonItem() {
return DerivedSettings.containerButtonItem.createItemStack();
}
// Delete button:
public static ItemStack createDeleteButtonItem() {
return DerivedSettings.deleteButtonItem.createItemStack();
}
// Hire item:
public static ItemStack createHireButtonItem() {
return DerivedSettings.hireButtonItem.createItemStack();
}
public static boolean isHireItem(ItemStack item) {
return hireItem.matches(item);
}
// CURRENCY
// Currency item:
public static ItemStack createCurrencyItem(int amount) {
return currencyItem.createItemStack(amount);
}
public static boolean isCurrencyItem(ItemStack item) {
return currencyItem.matches(item);
}
// High currency item:
public static boolean isHighCurrencyEnabled() {
return (highCurrencyValue > 0 && highCurrencyItem.getType() != Material.AIR);
}
public static ItemStack createHighCurrencyItem(int amount) {
if (!isHighCurrencyEnabled()) return null;
return highCurrencyItem.createItemStack(amount);
}
public static boolean isHighCurrencyItem(ItemStack item) {
if (!isHighCurrencyEnabled()) return false;
return highCurrencyItem.matches(item);
}
// Zero currency item:
public static ItemStack createZeroCurrencyItem() {
if (zeroCurrencyItem.getType() == Material.AIR) return null;
return zeroCurrencyItem.createItemStack();
}
public static boolean isZeroCurrencyItem(ItemStack item) {
if (zeroCurrencyItem.getType() == Material.AIR) {
return ItemUtils.isEmpty(item);
}
return zeroCurrencyItem.matches(item);
}
// Zero high currency item:
public static ItemStack createZeroHighCurrencyItem() {
if (zeroHighCurrencyItem.getType() == Material.AIR) return null;
return zeroHighCurrencyItem.createItemStack();
}
public static boolean isZeroHighCurrencyItem(ItemStack item) {
if (zeroHighCurrencyItem.getType() == Material.AIR) {
return ItemUtils.isEmpty(item);
}
return zeroHighCurrencyItem.matches(item);
}
// VARIOUS
// Integer.MAX_VALUE indicates no limit.
public static int getMaxShopsLimit(Player player) {
if (Settings.maxShopsPerPlayer == -1) {
return Integer.MAX_VALUE; // No limit by default
}
int maxShops = Settings.maxShopsPerPlayer; // Default
for (MaxShopsPermission entry : DerivedSettings.maxShopsPermissions) {
// Note: The max shops permission entries are sorted in descending order.
if (entry.maxShops <= maxShops) {
break;
}
if (PermissionUtils.hasPermission(player, entry.permission)) {
maxShops = entry.maxShops;
break;
}
}
return maxShops;
}
///// LOADING
// Returns null on success, otherwise a severe issue prevented loading the config.
public static ConfigLoadException loadConfig(Plugin plugin) {
Log.info("Loading config.");
// Save default config in case the config file does not exist:
plugin.saveDefaultConfig();
// Load config:
plugin.reloadConfig();
Configuration config = plugin.getConfig();
// Load settings from config:
boolean configChanged = false;
try {
configChanged = loadConfig(config);
} catch (ConfigLoadException e) {
// Config loading failed with a severe issue:
return e;
}
if (configChanged) {
// If the config was modified (migrations, adding missing settings, ..), save it:
// TODO Persist comments somehow.
plugin.saveConfig();
}
return null; // Config loaded successfully
}
// Returns true, if the config has changed and needs to be saved.
private static boolean loadConfig(Configuration config) throws ConfigLoadException {
boolean configChanged = false;
// Perform config migrations:
boolean migrated = ConfigMigrations.applyMigrations(config);
if (migrated) {
configChanged = true;
}
// Insert default values for settings missing inside the config:
boolean insertedDefaults = Settings.INSTANCE.insertMissingDefaultValues(config);
if (insertedDefaults) {
configChanged = true;
}
// Load and validate settings:
Settings.INSTANCE.load(config);
onSettingsChanged();
return configChanged;
}
/////
private static final Settings INSTANCE = new Settings();
private Settings() {
}
@Override
protected void validateSettings() {
if (maxContainerDistance > 50) {
Log.warning(this.getLogPrefix() + "'max-container-distance' can be at most 50.");
maxContainerDistance = 50;
}
if (gravityChunkRange < 0) {
Log.warning(this.getLogPrefix() + "'gravity-chunk-range' cannot be negative.");
gravityChunkRange = 0;
}
// Certain items cannot be of type AIR:
if (shopCreationItem.getType() == Material.AIR) {
Log.warning(this.getLogPrefix() + "'shop-creation-item' can not be AIR.");
shopCreationItem = shopCreationItem.withType(Material.VILLAGER_SPAWN_EGG);
}
if (hireItem.getType() == Material.AIR) {
Log.warning(this.getLogPrefix() + "'hire-item' can not be AIR.");
hireItem = hireItem.withType(Material.EMERALD);
}
if (currencyItem.getType() == Material.AIR) {
Log.warning(this.getLogPrefix() + "'currency-item' can not be AIR.");
currencyItem = currencyItem.withType(Material.EMERALD);
}
if (namingOfPlayerShopsViaItem) {
if (nameItem.getType() == Material.AIR) {
Log.warning(this.getLogPrefix() + "'name-item' can not be AIR if naming-of-player-shops-via-item is enabled!");
nameItem = nameItem.withType(Material.NAME_TAG);
}
}
if (maxTradesPages < 1) {
Log.warning(this.getLogPrefix() + "'max-trades-pages' can not be less than 1!");
maxTradesPages = 1;
} else if (maxTradesPages > 10) {
Log.warning(this.getLogPrefix() + "'max-trades-pages' can not be greater than 10!");
maxTradesPages = 10;
}
if (taxRate < 0) {
Log.warning(this.getLogPrefix() + "'tax-rate' can not be less than 0!");
taxRate = 0;
} else if (taxRate > 100) {
Log.warning(this.getLogPrefix() + "'tax-rate' can not be larger than 100!");
taxRate = 100;
}
}
}