sizeCreator;
+
+ /**
+ * A new inventory creator which should be able to create an inventory based on the type and the size.
+ *
+ * By default the creators are implemented as follows:
+ *
+ * typeCreator = (gui, who, type) -> plugin.getServer().createInventory(new Holder(gui), type, gui.replaceVars(who, title));
+ * sizeCreator = (gui, who, size) -> plugin.getServer().createInventory(new Holder(gui), size, gui.replaceVars(who, title));
+ *
+ * @param typeCreator The type creator.
+ * @param sizeCreator The size creator
+ */
+ public InventoryCreator(CreatorImplementation typeCreator, CreatorImplementation sizeCreator) {
+ this.typeCreator = typeCreator;
+ this.sizeCreator = sizeCreator;
+ }
+
+ public CreatorImplementation getTypeCreator() {
+ return typeCreator;
+ }
+
+ public CreatorImplementation getSizeCreator() {
+ return sizeCreator;
+ }
+
+ public interface CreatorImplementation {
+ /**
+ * Creates a new inventory
+ * @param gui The InventoryGui instance
+ * @param who The player to create the inventory for
+ * @param t The size or type of the inventory
+ * @return The created inventory
+ */
+ Inventory create(InventoryGui gui, HumanEntity who, T t);
+ }
+ }
+}
diff --git a/plugin/src/main/java/net/momirealms/customfishing/libraries/inventorygui/StaticGuiElement.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/inventorygui/StaticGuiElement.java
new file mode 100644
index 00000000..c6a23269
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/inventorygui/StaticGuiElement.java
@@ -0,0 +1,159 @@
+package net.momirealms.customfishing.libraries.inventorygui;
+
+/*
+ * Copyright 2017 Max Lee (https://github.com/Phoenix616)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Represents a simple element in a gui to which an action can be assigned.
+ * If you want the item to change on click you have to do that yourself.
+ */
+public class StaticGuiElement extends GuiElement {
+ private ItemStack item;
+ private int number;
+ private String[] text;
+
+ /**
+ * Represents an element in a gui
+ * @param slotChar The character to replace in the gui setup string
+ * @param item The item this element displays
+ * @param number The number, 1 will not display the number
+ * @param action The action to run when the player clicks on this element
+ * @param text The text to display on this element, placeholders are automatically
+ * replaced, see for a list of the
+ * placeholder variables. Empty text strings are also filter out, use
+ * a single space if you want to add an empty line!
+ * If it's not set/empty the item's default name will be used
+ * @throws IllegalArgumentException If the number is below 1 or above the max stack count (currently 64)
+ */
+ public StaticGuiElement(char slotChar, ItemStack item, int number, Action action, String... text) throws IllegalArgumentException {
+ super(slotChar, action);
+ this.item = item;
+ this.text = text;
+ setNumber(number);
+ }
+
+ /**
+ * Represents an element in a gui
+ * @param slotChar The character to replace in the gui setup string
+ * @param item The item this element displays
+ * @param action The action to run when the player clicks on this element
+ * @param text The text to display on this element, placeholders are automatically
+ * replaced, see {@link InventoryGui#replaceVars} for a list of the
+ * placeholder variables. Empty text strings are also filter out, use
+ * a single space if you want to add an empty line!
+ * If it's not set/empty the item's default name will be used
+ */
+ public StaticGuiElement(char slotChar, ItemStack item, Action action, String... text) {
+ this(slotChar, item, item != null ? item.getAmount() : 1, action, text);
+ }
+
+ /**
+ * Represents an element in a gui that doesn't have any action when clicked
+ * @param slotChar The character to replace in the gui setup string
+ * @param item The item this element displays
+ * @param text The text to display on this element, placeholders are automatically
+ * replaced, see {@link InventoryGui#replaceVars} for a list of the
+ * placeholder variables. Empty text strings are also filter out, use
+ * a single space if you want to add an empty line!
+ * If it's not set/empty the item's default name will be used
+ */
+ public StaticGuiElement(char slotChar, ItemStack item, String... text) {
+ this(slotChar, item, item != null ? item.getAmount() : 1, null, text);
+ }
+
+
+ /**
+ * Set the item that is displayed by this element
+ * @param item The item that should be displayed by this element
+ */
+ public void setItem(ItemStack item) {
+ this.item = item;
+ }
+
+ /**
+ * Get the raw item displayed by this element which was passed to the constructor or set with {@link #setItem(ItemStack)}.
+ * This item will not have the amount or text applied! Use {@link #getItem(HumanEntity, int)} for that!
+ * @return The raw item
+ */
+ public ItemStack getRawItem() {
+ return item;
+ }
+
+ @Override
+ public ItemStack getItem(HumanEntity who, int slot) {
+ if (item == null) {
+ return null;
+ }
+ ItemStack clone = item.clone();
+ gui.setItemText(who, clone, getText());
+ if (number > 0 && number <= 64) {
+ clone.setAmount(number);
+ }
+ return clone;
+ }
+
+ /**
+ * Set this element's display text. If this is an empty array the item's name will be displayed
+ * @param text The text to display on this element, placeholders are automatically
+ * replaced, see {@link InventoryGui#replaceVars} for a list of the
+ * placeholder variables. Empty text strings are also filter out, use
+ * a single space if you want to add an empty line!
+ * If it's not set/empty the item's default name will be used
+ */
+ public void setText(String... text) {
+ this.text = text;
+ }
+
+ /**
+ * Get the text that this element displays
+ * @return The text that is displayed on this element
+ */
+ public String[] getText() {
+ return text;
+ }
+
+ /**
+ * Set the number that this element should display (via the Item's amount)
+ * @param number The number, 1 will not display the number
+ * @return true if the number was set; false if it was below 1 or above 64
+ */
+ public boolean setNumber(int number) {
+ if (number < 1 || number > 64) {
+ this.number = 1;
+ return false;
+ }
+ this.number = number;
+ return true;
+ }
+
+ /**
+ * Get the number that this element should display
+ * @return The number (item amount) that this element currently has
+ */
+ public int getNumber() {
+ return number;
+ }
+
+}
diff --git a/src/main/java/net/momirealms/customfishing/helper/LibraryLoader.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/LibraryLoader.java
similarity index 53%
rename from src/main/java/net/momirealms/customfishing/helper/LibraryLoader.java
rename to plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/LibraryLoader.java
index 434d81b1..3bc61b80 100644
--- a/src/main/java/net/momirealms/customfishing/helper/LibraryLoader.java
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/LibraryLoader.java
@@ -23,11 +23,12 @@
* SOFTWARE.
*/
-package net.momirealms.customfishing.helper;
+package net.momirealms.customfishing.libraries.libraryloader;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
-import net.momirealms.customfishing.CustomFishing;
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.util.LogUtils;
import java.io.File;
import java.io.InputStream;
@@ -45,7 +46,7 @@ import java.util.StringJoiner;
public final class LibraryLoader {
@SuppressWarnings("Guava")
- private static final Supplier URL_INJECTOR = Suppliers.memoize(() -> URLClassLoaderAccess.create((URLClassLoader) CustomFishing.getInstance().getClass().getClassLoader()));
+ private static final Supplier URL_INJECTOR = Suppliers.memoize(() -> URLClassLoaderAccess.create((URLClassLoader) CustomFishingPlugin.getInstance().getClass().getClassLoader()));
/**
* Resolves all {@link MavenLibrary} annotations on the given object.
@@ -63,9 +64,6 @@ public final class LibraryLoader {
*/
public static void loadAll(Class> clazz) {
MavenLibrary[] libs = clazz.getDeclaredAnnotationsByType(MavenLibrary.class);
- if (libs == null) {
- return;
- }
for (MavenLibrary lib : libs) {
load(lib.groupId(), lib.artifactId(), lib.version(), lib.repo().url());
}
@@ -75,31 +73,38 @@ public final class LibraryLoader {
load(new Dependency(groupId, artifactId, version, repoUrl));
}
- public static void load(Dependency d) {
- //Log.info(String.format("Loading dependency %s:%s:%s from %s", d.getGroupId(), d.getArtifactId(), d.getVersion(), d.getRepoUrl()));
- String name = d.getArtifactId() + "-" + d.getVersion();
+ public static void loadDependencies(String... libs) {
+ if (libs == null || libs.length % 2 != 0)
+ return;
+ for (int i = 0; i < libs.length; i+=2) {
+ String[] split = libs[i].split(":");
+ load(new Dependency(
+ split[0],
+ split[1],
+ split[2],
+ libs[i+1]
+ ));
+ }
+ }
+ public static void load(Dependency d) {
+ LogUtils.info(String.format("Loading dependency %s:%s:%s from %s", d.groupId, d.artifactId, d.version, d.repoUrl));
+ String name = d.artifactId() + "-" + d.version();
File saveLocation = new File(getLibFolder(d), name + ".jar");
if (!saveLocation.exists()) {
-
try {
- Log.info("Dependency '" + name + "' is not already in the libraries folder. Attempting to download...");
+ LogUtils.info("Dependency '" + name + "' is not already in the libraries folder. Attempting to download...");
URL url = d.getUrl();
-
try (InputStream is = url.openStream()) {
Files.copy(is, saveLocation.toPath());
- Log.info("Dependency '" + name + "' successfully downloaded.");
+ LogUtils.info("Dependency '" + name + "' successfully downloaded.");
}
-
} catch (Exception e) {
e.printStackTrace();
}
}
-
- if (!saveLocation.exists()) {
+ if (!saveLocation.exists())
throw new RuntimeException("Unable to download dependency: " + d);
- }
-
try {
URL_INJECTOR.get().addURL(saveLocation.toURI().toURL());
} catch (Exception e) {
@@ -107,12 +112,13 @@ public final class LibraryLoader {
}
}
+ @SuppressWarnings("all")
private static File getLibFolder(Dependency dependency) {
- File pluginDataFolder = CustomFishing.getInstance().getDataFolder();
+ File pluginDataFolder = CustomFishingPlugin.getInstance().getDataFolder();
File serverDir = pluginDataFolder.getParentFile().getParentFile();
File helperDir = new File(serverDir, "libraries");
- String[] split = dependency.getGroupId().split("\\.");
+ String[] split = dependency.groupId().split("\\.");
File jarDir;
StringJoiner stringJoiner = new StringJoiner(File.separator);
for (String str : split) {
@@ -123,75 +129,53 @@ public final class LibraryLoader {
return jarDir;
}
- public static final class Dependency {
- private final String groupId;
- private final String artifactId;
- private final String version;
- private final String repoUrl;
-
- public Dependency(String groupId, String artifactId, String version, String repoUrl) {
- this.groupId = Objects.requireNonNull(groupId, "groupId");
- this.artifactId = Objects.requireNonNull(artifactId, "artifactId");
- this.version = Objects.requireNonNull(version, "version");
- this.repoUrl = Objects.requireNonNull(repoUrl, "repoUrl");
- }
-
- public String getGroupId() {
- return this.groupId;
- }
-
- public String getArtifactId() {
- return this.artifactId;
- }
-
- public String getVersion() {
- return this.version;
- }
-
- public String getRepoUrl() {
- return this.repoUrl;
- }
-
- public URL getUrl() throws MalformedURLException {
- String repo = this.repoUrl;
- if (!repo.endsWith("/")) {
- repo += "/";
+ public record Dependency(String groupId, String artifactId, String version, String repoUrl) {
+ public Dependency(String groupId, String artifactId, String version, String repoUrl) {
+ this.groupId = Objects.requireNonNull(groupId, "groupId");
+ this.artifactId = Objects.requireNonNull(artifactId, "artifactId");
+ this.version = Objects.requireNonNull(version, "version");
+ this.repoUrl = Objects.requireNonNull(repoUrl, "repoUrl");
}
- repo += "%s/%s/%s/%s-%s.jar";
- String url = String.format(repo, this.groupId.replace(".", "/"), this.artifactId, this.version, this.artifactId, this.version);
- return new URL(url);
- }
+ public URL getUrl() throws MalformedURLException {
+ String repo = this.repoUrl;
+ if (!repo.endsWith("/")) {
+ repo += "/";
+ }
+ repo += "%s/%s/%s/%s-%s.jar";
- @Override
- public boolean equals(Object o) {
- if (o == this) return true;
- if (!(o instanceof Dependency)) return false;
- final Dependency other = (Dependency) o;
- return this.getGroupId().equals(other.getGroupId()) &&
- this.getArtifactId().equals(other.getArtifactId()) &&
- this.getVersion().equals(other.getVersion()) &&
- this.getRepoUrl().equals(other.getRepoUrl());
- }
+ String url = String.format(repo, this.groupId.replace(".", "/"), this.artifactId, this.version, this.artifactId, this.version);
+ return new URL(url);
+ }
- @Override
- public int hashCode() {
- final int PRIME = 59;
- int result = 1;
- result = result * PRIME + this.getGroupId().hashCode();
- result = result * PRIME + this.getArtifactId().hashCode();
- result = result * PRIME + this.getVersion().hashCode();
- result = result * PRIME + this.getRepoUrl().hashCode();
- return result;
- }
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof Dependency other)) return false;
+ return this.groupId().equals(other.groupId()) &&
+ this.artifactId().equals(other.artifactId()) &&
+ this.version().equals(other.version()) &&
+ this.repoUrl().equals(other.repoUrl());
+ }
- @Override
- public String toString() {
- return "LibraryLoader.Dependency(" +
- "groupId=" + this.getGroupId() + ", " +
- "artifactId=" + this.getArtifactId() + ", " +
- "version=" + this.getVersion() + ", " +
- "repoUrl=" + this.getRepoUrl() + ")";
+ @Override
+ public int hashCode() {
+ final int PRIME = 59;
+ int result = 1;
+ result = result * PRIME + this.groupId().hashCode();
+ result = result * PRIME + this.artifactId().hashCode();
+ result = result * PRIME + this.version().hashCode();
+ result = result * PRIME + this.repoUrl().hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "LibraryLoader.Dependency(" +
+ "groupId=" + this.groupId() + ", " +
+ "artifactId=" + this.artifactId() + ", " +
+ "version=" + this.version() + ", " +
+ "repoUrl=" + this.repoUrl() + ")";
+ }
}
- }
}
diff --git a/src/main/java/net/momirealms/customfishing/helper/MavenLibraries.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibraries.java
similarity index 96%
rename from src/main/java/net/momirealms/customfishing/helper/MavenLibraries.java
rename to plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibraries.java
index dae6f3cf..7179e34f 100644
--- a/src/main/java/net/momirealms/customfishing/helper/MavenLibraries.java
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibraries.java
@@ -23,7 +23,7 @@
* SOFTWARE.
*/
-package net.momirealms.customfishing.helper;
+package net.momirealms.customfishing.libraries.libraryloader;
import org.jetbrains.annotations.NotNull;
diff --git a/src/main/java/net/momirealms/customfishing/helper/MavenLibrary.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibrary.java
similarity index 97%
rename from src/main/java/net/momirealms/customfishing/helper/MavenLibrary.java
rename to plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibrary.java
index 7a979775..60ad69c8 100644
--- a/src/main/java/net/momirealms/customfishing/helper/MavenLibrary.java
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/MavenLibrary.java
@@ -23,7 +23,7 @@
* SOFTWARE.
*/
-package net.momirealms.customfishing.helper;
+package net.momirealms.customfishing.libraries.libraryloader;
import org.jetbrains.annotations.NotNull;
diff --git a/src/main/java/net/momirealms/customfishing/helper/Repository.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/Repository.java
similarity index 96%
rename from src/main/java/net/momirealms/customfishing/helper/Repository.java
rename to plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/Repository.java
index 03c3affc..c0798824 100644
--- a/src/main/java/net/momirealms/customfishing/helper/Repository.java
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/Repository.java
@@ -23,7 +23,7 @@
* SOFTWARE.
*/
-package net.momirealms.customfishing.helper;
+package net.momirealms.customfishing.libraries.libraryloader;
import java.lang.annotation.*;
diff --git a/src/main/java/net/momirealms/customfishing/helper/URLClassLoaderAccess.java b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/URLClassLoaderAccess.java
similarity index 98%
rename from src/main/java/net/momirealms/customfishing/helper/URLClassLoaderAccess.java
rename to plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/URLClassLoaderAccess.java
index adb705ca..43e6df69 100644
--- a/src/main/java/net/momirealms/customfishing/helper/URLClassLoaderAccess.java
+++ b/plugin/src/main/java/net/momirealms/customfishing/libraries/libraryloader/URLClassLoaderAccess.java
@@ -23,7 +23,7 @@
* SOFTWARE.
*/
-package net.momirealms.customfishing.helper;
+package net.momirealms.customfishing.libraries.libraryloader;
import org.jetbrains.annotations.NotNull;
diff --git a/plugin/src/main/java/net/momirealms/customfishing/mechanic/action/ActionManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/mechanic/action/ActionManagerImpl.java
new file mode 100644
index 00000000..102c5f20
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/mechanic/action/ActionManagerImpl.java
@@ -0,0 +1,317 @@
+package net.momirealms.customfishing.mechanic.action;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.sound.Sound;
+import net.momirealms.customfishing.adventure.AdventureManagerImpl;
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.manager.ActionManager;
+import net.momirealms.customfishing.api.mechanic.action.Action;
+import net.momirealms.customfishing.api.mechanic.action.ActionBuilder;
+import net.momirealms.customfishing.api.util.LogUtils;
+import net.momirealms.customfishing.compatibility.papi.PlaceholderManagerImpl;
+import net.momirealms.customfishing.util.ConfigUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.entity.ExperienceOrb;
+import org.bukkit.entity.Player;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class ActionManagerImpl implements ActionManager {
+
+ private final CustomFishingPlugin plugin;
+ private final HashMap actionBuilderMap;
+
+ public ActionManagerImpl(CustomFishingPlugin plugin) {
+ this.plugin = plugin;
+ this.actionBuilderMap = new HashMap<>();
+ this.registerInbuiltActions();
+ }
+
+ private void registerInbuiltActions() {
+ this.registerMessageAction();
+ this.registerCommandAction();
+ this.registerMendingAction();
+ this.registerExpAction();
+ this.registerChainAction();
+ this.registerPotionAction();
+ this.registerSoundAction();
+ this.registerPluginExpAction();
+ this.registerTitleAction();
+ this.registerActionBarAction();
+ }
+
+ @Override
+ public boolean registerAction(String type, ActionBuilder actionBuilder) {
+ if (this.actionBuilderMap.containsKey(type)) return false;
+ this.actionBuilderMap.put(type, actionBuilder);
+ return true;
+ }
+
+ @Override
+ public boolean unregisterAction(String type) {
+ return this.actionBuilderMap.remove(type) != null;
+ }
+
+ @Override
+ public Action getAction(ConfigurationSection section) {
+ return getActionBuilder(section.getString("type")).build(section.get("value"), section.getDouble("chance", 1d));
+ }
+
+ @Nullable
+ @Override
+ public Action[] getActions(ConfigurationSection section) {
+ if (section == null) return null;
+ ArrayList actionList = new ArrayList<>();
+ for (Map.Entry entry : section.getValues(false).entrySet()) {
+ if (entry.getValue() instanceof ConfigurationSection innerSection) {
+ actionList.add(getAction(innerSection));
+ }
+ }
+ return actionList.toArray(new Action[0]);
+ }
+
+ @Override
+ public ActionBuilder getActionBuilder(String type) {
+ return actionBuilderMap.get(type);
+ }
+
+ private void registerMessageAction() {
+ registerAction("message", (args, chance) -> {
+ ArrayList msg = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ List replaced = PlaceholderManagerImpl.getInstance().parse(
+ condition.getPlayer(),
+ msg,
+ condition.getArgs()
+ );
+ for (String text : replaced) {
+ AdventureManagerImpl.getInstance().sendPlayerMessage(condition.getPlayer(), text);
+ }
+ };
+ });
+ registerAction("broadcast", (args, chance) -> {
+ ArrayList msg = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ List replaced = PlaceholderManagerImpl.getInstance().parse(
+ condition.getPlayer(),
+ msg,
+ condition.getArgs()
+ );
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ for (String text : replaced) {
+ AdventureManagerImpl.getInstance().sendPlayerMessage(player, text);
+ }
+ }
+ };
+ });
+ registerAction("random-message", (args, chance) -> {
+ ArrayList msg = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ String random = msg.get(ThreadLocalRandom.current().nextInt(msg.size()));
+ random = PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), random, condition.getArgs());
+ AdventureManagerImpl.getInstance().sendPlayerMessage(condition.getPlayer(), random);
+ };
+ });
+ }
+
+ private void registerCommandAction() {
+ registerAction("command", (args, chance) -> {
+ ArrayList cmd = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ List replaced = PlaceholderManagerImpl.getInstance().parse(
+ condition.getPlayer(),
+ cmd,
+ condition.getArgs()
+ );
+ plugin.getScheduler().runTaskSync(() -> {
+ for (String text : replaced) {
+ Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), text);
+ }
+ }, condition.getLocation());
+ };
+ });
+ registerAction("random-command", (args, chance) -> {
+ ArrayList cmd = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ String random = cmd.get(ThreadLocalRandom.current().nextInt(cmd.size()));
+ random = PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), random, condition.getArgs());
+ String finalRandom = random;
+ plugin.getScheduler().runTaskSync(() -> {
+ Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalRandom);
+ }, condition.getLocation());
+ };
+ });
+ }
+
+ private void registerActionBarAction() {
+ registerAction("actionbar", (args, chance) -> {
+ String text = (String) args;
+ return condition -> {
+ if (Math.random() > chance) return;
+ String parsed = PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), text, condition.getArgs());
+ AdventureManagerImpl.getInstance().sendActionbar(condition.getPlayer(), parsed);
+ };
+ });
+ registerAction("random-actionbar", (args, chance) -> {
+ ArrayList texts = ConfigUtils.stringListArgs(args);
+ return condition -> {
+ if (Math.random() > chance) return;
+ String random = texts.get(ThreadLocalRandom.current().nextInt(texts.size()));
+ random = PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), random, condition.getArgs());
+ AdventureManagerImpl.getInstance().sendActionbar(condition.getPlayer(), random);
+ };
+ });
+ }
+
+ private void registerMendingAction() {
+ registerAction("mending", (args, chance) -> {
+ int xp = (int) args;
+ return condition -> {
+ if (Math.random() > chance) return;
+ if (CustomFishingPlugin.get().getVersionManager().isSpigot()) {
+ condition.getPlayer().getLocation().getWorld().spawn(condition.getPlayer().getLocation(), ExperienceOrb.class, e -> e.setExperience(xp));
+ } else {
+ condition.getPlayer().giveExp(xp, true);
+ AdventureManagerImpl.getInstance().sendSound(condition.getPlayer(), Sound.Source.PLAYER, Key.key("minecraft:entity.experience_orb.pickup"), 1, 1);
+ }
+ };
+ });
+ }
+
+ private void registerExpAction() {
+ registerAction("exp", (args, chance) -> {
+ int xp = (int) args;
+ return condition -> {
+ if (Math.random() > chance) return;
+ condition.getPlayer().giveExp(xp);
+ };
+ });
+ }
+
+ private void registerChainAction() {
+ registerAction("chain", (args, chance) -> {
+ List actions = new ArrayList<>();
+ if (args instanceof ConfigurationSection section) {
+ for (Map.Entry entry : section.getValues(false).entrySet()) {
+ if (entry.getValue() instanceof ConfigurationSection innerSection) {
+ actions.add(getAction(innerSection));
+ }
+ }
+ }
+ return condition -> {
+ if (Math.random() > chance) return;
+ for (Action action : actions) {
+ action.trigger(condition);
+ }
+ };
+ });
+ }
+
+ private void registerTitleAction() {
+ registerAction("title", (args, chance) -> {
+ if (args instanceof ConfigurationSection section) {
+ String title = section.getString("title");
+ String subtitle = section.getString("subtitle");
+ int fadeIn = section.getInt("fade-in", 20);
+ int stay = section.getInt("stay", 30);
+ int fadeOut = section.getInt("fade-out", 10);
+ return condition -> {
+ if (Math.random() > chance) return;
+ AdventureManagerImpl.getInstance().sendTitle(
+ condition.getPlayer(),
+ PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), title, condition.getArgs()),
+ PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), subtitle, condition.getArgs()),
+ fadeIn * 50,
+ stay * 50,
+ fadeOut * 50
+ );
+ };
+ }
+ return null;
+ });
+ registerAction("random-title", (args, chance) -> {
+ if (args instanceof ConfigurationSection section) {
+ List titles = section.getStringList("titles");
+ List subtitles = section.getStringList("subtitles");
+ int fadeIn = section.getInt("fade-in", 20);
+ int stay = section.getInt("stay", 30);
+ int fadeOut = section.getInt("fade-out", 10);
+ return condition -> {
+ if (Math.random() > chance) return;
+ AdventureManagerImpl.getInstance().sendTitle(
+ condition.getPlayer(),
+ PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), titles.get(ThreadLocalRandom.current().nextInt(titles.size())), condition.getArgs()),
+ PlaceholderManagerImpl.getInstance().parse(condition.getPlayer(), subtitles.get(ThreadLocalRandom.current().nextInt(subtitles.size())), condition.getArgs()),
+ fadeIn * 50,
+ stay * 50,
+ fadeOut * 50
+ );
+ };
+ }
+ return null;
+ });
+ }
+
+ private void registerPotionAction() {
+ registerAction("potion-effect", (args, chance) -> {
+ if (args instanceof ConfigurationSection section) {
+ PotionEffect potionEffect = new PotionEffect(
+ Objects.requireNonNull(PotionEffectType.getByName(section.getString("type", "BLINDNESS").toUpperCase(Locale.ENGLISH))),
+ section.getInt("duration", 20),
+ section.getInt("amplifier", 0)
+ );
+ return condition -> {
+ if (Math.random() > chance) return;
+ condition.getPlayer().addPotionEffect(potionEffect);
+ };
+ }
+ return null;
+ });
+ }
+
+ @SuppressWarnings("all")
+ private void registerSoundAction() {
+ registerAction("sound", (args, chance) -> {
+ if (args instanceof ConfigurationSection section) {
+ Sound sound = Sound.sound(
+ Key.key(section.getString("key")),
+ Sound.Source.valueOf(section.getString("source", "PLAYER").toUpperCase(Locale.ENGLISH)),
+ (float) section.getDouble("volume", 1),
+ (float) section.getDouble("pitch", 1)
+ );
+ return condition -> {
+ if (Math.random() > chance) return;
+ AdventureManagerImpl.getInstance().sendSound(condition.getPlayer(), sound);
+ };
+ }
+ return null;
+ });
+ }
+
+ private void registerPluginExpAction() {
+ registerAction("plugin-exp", (args, chance) -> {
+ if (args instanceof ConfigurationSection section) {
+ String pluginName = section.getString("plugin");
+ double exp = section.getDouble("exp", 1);
+ String target = section.getString("target");
+ return condition -> {
+ if (Math.random() > chance) return;
+ Optional.ofNullable(plugin.getIntegrationManager().getLevelHook(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."));
+ };
+ }
+ return null;
+ });
+ }
+}
diff --git a/plugin/src/main/java/net/momirealms/customfishing/mechanic/bag/BagManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/mechanic/bag/BagManagerImpl.java
new file mode 100644
index 00000000..46ee1919
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/mechanic/bag/BagManagerImpl.java
@@ -0,0 +1,97 @@
+package net.momirealms.customfishing.mechanic.bag;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.comphenix.protocol.wrappers.WrappedChatComponent;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ScoreComponent;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.momirealms.customfishing.CustomFishingPluginImpl;
+import net.momirealms.customfishing.adventure.AdventureManagerImpl;
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.manager.BagManager;
+import net.momirealms.customfishing.api.mechanic.bag.FishingBagHolder;
+import net.momirealms.customfishing.api.util.LogUtils;
+import net.momirealms.customfishing.compatibility.papi.PlaceholderManagerImpl;
+import net.momirealms.customfishing.setting.Config;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.inventory.Inventory;
+
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class BagManagerImpl implements BagManager {
+
+ private final CustomFishingPlugin plugin;
+ private final ConcurrentHashMap bagMap;
+ private final WindowPacketListener windowPacketListener;
+
+ public BagManagerImpl(CustomFishingPluginImpl plugin) {
+ this.plugin = plugin;
+ this.bagMap = new ConcurrentHashMap<>();
+ this.windowPacketListener = new WindowPacketListener();
+ }
+
+ @Override
+ public boolean isBagEnabled() {
+ return Config.enableFishingBag;
+ }
+
+ public void load() {
+ CustomFishingPluginImpl.getProtocolManager().addPacketListener(windowPacketListener);
+ }
+
+ public void unload() {
+ CustomFishingPluginImpl.getProtocolManager().removePacketListener(windowPacketListener);
+ }
+
+ public void disable() {
+ unload();
+ }
+
+ @Override
+ public Inventory getOnlineBagInventory(UUID uuid) {
+ var onlinePlayer = plugin.getStorageManager().getOnlineUser(uuid);
+ if (onlinePlayer == null) {
+ LogUtils.warn("Player " + uuid + "'s bag data is not loaded.");
+ return null;
+ }
+ return onlinePlayer.getHolder().getInventory();
+ }
+
+ public static class WindowPacketListener extends PacketAdapter {
+
+ public WindowPacketListener() {
+ super(CustomFishingPlugin.getInstance(), PacketType.Play.Server.OPEN_WINDOW);
+ }
+
+ @Override
+ public void onPacketSending(PacketEvent event) {
+ final PacketContainer packet = event.getPacket();
+ StructureModifier wrappedChatComponentStructureModifier = packet.getChatComponents();
+ WrappedChatComponent component = wrappedChatComponentStructureModifier.getValues().get(0);
+ String windowTitleJson = component.getJson();
+ Component titleComponent = GsonComponentSerializer.gson().deserialize(windowTitleJson);
+ if (titleComponent instanceof ScoreComponent scoreComponent && scoreComponent.name().equals("bag")) {
+ HashMap placeholders = new HashMap<>();
+ String uuidStr = scoreComponent.objective();
+ UUID uuid = UUID.fromString(uuidStr);
+ placeholders.put("{uuid}", uuidStr);
+ OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
+ placeholders.put("{player}", Optional.ofNullable(offlinePlayer.getName()).orElse(uuidStr));
+ wrappedChatComponentStructureModifier.write(0,
+ WrappedChatComponent.fromJson(
+ GsonComponentSerializer.gson().serialize(
+ AdventureManagerImpl.getInstance().getComponentFromMiniMessage(
+ PlaceholderManagerImpl.getInstance().parse(offlinePlayer, Config.bagTitle, placeholders)
+ ))));
+ }
+ }
+ }
+}
diff --git a/plugin/src/main/java/net/momirealms/customfishing/mechanic/block/BlockManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/mechanic/block/BlockManagerImpl.java
new file mode 100644
index 00000000..0d4ec516
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/mechanic/block/BlockManagerImpl.java
@@ -0,0 +1,288 @@
+package net.momirealms.customfishing.mechanic.block;
+
+import net.momirealms.customfishing.CustomFishingPluginImpl;
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.common.Pair;
+import net.momirealms.customfishing.api.common.Tuple;
+import net.momirealms.customfishing.api.manager.BlockManager;
+import net.momirealms.customfishing.api.mechanic.block.*;
+import net.momirealms.customfishing.api.mechanic.loot.Loot;
+import net.momirealms.customfishing.api.util.LogUtils;
+import net.momirealms.customfishing.compatibility.block.VanillaBlockImpl;
+import net.momirealms.customfishing.util.ConfigUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.block.Barrel;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.Chest;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.block.data.Directional;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.FallingBlock;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityChangeBlockEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.util.Vector;
+
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+public class BlockManagerImpl implements BlockManager, Listener {
+
+ private final CustomFishingPlugin plugin;
+ private final HashMap blockLibraryMap;
+ private final HashMap blockConfigMap;
+ private final HashMap dataBuilderMap;
+ private final HashMap stateBuilderMap;
+
+ public BlockManagerImpl(CustomFishingPluginImpl plugin) {
+ this.plugin = plugin;
+ this.blockLibraryMap = new HashMap<>();
+ this.blockConfigMap = new HashMap<>();
+ this.dataBuilderMap = new HashMap<>();
+ this.stateBuilderMap = new HashMap<>();
+ this.registerBlockLibrary(new VanillaBlockImpl());
+ this.registerInbuiltProperties();
+ this.registerStorage();
+ }
+
+ @Override
+ public boolean registerBlockLibrary(BlockLibrary library) {
+ if (this.blockLibraryMap.containsKey(library.identification())) return false;
+ this.blockLibraryMap.put(library.identification(), library);
+ return true;
+ }
+
+ @Override
+ public boolean unregisterBlockLibrary(BlockLibrary library) {
+ return unregisterBlockLibrary(library.identification());
+ }
+
+ @Override
+ public boolean unregisterBlockLibrary(String library) {
+ return blockLibraryMap.remove(library) != null;
+ }
+
+ @Override
+ public boolean registerBlockDataModifierBuilder(String type, BlockDataModifierBuilder builder) {
+ if (dataBuilderMap.containsKey(type)) return false;
+ dataBuilderMap.put(type, builder);
+ return true;
+ }
+
+ @Override
+ public boolean registerBlockStateModifierBuilder(String type, BlockStateModifierBuilder builder) {
+ if (stateBuilderMap.containsKey(type)) return false;
+ stateBuilderMap.put(type, builder);
+ return true;
+ }
+
+ public void load() {
+ this.loadConfig();
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+ }
+
+ private void registerInbuiltProperties() {
+ this.registerDirectional();
+ this.registerStorage();
+ }
+
+ public void unload() {
+ HandlerList.unregisterAll(this);
+ HashMap tempMap = new HashMap<>(this.blockConfigMap);
+ this.blockConfigMap.clear();
+ for (Map.Entry entry : tempMap.entrySet()) {
+ if (entry.getValue().isPersist()) {
+ tempMap.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ public void disable() {
+ this.blockLibraryMap.clear();
+ }
+
+ @SuppressWarnings("DuplicatedCode")
+ private void loadConfig() {
+ Deque fileDeque = new ArrayDeque<>();
+ for (String type : List.of("blocks")) {
+ File typeFolder = new File(plugin.getDataFolder() + File.separator + "contents" + File.separator + type);
+ if (!typeFolder.exists()) {
+ if (!typeFolder.mkdirs()) return;
+ plugin.saveResource("contents" + File.separator + type + File.separator + "default.yml", false);
+ }
+ fileDeque.push(typeFolder);
+ while (!fileDeque.isEmpty()) {
+ File file = fileDeque.pop();
+ File[] files = file.listFiles();
+ if (files == null) continue;
+ for (File subFile : files) {
+ if (subFile.isDirectory()) {
+ fileDeque.push(subFile);
+ } else if (subFile.isFile()) {
+ this.loadSingleFile(subFile);
+ }
+ }
+ }
+ }
+ }
+
+ private void loadSingleFile(File file) {
+ YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
+ for (Map.Entry entry : config.getValues(false).entrySet()) {
+ if (entry.getValue() instanceof ConfigurationSection section) {
+ String blockID = section.getString("block");
+ if (blockID == null) {
+ LogUtils.warn("Block can't be null. File:" + file.getAbsolutePath() + "; Section:" + section.getCurrentPath());
+ continue;
+ }
+ List dataModifiers = new ArrayList<>();
+ List stateModifiers = new ArrayList<>();
+ ConfigurationSection property = section.getConfigurationSection("properties");
+ if (property != null) {
+ for (Map.Entry innerEntry : property.getValues(false).entrySet()) {
+ BlockDataModifierBuilder dataBuilder = dataBuilderMap.get(innerEntry.getKey());
+ if (dataBuilder != null) {
+ dataModifiers.add(dataBuilder.build(innerEntry.getValue()));
+ continue;
+ }
+ BlockStateModifierBuilder stateBuilder = stateBuilderMap.get(innerEntry.getKey());
+ if (stateBuilder != null) {
+ stateModifiers.add(stateBuilder.build(innerEntry.getValue()));
+ }
+ }
+ }
+ BlockConfig blockConfig = new BlockConfig.Builder()
+ .blockID(blockID)
+ .persist(false)
+ .horizontalVector(section.getDouble("vector.horizontal", 1.1))
+ .verticalVector(section.getDouble("vector.vertical", 1.2))
+ .dataModifiers(dataModifiers)
+ .stateModifiers(stateModifiers)
+ .build();
+ blockConfigMap.put(entry.getKey(), blockConfig);
+ }
+ }
+ }
+
+ @Override
+ public void summonBlock(Player player, Location hookLocation, Location playerLocation, Loot loot) {
+ BlockConfig config = blockConfigMap.get(loot.getID());
+ if (config == null) {
+ LogUtils.warn("Block: " + loot.getID() + " doesn't exist.");
+ return;
+ }
+ String blockID = config.getBlockID();
+ BlockData blockData;
+ if (blockID.contains(":")) {
+ String[] split = blockID.split(":", 2);
+ String lib = split[0];
+ String id = split[1];
+ blockData = blockLibraryMap.get(lib).getBlockData(player, id, config.getDataModifier());
+ } else {
+ blockData = blockLibraryMap.get("vanilla").getBlockData(player, blockID, config.getDataModifier());
+ }
+ FallingBlock fallingBlock = hookLocation.getWorld().spawnFallingBlock(hookLocation, blockData);
+ fallingBlock.getPersistentDataContainer().set(
+ Objects.requireNonNull(NamespacedKey.fromString("block", CustomFishingPlugin.get())),
+ PersistentDataType.STRING,
+ loot.getID() + ";" + player.getName()
+ );
+ fallingBlock.setDropItem(false);
+ Vector vector = playerLocation.subtract(hookLocation).toVector().multiply((config.getHorizontalVector()) - 1);
+ vector = vector.setY((vector.getY() + 0.2) * config.getVerticalVector());
+ fallingBlock.setVelocity(vector);
+ }
+
+ private void registerDirectional() {
+ this.registerBlockDataModifierBuilder("directional", (args) -> {
+ boolean arg = (boolean) args;
+ return (player, blockData) -> {
+ if (arg && blockData instanceof Directional directional) {
+ directional.setFacing(BlockFace.values()[ThreadLocalRandom.current().nextInt(0,6)]);
+ }
+ };
+ });
+ }
+
+ private void registerStorage() {
+ this.registerBlockStateModifierBuilder("storage", (args) -> {
+ if (args instanceof ConfigurationSection section) {
+ ArrayList>> tempChanceList = new ArrayList<>();
+ for (Map.Entry entry : section.getValues(false).entrySet()) {
+ if (entry.getValue() instanceof ConfigurationSection inner) {
+ String item = inner.getString("item");
+ Pair amountPair = ConfigUtils.splitStringIntegerArgs(inner.getString("amount","1~1"));
+ double chance = inner.getDouble("chance", 1);
+ tempChanceList.add(Tuple.of(chance, item, amountPair));
+ }
+ }
+ return (player, blockState) -> {
+ LinkedList unused = new LinkedList<>();
+ for (int i = 0; i < 27; i++) {
+ unused.add(i);
+ }
+ Collections.shuffle(unused);
+ if (blockState instanceof Chest chest) {
+ for (Tuple> tuple : tempChanceList) {
+ ItemStack itemStack = plugin.getItemManager().buildAnyItemByID(player, tuple.getMid());
+ itemStack.setAmount(ThreadLocalRandom.current().nextInt(tuple.getRight().left(), tuple.getRight().right() + 1));
+ if (tuple.getLeft() > Math.random()) {
+ chest.getBlockInventory().setItem(unused.pop(), itemStack);
+ }
+ }
+ return;
+ }
+ if (blockState instanceof Barrel barrel) {
+ for (Tuple> tuple : tempChanceList) {
+ ItemStack itemStack = plugin.getItemManager().buildAnyItemByID(player, tuple.getMid());
+ itemStack.setAmount(ThreadLocalRandom.current().nextInt(tuple.getRight().left(), tuple.getRight().right() + 1));
+ if (tuple.getLeft() > Math.random()) {
+ barrel.getInventory().setItem(unused.pop(), itemStack);
+ }
+ }
+ }
+ };
+ } else {
+ LogUtils.warn("Invalid property format found at block storage.");
+ return null;
+ }
+ });
+ }
+
+ @EventHandler
+ public void onBlockLands(EntityChangeBlockEvent event) {
+ if (event.isCancelled()) return;
+ String temp = event.getEntity().getPersistentDataContainer().get(
+ Objects.requireNonNull(NamespacedKey.fromString("block", CustomFishingPlugin.get())),
+ PersistentDataType.STRING
+ );
+ if (temp == null) return;
+ String[] split = temp.split(";");
+ BlockConfig blockConfig = blockConfigMap.get(split[0]);
+ if (blockConfig == null) return;
+ Player player = Bukkit.getPlayer(split[1]);
+ if (player == null) {
+ event.getEntity().remove();
+ event.getBlock().setType(Material.AIR);
+ return;
+ }
+ Location location = event.getBlock().getLocation();
+ plugin.getScheduler().runTaskSyncLater(() -> {
+ BlockState state = location.getBlock().getState();
+ for (BlockStateModifier modifier : blockConfig.getStateModifierList()) {
+ modifier.apply(player, state);
+ }
+ }, location, 50, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/Competition.java b/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/Competition.java
new file mode 100644
index 00000000..d4d8afb4
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/Competition.java
@@ -0,0 +1,265 @@
+package net.momirealms.customfishing.mechanic.competition;
+
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.common.Pair;
+import net.momirealms.customfishing.api.mechanic.action.Action;
+import net.momirealms.customfishing.api.mechanic.competition.CompetitionConfig;
+import net.momirealms.customfishing.api.mechanic.competition.CompetitionGoal;
+import net.momirealms.customfishing.api.mechanic.competition.FishingCompetition;
+import net.momirealms.customfishing.api.mechanic.competition.Ranking;
+import net.momirealms.customfishing.api.mechanic.condition.Condition;
+import net.momirealms.customfishing.api.scheduler.CancellableTask;
+import net.momirealms.customfishing.mechanic.competition.actionbar.ActionBarManager;
+import net.momirealms.customfishing.mechanic.competition.bossbar.BossBarManager;
+import net.momirealms.customfishing.mechanic.competition.ranking.LocalRankingImpl;
+import net.momirealms.customfishing.mechanic.competition.ranking.RedisRankingImpl;
+import net.momirealms.customfishing.setting.Config;
+import net.momirealms.customfishing.setting.Locale;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+public class Competition implements FishingCompetition {
+
+ private final CompetitionConfig config;
+ private CancellableTask competitionTimerTask;
+ private final CompetitionGoal goal;
+ private final ConcurrentHashMap publicPlaceholders;
+ private final Ranking ranking;
+ private float progress;
+ private long remainingTime;
+ private long startTime;
+ private BossBarManager bossBarManager;
+ private ActionBarManager actionBarManager;
+
+ public Competition(CompetitionConfig config) {
+ this.config = config;
+ this.goal = config.getGoal() == CompetitionGoal.RANDOM ? CompetitionGoal.getRandom() : config.getGoal();
+ if (Config.redisRanking) this.ranking = new RedisRankingImpl();
+ else this.ranking = new LocalRankingImpl();
+ this.publicPlaceholders = new ConcurrentHashMap<>();
+ this.publicPlaceholders.put("{goal}", getCompetitionLocale(goal));
+ }
+
+ @Override
+ public void start() {
+ this.progress = 1;
+ this.remainingTime = config.getDuration();
+ this.startTime = Instant.now().getEpochSecond();
+ this.updatePublicPlaceholders();
+
+ this.arrangeTimerTask();
+ if (config.getBossBarConfig() != null) {
+ this.bossBarManager = new BossBarManager(config.getBossBarConfig(), this);
+ this.bossBarManager.load();
+ }
+ if (config.getActionBarConfig() != null) {
+ this.actionBarManager = new ActionBarManager(config.getActionBarConfig(), this);
+ this.actionBarManager.load();
+ }
+
+ Action[] actions = config.getStartActions();
+ if (actions != null) {
+ Condition condition = new Condition();
+ for (Action action : actions) {
+ action.trigger(condition);
+ }
+ }
+ }
+
+ private void arrangeTimerTask() {
+ this.competitionTimerTask = CustomFishingPlugin.get().getScheduler().runTaskAsyncTimer(() -> {
+ if (decreaseTime()) {
+ end();
+ return;
+ }
+ updatePublicPlaceholders();
+ }, 1, 1, TimeUnit.SECONDS);
+ }
+
+ private void updatePublicPlaceholders() {
+ for (int i = 1; i < Config.placeholderLimit + 1; i++) {
+ int finalI = i;
+ Optional.ofNullable(ranking.getPlayerAt(i)).ifPresentOrElse(player -> {
+ publicPlaceholders.put("{" + finalI + "_player}", player);
+ publicPlaceholders.put("{" + finalI + "_score}", String.format("%.2f", ranking.getScoreAt(finalI)));
+ }, () -> {
+ publicPlaceholders.put("{" + finalI + "_player}", Locale.MSG_No_Player);
+ publicPlaceholders.put("{" + finalI + "_score}", Locale.MSG_No_Score);
+ });
+ }
+ publicPlaceholders.put("{hour}", String.valueOf(remainingTime / 3600));
+ publicPlaceholders.put("{minute}", String.valueOf((remainingTime % 3600) / 60));
+ publicPlaceholders.put("{second}", String.valueOf(remainingTime % 60));
+ publicPlaceholders.put("{time}", String.valueOf(remainingTime));
+ }
+
+ @Override
+ public void stop() {
+ if (!competitionTimerTask.isCancelled()) this.competitionTimerTask.cancel();
+ if (this.bossBarManager != null) this.bossBarManager.unload();
+ if (this.actionBarManager != null) this.actionBarManager.unload();
+ this.ranking.clear();
+ this.remainingTime = 0;
+ }
+
+ @Override
+ public void end() {
+ // mark it as ended
+ this.remainingTime = 0;
+
+ // cancel some sub tasks
+ if (!competitionTimerTask.isCancelled()) this.competitionTimerTask.cancel();
+ if (this.bossBarManager != null) this.bossBarManager.unload();
+ if (this.actionBarManager != null) this.actionBarManager.unload();
+
+ // give prizes
+ HashMap rewardsMap = config.getRewards();
+ if (ranking.getSize() != 0 && rewardsMap != null) {
+ Iterator> iterator = ranking.getIterator();
+ int i = 1;
+ while (iterator.hasNext()) {
+ Pair competitionPlayer = iterator.next();
+ this.publicPlaceholders.put("{" + i + "_player}", competitionPlayer.left());
+ this.publicPlaceholders.put("{" + i + "_score}", String.format("%.2f", competitionPlayer.right()));
+ if (i < rewardsMap.size()) {
+ Player player = Bukkit.getPlayer(competitionPlayer.left());
+ if (player != null)
+ for (Action action : rewardsMap.get(String.valueOf(i)))
+ action.trigger(new Condition(player));
+ i++;
+ } else {
+ Action[] actions = rewardsMap.get("participation");
+ if (actions != null) {
+ iterator.forEachRemaining(playerName -> {
+ Player player = Bukkit.getPlayer(competitionPlayer.left());
+ if (player != null)
+ for (Action action : actions)
+ action.trigger(new Condition(player));
+ });
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ // do end actions
+ Action[] actions = config.getEndActions();
+ if (actions != null) {
+ Condition condition = new Condition(new HashMap<>(publicPlaceholders));
+ for (Action action : actions) {
+ action.trigger(condition);
+ }
+ }
+
+ // 1.5 seconds delay for other servers to read the redis data
+ CustomFishingPlugin.get().getScheduler().runTaskAsyncLater(this.ranking::clear, 1500, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public boolean isOnGoing() {
+ return remainingTime > 0;
+ }
+
+ private boolean decreaseTime() {
+ long current = Instant.now().getEpochSecond();
+ int duration = config.getDuration();
+ remainingTime = duration - (current - startTime);
+ progress = (float) remainingTime / duration;
+ return remainingTime <= 0;
+ }
+
+ @Override
+ public void refreshData(Player player, double score, boolean doubleScore) {
+ // if player join for the first time, trigger join actions
+ if (!hasPlayerJoined(player)) {
+ Action[] actions = config.getJoinActions();
+ if (actions != null) {
+ Condition condition = new Condition(player);
+ for (Action action : actions) {
+ action.trigger(condition);
+ }
+ }
+ }
+
+ // show competition info
+ if (this.bossBarManager != null) this.bossBarManager.showBossBarTo(player);
+ if (this.actionBarManager != null) this.actionBarManager.showActionBarTo(player);
+
+ // refresh data
+ switch (this.goal) {
+ case CATCH_AMOUNT -> ranking.refreshData(player.getName(), doubleScore ? 2 : 1);
+ case TOTAL_SIZE, TOTAL_SCORE -> ranking.refreshData(player.getName(), doubleScore ? 2 * score : score);
+ case MAX_SIZE -> {
+ if (score > ranking.getPlayerScore(player.getName())) {
+ ranking.setData(player.getName(), score);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean hasPlayerJoined(OfflinePlayer player) {
+ return ranking.getPlayerRank(player.getName()) != -1;
+ }
+
+ @Override
+ public float getProgress() {
+ return progress;
+ }
+
+ @Override
+ public long getRemainingTime() {
+ return remainingTime;
+ }
+
+ @Override
+ public long getStartTime() {
+ return startTime;
+ }
+
+ @Override
+ public CompetitionConfig getConfig() {
+ return config;
+ }
+
+ @Override
+ public CompetitionGoal getGoal() {
+ return goal;
+ }
+
+ @Override
+ public Ranking getRanking() {
+ return ranking;
+ }
+
+ public ConcurrentHashMap getPublicPlaceholders() {
+ return publicPlaceholders;
+ }
+
+ private String getCompetitionLocale(CompetitionGoal goal) {
+ switch (goal) {
+ case MAX_SIZE -> {
+ return Locale.MSG_Max_Size;
+ }
+ case CATCH_AMOUNT -> {
+ return Locale.MSG_Catch_Amount;
+ }
+ case TOTAL_SCORE -> {
+ return Locale.MSG_Total_Score;
+ }
+ case TOTAL_SIZE -> {
+ return Locale.MSG_Total_Size;
+ }
+ }
+ return "";
+ }
+}
diff --git a/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/CompetitionManagerImpl.java b/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/CompetitionManagerImpl.java
new file mode 100644
index 00000000..7ed06e2c
--- /dev/null
+++ b/plugin/src/main/java/net/momirealms/customfishing/mechanic/competition/CompetitionManagerImpl.java
@@ -0,0 +1,259 @@
+package net.momirealms.customfishing.mechanic.competition;
+
+import net.momirealms.customfishing.api.CustomFishingPlugin;
+import net.momirealms.customfishing.api.common.Pair;
+import net.momirealms.customfishing.api.manager.CompetitionManager;
+import net.momirealms.customfishing.api.mechanic.action.Action;
+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.Config;
+import net.momirealms.customfishing.storage.method.database.nosql.RedisManager;
+import org.bukkit.Bukkit;
+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.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 {
+
+ private final CustomFishingPlugin plugin;
+ private final HashMap timeConfigMap;
+ private final HashMap commandConfigMap;
+ private Competition currentCompetition;
+ private CancellableTask timerCheckTask;
+ private int nextCompetitionSeconds;
+
+ public CompetitionManagerImpl(CustomFishingPlugin plugin) {
+ this.plugin = plugin;
+ this.timeConfigMap = new HashMap<>();
+ this.commandConfigMap = new HashMap<>();
+ }
+
+ public void load() {
+ loadConfig();
+ this.timerCheckTask = plugin.getScheduler().runTaskAsyncTimer(
+ this::timerCheck,
+ 1,
+ 1,
+ TimeUnit.SECONDS
+ );
+ }
+
+ public void unload() {
+ if (this.timerCheckTask != null && !this.timerCheckTask.isCancelled())
+ this.timerCheckTask.cancel();
+ this.commandConfigMap.clear();
+ this.timeConfigMap.clear();
+ if (currentCompetition != null && currentCompetition.isOnGoing())
+ currentCompetition.end();
+ }
+
+ public void disable() {
+ if (this.timerCheckTask != null && !this.timerCheckTask.isCancelled())
+ this.timerCheckTask.cancel();
+ this.commandConfigMap.clear();
+ this.timeConfigMap.clear();
+ if (currentCompetition != null && currentCompetition.isOnGoing())
+ currentCompetition.stop();
+ }
+
+ @Override
+ public Set getAllCompetitions() {
+ return commandConfigMap.keySet();
+ }
+
+ @SuppressWarnings("DuplicatedCode")
+ private void loadConfig() {
+ Deque fileDeque = new ArrayDeque<>();
+ for (String type : List.of("competitions")) {
+ File typeFolder = new File(plugin.getDataFolder() + File.separator + "contents" + File.separator + type);
+ if (!typeFolder.exists()) {
+ if (!typeFolder.mkdirs()) return;
+ plugin.saveResource("contents" + File.separator + type + File.separator + "default.yml", false);
+ }
+ fileDeque.push(typeFolder);
+ while (!fileDeque.isEmpty()) {
+ File file = fileDeque.pop();
+ File[] files = file.listFiles();
+ if (files == null) continue;
+ for (File subFile : files) {
+ if (subFile.isDirectory()) {
+ fileDeque.push(subFile);
+ } else if (subFile.isFile()) {
+ this.loadSingleFileCompetition(subFile);
+ }
+ }
+ }
+ }
+ }
+
+ private void loadSingleFileCompetition(File file) {
+ YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
+ for (Map.Entry