9
0
mirror of https://github.com/Xiao-MoMi/Custom-Fishing.git synced 2025-12-29 11:59:11 +00:00
This commit is contained in:
XiaoMoMi
2023-09-21 04:43:58 +08:00
parent 524203c617
commit e503eac3f9
85 changed files with 3991 additions and 578 deletions

View File

@@ -137,6 +137,9 @@ public class CustomFishingPluginImpl extends CustomFishingPlugin {
HandlerList.unregisterAll(this);
}
/**
* Reload the plugin
*/
@Override
public void reload() {
CFConfig.load();
@@ -181,6 +184,9 @@ public class CustomFishingPluginImpl extends CustomFishingPlugin {
this.coolDownManager.load();
}
/**
* Load plugin dependencies
*/
private void loadDependencies() {
String mavenRepo = TimeZone.getDefault().getID().startsWith("Asia") ?
"https://maven.aliyun.com/repository/public/" : "https://repo.maven.apache.org/maven2/";
@@ -203,6 +209,9 @@ public class CustomFishingPluginImpl extends CustomFishingPlugin {
);
}
/**
* Disable NBT API logs
*/
private void disableNBTAPILogs() {
MinecraftVersion.disableBStats();
MinecraftVersion.disableUpdateCheck();
@@ -236,6 +245,12 @@ public class CustomFishingPluginImpl extends CustomFishingPlugin {
}
}
/**
* Retrieves a YAML configuration from a file within the plugin's data folder.
*
* @param file The name of the configuration file.
* @return A YamlConfiguration object representing the configuration.
*/
@Override
public YamlConfiguration getConfig(String file) {
File config = new File(this.getDataFolder(), file);
@@ -243,23 +258,44 @@ public class CustomFishingPluginImpl extends CustomFishingPlugin {
return YamlConfiguration.loadConfiguration(config);
}
/**
* Checks if a specified plugin is enabled on the Bukkit server.
*
* @param plugin The name of the plugin to check.
* @return True if the plugin is enabled, false otherwise.
*/
@Override
public boolean isHookedPluginEnabled(String plugin) {
return Bukkit.getPluginManager().isPluginEnabled(plugin);
}
@NotNull
public static ProtocolManager getProtocolManager() {
return protocolManager;
}
/**
* Outputs a debugging message if the debug mode is enabled.
*
* @param message The debugging message to be logged.
*/
@Override
public void debug(String message) {
if (!CFConfig.debug) return;
LogUtils.info(message);
}
/**
* Gets the CoolDownManager instance associated with the plugin.
*
* @return The CoolDownManager instance.
*/
public CoolDownManager getCoolDownManager() {
return coolDownManager;
}
/**
* Retrieves the ProtocolManager instance used for managing packets.
*
* @return The ProtocolManager instance.
*/
@NotNull
public static ProtocolManager getProtocolManager() {
return protocolManager;
}
}

View File

@@ -47,7 +47,7 @@ public class CompetitionCommand {
}
private CommandAPICommand getCompetitionStartCommand() {
Set<String> allCompetitions = CustomFishingPlugin.get().getCompetitionManager().getAllCompetitions();
Set<String> allCompetitions = CustomFishingPlugin.get().getCompetitionManager().getAllCompetitionKeys();
var command = new CommandAPICommand("start")
.withArguments(
new StringArgument("id")

View File

@@ -120,7 +120,7 @@ public class DebugCommand {
for (EffectModifier modifier : totemEffect.getEffectModifiers()) {
modifier.modify(initialEffect, fishingPreparation);
}
var map = CustomFishingPlugin.get().getFishingManager().getPossibleLootKeysWithWeight(initialEffect, fishingPreparation);
var map = CustomFishingPlugin.get().getLootManager().getPossibleLootKeysWithWeight(initialEffect, fishingPreparation);
List<LootWithWeight> loots = new ArrayList<>();
double sum = 0;
for (Map.Entry<String, Double> entry : map.entrySet()) {

View File

@@ -48,18 +48,18 @@ public class IntegrationManagerImpl implements IntegrationManager {
private final CustomFishingPlugin plugin;
private final HashMap<String, LevelInterface> levelPluginMap;
private final HashMap<String, EnchantmentInterface> enchantments;
private final HashMap<String, EnchantmentInterface> enchantmentPluginMap;
private SeasonInterface seasonInterface;
public IntegrationManagerImpl(CustomFishingPlugin plugin) {
this.plugin = plugin;
this.levelPluginMap = new HashMap<>();
this.enchantments = new HashMap<>();
this.enchantmentPluginMap = new HashMap<>();
this.load();
}
public void disable() {
this.enchantments.clear();
this.enchantmentPluginMap.clear();
this.levelPluginMap.clear();
}
@@ -121,13 +121,13 @@ public class IntegrationManagerImpl implements IntegrationManager {
hookMessage("AureliumSkills");
}
if (plugin.isHookedPluginEnabled("EcoEnchants")) {
this.enchantments.put("EcoEnchants", new VanillaEnchantmentsImpl());
this.enchantmentPluginMap.put("EcoEnchants", new VanillaEnchantmentsImpl());
hookMessage("EcoEnchants");
} else {
this.enchantments.put("vanilla", new VanillaEnchantmentsImpl());
this.enchantmentPluginMap.put("vanilla", new VanillaEnchantmentsImpl());
}
if (plugin.isHookedPluginEnabled("AdvancedEnchantments")) {
this.enchantments.put("AdvancedEnchantments", new AdvancedEnchantmentsImpl());
this.enchantmentPluginMap.put("AdvancedEnchantments", new AdvancedEnchantmentsImpl());
hookMessage("AdvancedEnchantments");
}
if (plugin.isHookedPluginEnabled("RealisticSeasons")) {
@@ -161,6 +161,13 @@ public class IntegrationManagerImpl implements IntegrationManager {
// }
}
/**
* Registers a level plugin with the specified name.
*
* @param plugin The name of the level plugin.
* @param level The implementation of the LevelInterface.
* @return true if the registration was successful, false if the plugin name is already registered.
*/
@Override
public boolean registerLevelPlugin(String plugin, LevelInterface level) {
if (levelPluginMap.containsKey(plugin)) return false;
@@ -168,46 +175,100 @@ public class IntegrationManagerImpl implements IntegrationManager {
return true;
}
/**
* Unregisters a level plugin with the specified name.
*
* @param plugin The name of the level plugin to unregister.
* @return true if the unregistration was successful, false if the plugin name is not found.
*/
@Override
public boolean unregisterLevelPlugin(String plugin) {
return levelPluginMap.remove(plugin) != null;
}
/**
* Registers an enchantment provided by a plugin.
*
* @param plugin The name of the plugin providing the enchantment.
* @param enchantment The enchantment to register.
* @return true if the registration was successful, false if the enchantment name is already in use.
*/
@Override
public boolean registerEnchantment(String plugin, EnchantmentInterface enchantment) {
if (enchantments.containsKey(plugin)) return false;
enchantments.put(plugin, enchantment);
if (enchantmentPluginMap.containsKey(plugin)) return false;
enchantmentPluginMap.put(plugin, enchantment);
return true;
}
/**
* Unregisters an enchantment provided by a plugin.
*
* @param plugin The name of the plugin providing the enchantment.
* @return true if the enchantment was successfully unregistered, false if the enchantment was not found.
*/
@Override
public boolean unregisterEnchantment(String plugin) {
return enchantments.remove(plugin) != null;
return enchantmentPluginMap.remove(plugin) != null;
}
private void hookMessage(String plugin) {
LogUtils.info( plugin + " hooked!");
}
/**
* Get the LevelInterface provided by a plugin.
*
* @param plugin The name of the plugin providing the LevelInterface.
* @return The LevelInterface provided by the specified plugin, or null if the plugin is not registered.
*/
@Override
public LevelInterface getLevelHook(String plugin) {
@Nullable
public LevelInterface getLevelPlugin(String plugin) {
return levelPluginMap.get(plugin);
}
/**
* Get an enchantment plugin by its plugin name.
*
* @param plugin The name of the enchantment plugin.
* @return The enchantment plugin interface, or null if not found.
*/
@Override
@Nullable
public EnchantmentInterface getEnchantmentPlugin(String plugin) {
return enchantmentPluginMap.get(plugin);
}
/**
* Get a list of enchantment keys with level applied to the given ItemStack.
*
* @param itemStack The ItemStack to check for enchantments.
* @return A list of enchantment names applied to the ItemStack.
*/
@Override
public List<String> getEnchantments(ItemStack itemStack) {
ArrayList<String> list = new ArrayList<>();
for (EnchantmentInterface enchantmentInterface : enchantments.values()) {
for (EnchantmentInterface enchantmentInterface : enchantmentPluginMap.values()) {
list.addAll(enchantmentInterface.getEnchants(itemStack));
}
return list;
}
/**
* Get the current season interface, if available.
*
* @return The current season interface, or null if not available.
*/
@Nullable
public SeasonInterface getSeasonInterface() {
return seasonInterface;
}
/**
* Set the current season interface.
*
* @param season The season interface to set.
*/
@Override
public void setSeasonInterface(SeasonInterface season) {
this.seasonInterface = season;

View File

@@ -37,6 +37,6 @@ public class CustomFishingItemImpl implements ItemLibrary {
@Override
public String getItemID(ItemStack itemStack) {
return CustomFishingPlugin.get().getItemManager().getItemID(itemStack);
return CustomFishingPlugin.get().getItemManager().getCustomFishingItemID(itemStack);
}
}

View File

@@ -92,16 +92,36 @@ public class PlaceholderManagerImpl implements PlaceholderManager, Listener {
}
}
/**
* Set placeholders in a text string for a player.
*
* @param player The player for whom the placeholders should be set.
* @param text The text string containing placeholders.
* @return The text string with placeholders replaced if PlaceholderAPI is available; otherwise, the original text.
*/
@Override
public String setPlaceholders(Player player, String text) {
return hasPapi ? ParseUtils.setPlaceholders(player, text) : text;
}
/**
* Set placeholders in a text string for an offline player.
*
* @param player The offline player for whom the placeholders should be set.
* @param text The text string containing placeholders.
* @return The text string with placeholders replaced if PlaceholderAPI is available; otherwise, the original text.
*/
@Override
public String setPlaceholders(OfflinePlayer player, String text) {
return hasPapi ? ParseUtils.setPlaceholders(player, text) : text;
}
/**
* Detect and extract placeholders from a text string.
*
* @param text The text string to search for placeholders.
* @return A list of detected placeholders in the text.
*/
@Override
public List<String> detectPlaceholders(String text) {
List<String> placeholders = new ArrayList<>();
@@ -110,6 +130,14 @@ public class PlaceholderManagerImpl implements PlaceholderManager, Listener {
return placeholders;
}
/**
* Get the value associated with a single placeholder.
*
* @param player The player for whom the placeholders are being resolved (nullable).
* @param placeholder The placeholder to look up.
* @param placeholders A map of placeholders to their corresponding values.
* @return The value associated with the placeholder, or the original placeholder if not found.
*/
@Override
public String getSingleValue(@Nullable Player player, String placeholder, Map<String, String> placeholders) {
String result = null;
@@ -123,6 +151,14 @@ public class PlaceholderManagerImpl implements PlaceholderManager, Listener {
return setPlaceholders(player, custom);
}
/**
* Parse a text string by replacing placeholders with their corresponding values.
*
* @param player The offline player for whom the placeholders are being resolved (nullable).
* @param text The text string containing placeholders.
* @param placeholders A map of placeholders to their corresponding values.
* @return The text string with placeholders replaced by their values.
*/
@Override
public String parse(@Nullable OfflinePlayer player, String text, Map<String, String> placeholders) {
var list = detectPlaceholders(text);
@@ -144,8 +180,30 @@ public class PlaceholderManagerImpl implements PlaceholderManager, Listener {
return text;
}
/**
* Parse a list of text strings by replacing placeholders with their corresponding values.
*
* @param player The player for whom the placeholders are being resolved (can be null for offline players).
* @param list The list of text strings containing placeholders.
* @param replacements A map of custom replacements for placeholders.
* @return The list of text strings with placeholders replaced by their values.
*/
@Override
public String parseCacheable(Player player, String text) {
public List<String> parse(@Nullable OfflinePlayer player, List<String> list, Map<String, String> replacements) {
return list.stream()
.map(s -> parse(player, s, replacements))
.collect(Collectors.toList());
}
/**
* Parse a text string by replacing placeholders with their corresponding values, using caching for the player's placeholders.
*
* @param player The player for whom the placeholders are being resolved.
* @param text The text string containing placeholders.
* @return The text string with placeholders replaced by their values.
*/
@Override
public String parseCacheablePlaceholders(Player player, String text) {
var list = detectPlaceholders(text);
CachedPlaceholder cachedPlaceholder = cachedPlaceholders.computeIfAbsent(player.getUniqueId(), k -> new CachedPlaceholder());
for (String papi : list) {
@@ -157,13 +215,6 @@ public class PlaceholderManagerImpl implements PlaceholderManager, Listener {
return text;
}
@Override
public List<String> parse(@Nullable OfflinePlayer player, List<String> list, Map<String, String> replacements) {
return list.stream()
.map(s -> parse(player, s, replacements))
.collect(Collectors.toList());
}
public static PlaceholderManagerImpl getInstance() {
return instance;
}

View File

@@ -29,6 +29,7 @@ import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.action.ActionExpansion;
import net.momirealms.customfishing.api.mechanic.action.ActionFactory;
import net.momirealms.customfishing.api.mechanic.action.ActionTrigger;
import net.momirealms.customfishing.api.mechanic.condition.Condition;
import net.momirealms.customfishing.api.mechanic.loot.Loot;
import net.momirealms.customfishing.api.mechanic.requirement.Requirement;
import net.momirealms.customfishing.api.util.LogUtils;
@@ -47,6 +48,7 @@ import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -68,6 +70,7 @@ public class ActionManagerImpl implements ActionManager {
this.registerInbuiltActions();
}
// Method to register various built-in actions during initialization.
private void registerInbuiltActions() {
this.registerMessageAction();
this.registerCommandAction();
@@ -94,6 +97,7 @@ public class ActionManagerImpl implements ActionManager {
this.registerMoneyAction();
}
// Method to load expansions and global event actions.
public void load() {
this.loadExpansions();
this.loadGlobalEventActions();
@@ -108,11 +112,20 @@ public class ActionManagerImpl implements ActionManager {
this.actionBuilderMap.clear();
}
// Method to load global event actions from the plugin's configuration file.
private void loadGlobalEventActions() {
YamlConfiguration config = plugin.getConfig("config.yml");
GlobalSettings.load(config.getConfigurationSection("mechanics.global-events"));
}
/**
* Registers an ActionFactory for a specific action type.
* This method allows you to associate an ActionFactory with a custom action type.
*
* @param type The custom action type to register.
* @param actionFactory The ActionFactory responsible for creating actions of the specified type.
* @return True if the registration was successful (the action type was not already registered), false otherwise.
*/
@Override
public boolean registerAction(String type, ActionFactory actionFactory) {
if (this.actionBuilderMap.containsKey(type)) return false;
@@ -120,36 +133,92 @@ public class ActionManagerImpl implements ActionManager {
return true;
}
/**
* Unregisters an ActionFactory for a specific action type.
* This method allows you to remove the association between an action type and its ActionFactory.
*
* @param type The custom action type to unregister.
* @return True if the action type was successfully unregistered, false if it was not found.
*/
@Override
public boolean unregisterAction(String type) {
return this.actionBuilderMap.remove(type) != null;
}
/**
* Retrieves an Action object based on the configuration provided in a ConfigurationSection.
* This method reads the type of action from the section, obtains the corresponding ActionFactory,
* and builds an Action object using the specified values and chance.
*
* @param section The ConfigurationSection containing the action configuration.
* @return An Action object created based on the configuration, or an EmptyAction instance if the action type is invalid.
*/
@Override
@NotNull
public Action getAction(ConfigurationSection section) {
return getActionBuilder(section.getString("type")).build(section.get("value"), section.getDouble("chance", 1d));
ActionFactory factory = getActionFactory(section.getString("type"));
if (factory == null) {
LogUtils.warn("Action type: " + section.getString("type") + " doesn't exist.");
// to prevent NPE
return EmptyAction.instance;
}
return factory.build(
section.get("value"),
section.getDouble("chance", 1d)
);
}
/**
* Retrieves a mapping of ActionTriggers to arrays of Actions from a ConfigurationSection.
* This method iterates through the provided ConfigurationSection to extract action triggers
* and their associated arrays of Actions.
*
* @param section The ConfigurationSection containing action mappings.
* @return A HashMap where keys are ActionTriggers and values are arrays of Action objects.
*/
@Override
@NotNull
public HashMap<ActionTrigger, Action[]> getActionMap(ConfigurationSection section) {
// Create an empty HashMap to store the action mappings
HashMap<ActionTrigger, Action[]> actionMap = new HashMap<>();
// If the provided ConfigurationSection is null, return the empty actionMap
if (section == null) return actionMap;
// Iterate through all key-value pairs in the ConfigurationSection
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
actionMap.put(
ActionTrigger.valueOf(entry.getKey().toUpperCase(Locale.ENGLISH)),
getActions(innerSection)
);
// Convert the key to an ActionTrigger enum (assuming it's in uppercase English)
// and map it to an array of Actions obtained from the inner section
try {
actionMap.put(
ActionTrigger.valueOf(entry.getKey().toUpperCase(Locale.ENGLISH)),
getActions(innerSection)
);
} catch (IllegalArgumentException e) {
LogUtils.warn("Event: " + entry.getKey() + " doesn't exist!");
}
}
}
return actionMap;
}
@Nullable
/**
* Retrieves an array of Action objects from a ConfigurationSection.
* This method iterates through the provided ConfigurationSection to extract Action configurations
* and build an array of Action objects.
*
* @param section The ConfigurationSection containing action configurations.
* @return An array of Action objects created based on the configurations in the section.
*/
@NotNull
@Override
public Action[] getActions(ConfigurationSection section) {
if (section == null) return null;
// Create an ArrayList to store the Actions
ArrayList<Action> actionList = new ArrayList<>();
if (section == null) return actionList.toArray(new Action[0]);
// Iterate through all key-value pairs in the ConfigurationSection
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
actionList.add(getAction(innerSection));
@@ -158,11 +227,86 @@ public class ActionManagerImpl implements ActionManager {
return actionList.toArray(new Action[0]);
}
/**
* Retrieves an ActionFactory associated with a specific action type.
*
* @param type The action type for which to retrieve the ActionFactory.
* @return The ActionFactory associated with the specified action type, or null if not found.
*/
@Nullable
@Override
public ActionFactory getActionBuilder(String type) {
public ActionFactory getActionFactory(String type) {
return actionBuilderMap.get(type);
}
/**
* Loads custom ActionExpansions from JAR files located in the expansion directory.
* This method scans the expansion folder for JAR files, loads classes that extend ActionExpansion,
* and registers them with the appropriate action type and ActionFactory.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private void loadExpansions() {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);
if (!expansionFolder.exists())
expansionFolder.mkdirs();
List<Class<? extends ActionExpansion>> classes = new ArrayList<>();
File[] expansionJars = expansionFolder.listFiles();
if (expansionJars == null) return;
for (File expansionJar : expansionJars) {
if (expansionJar.getName().endsWith(".jar")) {
try {
Class<? extends ActionExpansion> expansionClass = ClassUtils.findClass(expansionJar, ActionExpansion.class);
classes.add(expansionClass);
} catch (IOException | ClassNotFoundException e) {
LogUtils.warn("Failed to load expansion: " + expansionJar.getName(), e);
}
}
}
try {
for (Class<? extends ActionExpansion> expansionClass : classes) {
ActionExpansion expansion = expansionClass.getDeclaredConstructor().newInstance();
unregisterAction(expansion.getActionType());
registerAction(expansion.getActionType(), expansion.getActionFactory());
LogUtils.info("Loaded action expansion: " + expansion.getActionType() + "[" + expansion.getVersion() + "]" + " by " + expansion.getAuthor() );
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
LogUtils.warn("Error occurred when creating expansion instance.", e);
}
}
/**
* Retrieves a mapping of success times to corresponding arrays of actions from a ConfigurationSection.
*
* @param section The ConfigurationSection containing success times actions.
* @return A HashMap where success times associated with actions.
*/
@Override
public HashMap<Integer, Action[]> getTimesActionMap(ConfigurationSection section) {
HashMap<Integer, Action[]> actionMap = new HashMap<>();
if (section == null) return actionMap;
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
actionMap.put(Integer.parseInt(entry.getKey()), plugin.getActionManager().getActions(innerSection));
}
}
return actionMap;
}
/**
* Triggers a list of actions with the given condition.
* If the list of actions is not null, each action in the list is triggered.
*
* @param actions The list of actions to trigger.
* @param condition The condition associated with the actions.
*/
@Override
public void triggerActions(List<Action> actions, Condition condition) {
if (actions != null)
for (Action action : actions)
action.trigger(condition);
}
private void registerMessageAction() {
registerAction("message", (args, chance) -> {
ArrayList<String> msg = ConfigUtils.stringListArgs(args);
@@ -462,9 +606,9 @@ public class ActionManagerImpl implements ActionManager {
Player player = condition.getPlayer();
ItemStack itemStack = player.getInventory().getItem(slot);
if (amount > 0) {
ItemUtils.addDurability(itemStack, amount, true);
ItemUtils.increaseDurability(itemStack, amount, true);
} else {
ItemUtils.loseDurability(itemStack, -amount, true);
ItemUtils.decreaseDurability(itemStack, -amount, true);
}
};
} else {
@@ -482,7 +626,7 @@ public class ActionManagerImpl implements ActionManager {
return condition -> {
if (Math.random() > chance) return;
Player player = condition.getPlayer();
ItemUtils.giveCertainAmountOfItem(player, CustomFishingPlugin.get().getItemManager().buildAnyItemByID(player, id), amount);
ItemUtils.giveCertainAmountOfItem(player, CustomFishingPlugin.get().getItemManager().buildAnyPluginItemByID(player, id), amount);
};
} else {
LogUtils.warn("Illegal value format found at action: give-item");
@@ -780,7 +924,7 @@ public class ActionManagerImpl implements ActionManager {
String target = section.getString("target");
return condition -> {
if (Math.random() > chance) return;
Optional.ofNullable(plugin.getIntegrationManager().getLevelHook(pluginName)).ifPresentOrElse(it -> {
Optional.ofNullable(plugin.getIntegrationManager().getLevelPlugin(pluginName)).ifPresentOrElse(it -> {
it.addXp(condition.getPlayer(), target, exp);
}, () -> LogUtils.warn("Plugin (" + pluginName + "'s) level is not compatible. Please double check if it's a problem caused by pronunciation."));
};
@@ -796,7 +940,7 @@ public class ActionManagerImpl implements ActionManager {
if (Math.random() > chance) return;
condition.insertArg("{lava}", String.valueOf(arg));
LootManager lootManager = plugin.getLootManager();
List<String> loots = plugin.getFishingManager().getPossibleLootKeys(condition).stream().map(lootManager::getLoot).filter(Objects::nonNull).filter(Loot::showInFinder).map(Loot::getNick).toList();
List<String> loots = plugin.getLootManager().getPossibleLootKeys(condition).stream().map(lootManager::getLoot).filter(Objects::nonNull).filter(Loot::showInFinder).map(Loot::getNick).toList();
StringJoiner stringJoiner = new StringJoiner(CFLocale.MSG_Split_Char);
for (String loot : loots) {
stringJoiner.add(loot);
@@ -806,35 +950,4 @@ public class ActionManagerImpl implements ActionManager {
};
});
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void loadExpansions() {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);
if (!expansionFolder.exists())
expansionFolder.mkdirs();
List<Class<? extends ActionExpansion>> classes = new ArrayList<>();
File[] expansionJars = expansionFolder.listFiles();
if (expansionJars == null) return;
for (File expansionJar : expansionJars) {
if (expansionJar.getName().endsWith(".jar")) {
try {
Class<? extends ActionExpansion> expansionClass = ClassUtils.findClass(expansionJar, ActionExpansion.class);
classes.add(expansionClass);
} catch (IOException | ClassNotFoundException e) {
LogUtils.warn("Failed to load expansion: " + expansionJar.getName(), e);
}
}
}
try {
for (Class<? extends ActionExpansion> expansionClass : classes) {
ActionExpansion expansion = expansionClass.getDeclaredConstructor().newInstance();
unregisterAction(expansion.getActionType());
registerAction(expansion.getActionType(), expansion.getActionFactory());
LogUtils.info("Loaded action expansion: " + expansion.getActionType() + "[" + expansion.getVersion() + "]" + " by " + expansion.getAuthor() );
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
LogUtils.warn("Error occurred when creating expansion instance.", e);
}
}
}

View File

@@ -0,0 +1,17 @@
package net.momirealms.customfishing.mechanic.action;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.condition.Condition;
/**
* An implementation of the Action interface that represents an empty action with no behavior.
* This class serves as a default action to prevent NPE.
*/
public class EmptyAction implements Action {
public static EmptyAction instance = new EmptyAction();
@Override
public void trigger(Condition condition) {
}
}

View File

@@ -21,6 +21,7 @@ import net.momirealms.customfishing.CustomFishingPluginImpl;
import net.momirealms.customfishing.api.CustomFishingPlugin;
import net.momirealms.customfishing.api.data.user.OfflineUser;
import net.momirealms.customfishing.api.manager.BagManager;
import net.momirealms.customfishing.api.manager.EffectManager;
import net.momirealms.customfishing.api.mechanic.bag.FishingBagHolder;
import net.momirealms.customfishing.setting.CFConfig;
import org.bukkit.Bukkit;
@@ -34,6 +35,7 @@ import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.UUID;
@@ -66,6 +68,13 @@ public class BagManagerImpl implements BagManager, Listener {
plugin.getStorageManager().getDataSource().updateManyPlayersData(tempEditMap.values(), true);
}
/**
* Retrieves the online bag inventory associated with a player's UUID.
*
* @param uuid The UUID of the player for whom the bag inventory is retrieved.
* @return The online bag inventory if the player is online, or null if not found.
*/
@Nullable
@Override
public Inventory getOnlineBagInventory(UUID uuid) {
var onlinePlayer = plugin.getStorageManager().getOnlineUser(uuid);
@@ -75,12 +84,23 @@ public class BagManagerImpl implements BagManager, Listener {
return onlinePlayer.getHolder().getInventory();
}
/**
* Initiates the process of editing the bag inventory of an offline player by an admin.
*
* @param admin The admin player performing the edit.
* @param userData The OfflineUser data of the player whose bag is being edited.
*/
@Override
public void editOfflinePlayerBag(Player admin, OfflineUser userData) {
this.tempEditMap.put(admin.getUniqueId(), userData);
admin.openInventory(userData.getHolder().getInventory());
}
/**
* Handles the InventoryCloseEvent to save changes made to an offline player's bag inventory when it's closed.
*
* @param event The InventoryCloseEvent triggered when the inventory is closed.
*/
@EventHandler
public void onInvClose(InventoryCloseEvent event) {
if (!(event.getInventory().getHolder() instanceof FishingBagHolder))
@@ -92,6 +112,12 @@ public class BagManagerImpl implements BagManager, Listener {
plugin.getStorageManager().saveUserData(offlineUser, true);
}
/**
* Handles InventoryClickEvent to prevent certain actions on the Fishing Bag inventory.
* This method cancels the event if specific conditions are met to restrict certain item interactions.
*
* @param event The InventoryClickEvent triggered when an item is clicked in an inventory.
*/
@EventHandler
public void onInvClick(InventoryClickEvent event) {
if (event.isCancelled())
@@ -106,18 +132,28 @@ public class BagManagerImpl implements BagManager, Listener {
return;
if (CFConfig.bagWhiteListItems.contains(clickedItem.getType()))
return;
String id = plugin.getItemManager().getAnyItemID(clickedItem);
if (plugin.getEffectManager().getEffect("rod", id) != null)
return;
if (plugin.getEffectManager().getEffect("bait", id) != null)
return;
if (plugin.getEffectManager().getEffect("util", id) != null)
String id = plugin.getItemManager().getAnyPluginItemID(clickedItem);
EffectManager effectManager = plugin.getEffectManager();
if (effectManager.hasEffectCarrier("rod", id)
|| effectManager.hasEffectCarrier("bait", id)
|| effectManager.hasEffectCarrier("util", id)
|| effectManager.hasEffectCarrier("hook", id)
) {
return;
}
if (CFConfig.bagStoreLoots && plugin.getLootManager().getLoot(id) != null)
return;
event.setCancelled(true);
}
/**
* Event handler for the PlayerQuitEvent.
* This method is triggered when a player quits the server.
* It checks if the player was in the process of editing an offline player's bag inventory,
* and if so, saves the offline player's data if necessary.
*
* @param event The PlayerQuitEvent triggered when a player quits.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
OfflineUser offlineUser = tempEditMap.remove(event.getPlayer().getUniqueId());

View File

@@ -50,6 +50,7 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.*;
@@ -74,17 +75,32 @@ public class BlockManagerImpl implements BlockManager, Listener {
this.registerInbuiltProperties();
}
/**
* Event handler for the EntityChangeBlockEvent.
* This method is triggered when an entity changes a block, typically when a block falls or lands.
*/
@EventHandler
public void onBlockLands(EntityChangeBlockEvent event) {
if (event.isCancelled()) return;
if (event.isCancelled())
return;
// Retrieve a custom string value stored in the entity's persistent data container.
String temp = event.getEntity().getPersistentDataContainer().get(
Objects.requireNonNull(NamespacedKey.fromString("block", CustomFishingPlugin.get())),
PersistentDataType.STRING
);
// If the custom string value is not present, return without further action.
if (temp == null) return;
// "BLOCK;PLAYER"
String[] split = temp.split(";");
// If no BlockConfig is found for the specified key, return without further action.
BlockConfig blockConfig = blockConfigMap.get(split[0]);
if (blockConfig == null) return;
// If the player is not online or not found, remove the entity and set the block to air
Player player = Bukkit.getPlayer(split[1]);
if (player == null) {
event.getEntity().remove();
@@ -92,6 +108,8 @@ public class BlockManagerImpl implements BlockManager, Listener {
return;
}
Location location = event.getBlock().getLocation();
// Apply block state modifiers from the BlockConfig to the block 1 tick later.
plugin.getScheduler().runTaskSyncLater(() -> {
BlockState state = location.getBlock().getState();
for (BlockStateModifier modifier : blockConfig.getStateModifierList()) {
@@ -100,23 +118,40 @@ public class BlockManagerImpl implements BlockManager, Listener {
}, location, 50, TimeUnit.MILLISECONDS);
}
/**
* Registers a BlockLibrary instance.
* This method associates a BlockLibrary with its unique identification and adds it to the registry.
*
* @param blockLibrary The BlockLibrary instance to register.
* @return True if the registration was successful (the identification is not already registered), false otherwise.
*/
@Override
public boolean registerBlockLibrary(BlockLibrary library) {
if (this.blockLibraryMap.containsKey(library.identification())) return false;
this.blockLibraryMap.put(library.identification(), library);
public boolean registerBlockLibrary(BlockLibrary blockLibrary) {
if (this.blockLibraryMap.containsKey(blockLibrary.identification())) return false;
this.blockLibraryMap.put(blockLibrary.identification(), blockLibrary);
return true;
}
/**
* Unregisters a BlockLibrary instance by its identification.
* This method removes a BlockLibrary from the registry based on its unique identification.
*
* @param identification The unique identification of the BlockLibrary to unregister.
* @return True if the BlockLibrary was successfully unregistered, false if it was not found.
*/
@Override
public boolean unregisterBlockLibrary(BlockLibrary library) {
return unregisterBlockLibrary(library.identification());
}
@Override
public boolean unregisterBlockLibrary(String library) {
return blockLibraryMap.remove(library) != null;
public boolean unregisterBlockLibrary(String identification) {
return blockLibraryMap.remove(identification) != null;
}
/**
* Registers a BlockDataModifierBuilder for a specific type.
* This method associates a BlockDataModifierBuilder with its type and adds it to the registry.
*
* @param type The type of the BlockDataModifierBuilder to register.
* @param builder The BlockDataModifierBuilder instance to register.
* @return True if the registration was successful (the type is not already registered), false otherwise.
*/
@Override
public boolean registerBlockDataModifierBuilder(String type, BlockDataModifierBuilder builder) {
if (dataBuilderMap.containsKey(type)) return false;
@@ -124,6 +159,14 @@ public class BlockManagerImpl implements BlockManager, Listener {
return true;
}
/**
* Registers a BlockStateModifierBuilder for a specific type.
* This method associates a BlockStateModifierBuilder with its type and adds it to the registry.
*
* @param type The type of the BlockStateModifierBuilder to register.
* @param builder The BlockStateModifierBuilder instance to register.
* @return True if the registration was successful (the type is not already registered), false otherwise.
*/
@Override
public boolean registerBlockStateModifierBuilder(String type, BlockStateModifierBuilder builder) {
if (stateBuilderMap.containsKey(type)) return false;
@@ -131,6 +174,28 @@ public class BlockManagerImpl implements BlockManager, Listener {
return true;
}
/**
* Unregisters a BlockDataModifierBuilder with the specified type.
*
* @param type The type of the BlockDataModifierBuilder to unregister.
* @return True if the BlockDataModifierBuilder was successfully unregistered, false otherwise.
*/
@Override
public boolean unregisterBlockDataModifierBuilder(String type) {
return dataBuilderMap.remove(type) != null;
}
/**
* Unregisters a BlockStateModifierBuilder with the specified type.
*
* @param type The type of the BlockStateModifierBuilder to unregister.
* @return True if the BlockStateModifierBuilder was successfully unregistered, false otherwise.
*/
@Override
public boolean unregisterBlockStateModifierBuilder(String type) {
return stateBuilderMap.remove(type) != null;
}
public void load() {
this.loadConfig();
Bukkit.getPluginManager().registerEvents(this, plugin);
@@ -162,6 +227,10 @@ public class BlockManagerImpl implements BlockManager, Listener {
this.blockLibraryMap.clear();
}
/**
* Loads configuration files from the plugin's data folder and processes them.
* Configuration files are organized by type (e.g., "block").
*/
@SuppressWarnings("DuplicatedCode")
private void loadConfig() {
Deque<File> fileDeque = new ArrayDeque<>();
@@ -187,10 +256,17 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
}
/**
* Loads configuration data from a single YAML file and processes it to create BlockConfig instances.
*
* @param file The YAML file to load and process.
*/
private void loadSingleFile(File file) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
for (Map.Entry<String, Object> entry : config.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection section) {
// Check if the "block" is null and log a warning if so.
String blockID = section.getString("block");
if (blockID == null) {
LogUtils.warn("Block can't be null. File:" + file.getAbsolutePath() + "; Section:" + section.getCurrentPath());
@@ -198,6 +274,8 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
List<BlockDataModifier> dataModifiers = new ArrayList<>();
List<BlockStateModifier> stateModifiers = new ArrayList<>();
// If a "properties" section exists, process its entries.
ConfigurationSection property = section.getConfigurationSection("properties");
if (property != null) {
for (Map.Entry<String, Object> innerEntry : property.getValues(false).entrySet()) {
@@ -212,6 +290,8 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
}
}
// Create a BlockConfig instance with the processed data and add it to the blockConfigMap.
BlockConfig blockConfig = new BlockConfig.Builder()
.blockID(blockID)
.persist(false)
@@ -225,6 +305,15 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
}
/**
* Summons a falling block at a specified location based on the provided loot.
* This method spawns a falling block at the given hookLocation with specific properties determined by the loot.
*
* @param player The player who triggered the action.
* @param hookLocation The location where the hook is positioned.
* @param playerLocation The location of the player.
* @param loot The loot to be associated with the summoned block.
*/
@Override
public void summonBlock(Player player, Location hookLocation, Location playerLocation, Loot loot) {
BlockConfig config = blockConfigMap.get(loot.getID());
@@ -253,8 +342,17 @@ public class BlockManagerImpl implements BlockManager, Listener {
fallingBlock.setVelocity(vector);
}
/**
* Retrieves the block ID associated with a given Block instance using block detection order.
* This method iterates through the configured block detection order to find the block's ID
* by checking different BlockLibrary instances in the specified order.
*
* @param block The Block instance for which to retrieve the block ID.
* @return The block ID
*/
@Override
public String getAnyBlockID(Block block) {
@NotNull
public String getAnyPluginBlockID(Block block) {
for (String plugin : CFConfig.blockDetectOrder) {
BlockLibrary blockLibrary = blockLibraryMap.get(plugin);
if (blockLibrary != null) {
@@ -264,8 +362,8 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
}
}
// should not reach this because vanilla library would always work
return null;
// Should not reach this because vanilla library would always work
return "AIR";
}
private void registerDirectional() {
@@ -389,6 +487,13 @@ public class BlockManagerImpl implements BlockManager, Listener {
});
}
/**
* Sets items in the BLOCK's inventory based on chance and configuration.
*
* @param tempChanceList A list of tuples containing chance, item ID, and quantity range for each item.
* @param player The inventory items are being set.
* @param inventory The inventory where the items will be placed.
*/
private void setInventoryItems(
ArrayList<Tuple<Double, String, Pair<Integer, Integer>>> tempChanceList,
Player player,
@@ -400,11 +505,11 @@ public class BlockManagerImpl implements BlockManager, Listener {
}
Collections.shuffle(unused);
for (Tuple<Double, String, Pair<Integer, Integer>> tuple : tempChanceList) {
ItemStack itemStack = plugin.getItemManager().buildAnyItemByID(player, tuple.getMid());
ItemStack itemStack = plugin.getItemManager().buildAnyPluginItemByID(player, tuple.getMid());
itemStack.setAmount(ThreadLocalRandom.current().nextInt(tuple.getRight().left(), tuple.getRight().right() + 1));
if (tuple.getLeft() > Math.random()) {
inventory.setItem(unused.pop(), itemStack);
}
}
}
}
}

View File

@@ -35,10 +35,12 @@ import net.momirealms.customfishing.setting.CFLocale;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -62,13 +64,19 @@ public class Competition implements FishingCompetition {
if (CFConfig.redisRanking) this.ranking = new RedisRankingImpl();
else this.ranking = new LocalRankingImpl();
this.publicPlaceholders = new ConcurrentHashMap<>();
this.publicPlaceholders.put("{goal}", CustomFishingPlugin.get().getCompetitionManager().getCompetitionLocale(goal));
this.publicPlaceholders.put("{goal}", CustomFishingPlugin.get().getCompetitionManager().getCompetitionGoalLocale(goal));
}
/**
* Starts the fishing competition, initializing its settings and actions.
* This method sets the initial progress, remaining time, start time, and updates public placeholders.
* It also arranges timer tasks for competition timing and initializes boss bar and action bar managers if configured.
* Additionally, it triggers the start actions defined in the competition's configuration.
*/
@Override
public void start() {
this.progress = 1;
this.remainingTime = config.getDuration();
this.remainingTime = config.getDurationInSeconds();
this.startTime = Instant.now().getEpochSecond();
this.updatePublicPlaceholders();
@@ -92,6 +100,11 @@ public class Competition implements FishingCompetition {
}
}
/**
* Arranges the timer task for the fishing competition.
* This method schedules a recurring task that updates the competition's remaining time and public placeholders.
* If the remaining time reaches zero, the competition is ended.
*/
private void arrangeTimerTask() {
this.competitionTimerTask = CustomFishingPlugin.get().getScheduler().runTaskAsyncTimer(() -> {
if (decreaseTime()) {
@@ -102,6 +115,12 @@ public class Competition implements FishingCompetition {
}, 1, 1, TimeUnit.SECONDS);
}
/**
* Update public placeholders for the fishing competition.
* This method updates placeholders representing player rankings, remaining time, and score in public messages.
* Placeholders for player rankings include {1_player}, {1_score}, {2_player}, {2_score}, and so on.
* The placeholders for time include {hour}, {minute}, {second}, and {seconds}.
*/
private void updatePublicPlaceholders() {
for (int i = 1; i < CFConfig.placeholderLimit + 1; i++) {
int finalI = i;
@@ -119,6 +138,11 @@ public class Competition implements FishingCompetition {
publicPlaceholders.put("{seconds}", String.valueOf(remainingTime));
}
/**
* Stop the fishing competition.
* This method cancels the competition timer task, unloads boss bars and action bars, clears the ranking,
* and sets the remaining time to zero.
*/
@Override
public void stop() {
if (!competitionTimerTask.isCancelled()) this.competitionTimerTask.cancel();
@@ -128,6 +152,11 @@ public class Competition implements FishingCompetition {
this.remainingTime = 0;
}
/**
* End the fishing competition.
* This method marks the competition as ended, cancels sub-tasks such as timers and bar management,
* gives prizes to top participants and participation rewards, performs end actions, and clears the ranking.
*/
@Override
public void end() {
// mark it as ended
@@ -182,19 +211,36 @@ public class Competition implements FishingCompetition {
CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(this.ranking::clear, 1500, TimeUnit.MILLISECONDS);
}
/**
* Check if the fishing competition is ongoing.
*
* @return {@code true} if the competition is still ongoing, {@code false} if it has ended.
*/
@Override
public boolean isOnGoing() {
return remainingTime > 0;
}
/**
* Decreases the remaining time for the fishing competition and updates the progress.
*
* @return {@code true} if the remaining time becomes zero or less, indicating the competition has ended.
*/
private boolean decreaseTime() {
long current = Instant.now().getEpochSecond();
int duration = config.getDuration();
int duration = config.getDurationInSeconds();
remainingTime = duration - (current - startTime);
progress = (float) remainingTime / duration;
return remainingTime <= 0;
}
/**
* Refreshes the data for a player in the fishing competition, including updating their score and triggering
* actions if it's their first time joining the competition.
*
* @param player The player whose data needs to be refreshed.
* @param score The player's current score in the competition.
*/
@Override
public void refreshData(Player player, double score) {
// if player join for the first time, trigger join actions
@@ -224,46 +270,97 @@ public class Competition implements FishingCompetition {
}
}
/**
* Checks if a player has joined the fishing competition based on their name.
*
* @param player The player to check for participation.
* @return {@code true} if the player has joined the competition; {@code false} otherwise.
*/
@Override
public boolean hasPlayerJoined(OfflinePlayer player) {
return ranking.getPlayerRank(player.getName()) != -1;
}
/**
* Gets the progress of the fishing competition as a float value (0~1).
*
* @return The progress of the fishing competition as a float.
*/
@Override
public float getProgress() {
return progress;
}
/**
* Gets the remaining time in seconds for the fishing competition.
*
* @return The remaining time in seconds.
*/
@Override
public long getRemainingTime() {
return remainingTime;
}
/**
* Gets the start time of the fishing competition.
*
* @return The start time of the fishing competition.
*/
@Override
public long getStartTime() {
return startTime;
}
/**
* Gets the configuration of the fishing competition.
*
* @return The configuration of the fishing competition.
*/
@NotNull
@Override
public CompetitionConfig getConfig() {
return config;
}
/**
* Gets the goal of the fishing competition.
*
* @return The goal of the fishing competition.
*/
@NotNull
@Override
public CompetitionGoal getGoal() {
return goal;
}
/**
* Gets the ranking data for the fishing competition.
*
* @return The ranking data for the fishing competition.
*/
@NotNull
@Override
public Ranking getRanking() {
return ranking;
}
/**
* Gets the cached placeholders for the fishing competition.
*
* @return A ConcurrentHashMap containing cached placeholders.
*/
@NotNull
@Override
public ConcurrentHashMap<String, String> getCachedPlaceholders() {
public Map<String, String> getCachedPlaceholders() {
return publicPlaceholders;
}
/**
* Gets a specific cached placeholder value by its key.
*
* @param papi The key of the cached placeholder.
* @return The cached placeholder value as a string, or null if not found.
*/
@Override
public String getCachedPlaceholder(String papi) {
return publicPlaceholders.get(papi);

View File

@@ -25,7 +25,6 @@ import net.momirealms.customfishing.api.mechanic.competition.*;
import net.momirealms.customfishing.api.mechanic.condition.Condition;
import net.momirealms.customfishing.api.scheduler.CancellableTask;
import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.setting.CFConfig;
import net.momirealms.customfishing.setting.CFLocale;
import net.momirealms.customfishing.storage.method.database.nosql.RedisManager;
import org.bukkit.Bukkit;
@@ -33,12 +32,12 @@ import org.bukkit.boss.BarColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompetitionManagerImpl implements CompetitionManager {
@@ -84,8 +83,14 @@ public class CompetitionManagerImpl implements CompetitionManager {
currentCompetition.stop();
}
/**
* Retrieves a set of all competition names.
*
* @return A set of competition names.
*/
@NotNull
@Override
public Set<String> getAllCompetitions() {
public Set<String> getAllCompetitionKeys() {
return commandConfigMap.keySet();
}
@@ -202,8 +207,15 @@ public class CompetitionManagerImpl implements CompetitionManager {
}
}
/**
* Retrieves the localization key for a given competition goal.
*
* @param goal The competition goal to retrieve the localization key for.
* @return The localization key for the specified competition goal.
*/
@NotNull
@Override
public String getCompetitionLocale(CompetitionGoal goal) {
public String getCompetitionGoalLocale(CompetitionGoal goal) {
switch (goal) {
case MAX_SIZE -> {
return CFLocale.MSG_Max_Size;
@@ -221,16 +233,29 @@ public class CompetitionManagerImpl implements CompetitionManager {
return "";
}
/**
* Starts a competition with the specified name, allowing for the option to force start it or apply it to the entire server.
*
* @param competition The name of the competition to start.
* @param force Whether to force start the competition even if amount of the online players is lower than the requirement
* @param allServer Whether to apply the competition to the servers that connected to Redis.
* @return {@code true} if the competition was started successfully, {@code false} otherwise.
*/
@Override
public void startCompetition(String competition, boolean force, boolean allServer) {
public boolean startCompetition(String competition, boolean force, boolean allServer) {
CompetitionConfig config = commandConfigMap.get(competition);
if (config == null) {
LogUtils.warn("Competition " + competition + " doesn't exist.");
return;
return false;
}
startCompetition(config, force, allServer);
return startCompetition(config, force, allServer);
}
/**
* Gets the ongoing fishing competition, if one is currently in progress.
*
* @return The ongoing fishing competition, or null if there is none.
*/
@Override
@Nullable
public FishingCompetition getOnGoingCompetition() {
@@ -238,26 +263,36 @@ public class CompetitionManagerImpl implements CompetitionManager {
return currentCompetition.isOnGoing() ? currentCompetition : null;
}
/**
* Starts a competition using the specified configuration.
*
* @param config The configuration of the competition to start.
* @param force Whether to force the start of the competition.
* @param allServer Whether the competition should start across all servers in the network.
* @return True if the competition was started successfully, false otherwise.
*/
@Override
public void startCompetition(CompetitionConfig config, boolean force, boolean allServer) {
if (!force)
this.getPlayerCount().thenAccept(count -> {
if (count < config.getMinPlayers()) {
var actions = config.getSkipActions();
if (actions != null)
for (Player player : Bukkit.getOnlinePlayers()) {
for (Action action : actions) {
action.trigger(new Condition(player));
}
public boolean startCompetition(CompetitionConfig config, boolean force, boolean allServer) {
if (!force) {
int players = Bukkit.getOnlinePlayers().size();
if (players < config.getMinPlayersToStart()) {
var actions = config.getSkipActions();
if (actions != null)
for (Player player : Bukkit.getOnlinePlayers()) {
for (Action action : actions) {
action.trigger(new Condition(player));
}
return;
}
start(config);
});
else if (!allServer) {
}
return false;
}
start(config);
return true;
} else if (!allServer) {
start(config);
return true;
} else {
RedisManager.getInstance().sendRedisMessage("cf_competition", "start;" + config.getKey());
return true;
}
}
@@ -274,20 +309,22 @@ public class CompetitionManagerImpl implements CompetitionManager {
}
}
/**
* Gets the number of seconds until the next competition.
*
* @return The number of seconds until the next competition.
*/
@Override
public int getNextCompetitionSeconds() {
return nextCompetitionSeconds;
}
@Override
public CompletableFuture<Integer> getPlayerCount() {
if (!CFConfig.redisRanking) {
return CompletableFuture.completedFuture(Bukkit.getOnlinePlayers().size());
} else {
return plugin.getStorageManager().getRedisPlayerCount();
}
}
/**
* Retrieves the configuration for a competition based on its key.
*
* @param key The key of the competition configuration to retrieve.
* @return The {@link CompetitionConfig} for the specified key, or {@code null} if no configuration exists with that key.
*/
@Nullable
@Override
public CompetitionConfig getConfig(String key) {

View File

@@ -21,7 +21,6 @@ import net.momirealms.customfishing.api.CustomFishingPlugin;
import net.momirealms.customfishing.api.common.Key;
import net.momirealms.customfishing.api.common.Pair;
import net.momirealms.customfishing.api.manager.EffectManager;
import net.momirealms.customfishing.api.mechanic.effect.Effect;
import net.momirealms.customfishing.api.mechanic.effect.EffectCarrier;
import net.momirealms.customfishing.api.mechanic.effect.EffectModifier;
import net.momirealms.customfishing.api.mechanic.effect.FishingEffect;
@@ -31,6 +30,7 @@ import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.util.ConfigUtils;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -51,24 +51,61 @@ public class EffectManagerImpl implements EffectManager {
this.effectMap.clear();
}
/**
* Registers an EffectCarrier with a unique Key.
*
* @param key The unique Key associated with the EffectCarrier.
* @param effect The EffectCarrier to be registered.
* @return True if the registration was successful, false if the Key already exists.
*/
@Override
public boolean registerEffectItem(Key key, EffectCarrier effect) {
public boolean registerEffectCarrier(Key key, EffectCarrier effect) {
if (effectMap.containsKey(key)) return false;
this.effectMap.put(key, effect);
return true;
}
/**
* Unregisters an EffectCarrier associated with the specified Key.
*
* @param key The unique Key of the EffectCarrier to unregister.
* @return True if the EffectCarrier was successfully unregistered, false if the Key does not exist.
*/
@Override
public boolean unregisterEffectItem(Key key) {
public boolean unregisterEffectCarrier(Key key) {
return this.effectMap.remove(key) != null;
}
/**
* Checks if an EffectCarrier with the specified namespace and id exists.
*
* @param namespace The namespace of the EffectCarrier.
* @param id The unique identifier of the EffectCarrier.
* @return True if an EffectCarrier with the given namespace and id exists, false otherwise.
*/
@Override
public boolean hasEffectCarrier(String namespace, String id) {
return effectMap.containsKey(Key.of(namespace, id));
}
/**
* Retrieves an EffectCarrier with the specified namespace and id.
*
* @param namespace The namespace of the EffectCarrier.
* @param id The unique identifier of the EffectCarrier.
* @return The EffectCarrier with the given namespace and id, or null if it doesn't exist.
*/
@Nullable
@Override
public EffectCarrier getEffect(String namespace, String id) {
public EffectCarrier getEffectCarrier(String namespace, String id) {
return effectMap.get(Key.of(namespace, id));
}
/**
* Loads EffectCarrier configurations from YAML files in different content folders.
* EffectCarrier configurations are organized by type (rod, bait, enchant, util, totem, hook) in separate folders.
* Each YAML file within these folders is processed to populate the effectMap.
*/
@SuppressWarnings("DuplicatedCode")
public void load() {
Deque<File> fileDeque = new ArrayDeque<>();
@@ -94,20 +131,35 @@ public class EffectManagerImpl implements EffectManager {
}
}
/**
* Loads EffectCarrier configurations from a YAML file and populates the effectMap.
*
* @param file The YAML file to load configurations from.
* @param namespace The namespace to use when creating keys for EffectCarriers.
*/
private void loadSingleFile(File file, String namespace) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (Map.Entry<String, Object> entry : yaml.getValues(false).entrySet()) {
String value = entry.getKey();
if (entry.getValue() instanceof ConfigurationSection section) {
Key key = Key.of(namespace, value);
EffectCarrier item = getEffectItemFromSection(key, section);
EffectCarrier item = getEffectCarrierFromSection(key, section);
if (item != null)
effectMap.put(key, item);
}
}
}
private EffectCarrier getEffectItemFromSection(Key key, ConfigurationSection section) {
/**
* Parses a ConfigurationSection to create an EffectCarrier based on the specified key and configuration.
*
* @param key The key that uniquely identifies the EffectCarrier.
* @param section The ConfigurationSection containing the EffectCarrier configuration.
* @return An EffectCarrier instance based on the key and configuration, or null if the section is null.
*/
@Override
@Nullable
public EffectCarrier getEffectCarrierFromSection(Key key, ConfigurationSection section) {
if (section == null) return null;
return new EffectCarrier.Builder()
.key(key)
@@ -117,23 +169,6 @@ public class EffectManagerImpl implements EffectManager {
.build();
}
public Effect getEffectFromSection(ConfigurationSection section) {
if (section == null) return getInitialEffect();
return new FishingEffect.Builder()
.addWeightModifier(ConfigUtils.getModifiers(section.getStringList("weight-single")))
.addWeightModifier(getGroupModifiers(section.getStringList("weight-group")))
.addWeightModifierIgnored(ConfigUtils.getModifiers(section.getStringList("weight-single-ignore-condition")))
.addWeightModifierIgnored(getGroupModifiers(section.getStringList("weight-group-ignore-condition")))
.timeModifier(section.getDouble("hook-time", 1))
.difficultyModifier(section.getDouble("difficulty", 0))
.multipleLootChance(section.getDouble("multiple-loot", 0))
.lavaFishing(section.getBoolean("lava-fishing", false))
.scoreMultiplier(section.getDouble("score-bonus", 1))
.sizeMultiplier(section.getDouble("size-bonus", 1))
.gameTimeModifier(section.getDouble("game-time", 0))
.build();
}
public void unload() {
HashMap<Key, EffectCarrier> temp = new HashMap<>(effectMap);
effectMap.clear();
@@ -144,11 +179,23 @@ public class EffectManagerImpl implements EffectManager {
}
}
/**
* Retrieves the initial FishingEffect that represents no special effects.
*
* @return The initial FishingEffect.
*/
@NotNull
@Override
public FishingEffect getInitialEffect() {
return new FishingEffect.Builder().build();
}
/**
* Retrieves a list of modifiers based on specified loot groups.
*
* @param modList A list of strings containing group modifiers in the format "group:modifier".
* @return A list of pairs where each pair represents a loot item and its associated modifier.
*/
private List<Pair<String, WeightModifier>> getGroupModifiers(List<String> modList) {
List<Pair<String, WeightModifier>> result = new ArrayList<>();
for (String group : modList) {
@@ -166,6 +213,14 @@ public class EffectManagerImpl implements EffectManager {
return result;
}
/**
* Parses a ConfigurationSection to retrieve an array of EffectModifiers.
*
* @param section The ConfigurationSection to parse.
* @return An array of EffectModifiers based on the values found in the section.
*/
@NotNull
@Override
public EffectModifier[] getEffectModifiers(ConfigurationSection section) {
if (section == null) return new EffectModifier[0];
ArrayList<EffectModifier> modifiers = new ArrayList<>();
@@ -179,6 +234,14 @@ public class EffectManagerImpl implements EffectManager {
return modifiers.toArray(new EffectModifier[0]);
}
/**
* Parses a ConfigurationSection to create an EffectModifier based on the specified type and configuration.
*
* @param section The ConfigurationSection containing the effect modifier configuration.
* @return An EffectModifier instance based on the type and configuration.
*/
@Override
@Nullable
public EffectModifier getEffectModifier(ConfigurationSection section) {
String type = section.getString("type");
if (type == null) return null;

View File

@@ -60,6 +60,12 @@ public class EntityManagerImpl implements EntityManager {
}
}
/**
* Registers an entity library for use in the plugin.
*
* @param entityLibrary The entity library to register.
* @return {@code true} if the entity library was successfully registered, {@code false} if it already exists.
*/
@Override
public boolean registerEntityLibrary(EntityLibrary entityLibrary) {
if (entityLibraryMap.containsKey(entityLibrary.identification())) return false;
@@ -67,14 +73,15 @@ public class EntityManagerImpl implements EntityManager {
return true;
}
/**
* Unregisters an entity library by its identification key.
*
* @param identification The identification key of the entity library to unregister.
* @return {@code true} if the entity library was successfully unregistered, {@code false} if it does not exist.
*/
@Override
public boolean unregisterEntityLibrary(String lib) {
return entityLibraryMap.remove(lib) != null;
}
@Override
public boolean unregisterEntityLibrary(EntityLibrary entityLibrary) {
return unregisterEntityLibrary(entityLibrary.identification());
public boolean unregisterEntityLibrary(String identification) {
return entityLibraryMap.remove(identification) != null;
}
@SuppressWarnings("DuplicatedCode")
@@ -134,6 +141,13 @@ public class EntityManagerImpl implements EntityManager {
this.entityLibraryMap.clear();
}
/**
* Summons an entity based on the given loot configuration to a specified location.
*
* @param hookLocation The location where the entity will be summoned, typically where the fishing hook is.
* @param playerLocation The location of the player who triggered the entity summoning.
* @param loot The loot configuration that defines the entity to be summoned.
*/
@Override
public void summonEntity(Location hookLocation, Location playerLocation, Loot loot) {
EntityConfig config = entityConfigMap.get(loot.getID());

View File

@@ -54,7 +54,7 @@ public class BaitAnimationTask implements Runnable {
) {
cancelAnimation();
} else {
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, FakeItemUtils.getVelocity(entityID, fishHook.getVelocity()));
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, FakeItemUtils.getVelocityPacket(entityID, fishHook.getVelocity()));
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, FakeItemUtils.getTpPacket(entityID, fishHook.getLocation()));
}
}

View File

@@ -43,7 +43,6 @@ import net.momirealms.customfishing.api.mechanic.game.GameSettings;
import net.momirealms.customfishing.api.mechanic.game.GamingPlayer;
import net.momirealms.customfishing.api.mechanic.loot.Loot;
import net.momirealms.customfishing.api.mechanic.loot.LootType;
import net.momirealms.customfishing.api.mechanic.loot.WeightModifier;
import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.api.util.WeightUtils;
import net.momirealms.customfishing.mechanic.requirement.RequirementManagerImpl;
@@ -55,7 +54,6 @@ import org.bukkit.event.*;
import org.bukkit.event.player.*;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -142,7 +140,9 @@ public class FishingManagerImpl implements Listener, FishingManager {
@EventHandler
public void onQuit(PlayerQuitEvent event) {
final Player player = event.getPlayer();
this.removeHook(event.getPlayer().getUniqueId());
this.removeTempFishingState(player);
}
@EventHandler
@@ -189,6 +189,12 @@ public class FishingManagerImpl implements Listener, FishingManager {
}
}
/**
* Removes a fishing hook entity associated with a given UUID.
*
* @param uuid The UUID of the fishing hook entity to be removed.
* @return {@code true} if the fishing hook was successfully removed, {@code false} otherwise.
*/
@Override
public boolean removeHook(UUID uuid) {
FishHook hook = hookCacheMap.remove(uuid);
@@ -200,11 +206,32 @@ public class FishingManagerImpl implements Listener, FishingManager {
}
}
/**
* Retrieves a FishHook object associated with the provided player's UUID
*
* @param uuid The UUID of the player
* @return fishhook entity, null if not exists
*/
@Override
public Optional<FishHook> getHook(UUID uuid) {
return Optional.ofNullable(hookCacheMap.get(uuid));
@Nullable
public FishHook getHook(UUID uuid) {
FishHook fishHook = hookCacheMap.get(uuid);
if (fishHook != null) {
if (!fishHook.isValid()) {
hookCacheMap.remove(uuid);
return null;
} else {
return fishHook;
}
}
return null;
}
/**
* Selects the appropriate fishing state based on the provided PlayerFishEvent and triggers the corresponding action.
*
* @param event The PlayerFishEvent that represents the fishing action.
*/
public void selectState(PlayerFishEvent event) {
if (event.isCancelled()) return;
switch (event.getState()) {
@@ -229,7 +256,7 @@ public class FishingManagerImpl implements Listener, FishingManager {
if (compound != null && compound.hasTag("max_dur")) {
event.setCancelled(true);
hook.remove();
ItemUtils.loseDurability(itemStack, 2, true);
ItemUtils.decreaseDurability(itemStack, 2, true);
}
}
}
@@ -243,9 +270,8 @@ public class FishingManagerImpl implements Listener, FishingManager {
return;
}
// Check mechanic requirements
if (!RequirementManager.isRequirementsMet(
RequirementManagerImpl.mechanicRequirements,
fishingPreparation
if (!RequirementManager.isRequirementMet(
fishingPreparation, RequirementManagerImpl.mechanicRequirements
)) {
return;
}
@@ -330,7 +356,7 @@ public class FishingManagerImpl implements Listener, FishingManager {
if (nbtCompound != null && nbtCompound.hasTag("max_dur")) {
event.getHook().remove();
event.setCancelled(true);
ItemUtils.loseDurability(itemStack, 5, true);
ItemUtils.decreaseDurability(itemStack, 5, true);
}
}
}
@@ -444,11 +470,21 @@ public class FishingManagerImpl implements Listener, FishingManager {
}
}
/**
* Removes the temporary fishing state associated with a player.
*
* @param player The player whose temporary fishing state should be removed.
*/
@Override
public void removeTempFishingState(Player player) {
this.tempFishingStateMap.remove(player.getUniqueId());
public TempFishingState removeTempFishingState(Player player) {
return this.tempFishingStateMap.remove(player.getUniqueId());
}
/**
* Processes the game result for a gaming player
*
* @param gamingPlayer The gaming player whose game result should be processed.
*/
@Override
public void processGameResult(GamingPlayer gamingPlayer) {
final Player player = gamingPlayer.getPlayer();
@@ -458,7 +494,7 @@ public class FishingManagerImpl implements Listener, FishingManager {
LogUtils.warn("Unexpected situation: Can't get player's fish hook when processing game results.");
return;
}
TempFishingState tempFishingState = tempFishingStateMap.remove(uuid);
TempFishingState tempFishingState = removeTempFishingState(player);
if (tempFishingState == null) {
LogUtils.warn("Unexpected situation: Can't get player's fishing state when processing game results.");
return;
@@ -479,8 +515,8 @@ public class FishingManagerImpl implements Listener, FishingManager {
if (damageEvent.isCancelled()) {
break outer;
}
ItemUtils.reduceHookDurability(rod, false);
ItemUtils.loseDurability(rod, 1, true);
ItemUtils.decreaseHookDurability(rod, 1, false);
ItemUtils.decreaseDurability(rod, 1, true);
}
fishHook.remove();
@@ -520,7 +556,7 @@ public class FishingManagerImpl implements Listener, FishingManager {
loot.triggerActions(ActionTrigger.FAILURE, fishingPreparation);
fishingPreparation.triggerActions(ActionTrigger.FAILURE);
ItemUtils.reduceHookDurability(fishingPreparation.getRodItemStack(), true);
ItemUtils.decreaseHookDurability(fishingPreparation.getRodItemStack(), 1, true);
}
public void success(TempFishingState state, FishHook hook) {
@@ -565,7 +601,7 @@ public class FishingManagerImpl implements Listener, FishingManager {
}
} else {
for (int i = 0; i < amount; i++) {
plugin.getItemManager().dropItem(player, hook.getLocation(), player.getLocation(), loot, fishingPreparation.getArgs());
plugin.getItemManager().dropItem(player, hook.getLocation(), player.getLocation(), loot.getID(), fishingPreparation.getArgs());
doSuccessActions(loot, effect, fishingPreparation, player);
}
}
@@ -629,67 +665,56 @@ public class FishingManagerImpl implements Listener, FishingManager {
).ifPresent(it -> it.addLootAmount(loot, fishingPreparation, 1));
}
@Override
public Collection<String> getPossibleLootKeys (Condition condition) {
return plugin.getRequirementManager().getLootWithWeight(condition).keySet();
}
@NotNull
@Override
public Map<String, Double> getPossibleLootKeysWithWeight(Effect initialEffect, Condition condition) {
Map<String, Double> lootWithWeight = plugin.getRequirementManager().getLootWithWeight(condition);
Player player = condition.getPlayer();
for (Pair<String, WeightModifier> pair : initialEffect.getWeightModifier()) {
Double previous = lootWithWeight.get(pair.left());
if (previous != null)
lootWithWeight.put(pair.left(), pair.right().modify(player, previous));
}
for (Pair<String, WeightModifier> pair : initialEffect.getWeightModifierIgnored()) {
double previous = lootWithWeight.getOrDefault(pair.left(), 0d);
lootWithWeight.put(pair.left(), pair.right().modify(player, previous));
}
return lootWithWeight;
}
@Override
@Nullable
public Loot getNextLoot(Effect initialEffect, Condition condition) {
String key = WeightUtils.getRandom(getPossibleLootKeysWithWeight(initialEffect, condition));
Loot loot = plugin.getLootManager().getLoot(key);
if (loot == null) {
LogUtils.warn(String.format("Loot %s doesn't exist!", key));
return null;
}
return loot;
}
/**
* Starts a fishing game for the specified player with the given condition and effect.
*
* @param player The player starting the fishing game.
* @param condition The condition used to determine the game.
* @param effect The effect applied to the game.
*/
@Override
public void startFishingGame(Player player, Condition condition, Effect effect) {
Map<String, Double> gameWithWeight = plugin.getRequirementManager().getGameWithWeight(condition);
Map<String, Double> gameWithWeight = plugin.getGameManager().getGameWithWeight(condition);
plugin.debug(gameWithWeight.toString());
String random = WeightUtils.getRandom(gameWithWeight);
Optional<Pair<BasicGameConfig, GameInstance>> gamePair = plugin.getGameManager().getGame(random);
if (gamePair.isEmpty()) {
LogUtils.warn(String.format("Game %s doesn't exist!", random));
Pair<BasicGameConfig, GameInstance> gamePair = plugin.getGameManager().getGameInstance(random);
if (random == null) {
LogUtils.warn("No game is available for player:" + player.getName() + " location:" + condition.getLocation());
return;
}
if (gamePair == null) {
LogUtils.warn(String.format("Game %s doesn't exist.", random));
return;
}
plugin.debug("Game: " + random);
startFishingGame(player, Objects.requireNonNull(gamePair.get().left().getGameSetting(effect)), gamePair.get().right());
startFishingGame(player, Objects.requireNonNull(gamePair.left().getGameSetting(effect)), gamePair.right());
}
/**
* Starts a fishing game for the specified player with the given settings and game instance.
*
* @param player The player starting the fishing game.
* @param settings The game settings for the fishing game.
* @param gameInstance The instance of the fishing game to start.
*/
@Override
public void startFishingGame(Player player, GameSettings settings, GameInstance gameInstance) {
plugin.debug("Difficulty:" + settings.getDifficulty());
plugin.debug("Time:" + settings.getTime());
Optional<FishHook> hook = getHook(player.getUniqueId());
if (hook.isPresent()) {
this.gamingPlayerMap.put(player.getUniqueId(), gameInstance.start(player, hook.get(), settings));
FishHook hook = getHook(player.getUniqueId());
if (hook != null) {
this.gamingPlayerMap.put(player.getUniqueId(), gameInstance.start(player, hook, settings));
} else {
LogUtils.warn("It seems that player " + player.getName() + " is not fishing. Fishing game failed to start.");
}
}
/**
* Checks if a player with the given UUID has cast their fishing hook.
*
* @param uuid The UUID of the player to check.
* @return {@code true} if the player has cast their fishing hook, {@code false} otherwise.
*/
@Override
public boolean hasPlayerCastHook(UUID uuid) {
FishHook fishHook = hookCacheMap.get(uuid);
@@ -701,13 +726,42 @@ public class FishingManagerImpl implements Listener, FishingManager {
return true;
}
/**
* Sets the temporary fishing state for a player.
*
* @param player The player for whom to set the temporary fishing state.
* @param tempFishingState The temporary fishing state to set for the player.
*/
@Override
public void setTempFishingState(Player player, TempFishingState tempFishingState) {
tempFishingStateMap.put(player.getUniqueId(), tempFishingState);
}
@Override
public void removeHookCheckTask(Player player) {
hookCheckMap.remove(player.getUniqueId());
}
/**
* Gets the {@link GamingPlayer} object associated with the given UUID.
*
* @param uuid The UUID of the player.
* @return The {@link GamingPlayer} object if found, or {@code null} if not found.
*/
@Override
@Nullable
public GamingPlayer getGamingPlayer(UUID uuid) {
return gamingPlayerMap.get(uuid);
}
/**
* Gets the {@link TempFishingState} object associated with the given UUID.
*
* @param uuid The UUID of the player.
* @return The {@link TempFishingState} object if found, or {@code null} if not found.
*/
@Override
@Nullable
public TempFishingState getTempFishingState(UUID uuid) {
return tempFishingStateMap.get(uuid);
}
}

View File

@@ -155,7 +155,7 @@ public class HookCheckTimerTask implements Runnable {
}
private void setTempState() {
Loot nextLoot = manager.getNextLoot(initialEffect, fishingPreparation);
Loot nextLoot = CustomFishingPlugin.get().getLootManager().getNextLoot(initialEffect, fishingPreparation);
if (nextLoot == null)
return;
this.loot = nextLoot;

View File

@@ -26,6 +26,7 @@ import net.momirealms.customfishing.api.mechanic.game.*;
import net.momirealms.customfishing.api.util.FontUtils;
import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.api.util.OffsetUtils;
import net.momirealms.customfishing.mechanic.requirement.RequirementManagerImpl;
import net.momirealms.customfishing.util.ClassUtils;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -42,13 +43,13 @@ public class GameManagerImpl implements GameManager {
private final CustomFishingPlugin plugin;
private final HashMap<String, GameFactory> gameCreatorMap;
private final HashMap<String, Pair<BasicGameConfig, GameInstance>> gameMap;
private final HashMap<String, Pair<BasicGameConfig, GameInstance>> gameInstanceMap;
private final String EXPANSION_FOLDER = "expansions/minigame";
public GameManagerImpl(CustomFishingPlugin plugin) {
this.plugin = plugin;
this.gameCreatorMap = new HashMap<>();
this.gameMap = new HashMap<>();
this.gameInstanceMap = new HashMap<>();
this.registerInbuiltGames();
}
@@ -64,7 +65,7 @@ public class GameManagerImpl implements GameManager {
}
public void unload() {
this.gameMap.clear();
this.gameInstanceMap.clear();
}
public void disable() {
@@ -72,6 +73,13 @@ public class GameManagerImpl implements GameManager {
this.gameCreatorMap.clear();
}
/**
* Registers a new game type with the specified type identifier.
*
* @param type The type identifier for the game.
* @param gameFactory The {@link GameFactory} that creates instances of the game.
* @return {@code true} if the registration was successful, {@code false} if the type identifier is already registered.
*/
@Override
public boolean registerGameType(String type, GameFactory gameFactory) {
if (gameCreatorMap.containsKey(type))
@@ -81,27 +89,56 @@ public class GameManagerImpl implements GameManager {
return true;
}
/**
* Unregisters a game type with the specified type identifier.
*
* @param type The type identifier of the game to unregister.
* @return {@code true} if the game type was successfully unregistered, {@code false} if the type identifier was not found.
*/
@Override
public boolean unregisterGameType(String type) {
return gameCreatorMap.remove(type) != null;
}
/**
* Retrieves the game factory associated with the specified game type.
*
* @param type The type identifier of the game.
* @return The {@code GameFactory} for the specified game type, or {@code null} if not found.
*/
@Override
@Nullable
public GameFactory getGameFactory(String type) {
return gameCreatorMap.get(type);
}
/**
* Retrieves a game instance and its basic configuration associated with the specified key.
*
* @param key The key identifying the game instance.
* @return An {@code Optional} containing a {@code Pair} of the basic game configuration and the game instance
* if found, or an empty {@code Optional} if not found.
*/
@Override
public Optional<Pair<BasicGameConfig, GameInstance>> getGame(String key) {
return Optional.ofNullable(gameMap.get(key));
public Pair<BasicGameConfig, GameInstance> getGameInstance(String key) {
return gameInstanceMap.get(key);
}
/**
* Retrieves a map of game names and their associated weights based on the specified conditions.
*
* @param condition The condition to evaluate game weights.
* @return A {@code HashMap} containing game names as keys and their associated weights as values.
*/
@Override
public HashMap<String, Double> getGameWithWeight(Condition condition) {
return plugin.getRequirementManager().getGameWithWeight(condition);
return ((RequirementManagerImpl) plugin.getRequirementManager()).getGameWithWeight(condition);
}
/**
* Loads minigames from the plugin folder.
* This method searches for minigame configuration files in the plugin's data folder and loads them.
*/
public void loadGamesFromPluginFolder() {
Deque<File> fileDeque = new ArrayDeque<>();
File typeFolder = new File(plugin.getDataFolder() + File.separator + "contents" + File.separator + "minigame");
@@ -124,11 +161,22 @@ public class GameManagerImpl implements GameManager {
}
}
/**
* Loads a minigame configuration from a YAML file.
* This method parses the YAML file and extracts minigame configurations to be used in the plugin.
*
* @param file The YAML file to load.
*/
private void loadSingleFile(File file) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
for (Map.Entry<String, Object> entry : config.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection section) {
GameFactory creator = this.getGameFactory(section.getString("game-type"));
if (creator == null) {
LogUtils.warn("Game type:" + section.getString("game-type") + " doesn't exist.");
continue;
}
BasicGameConfig.Builder basicGameBuilder = new BasicGameConfig.Builder();
Object time = section.get("time", 15);
if (time instanceof String str) {
@@ -144,9 +192,7 @@ public class GameManagerImpl implements GameManager {
} else if (difficulty instanceof Integer integer) {
basicGameBuilder.difficulty(integer);
}
if (creator != null) {
gameMap.put(entry.getKey(), Pair.of(basicGameBuilder.build(), creator.setArgs(section)));
}
gameInstanceMap.put(entry.getKey(), Pair.of(basicGameBuilder.build(), creator.setArgs(section)));
}
}
}
@@ -451,6 +497,9 @@ public class GameManagerImpl implements GameManager {
}));
}
/**
* Loads minigame expansions from the expansion folder.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private void loadExpansions() {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);

View File

@@ -67,6 +67,9 @@ public class HookManagerImpl implements Listener, HookManager {
unload();
}
/**
* Loads configuration files for the specified types.
*/
@SuppressWarnings("DuplicatedCode")
private void loadConfig() {
Deque<File> fileDeque = new ArrayDeque<>();
@@ -92,6 +95,11 @@ public class HookManagerImpl implements Listener, HookManager {
}
}
/**
* Loads data from a single configuration file.
*
* @param file The configuration file to load.
*/
private void loadSingleFile(File file) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
for (Map.Entry<String, Object> entry : config.getValues(false).entrySet()) {
@@ -105,13 +113,120 @@ public class HookManagerImpl implements Listener, HookManager {
}
}
/**
* Get the hook setting by its ID.
*
* @param id The ID of the hook setting to retrieve.
* @return The hook setting with the given ID, or null if not found.
*/
@Nullable
@Override
public HookSetting getHookSetting(String id) {
return hookSettingMap.get(id);
}
/**
* Decreases the durability of a fishing hook by a specified amount and optionally updates its lore.
*
* @param rod The fishing rod ItemStack to modify.
* @param amount The amount by which to decrease the durability.
* @param updateLore Whether to update the lore of the fishing rod.
*/
@Override
public void decreaseHookDurability(ItemStack rod, int amount, boolean updateLore) {
ItemUtils.decreaseHookDurability(rod, amount, updateLore);
}
/**
* Increases the durability of a fishing hook by a specified amount and optionally updates its lore.
*
* @param rod The fishing rod ItemStack to modify.
* @param amount The amount by which to increase the durability.
* @param updateLore Whether to update the lore of the fishing rod.
*/
@Override
public void increaseHookDurability(ItemStack rod, int amount, boolean updateLore) {
ItemUtils.increaseHookDurability(rod, amount, updateLore);
}
/**
* Sets the durability of a fishing hook to a specific amount and optionally updates its lore.
*
* @param rod The fishing rod ItemStack to modify.
* @param amount The new durability value to set.
* @param updateLore Whether to update the lore of the fishing rod.
*/
@Override
public void setHookDurability(ItemStack rod, int amount, boolean updateLore) {
ItemUtils.setHookDurability(rod, amount, updateLore);
}
/**
* Equips a fishing hook on a fishing rod.
*
* @param rod The fishing rod ItemStack.
* @param hook The fishing hook ItemStack.
* @return True if the hook was successfully equipped, false otherwise.
*/
@Override
public boolean equipHookOnRod(ItemStack rod, ItemStack hook) {
if (rod == null || hook == null || hook.getType() == Material.AIR || hook.getAmount() != 1)
return false;
if (rod.getType() != Material.FISHING_ROD)
return false;
String hookID = plugin.getItemManager().getAnyPluginItemID(hook);
HookSetting setting = getHookSetting(hookID);
if (setting == null)
return false;
NBTItem rodNBTItem = new NBTItem(rod);
NBTCompound cfCompound = rodNBTItem.getOrCreateCompound("CustomFishing");
cfCompound.setString("hook_id", hookID);
cfCompound.setItemStack("hook_item", hook);
cfCompound.setInteger("hook_dur", ItemUtils.getDurability(hook));
ItemUtils.updateNBTItemLore(rodNBTItem);
rod.setItemMeta(rodNBTItem.getItem().getItemMeta());
return true;
}
/**
* Removes the fishing hook from a fishing rod.
*
* @param rod The fishing rod ItemStack.
* @return The removed fishing hook ItemStack, or null if no hook was found.
*/
@Override
public ItemStack removeHookFromRod(ItemStack rod) {
if (rod == null || rod.getType() != Material.FISHING_ROD)
return null;
NBTItem rodNBTItem = new NBTItem(rod);
NBTCompound cfCompound = rodNBTItem.getCompound("CustomFishing");
if (cfCompound == null)
return null;
ItemStack hook = cfCompound.getItemStack("hook_item");
if (hook != null) {
cfCompound.removeKey("hook_item");
cfCompound.removeKey("hook_id");
cfCompound.removeKey("hook_dur");
ItemUtils.updateNBTItemLore(rodNBTItem);
rod.setItemMeta(rodNBTItem.getItem().getItemMeta());
}
return hook;
}
/**
* Handles the event when a player clicks on a fishing rod in their inventory.
*
* @param event The InventoryClickEvent to handle.
*/
@EventHandler
@SuppressWarnings("deprecation")
public void onDragDrop(InventoryClickEvent event) {
if (event.isCancelled())
return;
@@ -123,14 +238,12 @@ public class HookManagerImpl implements Listener, HookManager {
return;
if (player.getGameMode() != GameMode.SURVIVAL)
return;
if (plugin.getFishingManager().hasPlayerCastHook(player.getUniqueId()))
return;
ItemStack cursor = event.getCursor();
if (cursor == null || cursor.getType() == Material.AIR) {
if (event.getClick() == ClickType.RIGHT) {
if (plugin.getFishingManager().hasPlayerCastHook(player.getUniqueId())) {
return;
}
NBTItem nbtItem = new NBTItem(clicked);
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
if (cfCompound == null)
@@ -150,16 +263,16 @@ public class HookManagerImpl implements Listener, HookManager {
return;
}
String hookID = plugin.getItemManager().getAnyItemID(cursor);
String hookID = plugin.getItemManager().getAnyPluginItemID(cursor);
HookSetting setting = getHookSetting(hookID);
if (setting == null)
return;
Condition condition = new Condition(player, new HashMap<>());
condition.insertArg("{rod}", plugin.getItemManager().getAnyItemID(clicked));
EffectCarrier effectCarrier = plugin.getEffectManager().getEffect("hook", hookID);
condition.insertArg("{rod}", plugin.getItemManager().getAnyPluginItemID(clicked));
EffectCarrier effectCarrier = plugin.getEffectManager().getEffectCarrier("hook", hookID);
if (effectCarrier != null) {
if (!RequirementManager.isRequirementsMet(effectCarrier.getRequirements(), condition)) {
if (!RequirementManager.isRequirementMet(condition, effectCarrier.getRequirements())) {
return;
}
}

View File

@@ -101,6 +101,11 @@ public class ItemManagerImpl implements ItemManager, Listener {
}
}
/**
* Get a set of all item keys in the CustomFishing plugin.
*
* @return A set of item keys.
*/
@Override
public Set<Key> getAllItemsKey() {
return buildableItemMap.keySet();
@@ -151,24 +156,29 @@ public class ItemManagerImpl implements ItemManager, Listener {
}
}
@Override
public boolean registerCustomItem(String namespace, String value, BuildableItem buildableItem) {
Key key = Key.of(namespace, value);
if (buildableItemMap.containsKey(key)) return false;
buildableItemMap.put(key, buildableItem);
return true;
}
@Override
public boolean unregisterCustomItem(String namespace, String value) {
return buildableItemMap.remove(Key.of(namespace, value)) != null;
}
/**
* Build an ItemStack with a specified namespace and value for a player.
*
* @param player The player for whom the ItemStack is being built.
* @param namespace The namespace of the item.
* @param value The value of the item.
* @return The constructed ItemStack.
*/
@Override
public ItemStack build(Player player, String namespace, String value) {
return build(player, namespace, value, new HashMap<>());
}
/**
* Build an ItemStack with a specified namespace and value, replacing placeholders,
* for a player.
*
* @param player The player for whom the ItemStack is being built.
* @param namespace The namespace of the item.
* @param value The value of the item.
* @param placeholders The placeholders to replace in the item's attributes.
* @return The constructed ItemStack, or null if the item doesn't exist.
*/
@Override
public ItemStack build(Player player, String namespace, String value, Map<String, String> placeholders) {
BuildableItem buildableItem = buildableItemMap.get(Key.of(namespace, value));
@@ -176,20 +186,42 @@ public class ItemManagerImpl implements ItemManager, Listener {
return buildableItem.build(player, placeholders);
}
/**
* Build an ItemStack using an ItemBuilder for a player.
*
* @param player The player for whom the ItemStack is being built.
* @param builder The ItemBuilder used to construct the ItemStack.
* @return The constructed ItemStack.
*/
@NotNull
@Override
public ItemStack build(Player player, ItemBuilder builder) {
return build(player, builder, new HashMap<>());
}
/**
* Retrieve a BuildableItem by its namespace and value.
*
* @param namespace The namespace of the BuildableItem.
* @param value The value of the BuildableItem.
* @return The BuildableItem with the specified namespace and value, or null if not found.
*/
@Override
@Nullable
public BuildableItem getBuildableItem(String namespace, String value) {
return buildableItemMap.get(Key.of(namespace, value));
}
/**
* Get the item ID associated with the given ItemStack by checking all available item libraries.
* The detection order is determined by the configuration.
*
* @param itemStack The ItemStack to retrieve the item ID from.
* @return The item ID or "AIR" if not found or if the ItemStack is null or empty.
*/
@NotNull
@Override
public String getAnyItemID(ItemStack itemStack) {
public String getAnyPluginItemID(ItemStack itemStack) {
for (String plugin : CFConfig.itemDetectOrder) {
ItemLibrary itemLibrary = itemLibraryMap.get(plugin);
if (itemLibrary != null) {
@@ -200,11 +232,18 @@ public class ItemManagerImpl implements ItemManager, Listener {
}
}
// should not reach this because vanilla library would always work
return null;
return "AIR";
}
/**
* Build an ItemStack for a player based on the provided item ID.
*
* @param player The player for whom the ItemStack is being built.
* @param id The item ID, which may include a namespace (e.g., "namespace:id").
* @return The constructed ItemStack or null if the ID is not valid.
*/
@Override
public ItemStack buildAnyItemByID(Player player, String id) {
public ItemStack buildAnyPluginItemByID(Player player, String id) {
if (id.contains(":")) {
String[] split = id.split(":", 2);
return itemLibraryMap.get(split[0]).buildItem(player, split[1]);
@@ -213,16 +252,28 @@ public class ItemManagerImpl implements ItemManager, Listener {
}
}
/**
* Checks if the provided ItemStack is a custom fishing item
*
* @param itemStack The ItemStack to check.
* @return True if the ItemStack is a custom fishing item; otherwise, false.
*/
@Override
public boolean isCustomFishingItem(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR) return false;
NBTItem nbtItem = new NBTItem(itemStack);
return nbtItem.hasTag("CustomFishing");
return nbtItem.hasTag("CustomFishing") && !nbtItem.getCompound("CustomFishing").getString("id").equals("");
}
/**
* Get the item ID associated with the given ItemStack, if available.
*
* @param itemStack The ItemStack to retrieve the item ID from.
* @return The item ID or null if not found or if the ItemStack is null or empty.
*/
@Nullable
@Override
public String getItemID(ItemStack itemStack) {
public String getCustomFishingItemID(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR) return null;
NBTItem nbtItem = new NBTItem(itemStack);
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
@@ -230,6 +281,14 @@ public class ItemManagerImpl implements ItemManager, Listener {
return cfCompound.getString("id");
}
/**
* Create a CFBuilder instance for an item configuration section
*
* @param section The configuration section containing item settings.
* @param type The type of the item (e.g., "rod", "bait").
* @param id The unique identifier for the item.
* @return A CFBuilder instance representing the configured item, or null if the section is null.
*/
@Nullable
@Override
public CFBuilder getItemBuilder(ConfigurationSection section, String type, String id) {
@@ -265,7 +324,16 @@ public class ItemManagerImpl implements ItemManager, Listener {
return itemCFBuilder;
}
/**
* Build an ItemStack using the provided ItemBuilder, player, and placeholders.
*
* @param player The player for whom the item is being built.
* @param builder The ItemBuilder that defines the item's properties.
* @param placeholders A map of placeholders and their corresponding values to be applied to the item.
* @return The constructed ItemStack.
*/
@Override
@NotNull
public ItemStack build(Player player, ItemBuilder builder, Map<String, String> placeholders) {
ItemStack temp = itemLibraryMap.get(builder.getLibrary()).buildItem(player, builder.getId());
temp.setAmount(builder.getAmount());
@@ -277,6 +345,12 @@ public class ItemManagerImpl implements ItemManager, Listener {
return nbtItem.getItem();
}
/**
* Register an item library.
*
* @param itemLibrary The item library to register.
* @return True if the item library was successfully registered, false if it already exists.
*/
@Override
public boolean registerItemLibrary(ItemLibrary itemLibrary) {
if (itemLibraryMap.containsKey(itemLibrary.identification())) return false;
@@ -284,21 +358,31 @@ public class ItemManagerImpl implements ItemManager, Listener {
return true;
}
/**
* Unregister an item library.
*
* @param identification The item library to unregister.
* @return True if the item library was successfully unregistered, false if it doesn't exist.
*/
@Override
public boolean unRegisterItemLibrary(ItemLibrary itemLibrary) {
return itemLibraryMap.remove(itemLibrary.identification(), itemLibrary);
public boolean unRegisterItemLibrary(String identification) {
return itemLibraryMap.remove(identification) != null;
}
/**
* Drops an item based on the provided loot, applying velocity from a hook location to a player location.
*
* @param player The player for whom the item is intended.
* @param hookLocation The location where the item will initially drop.
* @param playerLocation The target location towards which the item's velocity is applied.
* @param id The loot object representing the item to be dropped.
* @param args A map of placeholders for item customization.
*/
@Override
public boolean unRegisterItemLibrary(String itemLibrary) {
return itemLibraryMap.remove(itemLibrary) != null;
}
@Override
public void dropItem(Player player, Location hookLocation, Location playerLocation, Loot loot, Map<String, String> args) {
ItemStack item = build(player, "item", loot.getID(), args);
public void dropItem(Player player, Location hookLocation, Location playerLocation, String id, Map<String, String> args) {
ItemStack item = build(player, "item", id, args);
if (item == null) {
LogUtils.warn(String.format("Item %s not exists", loot.getID()));
LogUtils.warn(String.format("Item %s not exists", id));
return;
}
if (item.getType() == Material.AIR) {
@@ -310,6 +394,13 @@ public class ItemManagerImpl implements ItemManager, Listener {
itemEntity.setVelocity(vector);
}
/**
* Drops an item entity at the specified location and applies velocity towards another location.
*
* @param hookLocation The location where the item will initially drop.
* @param playerLocation The target location towards which the item's velocity is applied.
* @param itemStack The item stack to be dropped as an entity.
*/
@Override
public void dropItem(Location hookLocation, Location playerLocation, ItemStack itemStack) {
Entity itemEntity = hookLocation.getWorld().dropItem(hookLocation, itemStack);
@@ -318,6 +409,42 @@ public class ItemManagerImpl implements ItemManager, Listener {
itemEntity.setVelocity(vector);
}
/**
* Decreases the durability of an ItemStack by a specified amount and optionally updates its lore.
*
* @param itemStack The ItemStack to modify.
* @param amount The amount by which to decrease the durability.
* @param updateLore Whether to update the lore of the ItemStack.
*/
@Override
public void decreaseDurability(ItemStack itemStack, int amount, boolean updateLore) {
ItemUtils.decreaseDurability(itemStack, amount, updateLore);
}
/**
* Increases the durability of an ItemStack by a specified amount and optionally updates its lore.
*
* @param itemStack The ItemStack to modify.
* @param amount The amount by which to increase the durability.
* @param updateLore Whether to update the lore of the ItemStack.
*/
@Override
public void increaseDurability(ItemStack itemStack, int amount, boolean updateLore) {
ItemUtils.increaseDurability(itemStack, amount, updateLore);
}
/**
* Sets the durability of an ItemStack to a specific amount and optionally updates its lore.
*
* @param itemStack The ItemStack to modify.
* @param amount The new durability value.
* @param updateLore Whether to update the lore of the ItemStack.
*/
@Override
public void setDurability(ItemStack itemStack, int amount, boolean updateLore) {
ItemUtils.setDurability(itemStack, amount, updateLore);
}
@NotNull
private List<Pair<String, Short>> getEnchantmentPair(ConfigurationSection section) {
List<Pair<String, Short>> list = new ArrayList<>();
@@ -526,7 +653,7 @@ public class ItemManagerImpl implements ItemManager, Listener {
placeholders.put("{bonus}", String.format("%.2f", bonus));
}
float size = Float.parseFloat(placeholders.getOrDefault("{size}", "0"));
double price = CustomFishingPlugin.get().getMarketManager().getPrice(
double price = CustomFishingPlugin.get().getMarketManager().getFishPrice(
base,
bonus,
size
@@ -665,7 +792,7 @@ public class ItemManagerImpl implements ItemManager, Listener {
public void onConsumeItem(PlayerItemConsumeEvent event) {
if (event.isCancelled()) return;
ItemStack itemStack = event.getItem();
String id = getAnyItemID(itemStack);
String id = getAnyPluginItemID(itemStack);
Loot loot = plugin.getLootManager().getLoot(id);
if (loot != null) {
Condition condition = new Condition(event.getPlayer());
@@ -697,7 +824,7 @@ public class ItemManagerImpl implements ItemManager, Listener {
NBTCompound compound = nbtItem.getCompound("CustomFishing");
if (compound == null) return;
event.setCancelled(true);
ItemUtils.addDurability(itemStack, event.getRepairAmount(), true);
ItemUtils.increaseDurability(itemStack, event.getRepairAmount(), true);
}
@EventHandler
@@ -710,12 +837,12 @@ public class ItemManagerImpl implements ItemManager, Listener {
if (event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_AIR || event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK)
return;
String id = getAnyItemID(itemStack);
EffectCarrier carrier = plugin.getEffectManager().getEffect("util", id);
String id = getAnyPluginItemID(itemStack);
EffectCarrier carrier = plugin.getEffectManager().getEffectCarrier("util", id);
if (carrier == null)
return;
Condition condition = new Condition(event.getPlayer());
if (!RequirementManager.isRequirementsMet(carrier.getRequirements(), condition))
if (!RequirementManager.isRequirementMet(condition, carrier.getRequirements()))
return;
Action[] actions = carrier.getActions(ActionTrigger.INTERACT);
if (actions != null)

View File

@@ -18,16 +18,22 @@
package net.momirealms.customfishing.mechanic.loot;
import net.momirealms.customfishing.api.CustomFishingPlugin;
import net.momirealms.customfishing.api.common.Pair;
import net.momirealms.customfishing.api.manager.LootManager;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.condition.Condition;
import net.momirealms.customfishing.api.mechanic.effect.Effect;
import net.momirealms.customfishing.api.mechanic.loot.CFLoot;
import net.momirealms.customfishing.api.mechanic.loot.Loot;
import net.momirealms.customfishing.api.mechanic.loot.LootType;
import net.momirealms.customfishing.api.mechanic.loot.WeightModifier;
import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.api.util.WeightUtils;
import net.momirealms.customfishing.mechanic.requirement.RequirementManagerImpl;
import net.momirealms.customfishing.util.ConfigUtils;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -36,7 +42,9 @@ import java.util.*;
public class LootManagerImpl implements LootManager {
private final CustomFishingPlugin plugin;
// A map that associates loot IDs with their respective loot configurations.
private final HashMap<String, Loot> lootMap;
// A map that associates loot group IDs with lists of loot IDs.
private final HashMap<String, List<String>> lootGroupMap;
public LootManagerImpl(CustomFishingPlugin plugin) {
@@ -58,6 +66,12 @@ public class LootManagerImpl implements LootManager {
unload();
}
/**
* Loads loot configurations from the plugin's content folders.
* This method scans the "item," "entity," and "block" subfolders within the plugin's data folder
* and loads loot configurations from YAML files.
* If the subfolders or default loot files don't exist, it creates them.
*/
@SuppressWarnings("DuplicatedCode")
public void loadLootsFromPluginFolder() {
Deque<File> fileDeque = new ArrayDeque<>();
@@ -83,33 +97,122 @@ public class LootManagerImpl implements LootManager {
}
}
/**
* Retrieves a list of loot IDs associated with a loot group key.
*
* @param key The key of the loot group.
* @return A list of loot IDs belonging to the specified loot group, or null if not found.
*/
@Nullable
@Override
public List<String> getLootGroup(String key) {
return lootGroupMap.get(key);
}
/**
* Retrieves a loot configuration based on a provided loot key.
*
* @param key The key of the loot configuration.
* @return The Loot object associated with the specified loot key, or null if not found.
*/
@Nullable
@Override
public Loot getLoot(String key) {
return lootMap.get(key);
}
/**
* Retrieves a collection of all loot configuration keys.
*
* @return A collection of all loot configuration keys.
*/
@Override
public Collection<String> getAllLootKeys() {
return lootMap.keySet();
}
/**
* Retrieves a collection of all loot configurations.
*
* @return A collection of all loot configurations.
*/
@Override
public Collection<Loot> getAllLoots() {
return lootMap.values();
}
/**
* Retrieves loot configurations with weights based on a given condition.
*
* @param condition The condition used to filter loot configurations.
* @return A mapping of loot configuration keys to their associated weights.
*/
@Override
public HashMap<String, Double> getLootWithWeight(Condition condition) {
return plugin.getRequirementManager().getLootWithWeight(condition);
return ((RequirementManagerImpl) plugin.getRequirementManager()).getLootWithWeight(condition);
}
/**
* Get a collection of possible loot keys based on a given condition.
*
* @param condition The condition to determine possible loot.
* @return A collection of loot keys.
*/
@Override
public Collection<String> getPossibleLootKeys(Condition condition) {
return ((RequirementManagerImpl) plugin.getRequirementManager()).getLootWithWeight(condition).keySet();
}
/**
* Get a map of possible loot keys with their corresponding weights, considering fishing effect and condition.
*
* @param initialEffect The effect to apply weight modifiers.
* @param condition The condition to determine possible loot.
* @return A map of loot keys and their weights.
*/
@NotNull
@Override
public Map<String, Double> getPossibleLootKeysWithWeight(Effect initialEffect, Condition condition) {
Map<String, Double> lootWithWeight = ((RequirementManagerImpl) plugin.getRequirementManager()).getLootWithWeight(condition);
Player player = condition.getPlayer();
for (Pair<String, WeightModifier> pair : initialEffect.getWeightModifier()) {
Double previous = lootWithWeight.get(pair.left());
if (previous != null)
lootWithWeight.put(pair.left(), pair.right().modify(player, previous));
}
for (Pair<String, WeightModifier> pair : initialEffect.getWeightModifierIgnored()) {
double previous = lootWithWeight.getOrDefault(pair.left(), 0d);
lootWithWeight.put(pair.left(), pair.right().modify(player, previous));
}
return lootWithWeight;
}
/**
* Get the next loot item based on fishing effect and condition.
*
* @param initialEffect The effect to apply weight modifiers.
* @param condition The condition to determine possible loot.
* @return The next loot item, or null if it doesn't exist.
*/
@Override
@Nullable
public Loot getNextLoot(Effect initialEffect, Condition condition) {
String key = WeightUtils.getRandom(getPossibleLootKeysWithWeight(initialEffect, condition));
Loot loot = getLoot(key);
if (loot == null) {
LogUtils.warn(String.format("Loot %s doesn't exist.", key));
return null;
}
return loot;
}
/**
* Loads loot configurations from a single YAML file and populates the lootMap and lootGroupMap.
*
* @param file The YAML file containing loot configurations.
* @param namespace The namespace indicating the type of loot (e.g., "item," "entity," "block").
*/
private void loadSingleFile(File file, String namespace) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (Map.Entry<String, Object> entry : yaml.getValues(false).entrySet()) {
@@ -119,12 +222,14 @@ public class LootManagerImpl implements LootManager {
namespace,
entry.getKey()
);
// Check for duplicate loot configurations and log an error if found.
if (lootMap.containsKey(entry.getKey())) {
LogUtils.severe("Duplicated loot found: " + entry.getKey() + ".");
} else {
lootMap.put(entry.getKey(), loot);
}
String[] group = loot.getLootGroup();
// If the loot configuration belongs to one or more groups, update lootGroupMap.
if (group != null) {
for (String g : group) {
List<String> groupMembers = lootGroupMap.computeIfAbsent(g, k -> new ArrayList<>());
@@ -135,6 +240,14 @@ public class LootManagerImpl implements LootManager {
}
}
/**
* Creates a single loot configuration item from a ConfigurationSection.
*
* @param section The ConfigurationSection containing loot configuration data.
* @param namespace The namespace indicating the type of loot (e.g., "item," "entity," "block").
* @param key The unique key identifying the loot configuration.
* @return A CFLoot object representing the loot configuration.
*/
private CFLoot getSingleSectionItem(ConfigurationSection section, String namespace, String key) {
return new CFLoot.Builder(key, LootType.valueOf(namespace.toUpperCase(Locale.ENGLISH)))
.disableStats(section.getBoolean("disable-stat", false))
@@ -146,18 +259,7 @@ public class LootManagerImpl implements LootManager {
.lootGroup(ConfigUtils.stringListArgs(section.get("group")).toArray(new String[0]))
.nick(section.getString("nick", section.getString("display.name", key)))
.addActions(plugin.getActionManager().getActionMap(section.getConfigurationSection("events")))
.addTimesActions(getTimesActionMap(section.getConfigurationSection("events.success-times")))
.addTimesActions(plugin.getActionManager().getTimesActionMap(section.getConfigurationSection("events.success-times")))
.build();
}
private HashMap<Integer, Action[]> getTimesActionMap(ConfigurationSection section) {
HashMap<Integer, Action[]> actionMap = new HashMap<>();
if (section == null) return actionMap;
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
actionMap.put(Integer.parseInt(entry.getKey()), plugin.getActionManager().getActions(innerSection));
}
}
return actionMap;
}
}

View File

@@ -34,13 +34,22 @@ import java.util.Map;
public class MarketGUI {
// A map that associates characters with MarketGUI elements.
private final HashMap<Character, MarketGUIElement> itemsCharMap;
// A map that associates slot indices with MarketGUI elements.
private final HashMap<Integer, MarketGUIElement> itemsSlotMap;
private final Inventory inventory;
private final MarketManagerImpl manager;
private final Player owner;
private final EarningData earningData;
/**
* Constructor for creating a MarketGUI.
*
* @param manager The Market Manager implementation associated with this MarketGUI.
* @param player The player who owns this MarketGUI.
* @param earningData Data related to earnings for this MarketGUI.
*/
public MarketGUI(MarketManagerImpl manager, Player player, EarningData earningData) {
this.manager = manager;
this.owner = player;
@@ -56,6 +65,9 @@ public class MarketGUI {
holder.setInventory(this.inventory);
}
/**
* Initialize the GUI layout by mapping elements to inventory slots.
*/
private void init() {
int line = 0;
for (String content : manager.getLayout()) {
@@ -79,6 +91,12 @@ public class MarketGUI {
}
}
/**
* Add one or more elements to the GUI.
* @param elements Elements to be added.
* @return The MarketGUI instance.
*/
@SuppressWarnings("UnusedReturnValue")
public MarketGUI addElement(MarketGUIElement... elements) {
for (MarketGUIElement element : elements) {
itemsCharMap.put(element.getSymbol(), element);
@@ -86,26 +104,47 @@ public class MarketGUI {
return this;
}
/**
* Build and initialize the GUI.
*/
public MarketGUI build() {
init();
return this;
}
/**
* Show the GUI to a player if the player is the owner.
* @param player The player to show the GUI to.
*/
public void show(Player player) {
if (player != owner) return;
player.openInventory(inventory);
}
/**
* Get the MarketGUIElement associated with a specific inventory slot.
* @param slot The slot index in the inventory.
* @return The associated MarketGUIElement or null if not found.
*/
@Nullable
public MarketGUIElement getElement(int slot) {
return itemsSlotMap.get(slot);
}
/**
* Get the MarketGUIElement associated with a specific character symbol.
* @param slot The character symbol.
* @return The associated MarketGUIElement or null if not found.
*/
@Nullable
public MarketGUIElement getElement(char slot) {
return itemsCharMap.get(slot);
}
/**
* Refresh the GUI, updating the display based on current data.
* @return The MarketGUI instance.
*/
public MarketGUI refresh() {
double totalWorth = getTotalWorth();
MarketDynamicGUIElement functionElement = (MarketDynamicGUIElement) getElement(manager.getFunctionSlot());
@@ -145,6 +184,10 @@ public class MarketGUI {
return this;
}
/**
* Calculate and return the total worth of items in the inventory.
* @return The total worth of items.
*/
public double getTotalWorth() {
double money = 0d;
MarketGUIElement itemElement = getElement(manager.getItemSlot());
@@ -158,10 +201,17 @@ public class MarketGUI {
return money;
}
/**
* Get the inventory associated with this MarketGUI.
* @return The Inventory object.
*/
public Inventory getInventory() {
return inventory;
}
/**
* Clear items with non-zero value from the inventory.
*/
public void clearWorthyItems() {
MarketGUIElement itemElement = getElement(manager.getItemSlot());
if (itemElement == null) {
@@ -175,6 +225,10 @@ public class MarketGUI {
}
}
/**
* Get an empty slot in the item section of the inventory.
* @return The index of an empty slot or -1 if none are found.
*/
public int getEmptyItemSlot() {
MarketGUIElement itemElement = getElement(manager.getItemSlot());
if (itemElement == null) {
@@ -189,6 +243,9 @@ public class MarketGUI {
return -1;
}
/**
* Return items to the owner's inventory.
*/
public void returnItems() {
MarketGUIElement itemElement = getElement(manager.getItemSlot());
if (itemElement == null) {
@@ -203,6 +260,10 @@ public class MarketGUI {
}
}
/**
* Get the earning data associated with this MarketGUI.
* @return The EarningData object.
*/
public EarningData getEarningData() {
return earningData;
}

View File

@@ -34,18 +34,22 @@ public class MarketGUIElement {
this.slots = new ArrayList<>();
}
// Method to add a slot to the list of slots for this element
public void addSlot(int slot) {
slots.add(slot);
}
// Getter method to retrieve the symbol associated with this element
public char getSymbol() {
return symbol;
}
// Getter method to retrieve the cloned ItemStack associated with this element
public ItemStack getItemStack() {
return itemStack;
return itemStack.clone();
}
// Getter method to retrieve the list of slots where this element can appear
public List<Integer> getSlots() {
return slots;
}

View File

@@ -93,10 +93,13 @@ public class MarketManagerImpl implements MarketManager, Listener {
unload();
}
// Load configuration from the plugin's config file
private void loadConfig() {
YamlConfiguration config = plugin.getConfig("market.yml");
this.enable = config.getBoolean("enable", true);
if (!this.enable) return;
// Load various configuration settings
this.layout = config.getStringList("layout").toArray(new String[0]);
this.title = config.getString("title", "market.title");
this.formula = config.getString("price-formula", "{base} + {bonus} * {size}");
@@ -111,12 +114,15 @@ public class MarketManagerImpl implements MarketManager, Listener {
this.earningLimit = config.getBoolean("limitation.enable", true) ? config.getDouble("limitation.earnings", 100) : -1;
this.allowItemWithNoPrice = config.getBoolean("item-slot.allow-items-with-no-price", true);
// Load item prices from the configuration
ConfigurationSection priceSection = config.getConfigurationSection("item-price");
if (priceSection != null) {
for (Map.Entry<String, Object> entry : priceSection.getValues(false).entrySet()) {
this.priceMap.put(entry.getKey(), ConfigUtils.getDoubleValue(entry.getValue()));
}
}
// Load decorative icons from the configuration
ConfigurationSection decorativeSection = config.getConfigurationSection("decorative-icons");
if (decorativeSection != null) {
for (Map.Entry<String, Object> entry : decorativeSection.getValues(false).entrySet()) {
@@ -129,8 +135,14 @@ public class MarketManagerImpl implements MarketManager, Listener {
}
}
/**
* Open the market GUI for a player
*
* @param player player
*/
@Override
public void openMarketGUI(Player player) {
if (!isEnable()) return;
OnlineUser user = plugin.getStorageManager().getOnlineUser(player.getUniqueId());
if (user == null) {
LogUtils.warn("Player " + player.getName() + "'s market data is not loaded yet.");
@@ -147,6 +159,11 @@ public class MarketManagerImpl implements MarketManager, Listener {
marketGUIMap.put(player.getUniqueId(), gui);
}
/**
* This method handles the closing of an inventory.
*
* @param event The InventoryCloseEvent that triggered this method.
*/
@EventHandler
public void onCloseInv(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player))
@@ -158,6 +175,11 @@ public class MarketManagerImpl implements MarketManager, Listener {
gui.returnItems();
}
/**
* This method handles a player quitting the server.
*
* @param event The PlayerQuitEvent that triggered this method.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
MarketGUI gui = marketGUIMap.remove(event.getPlayer().getUniqueId());
@@ -165,6 +187,11 @@ public class MarketManagerImpl implements MarketManager, Listener {
gui.returnItems();
}
/**
* This method handles dragging items in an inventory.
*
* @param event The InventoryDragEvent that triggered this method.
*/
@EventHandler
public void onDragInv(InventoryDragEvent event) {
if (event.isCancelled())
@@ -197,14 +224,23 @@ public class MarketManagerImpl implements MarketManager, Listener {
plugin.getScheduler().runTaskSyncLater(gui::refresh, player.getLocation(), 50, TimeUnit.MILLISECONDS);
}
/**
* This method handles inventory click events.
*
* @param event The InventoryClickEvent that triggered this method.
*/
@EventHandler
public void onClickInv(InventoryClickEvent event) {
if (event.isCancelled())
return;
Inventory clickedInv = event.getClickedInventory();
if (clickedInv == null)
return;
Player player = (Player) event.getWhoClicked();
// Check if the clicked inventory is a MarketGUI
if (!(event.getInventory().getHolder() instanceof MarketGUIHolder))
return;
@@ -241,14 +277,14 @@ public class MarketManagerImpl implements MarketManager, Listener {
)));
if (worth > 0) {
if (earningLimit != -1 && (earningLimit - data.earnings) < worth) {
// can't earn more money
// Can't earn more money
if (limitActions != null) {
for (Action action : limitActions) {
action.trigger(condition);
}
}
} else {
// clear items
// Clear items and update earnings
gui.clearWorthyItems();
data.earnings += worth;
condition.insertArg("{rest}", String.format("%.2f", (earningLimit - data.earnings)));
@@ -259,7 +295,7 @@ public class MarketManagerImpl implements MarketManager, Listener {
}
}
} else {
// nothing to sell
// Nothing to sell
if (denyActions != null) {
for (Action action : denyActions) {
action.trigger(condition);
@@ -268,6 +304,7 @@ public class MarketManagerImpl implements MarketManager, Listener {
}
}
} else {
// Handle interactions with the player's inventory
ItemStack current = event.getCurrentItem();
if (!allowItemWithNoPrice) {
double price = getItemPrice(current);
@@ -308,38 +345,69 @@ public class MarketManagerImpl implements MarketManager, Listener {
}
}
// Refresh the GUI
plugin.getScheduler().runTaskSyncLater(gui::refresh, player.getLocation(), 50, TimeUnit.MILLISECONDS);
}
/**
* Retrieves the current date as an integer in the format MMDD (e.g., September 21 as 0921).
*
* @return An integer representing the current date.
*/
@Override
public int getDate() {
Calendar calendar = Calendar.getInstance();
return (calendar.get(Calendar.MONTH) +1) * 100 + calendar.get(Calendar.DATE);
}
/**
* Calculates the price of an ItemStack based on custom data or a predefined price map.
*
* @param itemStack The ItemStack for which the price is calculated.
* @return The calculated price of the ItemStack.
*/
@Override
public double getItemPrice(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return 0;
NBTItem nbtItem = new NBTItem(itemStack);
Double price = nbtItem.getDouble("Price");
if (price != null && price != 0) {
// If a custom price is defined in the ItemStack's NBT data, use it.
return price * itemStack.getAmount();
}
// If no custom price is defined, attempt to fetch the price from a predefined price map.
String itemID = itemStack.getType().name();
if (nbtItem.hasTag("CustomModelData")) {
itemID = itemID + ":" + nbtItem.getInteger("CustomModelData");
}
// Use the price from the price map, or default to 0 if not found.
return priceMap.getOrDefault(itemID, 0d) * itemStack.getAmount();
}
/**
* Retrieves the formula used for calculating prices.
*
* @return The pricing formula as a string.
*/
@Override
public String getFormula() {
return formula;
}
/**
* Calculates the price based on a formula with provided variables.
*
* @param base The base value for the formula.
* @param bonus The bonus value for the formula.
* @param size The size value for the formula.
* @return The calculated price based on the formula and provided variables.
*/
@Override
public double getPrice(float base, float bonus, float size) {
public double getFishPrice(float base, float bonus, float size) {
Expression expression = new ExpressionBuilder(getFormula())
.variables("base", "bonus", "size")
.build()
@@ -349,38 +417,88 @@ public class MarketManagerImpl implements MarketManager, Listener {
return expression.evaluate();
}
/**
* Gets the character representing the item slot in the MarketGUI.
*
* @return The item slot character.
*/
@Override
public char getItemSlot() {
return itemSlot;
}
/**
* Gets the character representing the function slot in the MarketGUI.
*
* @return The function slot character.
*/
@Override
public char getFunctionSlot() {
return functionSlot;
}
/**
* Gets the layout of the MarketGUI as an array of strings.
*
* @return The layout of the MarketGUI.
*/
@Override
public String[] getLayout() {
return layout;
}
/**
* Gets the title of the MarketGUI.
*
* @return The title of the MarketGUI.
*/
@Override
public String getTitle() {
return title;
}
/**
* Gets the earning limit
*
* @return The earning limit
*/
@Override
public double getEarningLimit() {
return earningLimit;
}
/**
* Gets the builder for the function icon representing the limit in the MarketGUI.
*
* @return The function icon builder for the limit.
*/
public BuildableItem getFunctionIconLimitBuilder() {
return functionIconLimitBuilder;
}
/**
* Gets the builder for the function icon representing allow actions in the MarketGUI.
*
* @return The function icon builder for allow actions.
*/
public BuildableItem getFunctionIconAllowBuilder() {
return functionIconAllowBuilder;
}
/**
* Gets the builder for the function icon representing deny actions in the MarketGUI.
*
* @return The function icon builder for deny actions.
*/
public BuildableItem getFunctionIconDenyBuilder() {
return functionIconDenyBuilder;
}
/**
* Is market enabled
*
* @return enable or not
*/
@Override
public boolean isEnable() {
return enable;

View File

@@ -28,6 +28,10 @@ import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages cooldowns for various actions or events.
* Keeps track of cooldown times for different keys associated with player UUIDs.
*/
public class CoolDownManager implements Listener {
private final ConcurrentHashMap<UUID, Data> dataMap;
@@ -38,6 +42,14 @@ public class CoolDownManager implements Listener {
this.plugin = plugin;
}
/**
* Checks if a player is currently in cooldown for a specific key.
*
* @param uuid The UUID of the player.
* @param key The key associated with the cooldown.
* @param time The cooldown time in milliseconds.
* @return True if the player is in cooldown, false otherwise.
*/
public boolean isCoolDown(UUID uuid, String key, long time) {
Data data = this.dataMap.computeIfAbsent(uuid, k -> new Data());
return data.isCoolDown(key, time);
@@ -56,6 +68,11 @@ public class CoolDownManager implements Listener {
this.dataMap.clear();
}
/**
* Event handler for when a player quits the game. Removes their cooldown data.
*
* @param event The PlayerQuitEvent triggered when a player quits.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
dataMap.remove(event.getPlayer().getUniqueId());
@@ -69,14 +86,21 @@ public class CoolDownManager implements Listener {
this.coolDownMap = new HashMap<>();
}
/**
* Checks if the player is in cooldown for a specific key.
*
* @param key The key associated with the cooldown.
* @param delay The cooldown delay in milliseconds.
* @return True if the player is in cooldown, false otherwise.
*/
public synchronized boolean isCoolDown(String key, long delay) {
long time = System.currentTimeMillis();
long last = coolDownMap.getOrDefault(key, time - delay);
if (last + delay > time) {
return true;
return true; // Player is in cooldown
} else {
coolDownMap.put(key, time);
return false;
return false; // Player is not in cooldown
}
}
}

View File

@@ -35,13 +35,19 @@ public class ConditionalElement {
public ConditionalElement(
Requirement[] requirements,
List<Pair<String, WeightModifier>> modifierList,
HashMap<String, ConditionalElement> subLoots
HashMap<String, ConditionalElement> subElements
) {
this.modifierList = modifierList;
this.requirements = requirements;
this.subLoots = subLoots;
this.subLoots = subElements;
}
/**
* Combines the weight modifiers for this element.
*
* @param player The player for whom the modifiers are applied.
* @param weightMap The map of weight modifiers.
*/
synchronized public void combine(Player player, HashMap<String, Double> weightMap) {
for (Pair<String, WeightModifier> modifierPair : this.modifierList) {
double previous = weightMap.getOrDefault(modifierPair.left(), 0d);
@@ -49,16 +55,11 @@ public class ConditionalElement {
}
}
public boolean isConditionsMet(Condition condition) {
for (Requirement requirement : requirements) {
if (!requirement.isConditionMet(condition)) {
return false;
}
}
return true;
public Requirement[] getRequirements() {
return requirements;
}
public HashMap<String, ConditionalElement> getSubLoots() {
public HashMap<String, ConditionalElement> getSubElements() {
return subLoots;
}
}

View File

@@ -20,6 +20,9 @@ package net.momirealms.customfishing.mechanic.requirement;
import net.momirealms.customfishing.api.mechanic.condition.Condition;
import net.momirealms.customfishing.api.mechanic.requirement.Requirement;
/**
* Represents an empty requirement that always returns true when checking conditions.
*/
public class EmptyRequirement implements Requirement {
public static EmptyRequirement instance = new EmptyRequirement();

View File

@@ -82,10 +82,15 @@ public class RequirementManagerImpl implements RequirementManager {
this.conditionalLootsMap.clear();
}
/**
* Loads requirement group configuration data from various configuration files.
*/
public void loadRequirementGroupFileConfig() {
// Load mechanic requirements from the main configuration file
YamlConfiguration main = plugin.getConfig("config.yml");
mechanicRequirements = getRequirements(main.getConfigurationSection("mechanics.mechanic-requirements"), true);
// Load conditional loot data from the loot conditions configuration file
YamlConfiguration config1 = plugin.getConfig("loot-conditions.yml");
for (Map.Entry<String, Object> entry : config1.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection section) {
@@ -93,6 +98,7 @@ public class RequirementManagerImpl implements RequirementManager {
}
}
// Load conditional game data from the game conditions configuration file
YamlConfiguration config2 = plugin.getConfig("game-conditions.yml");
for (Map.Entry<String, Object> entry : config2.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection section) {
@@ -101,6 +107,13 @@ public class RequirementManagerImpl implements RequirementManager {
}
}
/**
* Registers a custom requirement type with its corresponding factory.
*
* @param type The type identifier of the requirement.
* @param requirementFactory The factory responsible for creating instances of the requirement.
* @return True if registration was successful, false if the type is already registered.
*/
@Override
public boolean registerRequirement(String type, RequirementFactory requirementFactory) {
if (this.requirementBuilderMap.containsKey(type)) return false;
@@ -108,11 +121,46 @@ public class RequirementManagerImpl implements RequirementManager {
return true;
}
/**
* Unregisters a custom requirement type.
*
* @param type The type identifier of the requirement to unregister.
* @return True if unregistration was successful, false if the type is not registered.
*/
@Override
public boolean unregisterRequirement(String type) {
return this.requirementBuilderMap.remove(type) != null;
}
/**
* Retrieves a ConditionalElement from a given ConfigurationSection.
*
* @param section The ConfigurationSection containing the conditional element data.
* @return A ConditionalElement instance representing the data in the section.
*/
private ConditionalElement getConditionalElements(ConfigurationSection section) {
var sub = section.getConfigurationSection("sub-groups");
if (sub == null) {
return new ConditionalElement(
getRequirements(section.getConfigurationSection("conditions"), false),
ConfigUtils.getModifiers(section.getStringList("list")),
null
);
} else {
HashMap<String, ConditionalElement> subElements = new HashMap<>();
for (Map.Entry<String, Object> entry : sub.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
subElements.put(entry.getKey(), getConditionalElements(innerSection));
}
}
return new ConditionalElement(
getRequirements(section.getConfigurationSection("conditions"), false),
ConfigUtils.getModifiers(section.getStringList("list")),
subElements
);
}
}
private void registerInbuiltRequirements() {
this.registerTimeRequirement();
this.registerYRequirement();
@@ -149,39 +197,21 @@ public class RequirementManagerImpl implements RequirementManager {
this.registerHookRequirement();
}
public ConditionalElement getConditionalElements(ConfigurationSection section) {
var sub = section.getConfigurationSection("sub-groups");
if (sub == null) {
return new ConditionalElement(
getRequirements(section.getConfigurationSection("conditions"), false),
ConfigUtils.getModifiers(section.getStringList("list")),
null
);
} else {
HashMap<String, ConditionalElement> subElements = new HashMap<>();
for (Map.Entry<String, Object> entry : sub.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {
subElements.put(entry.getKey(), getConditionalElements(innerSection));
}
}
return new ConditionalElement(
getRequirements(section.getConfigurationSection("conditions"), false),
ConfigUtils.getModifiers(section.getStringList("list")),
subElements
);
}
}
@Override
public HashMap<String, Double> getLootWithWeight(Condition condition) {
return getString2DoubleMap(condition, conditionalLootsMap);
}
@Override
public HashMap<String, Double> getGameWithWeight(Condition condition) {
return getString2DoubleMap(condition, conditionalGamesMap);
}
/**
* Retrieves a mapping of strings to doubles based on conditional elements and a player's condition.
*
* @param condition The player's condition.
* @param conditionalGamesMap The map of conditional elements representing loots/games.
* @return A HashMap with strings as keys and doubles as values representing loot/game weights.
*/
@NotNull
private HashMap<String, Double> getString2DoubleMap(Condition condition, LinkedHashMap<String, ConditionalElement> conditionalGamesMap) {
HashMap<String, Double> lootWeightMap = new HashMap<>();
@@ -191,10 +221,10 @@ public class RequirementManagerImpl implements RequirementManager {
while (!lootQueue.isEmpty()) {
HashMap<String, ConditionalElement> currentLootMap = lootQueue.poll();
for (ConditionalElement loots : currentLootMap.values()) {
if (loots.isConditionsMet(condition)) {
if (RequirementManager.isRequirementMet(condition, loots.getRequirements())) {
loots.combine(player, lootWeightMap);
if (loots.getSubLoots() != null) {
lootQueue.add(loots.getSubLoots());
if (loots.getSubElements() != null) {
lootQueue.add(loots.getSubElements());
}
}
}
@@ -202,15 +232,24 @@ public class RequirementManagerImpl implements RequirementManager {
return lootWeightMap;
}
@Nullable
/**
* Retrieves an array of requirements based on a configuration section.
*
* @param section The configuration section containing requirement definitions.
* @param advanced A flag indicating whether to use advanced requirements.
* @return An array of Requirement objects based on the configuration section
*/
@NotNull
@Override
public Requirement[] getRequirements(ConfigurationSection section, boolean advanced) {
if (section == null) return null;
List<Requirement> requirements = new ArrayList<>();
if (section == null) {
return requirements.toArray(new Requirement[0]);
}
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
String typeOrName = entry.getKey();
if (hasRequirement(typeOrName)) {
requirements.add(getRequirementBuilder(typeOrName).build(entry.getValue(), null, advanced));
requirements.add(getRequirement(typeOrName, entry.getValue()));
} else {
requirements.add(getRequirement(section.getConfigurationSection(typeOrName), advanced));
}
@@ -222,6 +261,13 @@ public class RequirementManagerImpl implements RequirementManager {
return requirementBuilderMap.containsKey(type);
}
/**
* Retrieves a Requirement object based on a configuration section and advanced flag.
*
* @param section The configuration section containing requirement definitions.
* @param advanced A flag indicating whether to use advanced requirements.
* @return A Requirement object based on the configuration section, or an EmptyRequirement if the section is null or invalid.
*/
@NotNull
@Override
public Requirement getRequirement(ConfigurationSection section, boolean advanced) {
@@ -244,31 +290,48 @@ public class RequirementManagerImpl implements RequirementManager {
LogUtils.warn("No requirement type found at " + section.getCurrentPath());
return EmptyRequirement.instance;
}
var builder = getRequirementBuilder(type);
var builder = getRequirementFactory(type);
if (builder == null) {
return EmptyRequirement.instance;
}
return builder.build(section.get("value"), actionList, advanced);
}
/**
* Gets a requirement based on the provided key and value.
* If a valid RequirementFactory is found for the key, it is used to create the requirement.
* If no factory is found, a warning is logged, and an empty requirement instance is returned.
*
* @param type The key representing the requirement type.
* @param value The value associated with the requirement.
* @return A Requirement instance based on the key and value, or an empty requirement if not found.
*/
@Override
public Requirement getRequirement(String key, Object value) {
return getRequirementBuilder(key).build(value);
}
private Pair<Integer, Integer> getIntegerPair(String range) {
String[] split = range.split("~");
return Pair.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
@NotNull
public Requirement getRequirement(String type, Object value) {
RequirementFactory factory = getRequirementFactory(type);
if (factory == null) {
LogUtils.warn("Requirement type: " + type + " doesn't exist.");
return EmptyRequirement.instance;
}
return factory.build(value);
}
/**
* Retrieves a RequirementFactory based on the specified requirement type.
*
* @param type The requirement type for which to retrieve a factory.
* @return A RequirementFactory for the specified type, or null if no factory is found.
*/
@Override
public RequirementFactory getRequirementBuilder(String type) {
@Nullable
public RequirementFactory getRequirementFactory(String type) {
return requirementBuilderMap.get(type);
}
private void registerTimeRequirement() {
registerRequirement("time", (args, actions, advanced) -> {
List<Pair<Integer, Integer>> timePairs = ConfigUtils.stringListArgs(args).stream().map(this::getIntegerPair).toList();
List<Pair<Integer, Integer>> timePairs = ConfigUtils.stringListArgs(args).stream().map(ConfigUtils::splitStringIntegerArgs).toList();
return condition -> {
long time = condition.getLocation().getWorld().getTime();
for (Pair<Integer, Integer> pair : timePairs)
@@ -322,7 +385,7 @@ public class RequirementManagerImpl implements RequirementManager {
});
}
@SuppressWarnings("all")
@SuppressWarnings("unchecked")
private void registerLootRequirement() {
registerRequirement("loot", (args, actions, advanced) -> {
List<String> arg = (List<String>) args;
@@ -346,7 +409,7 @@ public class RequirementManagerImpl implements RequirementManager {
private void registerYRequirement() {
registerRequirement("ypos", (args, actions, advanced) -> {
List<Pair<Integer, Integer>> timePairs = ConfigUtils.stringListArgs(args).stream().map(this::getIntegerPair).toList();
List<Pair<Integer, Integer>> timePairs = ConfigUtils.stringListArgs(args).stream().map(ConfigUtils::splitStringIntegerArgs).toList();
return condition -> {
int y = condition.getLocation().getBlockY();
for (Pair<Integer, Integer> pair : timePairs)
@@ -363,7 +426,6 @@ public class RequirementManagerImpl implements RequirementManager {
if (args instanceof ConfigurationSection section) {
Requirement[] requirements = getRequirements(section, advanced);
return condition -> {
if (requirements == null) return true;
for (Requirement requirement : requirements) {
if (requirement.isConditionMet(condition)) {
return true;
@@ -384,7 +446,6 @@ public class RequirementManagerImpl implements RequirementManager {
if (args instanceof ConfigurationSection section) {
Requirement[] requirements = getRequirements(section, advanced);
return condition -> {
if (requirements == null) return true;
outer: {
for (Requirement requirement : requirements) {
if (!requirement.isConditionMet(condition)) {
@@ -626,6 +687,7 @@ public class RequirementManagerImpl implements RequirementManager {
});
}
@SuppressWarnings("DuplicatedCode")
private void registerGreaterThanRequirement() {
registerRequirement(">=", (args, actions, advanced) -> {
if (args instanceof ConfigurationSection section) {
@@ -713,6 +775,7 @@ public class RequirementManagerImpl implements RequirementManager {
});
}
@SuppressWarnings("DuplicatedCode")
private void registerLessThanRequirement() {
registerRequirement("<", (args, actions, advanced) -> {
if (args instanceof ConfigurationSection section) {
@@ -919,7 +982,7 @@ public class RequirementManagerImpl implements RequirementManager {
ItemStack itemStack = mainOrOff ?
condition.getPlayer().getInventory().getItemInMainHand()
: condition.getPlayer().getInventory().getItemInOffHand();
String id = plugin.getItemManager().getAnyItemID(itemStack);
String id = plugin.getItemManager().getAnyPluginItemID(itemStack);
if (items.contains(id) && itemStack.getAmount() >= amount) return true;
if (advanced) triggerActions(actions, condition);
return false;
@@ -993,7 +1056,7 @@ public class RequirementManagerImpl implements RequirementManager {
int level = section.getInt("level");
String target = section.getString("target");
return condition -> {
LevelInterface levelInterface = plugin.getIntegrationManager().getLevelHook(pluginName);
LevelInterface levelInterface = plugin.getIntegrationManager().getLevelPlugin(pluginName);
if (levelInterface == null) {
LogUtils.warn("Plugin (" + pluginName + "'s) level is not compatible. Please double check if it's a problem caused by pronunciation.");
return true;
@@ -1010,12 +1073,25 @@ public class RequirementManagerImpl implements RequirementManager {
});
}
/**
* Triggers a list of actions with the given condition.
* If the list of actions is not null, each action in the list is triggered.
*
* @param actions The list of actions to trigger.
* @param condition The condition associated with the actions.
*/
private void triggerActions(List<Action> actions, Condition condition) {
if (actions != null)
for (Action action : actions)
action.trigger(condition);
}
/**
* Loads requirement expansions from external JAR files located in the expansion folder.
* Each expansion JAR should contain classes that extends the RequirementExpansion class.
* Expansions are registered and used to create custom requirements.
* If an error occurs while loading or initializing an expansion, a warning message is logged.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private void loadExpansions() {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);

View File

@@ -49,7 +49,14 @@ public class StatisticsManagerImpl implements StatisticsManager {
unload();
}
/**
* Get the statistics for a player with the given UUID.
*
* @param uuid The UUID of the player for whom statistics are retrieved.
* @return The player's statistics or null if the player is not found.
*/
@Override
@Nullable
public Statistics getStatistics(UUID uuid) {
OnlineUser onlineUser = plugin.getStorageManager().getOnlineUser(uuid);
if (onlineUser == null) return null;
@@ -88,6 +95,12 @@ public class StatisticsManagerImpl implements StatisticsManager {
}
}
/**
* Get a list of strings associated with a specific key in a category map.
*
* @param key The key to look up in the category map.
* @return A list of strings associated with the key or null if the key is not found.
*/
@Override
@Nullable
public List<String> getCategory(String key) {

View File

@@ -43,7 +43,7 @@ public class ActivatedTotem {
this.expireTime = System.currentTimeMillis() + config.getDuration() * 1000L;
this.coreLocation = coreLocation.clone().add(0.5,0,0.5);
this.totemConfig = config;
this.effectCarrier = CustomFishingPlugin.get().getEffectManager().getEffect("totem", config.getKey());
this.effectCarrier = CustomFishingPlugin.get().getEffectManager().getEffectCarrier("totem", config.getKey());
for (ParticleSetting particleSetting : config.getParticleSettings()) {
this.subTasks.add(particleSetting.start(coreLocation, config.getRadius()));
}

View File

@@ -105,6 +105,12 @@ public class TotemManagerImpl implements TotemManager, Listener {
unload();
}
/**
* Get the EffectCarrier associated with an activated totem located near the specified location.
*
* @param location The location to search for activated totems.
* @return The EffectCarrier associated with the nearest activated totem or null if none are found.
*/
@Override
@Nullable
public EffectCarrier getTotemEffect(Location location) {
@@ -139,7 +145,7 @@ public class TotemManagerImpl implements TotemManager, Listener {
return;
Block block = event.getClickedBlock();
assert block != null;
String id = plugin.getBlockManager().getAnyBlockID(block);
String id = plugin.getBlockManager().getAnyPluginBlockID(block);
List<TotemConfig> configs = totemConfigMap.get(id);
if (configs == null)
return;
@@ -153,7 +159,7 @@ public class TotemManagerImpl implements TotemManager, Listener {
if (config == null)
return;
String totemKey = config.getKey();
EffectCarrier carrier = plugin.getEffectManager().getEffect("totem", totemKey);
EffectCarrier carrier = plugin.getEffectManager().getEffectCarrier("totem", totemKey);
if (carrier == null)
return;
Condition condition = new Condition(block.getLocation(), event.getPlayer(), new HashMap<>());

View File

@@ -32,7 +32,7 @@ public class EqualType implements TypeCondition, Serializable {
@Override
public boolean isMet(Block type) {
return this.type.equals(CustomFishingPlugin.get().getBlockManager().getAnyBlockID(type));
return this.type.equals(CustomFishingPlugin.get().getBlockManager().getAnyPluginBlockID(type));
}
@Override

View File

@@ -23,6 +23,9 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitTask;
/**
* A scheduler implementation for synchronous tasks using Bukkit's Scheduler.
*/
public class BukkitSchedulerImpl implements SyncScheduler {
private final CustomFishingPlugin plugin;
@@ -31,6 +34,13 @@ public class BukkitSchedulerImpl implements SyncScheduler {
this.plugin = plugin;
}
/**
* Runs a synchronous task on the main server thread using Bukkit's Scheduler.
* If already on the main thread, the task is executed immediately.
*
* @param runnable The task to run.
* @param location The location associated with the task.
*/
@Override
public void runSyncTask(Runnable runnable, Location location) {
if (Bukkit.isPrimaryThread())
@@ -39,16 +49,36 @@ public class BukkitSchedulerImpl implements SyncScheduler {
Bukkit.getScheduler().runTask(plugin, runnable);
}
/**
* Runs a synchronous task repeatedly with a specified delay and period using Bukkit's Scheduler.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delay The delay in ticks before the first execution.
* @param period The period between subsequent executions in ticks.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delay, long period) {
return new BukkitCancellableTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period));
}
/**
* Runs a synchronous task with a specified delay using Bukkit's Scheduler.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delay The delay in ticks before the task execution.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delay) {
return new BukkitCancellableTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay));
}
/**
* Represents a scheduled task using Bukkit's Scheduler that can be cancelled.
*/
public static class BukkitCancellableTask implements CancellableTask {
private final BukkitTask bukkitTask;

View File

@@ -23,6 +23,9 @@ import net.momirealms.customfishing.api.scheduler.CancellableTask;
import org.bukkit.Bukkit;
import org.bukkit.Location;
/**
* A scheduler implementation for "synchronous" tasks using Folia's RegionScheduler.
*/
public class FoliaSchedulerImpl implements SyncScheduler {
private final CustomFishingPlugin plugin;
@@ -31,21 +34,47 @@ public class FoliaSchedulerImpl implements SyncScheduler {
this.plugin = plugin;
}
/**
* Runs a "synchronous" task on the region thread using Folia's RegionScheduler.
*
* @param runnable The task to run.
* @param location The location associated with the task.
*/
@Override
public void runSyncTask(Runnable runnable, Location location) {
Bukkit.getRegionScheduler().execute(plugin, location, runnable);
}
/**
* Runs a "synchronous" task repeatedly with a specified delay and period using Folia's RegionScheduler.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delay The delay in ticks before the first execution.
* @param period The period between subsequent executions in ticks.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delay, long period) {
return new FoliaCancellableTask(Bukkit.getRegionScheduler().runAtFixedRate(plugin, location, (scheduledTask -> runnable.run()), delay, period));
}
/**
* Runs a "synchronous" task with a specified delay using Folia's RegionScheduler.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delay The delay in ticks before the task execution.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delay) {
return new FoliaCancellableTask(Bukkit.getRegionScheduler().runDelayed(plugin, location, (scheduledTask -> runnable.run()), delay));
}
/**
* Represents a scheduled task using Folia's RegionScheduler that can be cancelled.
*/
public static class FoliaCancellableTask implements CancellableTask {
private final ScheduledTask scheduledTask;

View File

@@ -28,6 +28,9 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* A scheduler implementation responsible for scheduling and managing tasks in a multi-threaded environment.
*/
public class SchedulerImpl implements Scheduler {
private final SyncScheduler syncScheduler;
@@ -44,37 +47,80 @@ public class SchedulerImpl implements Scheduler {
this.schedule.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* Reloads the scheduler configuration based on CustomFishingPlugin settings.
*/
public void reload() {
this.schedule.setCorePoolSize(CFConfig.corePoolSize);
this.schedule.setKeepAliveTime(CFConfig.keepAliveTime, TimeUnit.SECONDS);
this.schedule.setMaximumPoolSize(CFConfig.maximumPoolSize);
}
/**
* Shuts down the scheduler.
*/
public void shutdown() {
if (this.schedule != null && !this.schedule.isShutdown())
this.schedule.shutdown();
}
/**
* Runs a task synchronously on the main server thread or region thread.
*
* @param runnable The task to run.
* @param location The location associated with the task.
*/
@Override
public void runTaskSync(Runnable runnable, Location location) {
this.syncScheduler.runSyncTask(runnable, location);
}
/**
* Runs a task asynchronously.
*
* @param runnable The task to run.
*/
@Override
public void runTaskAsync(Runnable runnable) {
this.schedule.execute(runnable);
}
/**
* Runs a task synchronously with a specified delay and period.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delayTicks The delay in ticks before the first execution.
* @param periodTicks The period between subsequent executions in ticks.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delayTicks, long periodTicks) {
return this.syncScheduler.runTaskSyncTimer(runnable, location, delayTicks, periodTicks);
}
/**
* Runs a task asynchronously with a specified delay.
*
* @param runnable The task to run.
* @param delay The delay before the task execution.
* @param timeUnit The time unit for the delay.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskAsyncLater(Runnable runnable, long delay, TimeUnit timeUnit) {
return new ScheduledTask(schedule.schedule(runnable, delay, timeUnit));
}
/**
* Runs a task synchronously with a specified delay.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delay The delay before the task execution.
* @param timeUnit The time unit for the delay.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delay, TimeUnit timeUnit) {
return new ScheduledTask(schedule.schedule(() -> {
@@ -82,16 +128,36 @@ public class SchedulerImpl implements Scheduler {
}, delay, timeUnit));
}
/**
* Runs a task synchronously with a specified delay in ticks.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delayTicks The delay in ticks before the task execution.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delayTicks) {
return this.syncScheduler.runTaskSyncLater(runnable, location, delayTicks);
}
/**
* Runs a task asynchronously with a specified delay and period.
*
* @param runnable The task to run.
* @param delay The delay before the first execution.
* @param period The period between subsequent executions.
* @param timeUnit The time unit for the delay and period.
* @return A CancellableTask for managing the scheduled task.
*/
@Override
public CancellableTask runTaskAsyncTimer(Runnable runnable, long delay, long period, TimeUnit timeUnit) {
return new ScheduledTask(schedule.scheduleAtFixedRate(runnable, delay, period, timeUnit));
}
/**
* Represents a thread-pool task that can be cancelled.
*/
public static class ScheduledTask implements CancellableTask {
private final ScheduledFuture<?> scheduledFuture;

View File

@@ -22,9 +22,32 @@ import org.bukkit.Location;
public interface SyncScheduler {
/**
* Runs a task synchronously on the main server thread or region thread.
*
* @param runnable The task to run.
* @param location The location associated with the task.
*/
void runSyncTask(Runnable runnable, Location location);
CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delay, long period);
/**
* Runs a task synchronously with a specified delay and period.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delayTicks The delay in ticks before the first execution.
* @param periodTicks The period between subsequent executions in ticks.
* @return A CancellableTask for managing the scheduled task.
*/
CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delayTicks, long periodTicks);
CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delay);
/**
* Runs a task synchronously with a specified delay in ticks.
*
* @param runnable The task to run.
* @param location The location associated with the task.
* @param delayTicks The delay in ticks before the task execution.
* @return A CancellableTask for managing the scheduled task.
*/
CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delayTicks);
}

View File

@@ -60,6 +60,10 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* This class implements the StorageManager interface and is responsible for managing player data storage.
* It includes methods to handle player data retrieval, storage, and serialization.
*/
public class StorageManagerImpl implements StorageManager, Listener {
private final CustomFishingPlugin plugin;
@@ -81,9 +85,14 @@ public class StorageManagerImpl implements StorageManager, Listener {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
/**
* Reloads the storage manager configuration.
*/
public void reload() {
YamlConfiguration config = plugin.getConfig("database.yml");
this.uniqueID = config.getString("unique-server-id", "default");
// Check if storage type has changed and reinitialize if necessary
StorageType storageType = StorageType.valueOf(config.getString("data-storage-method", "H2"));
if (storageType != previousType) {
if (this.dataSource != null) this.dataSource.disable();
@@ -100,19 +109,27 @@ public class StorageManagerImpl implements StorageManager, Listener {
if (this.dataSource != null) this.dataSource.initialize();
else LogUtils.severe("No storage type is set.");
}
// Handle Redis configuration
if (!this.hasRedis && config.getBoolean("Redis.enable", false)) {
this.hasRedis = true;
this.redisManager = new RedisManager(plugin);
this.redisManager.initialize();
}
// Disable Redis if it was enabled but is now disabled
if (this.hasRedis && !config.getBoolean("Redis.enable", false) && this.redisManager != null) {
this.redisManager.disable();
this.redisManager = null;
}
// Cancel any existing timerSaveTask
if (this.timerSaveTask != null && !this.timerSaveTask.isCancelled()) {
this.timerSaveTask.cancel();
}
if (CFConfig.dataSaveInterval != -1)
// Schedule periodic data saving if dataSaveInterval is configured
if (CFConfig.dataSaveInterval != -1 && CFConfig.dataSaveInterval != 0)
this.timerSaveTask = this.plugin.getScheduler().runTaskAsyncTimer(
() -> {
long time1 = System.currentTimeMillis();
@@ -125,6 +142,9 @@ public class StorageManagerImpl implements StorageManager, Listener {
);
}
/**
* Disables the storage manager and cleans up resources.
*/
public void disable() {
HandlerList.unregisterAll(this);
this.dataSource.updateManyPlayersData(onlineUserMap.values(), true);
@@ -135,16 +155,35 @@ public class StorageManagerImpl implements StorageManager, Listener {
this.redisManager.disable();
}
/**
* Gets the unique server identifier.
*
* @return The unique server identifier.
*/
@NotNull
@Override
public String getUniqueID() {
return uniqueID;
}
/**
* Gets an OnlineUser instance for the specified UUID.
*
* @param uuid The UUID of the player.
* @return An OnlineUser instance if the player is online, or null if not.
*/
@Override
public OnlineUser getOnlineUser(UUID uuid) {
return onlineUserMap.get(uuid);
}
/**
* Asynchronously retrieves an OfflineUser instance for the specified UUID.
*
* @param uuid The UUID of the player.
* @param lock Whether to lock the data during retrieval.
* @return A CompletableFuture that resolves to an Optional containing the OfflineUser instance if found, or empty if not found or locked.
*/
@Override
public CompletableFuture<Optional<OfflineUser>> getOfflineUser(UUID uuid, boolean lock) {
var optionalDataFuture = dataSource.getPlayerData(uuid, lock);
@@ -163,21 +202,38 @@ public class StorageManagerImpl implements StorageManager, Listener {
});
}
@Override
public boolean isLockedData(OfflineUser offlineUser) {
return OfflineUserImpl.LOCKED_USER == offlineUser;
}
/**
* Asynchronously saves user data for an OfflineUser.
*
* @param offlineUser The OfflineUser whose data needs to be saved.
* @param unlock Whether to unlock the data after saving.
* @return A CompletableFuture that resolves to a boolean indicating the success of the data saving operation.
*/
@Override
public CompletableFuture<Boolean> saveUserData(OfflineUser offlineUser, boolean unlock) {
return dataSource.updatePlayerData(offlineUser.getUUID(), offlineUser.getPlayerData(), unlock);
}
@Override
public CompletableFuture<Integer> getRedisPlayerCount() {
return redisManager.getPlayerCount();
}
/**
* Gets the data source used for data storage.
*
* @return The data source.
*/
@Override
public DataStorageInterface getDataSource() {
return dataSource;
}
/**
* Event handler for when a player joins the server.
* Locks the player's data and initiates data retrieval if Redis is not used,
* otherwise, it starts a Redis data retrieval task.
*/
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
@@ -186,10 +242,21 @@ public class StorageManagerImpl implements StorageManager, Listener {
if (!hasRedis) {
waitForDataLockRelease(uuid, 1);
} else {
redisReadingData(uuid);
plugin.getScheduler().runTaskAsyncLater(() -> redisManager.getChangeServer(uuid).thenAccept(changeServer -> {
if (!changeServer) {
waitForDataLockRelease(uuid, 3);
} else {
new RedisGetDataTask(uuid);
}
}), 500, TimeUnit.MILLISECONDS);
}
}
/**
* Event handler for when a player quits the server.
* If the player is not locked, it removes their OnlineUser instance,
* updates the player's data in Redis and the data source.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
@@ -216,17 +283,10 @@ public class StorageManagerImpl implements StorageManager, Listener {
}
}
public void redisReadingData(UUID uuid) {
// delay 0.5s for another server to insert the key
plugin.getScheduler().runTaskAsyncLater(() -> redisManager.getChangeServer(uuid).thenAccept(changeServer -> {
if (!changeServer) {
waitForDataLockRelease(uuid, 3);
} else {
new RedisGetDataTask(uuid);
}
}), 500, TimeUnit.MILLISECONDS);
}
/**
* Runnable task for asynchronously retrieving data from Redis.
* Retries up to 6 times and cancels the task if the player is offline.
*/
public class RedisGetDataTask implements Runnable {
private final UUID uuid;
@@ -255,21 +315,25 @@ public class StorageManagerImpl implements StorageManager, Listener {
if (optionalData.isPresent()) {
putDataInCache(player, optionalData.get());
task.cancel();
if (CFConfig.lockData) dataSource.lockPlayerData(uuid, true);
if (CFConfig.lockData) dataSource.lockOrUnlockPlayerData(uuid, true);
}
});
}
}
// wait 1 second for the lock to release
// try three times at most
/**
* Waits for data lock release with a delay and a maximum of three retries.
*
* @param uuid The UUID of the player.
* @param times The number of times this method has been retried.
*/
public void waitForDataLockRelease(UUID uuid, int times) {
plugin.getScheduler().runTaskAsyncLater(() -> {
var player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline() || times > 3)
return;
this.dataSource.getPlayerData(uuid, CFConfig.lockData).thenAccept(optionalData -> {
// should not be empty
// Data should not be empty
if (optionalData.isEmpty())
return;
if (optionalData.get() == PlayerData.LOCKED) {
@@ -281,38 +345,80 @@ public class StorageManagerImpl implements StorageManager, Listener {
}, 1, TimeUnit.SECONDS);
}
/**
* Puts player data in cache and removes the player from the locked set.
*
* @param player The player whose data is being cached.
* @param playerData The data to be cached.
*/
public void putDataInCache(Player player, PlayerData playerData) {
locked.remove(player.getUniqueId());
OnlineUserImpl bukkitUser = new OnlineUserImpl(player, playerData);
onlineUserMap.put(player.getUniqueId(), bukkitUser);
}
/**
* Checks if Redis is enabled.
*
* @return True if Redis is enabled; otherwise, false.
*/
@Override
public boolean isRedisEnabled() {
return hasRedis;
}
/**
* Gets the RedisManager instance.
*
* @return The RedisManager instance.
*/
@Nullable
public RedisManager getRedisManager() {
return redisManager;
}
/**
* Converts PlayerData to bytes.
*
* @param data The PlayerData to be converted.
* @return The byte array representation of PlayerData.
*/
@NotNull
@Override
public byte[] toBytes(@NotNull PlayerData data) {
return toJson(data).getBytes(StandardCharsets.UTF_8);
}
/**
* Converts PlayerData to JSON format.
*
* @param data The PlayerData to be converted.
* @return The JSON string representation of PlayerData.
*/
@Override
@NotNull
public String toJson(@NotNull PlayerData data) {
return gson.toJson(data);
}
/**
* Converts JSON string to PlayerData.
*
* @param json The JSON string to be converted.
* @return The PlayerData object.
*/
@NotNull
@Override
public PlayerData fromJson(String json) {
return gson.fromJson(json, PlayerData.class);
}
/**
* Converts bytes to PlayerData.
*
* @param data The byte array to be converted.
* @return The PlayerData object.
*/
@Override
@NotNull
public PlayerData fromBytes(byte[] data) {
@@ -323,6 +429,9 @@ public class StorageManagerImpl implements StorageManager, Listener {
}
}
/**
* Custom exception class for data serialization errors.
*/
public static class DataSerializationException extends RuntimeException {
protected DataSerializationException(String message, Throwable cause) {
super(message, cause);

View File

@@ -27,6 +27,9 @@ import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* An abstract class that implements the DataStorageInterface and provides common functionality for data storage.
*/
public abstract class AbstractStorage implements DataStorageInterface {
protected CustomFishingPlugin plugin;
@@ -37,31 +40,44 @@ public abstract class AbstractStorage implements DataStorageInterface {
@Override
public void initialize() {
// This method can be overridden in subclasses to perform initialization tasks specific to the storage type.
}
@Override
public void disable() {
// This method can be overridden in subclasses to perform cleanup or shutdown tasks specific to the storage type.
}
/**
* Get the current time in seconds since the Unix epoch.
*
* @return The current time in seconds.
*/
public int getCurrentSeconds() {
return (int) Instant.now().getEpochSecond();
}
@Override
public void updateManyPlayersData(Collection<? extends OfflineUser> users, boolean unlock) {
// Update data for multiple players by iterating through the collection of OfflineUser objects.
for (OfflineUser user : users) {
this.updatePlayerData(user.getUUID(), user.getPlayerData(), unlock);
}
}
public void lockPlayerData(UUID uuid, boolean lock) {
/**
* Lock or unlock player data based on the provided UUID and lock flag.
*
* @param uuid The UUID of the player.
* @param lock True to lock the player data, false to unlock it.
*/
public void lockOrUnlockPlayerData(UUID uuid, boolean lock) {
// Note: Only remote database would override this method
}
@Override
public CompletableFuture<Boolean> updateOrInsertPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
// By default, delegate to the updatePlayerData method to update or insert player data.
return updatePlayerData(uuid, playerData, unlock);
}
}

View File

@@ -39,6 +39,9 @@ import org.bukkit.configuration.file.YamlConfiguration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* An implementation of AbstractStorage that uses MongoDB for player data storage.
*/
public class MongoDBImpl extends AbstractStorage {
private MongoClient mongoClient;
@@ -49,6 +52,9 @@ public class MongoDBImpl extends AbstractStorage {
super(plugin);
}
/**
* Initialize the MongoDB connection and configuration based on the plugin's YAML configuration.
*/
@Override
public void initialize() {
YamlConfiguration config = plugin.getConfig("database.yml");
@@ -83,6 +89,9 @@ public class MongoDBImpl extends AbstractStorage {
this.database = mongoClient.getDatabase(section.getString("database", "minecraft"));
}
/**
* Disable the MongoDB connection by closing the MongoClient.
*/
@Override
public void disable() {
if (this.mongoClient != null) {
@@ -90,10 +99,21 @@ public class MongoDBImpl extends AbstractStorage {
}
}
public String getCollectionName(String sub) {
return getCollectionPrefix() + "_" + sub;
/**
* Get the collection name for a specific subcategory of data.
*
* @param value The subcategory identifier.
* @return The full collection name including the prefix.
*/
public String getCollectionName(String value) {
return getCollectionPrefix() + "_" + value;
}
/**
* Get the collection prefix used for MongoDB collections.
*
* @return The collection prefix.
*/
public String getCollectionPrefix() {
return collectionPrefix;
}
@@ -103,6 +123,13 @@ public class MongoDBImpl extends AbstractStorage {
return StorageType.MongoDB;
}
/**
* Asynchronously retrieve player data from the MongoDB database.
*
* @param uuid The UUID of the player.
* @param lock Flag indicating whether to lock the data.
* @return A CompletableFuture with an optional PlayerData.
*/
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
var future = new CompletableFuture<Optional<PlayerData>>();
@@ -111,7 +138,7 @@ public class MongoDBImpl extends AbstractStorage {
Document doc = collection.find(Filters.eq("uuid", uuid)).first();
if (doc == null) {
if (Bukkit.getPlayer(uuid) != null) {
if (lock) lockPlayerData(uuid, true);
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(PlayerData.empty()));
} else {
future.complete(Optional.empty());
@@ -122,13 +149,21 @@ public class MongoDBImpl extends AbstractStorage {
return;
}
Binary binary = (Binary) doc.get("data");
if (lock) lockPlayerData(uuid, true);
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(plugin.getStorageManager().fromBytes(binary.getData())));
}
});
return future;
}
/**
* Asynchronously update player data in the MongoDB database.
*
* @param uuid The UUID of the player.
* @param playerData The player's data to update.
* @param unlock Flag indicating whether to unlock the data.
* @return A CompletableFuture indicating the update result.
*/
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
@@ -149,6 +184,12 @@ public class MongoDBImpl extends AbstractStorage {
return future;
}
/**
* Asynchronously update data for multiple players in the MongoDB database.
*
* @param users A collection of OfflineUser instances to update.
* @param unlock Flag indicating whether to unlock the data.
*/
@Override
public void updateManyPlayersData(Collection<? extends OfflineUser> users, boolean unlock) {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
@@ -170,8 +211,14 @@ public class MongoDBImpl extends AbstractStorage {
}
}
/**
* Lock or unlock player data in the MongoDB database.
*
* @param uuid The UUID of the player.
* @param lock Flag indicating whether to lock or unlock the data.
*/
@Override
public void lockPlayerData(UUID uuid, boolean lock) {
public void lockOrUnlockPlayerData(UUID uuid, boolean lock) {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
Document query = new Document("uuid", uuid);
@@ -183,6 +230,12 @@ public class MongoDBImpl extends AbstractStorage {
}
}
/**
* Get a set of unique player UUIDs from the MongoDB database.
*
* @param legacy Flag indicating whether to retrieve legacy data.
* @return A set of unique player UUIDs.
*/
@Override
public Set<UUID> getUniqueUsers(boolean legacy) {
// no legacy files

View File

@@ -37,6 +37,9 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* A RedisManager class responsible for managing interactions with a Redis server for data storage.
*/
public class RedisManager extends AbstractStorage {
private static RedisManager instance;
@@ -44,7 +47,6 @@ public class RedisManager extends AbstractStorage {
private String password;
private int port;
private String host;
private JedisPoolConfig jedisPoolConfig;
private boolean useSSL;
public RedisManager(CustomFishingPlugin plugin) {
@@ -52,14 +54,27 @@ public class RedisManager extends AbstractStorage {
instance = this;
}
/**
* Get the singleton instance of the RedisManager.
*
* @return The RedisManager instance.
*/
public static RedisManager getInstance() {
return instance;
}
/**
* Get a Jedis resource for interacting with the Redis server.
*
* @return A Jedis resource.
*/
public Jedis getJedis() {
return jedisPool.getResource();
}
/**
* Initialize the Redis connection and configuration based on the plugin's YAML configuration.
*/
@Override
public void initialize() {
YamlConfiguration config = plugin.getConfig("database.yml");
@@ -69,7 +84,7 @@ public class RedisManager extends AbstractStorage {
return;
}
jedisPoolConfig = new JedisPoolConfig();
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setTestWhileIdle(true);
jedisPoolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(30000));
jedisPoolConfig.setNumTestsPerEvictionRun(-1);
@@ -99,13 +114,21 @@ public class RedisManager extends AbstractStorage {
subscribe();
}
/**
* Disable the Redis connection by closing the JedisPool.
*/
@Override
public void disable() {
this.removeServerPlayers(plugin.getStorageManager().getUniqueID());
if (jedisPool != null && !jedisPool.isClosed())
jedisPool.close();
}
/**
* Send a message to Redis on a specified channel.
*
* @param channel The Redis channel to send the message to.
* @param message The message to send.
*/
public void sendRedisMessage(@NotNull String channel, @NotNull String message) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(channel, message);
@@ -113,6 +136,9 @@ public class RedisManager extends AbstractStorage {
}
}
/**
* Subscribe to Redis messages on a separate thread and handle received messages.
*/
private void subscribe() {
Thread thread = new Thread(() -> {
try (final Jedis jedis = password.isBlank() ?
@@ -160,38 +186,12 @@ public class RedisManager extends AbstractStorage {
return StorageType.Redis;
}
public CompletableFuture<Integer> getPlayerCount() {
var future = new CompletableFuture<Integer>();
plugin.getScheduler().runTaskAsync(() -> {
int players = 0;
try (Jedis jedis = jedisPool.getResource()) {
var list = jedis.zrangeWithScores("cf_players",0, -1);
for (Tuple tuple : list) {
players += (int) tuple.getScore();
}
}
future.complete(players);
});
return future;
}
public CompletableFuture<Void> setServerPlayers(int amount, String unique) {
var future = new CompletableFuture<Void>();
plugin.getScheduler().runTaskAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
jedis.zadd("cf_players", amount, unique);
}
future.complete(null);
});
return future;
}
public void removeServerPlayers(String unique) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.zrem("cf_players", unique);
}
}
/**
* Set a "change server" flag for a specified player UUID in Redis.
*
* @param uuid The UUID of the player.
* @return A CompletableFuture indicating the operation's completion.
*/
public CompletableFuture<Void> setChangeServer(UUID uuid) {
var future = new CompletableFuture<Void>();
plugin.getScheduler().runTaskAsync(() -> {
@@ -208,6 +208,12 @@ public class RedisManager extends AbstractStorage {
return future;
}
/**
* Get the "change server" flag for a specified player UUID from Redis and remove it.
*
* @param uuid The UUID of the player.
* @return A CompletableFuture with a Boolean indicating whether the flag was set.
*/
public CompletableFuture<Boolean> getChangeServer(UUID uuid) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().runTaskAsync(() -> {
@@ -227,6 +233,13 @@ public class RedisManager extends AbstractStorage {
return future;
}
/**
* Asynchronously retrieve player data from Redis.
*
* @param uuid The UUID of the player.
* @param lock Flag indicating whether to lock the data.
* @return A CompletableFuture with an optional PlayerData.
*/
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
var future = new CompletableFuture<Optional<PlayerData>>();
@@ -250,6 +263,14 @@ public class RedisManager extends AbstractStorage {
return future;
}
/**
* Asynchronously update player data in Redis.
*
* @param uuid The UUID of the player.
* @param playerData The player's data to update.
* @param ignore Flag indicating whether to ignore the update (not used).
* @return A CompletableFuture indicating the update result.
*/
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
var future = new CompletableFuture<Boolean>();
@@ -270,11 +291,25 @@ public class RedisManager extends AbstractStorage {
return future;
}
/**
* Get a set of unique player UUIDs from Redis (Returns an empty set).
* This method is designed for importing and exporting so it would not actually be called.
*
* @param legacy Flag indicating whether to retrieve legacy data (not used).
* @return An empty set of UUIDs.
*/
@Override
public Set<UUID> getUniqueUsers(boolean legacy) {
return new HashSet<>();
}
/**
* Generate a Redis key for a specified key and UUID.
*
* @param key The key identifier.
* @param uuid The UUID to include in the key.
* @return A byte array representing the Redis key.
*/
private byte[] getRedisKey(String key, @NotNull UUID uuid) {
return (key + ":" + uuid).getBytes(StandardCharsets.UTF_8);
}

View File

@@ -32,6 +32,9 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* An abstract base class for SQL databases using the HikariCP connection pool, which handles player data storage.
*/
public abstract class AbstractHikariDatabase extends AbstractSQLDatabase implements LegacyDataStorageInterface {
private HikariDataSource dataSource;
@@ -58,6 +61,9 @@ public abstract class AbstractHikariDatabase extends AbstractSQLDatabase impleme
}
}
/**
* Initialize the database connection pool and create tables if they don't exist.
*/
@Override
public void initialize() {
YamlConfiguration config = plugin.getConfig("database.yml");
@@ -109,17 +115,32 @@ public abstract class AbstractHikariDatabase extends AbstractSQLDatabase impleme
super.createTableIfNotExist();
}
/**
* Disable the database by closing the connection pool.
*/
@Override
public void disable() {
if (dataSource != null && !dataSource.isClosed())
dataSource.close();
}
/**
* Get a connection to the SQL database from the connection pool.
*
* @return A database connection.
* @throws SQLException If there is an error establishing a connection.
*/
@Override
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* Retrieve legacy player data from the SQL database.
*
* @param uuid The UUID of the player.
* @return A CompletableFuture containing the optional legacy player data.
*/
@Override
public CompletableFuture<Optional<PlayerData>> getLegacyPlayerData(UUID uuid) {
var future = new CompletableFuture<Optional<PlayerData>>();

View File

@@ -33,6 +33,9 @@ import java.sql.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* An abstract base class for SQL database implementations that handle player data storage.
*/
public abstract class AbstractSQLDatabase extends AbstractStorage {
protected String tablePrefix;
@@ -41,8 +44,17 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
super(plugin);
}
/**
* Get a connection to the SQL database.
*
* @return A database connection.
* @throws SQLException If there is an error establishing a connection.
*/
public abstract Connection getConnection() throws SQLException;
/**
* Create tables for storing data if they don't exist in the database.
*/
public void createTableIfNotExist() {
try (Connection connection = getConnection()) {
final String[] databaseSchema = getSchema(getStorageType().name().toLowerCase(Locale.ENGLISH));
@@ -60,23 +72,54 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
}
/**
* Get the SQL schema from a resource file.
*
* @param fileName The name of the schema file.
* @return An array of SQL statements to create tables.
* @throws IOException If there is an error reading the schema resource.
*/
private String[] getSchema(@NotNull String fileName) throws IOException {
return replaceSchemaPlaceholder(new String(Objects.requireNonNull(plugin.getResource("schema/" + fileName + ".sql"))
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
}
/**
* Replace placeholder values in SQL schema with the table prefix.
*
* @param sql The SQL schema string.
* @return The SQL schema string with placeholders replaced.
*/
private String replaceSchemaPlaceholder(@NotNull String sql) {
return sql.replace("{prefix}", tablePrefix);
}
/**
* Get the name of a database table based on a sub-table name and the table prefix.
*
* @param sub The sub-table name.
* @return The full table name.
*/
public String getTableName(String sub) {
return getTablePrefix() + "_" + sub;
}
/**
* Get the current table prefix.
*
* @return The table prefix.
*/
public String getTablePrefix() {
return tablePrefix;
}
/**
* Retrieve a player's data from the SQL database.
*
* @param uuid The UUID of the player.
* @param lock Whether to lock the player data during retrieval.
* @return A CompletableFuture containing the optional player data.
*/
@SuppressWarnings("DuplicatedCode")
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
@@ -98,7 +141,7 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
final Blob blob = rs.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
if (lock) lockPlayerData(uuid, true);
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray)));
} else if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
@@ -115,6 +158,14 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
return future;
}
/**
* Update a player's data in the SQL database.
*
* @param uuid The UUID of the player.
* @param playerData The player data to update.
* @param unlock Whether to unlock the player data after updating.
* @return A CompletableFuture indicating the success of the update.
*/
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
@@ -137,6 +188,12 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
return future;
}
/**
* Update data for multiple players in the SQL database.
*
* @param users A collection of OfflineUser objects representing players.
* @param unlock Whether to unlock the player data after updating.
*/
@Override
public void updateManyPlayersData(Collection<? extends OfflineUser> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
@@ -160,6 +217,13 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
}
/**
* Insert a new player's data into the SQL database.
*
* @param uuid The UUID of the player.
* @param playerData The player data to insert.
* @param lock Whether to lock the player data upon insertion.
*/
public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) {
try (
Connection connection = getConnection();
@@ -174,8 +238,14 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
}
/**
* Lock or unlock a player's data in the SQL database.
*
* @param uuid The UUID of the player.
* @param lock Whether to lock or unlock the player data.
*/
@Override
public void lockPlayerData(UUID uuid, boolean lock) {
public void lockOrUnlockPlayerData(UUID uuid, boolean lock) {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_LOCK_BY_UUID, getTableName("data")))
@@ -188,6 +258,14 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
}
/**
* Update or insert a player's data into the SQL database.
*
* @param uuid The UUID of the player.
* @param playerData The player data to update or insert.
* @param unlock Whether to unlock the player data after updating or inserting.
* @return A CompletableFuture indicating the success of the operation.
*/
@Override
public CompletableFuture<Boolean> updateOrInsertPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
@@ -211,6 +289,12 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
return future;
}
/**
* Get a set of unique user UUIDs from the SQL database.
*
* @param legacy Whether to include legacy data in the retrieval.
* @return A set of unique user UUIDs.
*/
@Override
public Set<UUID> getUniqueUsers(boolean legacy) {
Set<UUID> uuids = new HashSet<>();
@@ -228,6 +312,9 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
return uuids;
}
/**
* Constants defining SQL statements used for database operations.
*/
public static class SqlConstants {
public static final String SQL_SELECT_BY_UUID = "SELECT * FROM `%s` WHERE `uuid` = ?";
public static final String SQL_SELECT_ALL_UUID = "SELECT uuid FROM `%s`";

View File

@@ -26,6 +26,9 @@ import java.io.File;
import java.sql.Connection;
import java.sql.SQLException;
/**
* An implementation of AbstractSQLDatabase that uses the H2 embedded database for player data storage.
*/
public class H2Impl extends AbstractSQLDatabase {
private JdbcConnectionPool connectionPool;
@@ -34,6 +37,9 @@ public class H2Impl extends AbstractSQLDatabase {
super(plugin);
}
/**
* Initialize the H2 database and connection pool based on the configuration.
*/
@Override
public void initialize() {
YamlConfiguration config = plugin.getConfig("database.yml");
@@ -45,6 +51,9 @@ public class H2Impl extends AbstractSQLDatabase {
super.createTableIfNotExist();
}
/**
* Disable the H2 database by disposing of the connection pool.
*/
@Override
public void disable() {
if (connectionPool != null) {

View File

@@ -35,6 +35,9 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* An implementation of AbstractSQLDatabase that uses the SQLite database for player data storage.
*/
public class SQLiteImpl extends AbstractSQLDatabase {
private Connection connection;
@@ -44,6 +47,9 @@ public class SQLiteImpl extends AbstractSQLDatabase {
super(plugin);
}
/**
* Initialize the SQLite database and connection based on the configuration.
*/
@Override
public void initialize() {
YamlConfiguration config = plugin.getConfig("database.yml");
@@ -52,6 +58,9 @@ public class SQLiteImpl extends AbstractSQLDatabase {
super.createTableIfNotExist();
}
/**
* Disable the SQLite database by closing the connection.
*/
@Override
public void disable() {
try {
@@ -67,6 +76,12 @@ public class SQLiteImpl extends AbstractSQLDatabase {
return StorageType.SQLite;
}
/**
* Get a connection to the SQLite database.
*
* @return A database connection.
* @throws SQLException If there is an error establishing a connection.
*/
@Override
public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
@@ -75,6 +90,13 @@ public class SQLiteImpl extends AbstractSQLDatabase {
return connection;
}
/**
* Asynchronously retrieve player data from the SQLite database.
*
* @param uuid The UUID of the player.
* @param lock Flag indicating whether to lock the data.
* @return A CompletableFuture with an optional PlayerData.
*/
@SuppressWarnings("DuplicatedCode")
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
@@ -94,7 +116,7 @@ public class SQLiteImpl extends AbstractSQLDatabase {
return;
}
final byte[] dataByteArray = rs.getBytes("data");
if (lock) lockPlayerData(uuid, true);
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray)));
} else if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
@@ -111,6 +133,14 @@ public class SQLiteImpl extends AbstractSQLDatabase {
return future;
}
/**
* Asynchronously update player data in the SQLite database.
*
* @param uuid The UUID of the player.
* @param playerData The player's data to update.
* @param unlock Flag indicating whether to unlock the data.
* @return A CompletableFuture indicating the update result.
*/
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
@@ -132,6 +162,12 @@ public class SQLiteImpl extends AbstractSQLDatabase {
return future;
}
/**
* Asynchronously update data for multiple players in the SQLite database.
*
* @param users A collection of OfflineUser instances to update.
* @param unlock Flag indicating whether to unlock the data.
*/
@Override
public void updateManyPlayersData(Collection<? extends OfflineUser> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
@@ -155,6 +191,13 @@ public class SQLiteImpl extends AbstractSQLDatabase {
}
}
/**
* Insert player data into the SQLite database.
*
* @param uuid The UUID of the player.
* @param playerData The player's data to insert.
* @param lock Flag indicating whether to lock the data.
*/
@Override
public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) {
try (
@@ -170,6 +213,10 @@ public class SQLiteImpl extends AbstractSQLDatabase {
}
}
/**
* Set up the connection to the SQLite database.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
private void setConnection() {
try {
if (!databaseFile.exists()) databaseFile.createNewFile();

View File

@@ -35,6 +35,9 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* A data storage implementation that uses JSON files to store player data.
*/
public class JsonImpl extends AbstractStorage {
@SuppressWarnings("ResultOfMethodCallIgnored")
@@ -69,10 +72,22 @@ public class JsonImpl extends AbstractStorage {
return CompletableFuture.completedFuture(true);
}
/**
* Get the file associated with a player's UUID for storing JSON data.
*
* @param uuid The UUID of the player.
* @return The file for the player's data.
*/
public File getPlayerDataFile(UUID uuid) {
return new File(plugin.getDataFolder(), "data" + File.separator + uuid + ".json");
}
/**
* Save an object to a JSON file.
*
* @param obj The object to be saved as JSON.
* @param filepath The file path where the JSON file should be saved.
*/
public void saveToJsonFile(Object obj, File filepath) {
Gson gson = new Gson();
try (FileWriter file = new FileWriter(filepath)) {
@@ -82,12 +97,27 @@ public class JsonImpl extends AbstractStorage {
}
}
/**
* Read JSON content from a file and parse it into an object of the specified class.
*
* @param file The JSON file to read.
* @param classOfT The class of the object to parse the JSON into.
* @param <T> The type of the object.
* @return The parsed object.
*/
public <T> T readFromJsonFile(File file, Class<T> classOfT) {
Gson gson = new Gson();
String jsonContent = new String(readFileToByteArray(file), StandardCharsets.UTF_8);
return gson.fromJson(jsonContent, classOfT);
}
/**
* Read the contents of a file and return them as a byte array.
*
* @param file The file to read.
* @return The byte array representing the file's content.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public byte[] readFileToByteArray(File file) {
byte[] fileBytes = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file)) {
@@ -98,6 +128,7 @@ public class JsonImpl extends AbstractStorage {
return fileBytes;
}
// Retrieve a set of unique user UUIDs based on JSON data files in the 'data' folder.
@Override
public Set<UUID> getUniqueUsers(boolean legacy) {
// No legacy files

View File

@@ -31,6 +31,9 @@ import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* A data storage implementation that uses YAML files to store player data, with support for legacy data.
*/
public class YAMLImpl extends AbstractStorage implements LegacyDataStorageInterface {
@SuppressWarnings("ResultOfMethodCallIgnored")
@@ -45,6 +48,12 @@ public class YAMLImpl extends AbstractStorage implements LegacyDataStorageInterf
return StorageType.YAML;
}
/**
* Get the file associated with a player's UUID for storing YAML data.
*
* @param uuid The UUID of the player.
* @return The file for the player's data.
*/
public File getPlayerDataFile(UUID uuid) {
return new File(plugin.getDataFolder(), "data" + File.separator + uuid + ".yml");
}
@@ -109,7 +118,13 @@ public class YAMLImpl extends AbstractStorage implements LegacyDataStorageInterf
return uuids;
}
public StatisticData getStatistics(ConfigurationSection section) {
/**
* Parse statistics data from a YAML ConfigurationSection.
*
* @param section The ConfigurationSection containing statistics data.
* @return The parsed StatisticData object.
*/
private StatisticData getStatistics(ConfigurationSection section) {
if (section == null)
return StatisticData.empty();
else {
@@ -123,6 +138,7 @@ public class YAMLImpl extends AbstractStorage implements LegacyDataStorageInterf
@Override
public CompletableFuture<Optional<PlayerData>> getLegacyPlayerData(UUID uuid) {
// Retrieve legacy player data (YAML format) for a given UUID.
var builder = new PlayerData.Builder().setName("");
File bagFile = new File(plugin.getDataFolder(), "data/fishingbag/" + uuid + ".yml");
if (bagFile.exists()) {

View File

@@ -36,6 +36,9 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Implementation of the OfflineUser interface for representing offline player data.
*/
public class OfflineUserImpl implements OfflineUser {
private final UUID uuid;
@@ -43,13 +46,22 @@ public class OfflineUserImpl implements OfflineUser {
private final FishingBagHolder holder;
private final EarningData earningData;
private final Statistics statistics;
public static OfflineUserImpl LOCKED_USER = new OfflineUserImpl(UUID.randomUUID(), "", PlayerData.empty());
public static OfflineUserImpl LOCKED_USER = new OfflineUserImpl(UUID.randomUUID(), "-locked-", PlayerData.empty());
/**
* Constructor to create an OfflineUserImpl instance.
*
* @param uuid The UUID of the player.
* @param name The name of the player.
* @param playerData The player's data, including bag contents, earnings, and statistics.
*/
public OfflineUserImpl(UUID uuid, String name, PlayerData playerData) {
this.name = name;
this.uuid = uuid;
this.holder = new FishingBagHolder(uuid);
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
// Set up the inventory for the FishingBagHolder
this.holder.setInventory(InventoryUtils.createInventory(this.holder, playerData.getBagData().size,
AdventureManagerImpl.getInstance().getComponentFromMiniMessage(
PlaceholderManagerImpl.getInstance().parse(
@@ -57,6 +69,7 @@ public class OfflineUserImpl implements OfflineUser {
)
)));
this.holder.setItems(InventoryUtils.getInventoryItems(playerData.getBagData().serialized));
this.earningData = playerData.getEarningData();
this.statistics = new Statistics(playerData.getStatistics());
}
@@ -94,6 +107,7 @@ public class OfflineUserImpl implements OfflineUser {
@Override
public PlayerData getPlayerData() {
// Create a new PlayerData instance based on the stored information
return new PlayerData.Builder()
.setBagData(new InventoryData(InventoryUtils.stacksToBase64(holder.getInventory().getStorageContents()), holder.getInventory().getSize()))
.setEarningData(earningData)

View File

@@ -21,10 +21,19 @@ import net.momirealms.customfishing.api.data.PlayerData;
import net.momirealms.customfishing.api.data.user.OnlineUser;
import org.bukkit.entity.Player;
/**
* Implementation of the OnlineUser interface, extending OfflineUserImpl to represent online player data.
*/
public class OnlineUserImpl extends OfflineUserImpl implements OnlineUser {
private final Player player;
/**
* Constructor to create an OnlineUserImpl instance.
*
* @param player The online player associated with this user.
* @param playerData The player's data, including bag contents, earnings, and statistics.
*/
public OnlineUserImpl(Player player, PlayerData playerData) {
super(player.getUniqueId(), player.getName(), playerData);
this.player = player;

View File

@@ -33,14 +33,30 @@ import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Utility class for managing armor stands and sending related packets.
*/
public class ArmorStandUtils {
/**
* Creates a destroy packet for removing an armor stand entity.
*
* @param id The ID of the armor stand entity to destroy
* @return The PacketContainer representing the destroy packet
*/
public static PacketContainer getDestroyPacket(int id) {
PacketContainer destroyPacket = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY);
destroyPacket.getIntLists().write(0, List.of(id));
return destroyPacket;
}
/**
* Creates a spawn packet for an armor stand entity at the specified location.
*
* @param id The ID of the armor stand entity to spawn
* @param location The location where the armor stand entity should be spawned
* @return The PacketContainer representing the spawn packet
*/
public static PacketContainer getSpawnPacket(int id, Location location) {
PacketContainer entityPacket = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY);
entityPacket.getModifier().write(0, id);
@@ -52,6 +68,12 @@ public class ArmorStandUtils {
return entityPacket;
}
/**
* Creates a metadata packet for updating the metadata of an armor stand entity.
*
* @param id The ID of the armor stand entity
* @return The PacketContainer representing the metadata packet
*/
public static PacketContainer getMetaPacket(int id) {
PacketContainer metaPacket = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA);
metaPacket.getIntegers().write(0, id);
@@ -64,6 +86,13 @@ public class ArmorStandUtils {
return metaPacket;
}
/**
* Sets the value list in a PacketContainer's DataWatcher from a WrappedDataWatcher.
*
* @param metaPacket The PacketContainer representing the metadata packet
* @param wrappedDataWatcher The WrappedDataWatcher containing the value list
*/
@SuppressWarnings("DuplicatedCode")
private static void setValueList(PacketContainer metaPacket, WrappedDataWatcher wrappedDataWatcher) {
List<WrappedDataValue> wrappedDataValueList = Lists.newArrayList();
wrappedDataWatcher.getWatchableObjects().stream().filter(Objects::nonNull).forEach(entry -> {
@@ -73,6 +102,13 @@ public class ArmorStandUtils {
metaPacket.getDataValueCollectionModifier().write(0, wrappedDataValueList);
}
/**
* Creates a metadata packet for updating the metadata of an armor stand entity with a custom Component.
*
* @param id The ID of the armor stand entity
* @param component The Component to set as metadata
* @return The PacketContainer representing the metadata packet
*/
public static PacketContainer getMetaPacket(int id, Component component) {
PacketContainer metaPacket = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA);
metaPacket.getIntegers().write(0, id);
@@ -85,6 +121,11 @@ public class ArmorStandUtils {
return metaPacket;
}
/**
* Creates a DataWatcher for an invisible armor stand entity.
*
* @return The created DataWatcher
*/
public static WrappedDataWatcher createDataWatcher() {
WrappedDataWatcher wrappedDataWatcher = new WrappedDataWatcher();
WrappedDataWatcher.Serializer serializer1 = WrappedDataWatcher.Registry.get(Boolean.class);
@@ -95,6 +136,12 @@ public class ArmorStandUtils {
return wrappedDataWatcher;
}
/**
* Creates a DataWatcher for an invisible armor stand entity with a custom Component.
*
* @param component The Component to set in the DataWatcher
* @return The created DataWatcher
*/
public static WrappedDataWatcher createDataWatcher(Component component) {
WrappedDataWatcher wrappedDataWatcher = new WrappedDataWatcher();
WrappedDataWatcher.Serializer serializer1 = WrappedDataWatcher.Registry.get(Boolean.class);
@@ -107,6 +154,13 @@ public class ArmorStandUtils {
return wrappedDataWatcher;
}
/**
* Creates an equipment packet for equipping an armor stand with an ItemStack.
*
* @param id The ID of the armor stand entity
* @param itemStack The ItemStack to equip
* @return The PacketContainer representing the equipment packet
*/
public static PacketContainer getEquipPacket(int id, ItemStack itemStack) {
PacketContainer equipPacket = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT);
equipPacket.getIntegers().write(0, id);
@@ -116,18 +170,34 @@ public class ArmorStandUtils {
return equipPacket;
}
public static void sendFakeItem(Player player, Location location, ItemStack itemStack, int time) {
/**
* Sends a fake armor stand entity with item on head to a player at the specified location.
*
* @param player The player to send the entity to
* @param location The location where the entity should appear
* @param itemStack The ItemStack to represent the entity
* @param seconds The duration (in seconds) the entity should be displayed
*/
public static void sendFakeItem(Player player, Location location, ItemStack itemStack, int seconds) {
int id = new Random().nextInt(Integer.MAX_VALUE);
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getSpawnPacket(id, location.clone().subtract(0,1,0)));
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getMetaPacket(id));
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getEquipPacket(id, itemStack));
CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(() -> CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getDestroyPacket(id)), time * 50L, TimeUnit.MILLISECONDS);
CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(() -> CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getDestroyPacket(id)), seconds * 50L, TimeUnit.MILLISECONDS);
}
public static void sendHologram(Player player, Location location, Component component, int time) {
/**
* Sends a hologram (armor stand with custom text) to a player at the specified location.
*
* @param player The player to send the hologram to
* @param location The location where the hologram should appear
* @param component The Component representing the hologram's text
* @param seconds The duration (in seconds) the hologram should be displayed
*/
public static void sendHologram(Player player, Location location, Component component, int seconds) {
int id = new Random().nextInt(Integer.MAX_VALUE);
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getSpawnPacket(id, location.clone().subtract(0,1,0)));
CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getMetaPacket(id, component));
CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(() -> CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getDestroyPacket(id)), time * 50L, TimeUnit.MILLISECONDS);
CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(() -> CustomFishingPluginImpl.getProtocolManager().sendServerPacket(player, getDestroyPacket(id)), seconds * 50L, TimeUnit.MILLISECONDS);
}
}

View File

@@ -31,6 +31,16 @@ import java.util.jar.JarInputStream;
public class ClassUtils {
/**
* Attempts to find a class within a JAR file that extends or implements a given class or interface.
*
* @param file The JAR file in which to search for the class.
* @param clazz The base class or interface to match against.
* @param <T> The type of the base class or interface.
* @return A Class object representing the found class, or null if not found.
* @throws IOException If there is an issue reading the JAR file.
* @throws ClassNotFoundException If the specified class cannot be found.
*/
@Nullable
public static <T> Class<? extends T> findClass(
@NotNull File file,

View File

@@ -24,9 +24,17 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.Collector;
import java.util.stream.Stream;
public final class CompletableFutures {
public class CompletableFutures {
private CompletableFutures() {}
/**
* A collector for collecting a stream of CompletableFuture instances into a single CompletableFuture that completes
* when all of the input CompletableFutures complete.
*
* @param <T> The type of CompletableFuture.
* @return A collector for CompletableFuture instances.
*/
public static <T extends CompletableFuture<?>> Collector<T, ImmutableList.Builder<T>, CompletableFuture<Void>> collector() {
return Collector.of(
ImmutableList.Builder::new,
@@ -36,11 +44,25 @@ public final class CompletableFutures {
);
}
/**
* Combines multiple CompletableFuture instances into a single CompletableFuture that completes when all of the input
* CompletableFutures complete.
*
* @param futures A stream of CompletableFuture instances.
* @return A CompletableFuture that completes when all input CompletableFutures complete.
*/
public static CompletableFuture<Void> allOf(Stream<? extends CompletableFuture<?>> futures) {
CompletableFuture<?>[] arr = futures.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(arr);
}
/**
* Combines multiple CompletableFuture instances into a single CompletableFuture that completes when all of the input
* CompletableFutures complete.
*
* @param futures A collection of CompletableFuture instances.
* @return A CompletableFuture that completes when all input CompletableFutures complete.
*/
public static CompletableFuture<Void> allOf(Collection<? extends CompletableFuture<?>> futures) {
CompletableFuture<?>[] arr = futures.toArray(new CompletableFuture[0]);
return CompletableFuture.allOf(arr);

View File

@@ -31,8 +31,17 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Utility class for configuration-related operations.
*/
public class ConfigUtils {
/**
* Converts an object into an ArrayList of strings.
*
* @param object The input object
* @return An ArrayList of strings
*/
@SuppressWarnings("unchecked")
public static ArrayList<String> stringListArgs(Object object) {
ArrayList<String> list = new ArrayList<>();
@@ -46,11 +55,23 @@ public class ConfigUtils {
return list;
}
/**
* Splits a string into a pair of integers using the "~" delimiter.
*
* @param value The input string
* @return A Pair of integers
*/
public static Pair<Integer, Integer> splitStringIntegerArgs(String value) {
String[] split = value.split("~");
return Pair.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
}
/**
* Converts a list of strings in the format "key:value" into a list of Pairs with keys and doubles.
*
* @param list The input list of strings
* @return A list of Pairs containing keys and doubles
*/
public static List<Pair<String, Double>> getWeights(List<String> list) {
List<Pair<String, Double>> result = new ArrayList<>(list.size());
for (String member : list) {
@@ -61,6 +82,12 @@ public class ConfigUtils {
return result;
}
/**
* Converts an object into a double value.
*
* @param arg The input object
* @return A double value
*/
public static double getDoubleValue(Object arg) {
if (arg instanceof Double d) {
return d;
@@ -70,6 +97,12 @@ public class ConfigUtils {
return 0;
}
/**
* Converts a list of strings in the format "key:value" into a list of Pairs with keys and WeightModifiers.
*
* @param modList The input list of strings
* @return A list of Pairs containing keys and WeightModifiers
*/
public static List<Pair<String, WeightModifier>> getModifiers(List<String> modList) {
List<Pair<String, WeightModifier>> result = new ArrayList<>(modList.size());
for (String member : modList) {
@@ -81,9 +114,10 @@ public class ConfigUtils {
}
/**
* Create a data file if not exists
* @param file file path
* @return yaml data
* Reads data from a YAML configuration file and creates it if it doesn't exist.
*
* @param file The file path
* @return The YamlConfiguration
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public static YamlConfiguration readData(File file) {
@@ -99,6 +133,13 @@ public class ConfigUtils {
return YamlConfiguration.loadConfiguration(file);
}
/**
* Parses a WeightModifier from a string representation.
*
* @param text The input string
* @return A WeightModifier based on the provided text
* @throws IllegalArgumentException if the weight format is invalid
*/
public static WeightModifier getModifier(String text) {
if (text.length() == 0) {
throw new IllegalArgumentException("Weight format is invalid.");
@@ -130,7 +171,7 @@ public class ConfigUtils {
return (player, weight) -> {
String temp = formula;
if (hasPapi)
temp = PlaceholderManagerImpl.getInstance().parseCacheable(player, formula);
temp = PlaceholderManagerImpl.getInstance().parseCacheablePlaceholders(player, formula);
Expression expression = new ExpressionBuilder(temp)
.variables("0")
.build()

View File

@@ -30,7 +30,6 @@ public class DynamicText {
private String originalValue;
private String latestValue;
private String[] placeholders;
private String[] previousParsedValues;
public DynamicText(Player owner, String rawValue) {
this.owner = owner;
@@ -38,6 +37,8 @@ public class DynamicText {
}
private void analyze(String value) {
// Analyze the provided text to find and replace placeholders with '%s'.
// Store the original value, placeholders, and the initial latest value.
List<String> placeholdersOwner = new ArrayList<>(PlaceholderManagerImpl.getInstance().detectPlaceholders(value));
String origin = value;
for (String placeholder : placeholdersOwner) {
@@ -53,6 +54,7 @@ public class DynamicText {
}
public boolean update(Map<String, String> placeholders) {
// Update the dynamic text by replacing placeholders with actual values.
String string = originalValue;
if (this.placeholders.length != 0) {
PlaceholderManagerImpl placeholderManagerImpl = PlaceholderManagerImpl.getInstance();
@@ -67,6 +69,7 @@ public class DynamicText {
}
}
if (!latestValue.equals(string)) {
// If the updated value is different from the latest value, update it.
latestValue = string;
return true;
}

View File

@@ -32,14 +32,30 @@ import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* Utility class for managing fake item entities using PacketContainers.
*/
public class FakeItemUtils {
/**
* Creates a destroy packet for removing a fake item entity.
*
* @param id The ID of the fake item entity to destroy
* @return The PacketContainer representing the destroy packet
*/
public static PacketContainer getDestroyPacket(int id) {
PacketContainer destroyPacket = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY);
destroyPacket.getIntLists().write(0, List.of(id));
return destroyPacket;
}
/**
* Creates a spawn packet for a fake item entity at the specified location.
*
* @param id The ID of the fake item entity to spawn
* @param location The location where the fake item entity should be spawned
* @return The PacketContainer representing the spawn packet
*/
public static PacketContainer getSpawnPacket(int id, Location location) {
PacketContainer entityPacket = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY);
entityPacket.getModifier().write(0, id);
@@ -51,6 +67,13 @@ public class FakeItemUtils {
return entityPacket;
}
/**
* Creates a metadata packet for updating the metadata of a fake item entity.
*
* @param id The ID of the fake item entity
* @param itemStack The ItemStack to update the metadata with
* @return The PacketContainer representing the metadata packet
*/
public static PacketContainer getMetaPacket(int id, ItemStack itemStack) {
PacketContainer metaPacket = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA);
metaPacket.getIntegers().write(0, id);
@@ -63,6 +86,13 @@ public class FakeItemUtils {
return metaPacket;
}
/**
* Creates a teleport packet for moving a fake item entity to the specified location.
*
* @param id The ID of the fake item entity to teleport
* @param location The location to teleport the fake item entity to
* @return The PacketContainer representing the teleport packet
*/
public static PacketContainer getTpPacket(int id, Location location) {
PacketContainer tpPacket = new PacketContainer(PacketType.Play.Server.ENTITY_TELEPORT);
tpPacket.getModifier().write(0, id);
@@ -72,7 +102,14 @@ public class FakeItemUtils {
return tpPacket;
}
public static PacketContainer getVelocity(int id, Vector vector) {
/**
* Creates a velocity packet for applying velocity to a fake item entity.
*
* @param id The ID of the fake item entity
* @param vector The velocity vector to apply
* @return The PacketContainer representing the velocity packet
*/
public static PacketContainer getVelocityPacket(int id, Vector vector) {
PacketContainer entityPacket = new PacketContainer(PacketType.Play.Server.ENTITY_VELOCITY);
entityPacket.getModifier().write(0, id);
entityPacket.getIntegers().write(1, (int) (vector.getX() * 8000));
@@ -81,6 +118,12 @@ public class FakeItemUtils {
return entityPacket;
}
/**
* Creates a DataWatcher for a given ItemStack.
*
* @param itemStack The ItemStack to create the DataWatcher for
* @return The created DataWatcher
*/
public static WrappedDataWatcher createDataWatcher(ItemStack itemStack) {
WrappedDataWatcher wrappedDataWatcher = new WrappedDataWatcher();
wrappedDataWatcher.setObject(new WrappedDataWatcher.WrappedDataWatcherObject(8, WrappedDataWatcher.Registry.getItemStackSerializer(false)), itemStack);
@@ -88,6 +131,13 @@ public class FakeItemUtils {
return wrappedDataWatcher;
}
/**
* Sets the value list in a PacketContainer's DataWatcher from a WrappedDataWatcher.
*
* @param metaPacket The PacketContainer representing the metadata packet
* @param wrappedDataWatcher The WrappedDataWatcher containing the value list
*/
@SuppressWarnings("DuplicatedCode")
private static void setValueList(PacketContainer metaPacket, WrappedDataWatcher wrappedDataWatcher) {
List<WrappedDataValue> wrappedDataValueList = Lists.newArrayList();
wrappedDataWatcher.getWatchableObjects().stream().filter(Objects::nonNull).forEach(entry -> {

View File

@@ -36,8 +36,17 @@ import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
/**
* Utility class for various item-related operations.
*/
public class ItemUtils {
/**
* Updates the lore of an NBTItem based on its custom NBT tags.
*
* @param nbtItem The NBTItem to update
* @return The updated NBTItem
*/
public static NBTItem updateNBTItemLore(NBTItem nbtItem) {
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
if (cfCompound == null)
@@ -84,6 +93,11 @@ public class ItemUtils {
return nbtItem;
}
/**
* Updates the lore of an ItemStack based on its custom NBT tags.
*
* @param itemStack The ItemStack to update
*/
public static void updateItemLore(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return;
@@ -91,26 +105,104 @@ public class ItemUtils {
itemStack.setItemMeta(nbtItem.getItem().getItemMeta());
}
public static void reduceHookDurability(ItemStack itemStack, boolean updateLore) {
if (itemStack == null || itemStack.getType() == Material.AIR)
/**
* Reduces the durability of a fishing hook item.
*
* @param rod The fishing rod ItemStack
* @param updateLore Whether to update the lore after reducing durability
*/
public static void decreaseHookDurability(ItemStack rod, int amount, boolean updateLore) {
if (rod == null || rod.getType() != Material.FISHING_ROD)
return;
NBTItem nbtItem = new NBTItem(itemStack);
NBTItem nbtItem = new NBTItem(rod);
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
if (cfCompound != null && cfCompound.hasTag("hook_dur")) {
int hookDur = cfCompound.getInteger("hook_dur");
if (hookDur > 0) {
cfCompound.setInteger("hook_dur", hookDur - 1);
} else if (hookDur != -1) {
cfCompound.removeKey("hook_id");
cfCompound.removeKey("hook_dur");
cfCompound.removeKey("hook_id");
if (hookDur != -1) {
hookDur = Math.max(0, hookDur - amount);
if (hookDur > 0) {
cfCompound.setInteger("hook_dur", hookDur);
} else {
cfCompound.removeKey("hook_id");
cfCompound.removeKey("hook_dur");
cfCompound.removeKey("hook_item");
}
}
}
if (updateLore) updateNBTItemLore(nbtItem);
itemStack.setItemMeta(nbtItem.getItem().getItemMeta());
rod.setItemMeta(nbtItem.getItem().getItemMeta());
}
public static void loseDurability(ItemStack itemStack, int amount, boolean updateLore) {
/**
* Increases the durability of a fishing hook by a specified amount and optionally updates its lore.
*
* @param rod The fishing rod ItemStack to modify.
* @param amount The amount by which to increase the durability.
* @param updateLore Whether to update the lore of the fishing rod.
*/
public static void increaseHookDurability(ItemStack rod, int amount, boolean updateLore) {
if (rod == null || rod.getType() != Material.FISHING_ROD)
return;
NBTItem nbtItem = new NBTItem(rod);
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
if (cfCompound != null && cfCompound.hasTag("hook_dur")) {
int hookDur = cfCompound.getInteger("hook_dur");
if (hookDur != -1) {
String id = cfCompound.getString("hook_id");
HookSetting setting = CustomFishingPlugin.get().getHookManager().getHookSetting(id);
if (setting == null) {
cfCompound.removeKey("hook_id");
cfCompound.removeKey("hook_dur");
cfCompound.removeKey("hook_item");
} else {
hookDur = Math.min(setting.getMaxDurability(), hookDur + amount);
cfCompound.setInteger("hook_dur", hookDur);
}
}
}
if (updateLore) updateNBTItemLore(nbtItem);
rod.setItemMeta(nbtItem.getItem().getItemMeta());
}
/**
* Sets the durability of a fishing hook to a specific amount and optionally updates its lore.
*
* @param rod The fishing rod ItemStack to modify.
* @param amount The new durability value to set.
* @param updateLore Whether to update the lore of the fishing rod.
*/
public static void setHookDurability(ItemStack rod, int amount, boolean updateLore) {
if (rod == null || rod.getType() != Material.FISHING_ROD)
return;
NBTItem nbtItem = new NBTItem(rod);
NBTCompound cfCompound = nbtItem.getCompound("CustomFishing");
if (cfCompound != null && cfCompound.hasTag("hook_dur")) {
int hookDur = cfCompound.getInteger("hook_dur");
if (hookDur != -1) {
String id = cfCompound.getString("hook_id");
HookSetting setting = CustomFishingPlugin.get().getHookManager().getHookSetting(id);
if (setting == null) {
cfCompound.removeKey("hook_id");
cfCompound.removeKey("hook_dur");
cfCompound.removeKey("hook_item");
} else {
hookDur = Math.min(setting.getMaxDurability(), amount);
cfCompound.setInteger("hook_dur", hookDur);
}
}
}
if (updateLore) updateNBTItemLore(nbtItem);
rod.setItemMeta(nbtItem.getItem().getItemMeta());
}
/**
* Decreases the durability of an item and updates its lore.
*
* @param itemStack The ItemStack to reduce durability for
* @param amount The amount by which to reduce durability
* @param updateLore Whether to update the lore after reducing durability
*/
public static void decreaseDurability(ItemStack itemStack, int amount, boolean updateLore) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return;
int unBreakingLevel = itemStack.getEnchantmentLevel(Enchantment.DURABILITY);
@@ -145,7 +237,14 @@ public class ItemUtils {
}
}
public static void addDurability(ItemStack itemStack, int amount, boolean updateLore) {
/**
* Increases the durability of an item and updates its lore.
*
* @param itemStack The ItemStack to increase durability for
* @param amount The amount by which to increase durability
* @param updateLore Whether to update the lore after increasing durability
*/
public static void increaseDurability(ItemStack itemStack, int amount, boolean updateLore) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return;
NBTItem nbtItem = new NBTItem(itemStack);
@@ -167,6 +266,13 @@ public class ItemUtils {
itemStack.setItemMeta(nbtItem.getItem().getItemMeta());
}
/**
* Sets the durability of an item and updates its lore.
*
* @param itemStack The ItemStack to set durability for
* @param amount The new durability value
* @param updateLore Whether to update the lore after setting durability
*/
public static void setDurability(ItemStack itemStack, int amount, boolean updateLore) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return;
@@ -192,6 +298,12 @@ public class ItemUtils {
itemStack.setItemMeta(nbtItem.getItem().getItemMeta());
}
/**
* Retrieves the current durability of an item.
*
* @param itemStack The ItemStack to get durability from
* @return The current durability value
*/
public static int getDurability(ItemStack itemStack) {
if (!(itemStack.getItemMeta() instanceof Damageable damageable))
return -1;
@@ -206,6 +318,14 @@ public class ItemUtils {
}
}
/**
* Gives a certain amount of an item to a player, handling stacking and item drops.
*
* @param player The player to give the item to
* @param itemStack The ItemStack to give
* @param amount The amount of items to give
* @return The actual amount of items given
*/
public static int giveCertainAmountOfItem(Player player, ItemStack itemStack, int amount) {
PlayerInventory inventory = player.getInventory();
ItemMeta meta = itemStack.getItemMeta();

View File

@@ -21,6 +21,13 @@ import org.bukkit.Location;
public class LocationUtils {
/**
* Calculates the Euclidean distance between two locations in 3D space.
*
* @param location1 The first location
* @param location2 The second location
* @return The Euclidean distance between the two locations
*/
public static double getDistance(Location location1, Location location2) {
return Math.sqrt(Math.pow(location2.getX() - location1.getX(), 2) +
Math.pow(location2.getY() - location1.getY(), 2) +

View File

@@ -24,8 +24,14 @@ import org.bukkit.configuration.MemorySection;
import java.util.*;
/**
* Utility class for working with NBT (Named Binary Tag) data.
*/
public class NBTUtils {
/**
* Inner class representing a stack element used during NBT data conversion.
*/
public static class StackElement {
final Map<String, Object> currentMap;
final NBTCompound currentNbtCompound;
@@ -36,6 +42,12 @@ public class NBTUtils {
}
}
/**
* Converts data from a Bukkit YAML configuration to NBT tags.
*
* @param nbtCompound The target NBT compound
* @param map The source map from Bukkit YAML
*/
@SuppressWarnings("unchecked")
public static void setTagsFromBukkitYAML(NBTCompound nbtCompound, Map<String, Object> map) {
@@ -69,6 +81,7 @@ public class NBTUtils {
}
}
// Private helper method
private static void setListValue(String key, String value, NBTCompound nbtCompound) {
String[] parts = getTypeAndData(value);
String type = parts[0];
@@ -89,6 +102,7 @@ public class NBTUtils {
}
}
// Private helper method
private static void setSingleValue(String key, String value, NBTCompound nbtCompound) {
String[] parts = getTypeAndData(value);
String type = parts[0];
@@ -120,6 +134,12 @@ public class NBTUtils {
}
}
/**
* Converts an NBT compound to a map of key-value pairs.
*
* @param nbtCompound The source NBT compound
* @return A map representing the NBT data
*/
public static Map<String, Object> compoundToMap(ReadWriteNBT nbtCompound){
Map<String, Object> map = new HashMap<>();
for (String key : nbtCompound.getKeys()) {
@@ -155,6 +175,12 @@ public class NBTUtils {
return map;
}
/**
* Splits a value into type and data components.
*
* @param str The input value string
* @return An array containing type and data strings
*/
private static String[] getTypeAndData(String str) {
String[] parts = str.split("\\s+", 2);
if (parts.length != 2) {
@@ -165,6 +191,12 @@ public class NBTUtils {
return new String[]{type, data};
}
/**
* Splits a value containing arrays into individual elements.
*
* @param value The input value containing arrays
* @return An array of individual elements
*/
private static String[] splitValue(String value) {
return value.substring(value.indexOf('[') + 1, value.lastIndexOf(']'))
.replaceAll("\\s", "")

View File

@@ -28,6 +28,9 @@ import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CompletableFuture;
/**
* This class implements the VersionManager interface and is responsible for managing version-related information.
*/
public class VersionManagerImpl implements VersionManager {
private final boolean isNewerThan1_19_R2;
@@ -41,9 +44,13 @@ public class VersionManagerImpl implements VersionManager {
@SuppressWarnings("deprecation")
public VersionManagerImpl(CustomFishingPluginImpl plugin) {
this.plugin = plugin;
// Get the server version
serverVersion = plugin.getServer().getClass().getPackage().getName().split("\\.")[3];
String[] split = serverVersion.split("_");
int main_ver = Integer.parseInt(split[1]);
// Determine if the server version is newer than 1_19_R2 and 1_20_R1
if (main_ver >= 20) {
isNewerThan1_19_R2 = true;
isNewerThan1_20 = true;
@@ -54,12 +61,20 @@ public class VersionManagerImpl implements VersionManager {
isNewerThan1_19_R2 = false;
isNewerThan1_20 = false;
}
// Check if the server is Spigot
String server_name = plugin.getServer().getName();
this.isSpigot = server_name.equals("CraftBukkit");
// Check if the server is Folia
try {
Class.forName("io.papermc.paper.threadedregions.scheduler.AsyncScheduler");
this.isFolia = true;
} catch (ClassNotFoundException ignored) {}
} catch (ClassNotFoundException ignored) {
}
// Get the plugin version
this.pluginVersion = plugin.getDescription().getVersion();
}
@@ -93,6 +108,7 @@ public class VersionManagerImpl implements VersionManager {
return serverVersion;
}
// Method to asynchronously check for plugin updates
@Override
public CompletableFuture<Boolean> checkUpdate() {
CompletableFuture<Boolean> updateFuture = new CompletableFuture<>();
@@ -119,6 +135,7 @@ public class VersionManagerImpl implements VersionManager {
return updateFuture;
}
// Method to compare two version strings
private boolean compareVer(String newV, String currentV) {
if (newV == null || currentV == null || newV.isEmpty() || currentV.isEmpty()) {
return false;

View File

@@ -136,11 +136,11 @@ other-settings:
thread-pool-settings:
# The size of the core Thread pool, that is, the size of the Thread pool when there is no task to execute
# Increase the size of corePoolSize when you are running a large server with many players fishing at the same time
corePoolSize: 4
corePoolSize: 8
# The maximum number of threads allowed to be created in the Thread pool. The current number of threads in the Thread pool will not exceed this value
maximumPoolSize: 8
maximumPoolSize: 16
# If a thread is idle for more than this attribute value, it will exit due to timeout
keepAliveTime: 10
keepAliveTime: 30
# Event priority: MONITOR HIGHEST HIGH NORMAL LOW LOWEST
event-priority: NORMAL

View File

@@ -7,11 +7,11 @@ double_loot_totem:
4:
- '*_STAIRS{face=east;half=top} OBSERVER{face=south} *_STAIRS{face=west;half=top}'
3:
- 'AIR CRYING_OBSIDIAN AIR'
- 'AIR CRYING_OBSIDIAN AIR'
2:
- 'AIR *_LOG{axis=y}||*_PILLAR{axis=y} AIR'
- 'AIR *_LOG{axis=y}||*_PILLAR{axis=y} AIR'
1:
- 'AIR ANVIL AIR'
- 'AIR||GRASS||SNOW ANVIL AIR||GRASS||SNOW'
effects:
double_loot:
type: multiple-loot