9
0
mirror of https://github.com/Xiao-MoMi/Custom-Fishing.git synced 2025-12-30 20:39:18 +00:00

I don't want to waste time on resolving conflicts

This commit is contained in:
XiaoMoMi
2024-07-10 03:24:56 +08:00
parent 0ed5d5745a
commit 80594731e6
551 changed files with 29876 additions and 31172 deletions

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import org.bukkit.plugin.java.JavaPlugin;
public class BukkitBootstrap extends JavaPlugin {
private BukkitCustomFishingPlugin plugin;
@Override
public void onLoad() {
this.plugin = new BukkitCustomFishingPluginImpl(this);
this.plugin.load();
}
@Override
public void onEnable() {
this.plugin.enable();
}
@Override
public void onDisable() {
this.plugin.disable();
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.event.CustomFishingReloadEvent;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.misc.cooldown.CoolDownManager;
import net.momirealms.customfishing.api.mechanic.misc.placeholder.BukkitPlaceholderManager;
import net.momirealms.customfishing.api.util.EventUtils;
import net.momirealms.customfishing.bukkit.action.BukkitActionManager;
import net.momirealms.customfishing.bukkit.bag.BukkitBagManager;
import net.momirealms.customfishing.bukkit.block.BukkitBlockManager;
import net.momirealms.customfishing.bukkit.command.BukkitCommandManager;
import net.momirealms.customfishing.bukkit.competition.BukkitCompetitionManager;
import net.momirealms.customfishing.bukkit.config.BukkitConfigManager;
import net.momirealms.customfishing.bukkit.effect.BukkitEffectManager;
import net.momirealms.customfishing.bukkit.entity.BukkitEntityManager;
import net.momirealms.customfishing.bukkit.event.BukkitEventManager;
import net.momirealms.customfishing.bukkit.fishing.BukkitFishingManager;
import net.momirealms.customfishing.bukkit.game.BukkitGameManager;
import net.momirealms.customfishing.bukkit.hook.BukkitHookManager;
import net.momirealms.customfishing.bukkit.integration.BukkitIntegrationManager;
import net.momirealms.customfishing.bukkit.item.BukkitItemManager;
import net.momirealms.customfishing.bukkit.loot.BukkitLootManager;
import net.momirealms.customfishing.bukkit.market.BukkitMarketManager;
import net.momirealms.customfishing.bukkit.requirement.BukkitRequirementManager;
import net.momirealms.customfishing.bukkit.scheduler.BukkitSchedulerAdapter;
import net.momirealms.customfishing.bukkit.sender.BukkitSenderFactory;
import net.momirealms.customfishing.bukkit.statistic.BukkitStatisticsManager;
import net.momirealms.customfishing.bukkit.storage.BukkitStorageManager;
import net.momirealms.customfishing.bukkit.totem.BukkitTotemManager;
import net.momirealms.customfishing.common.dependency.Dependency;
import net.momirealms.customfishing.common.dependency.DependencyManagerImpl;
import net.momirealms.customfishing.common.helper.VersionHelper;
import net.momirealms.customfishing.common.locale.TranslationManager;
import net.momirealms.customfishing.common.plugin.classpath.ClassPathAppender;
import net.momirealms.customfishing.common.plugin.classpath.ReflectionClassPathAppender;
import net.momirealms.customfishing.common.plugin.logging.JavaPluginLogger;
import net.momirealms.customfishing.common.plugin.logging.PluginLogger;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
public class BukkitCustomFishingPluginImpl extends BukkitCustomFishingPlugin {
private final ClassPathAppender classPathAppender;
private final PluginLogger logger;
private BukkitCommandManager commandManager;
private Consumer<Object> debugger;
public BukkitCustomFishingPluginImpl(Plugin boostrap) {
super(boostrap);
VersionHelper.init(getServerVersion());
this.scheduler = new BukkitSchedulerAdapter(this);
this.classPathAppender = new ReflectionClassPathAppender(this);
this.logger = new JavaPluginLogger(getBoostrap().getLogger());
this.dependencyManager = new DependencyManagerImpl(this);
}
@Override
public void load() {
this.dependencyManager.loadDependencies(
List.of(
Dependency.BOOSTED_YAML,
Dependency.BSTATS_BASE, Dependency.BSTATS_BUKKIT,
Dependency.CAFFEINE,
Dependency.GEANTY_REF,
Dependency.CLOUD_CORE, Dependency.CLOUD_SERVICES, Dependency.CLOUD_BUKKIT, Dependency.CLOUD_PAPER, Dependency.CLOUD_BRIGADIER, Dependency.CLOUD_MINECRAFT_EXTRAS,
Dependency.GSON,
Dependency.COMMONS_POOL_2,
Dependency.JEDIS,
Dependency.EXP4J,
Dependency.MYSQL_DRIVER, Dependency.MARIADB_DRIVER,
Dependency.SQLITE_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE,
Dependency.H2_DRIVER,
Dependency.MONGODB_DRIVER_CORE, Dependency.MONGODB_DRIVER_SYNC, Dependency.MONGODB_DRIVER_BSON,
Dependency.HIKARI_CP,
Dependency.LZ4
)
);
}
@Override
public void enable() {
this.eventManager = new BukkitEventManager(this);
this.configManager = new BukkitConfigManager(this);
this.requirementManager = new BukkitRequirementManager(this);
this.actionManager = new BukkitActionManager(this);
this.senderFactory = new BukkitSenderFactory(this);
this.placeholderManager = new BukkitPlaceholderManager(this);
this.itemManager = new BukkitItemManager(this);
this.competitionManager = new BukkitCompetitionManager(this);
this.marketManager = new BukkitMarketManager(this);
this.storageManager = new BukkitStorageManager(this);
this.lootManager = new BukkitLootManager(this);
this.coolDownManager = new CoolDownManager(this);
this.entityManager = new BukkitEntityManager(this);
this.blockManager = new BukkitBlockManager(this);
this.statisticsManager = new BukkitStatisticsManager(this);
this.effectManager = new BukkitEffectManager(this);
this.hookManager = new BukkitHookManager(this);
this.fishingManager = new BukkitFishingManager(this);
this.bagManager = new BukkitBagManager(this);
this.totemManager = new BukkitTotemManager(this);
this.translationManager = new TranslationManager(this);
this.integrationManager = new BukkitIntegrationManager(this);
this.gameManager = new BukkitGameManager(this);
this.commandManager = new BukkitCommandManager(this);
this.commandManager.registerDefaultFeatures();
this.reload();
if (ConfigManager.metrics()) new Metrics((JavaPlugin) getBoostrap(), 16648);
if (ConfigManager.checkUpdate()) {
VersionHelper.UPDATE_CHECKER.apply(this).thenAccept(result -> {
if (!result) this.getPluginLogger().info("You are using the latest version.");
else this.getPluginLogger().warn("Update is available: https://polymart.org/resource/2723");
});
}
}
@Override
public void reload() {
MechanicType.reset();
this.itemManager.unload();
this.eventManager.unload();
this.entityManager.unload();
this.lootManager.unload();
this.blockManager.unload();
this.effectManager.unload();
this.hookManager.unload();
this.totemManager.unload();
this.gameManager.unload();
// before ConfigManager
this.placeholderManager.reload();
this.configManager.reload();
// after ConfigManager
this.debugger = ConfigManager.debug() ? (s) -> logger.info("[DEBUG] " + s.toString()) : (s) -> {};
this.actionManager.reload();
this.requirementManager.reload();
this.coolDownManager.reload();
this.translationManager.reload();
this.marketManager.reload();
this.competitionManager.reload();
this.statisticsManager.reload();
this.bagManager.reload();
this.storageManager.reload();
this.fishingManager.reload();
this.itemManager.load();
this.eventManager.load();
this.entityManager.load();
this.lootManager.load();
this.blockManager.load();
this.effectManager.load();
this.hookManager.load();
this.totemManager.load();
this.gameManager.load();
EventUtils.fireAndForget(new CustomFishingReloadEvent(this));
}
@Override
public void disable() {
this.eventManager.disable();
this.configManager.disable();
this.requirementManager.disable();
this.actionManager.disable();
this.placeholderManager.disable();
this.itemManager.disable();
this.competitionManager.disable();
this.marketManager.disable();
this.lootManager.disable();
this.coolDownManager.disable();
this.entityManager.disable();
this.blockManager.disable();
this.statisticsManager.disable();
this.effectManager.disable();
this.hookManager.disable();
this.bagManager.disable();
this.integrationManager.disable();
this.storageManager.disable();
this.commandManager.unregisterFeatures();
}
@Override
public InputStream getResourceStream(String filePath) {
return getBoostrap().getResource(filePath);
}
@Override
public PluginLogger getPluginLogger() {
return logger;
}
@Override
public ClassPathAppender getClassPathAppender() {
return classPathAppender;
}
@Override
public Path getDataDirectory() {
return getBoostrap().getDataFolder().toPath().toAbsolutePath();
}
@Override
public String getServerVersion() {
return Bukkit.getServer().getBukkitVersion().split("-")[0];
}
@SuppressWarnings("deprecation")
@Override
public String getPluginVersion() {
return getBoostrap().getDescription().getVersion();
}
@Override
public void debug(Object message) {
this.debugger.accept(message);
}
}

View File

@@ -0,0 +1,938 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.action;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.action.*;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.effect.Effect;
import net.momirealms.customfishing.api.mechanic.misc.placeholder.BukkitPlaceholderManager;
import net.momirealms.customfishing.api.mechanic.misc.value.MathValue;
import net.momirealms.customfishing.api.mechanic.misc.value.TextValue;
import net.momirealms.customfishing.api.mechanic.requirement.Requirement;
import net.momirealms.customfishing.bukkit.integration.VaultHook;
import net.momirealms.customfishing.bukkit.util.LocationUtils;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.locale.TranslationManager;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.ClassUtils;
import net.momirealms.customfishing.common.util.ListUtils;
import net.momirealms.customfishing.common.util.Pair;
import net.momirealms.customfishing.common.util.RandomUtils;
import net.momirealms.sparrow.heart.SparrowHeart;
import net.momirealms.sparrow.heart.feature.armorstand.FakeArmorStand;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Player;
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;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static java.util.Objects.requireNonNull;
public class BukkitActionManager implements ActionManager<Player> {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, ActionFactory<Player>> actionFactoryMap = new HashMap<>();
private static final String EXPANSION_FOLDER = "expansions/action";
public BukkitActionManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.registerBuiltInActions();
}
@Override
public void disable() {
this.actionFactoryMap.clear();
}
@Override
public void reload() {
this.loadExpansions();
}
@Override
public boolean registerAction(String type, ActionFactory<Player> actionFactory) {
if (this.actionFactoryMap.containsKey(type)) return false;
this.actionFactoryMap.put(type, actionFactory);
return true;
}
@Override
public boolean unregisterAction(String type) {
return this.actionFactoryMap.remove(type) != null;
}
@Nullable
@Override
public ActionFactory<Player> getActionFactory(@NotNull String type) {
return actionFactoryMap.get(type);
}
@Override
public boolean hasAction(@NotNull String type) {
return actionFactoryMap.containsKey(type);
}
@Override
public Action<Player> parseAction(Section section) {
if (section == null) return EmptyAction.INSTANCE;
ActionFactory<Player> factory = getActionFactory(section.getString("type"));
if (factory == null) {
plugin.getPluginLogger().warn("Action type: " + section.getString("type") + " doesn't exist.");
return EmptyAction.INSTANCE;
}
return factory.process(section.get("value"), section.getDouble("chance", 1d));
}
@NotNull
@Override
@SuppressWarnings("unchecked")
public Action<Player>[] parseActions(Section section) {
ArrayList<Action<Player>> actionList = new ArrayList<>();
if (section != null)
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
Action<Player> action = parseAction(innerSection);
if (action != null)
actionList.add(action);
}
}
return actionList.toArray(new Action[0]);
}
@Override
public Action<Player> parseAction(@NotNull String type, @NotNull Object args) {
ActionFactory<Player> factory = getActionFactory(type);
if (factory == null) {
plugin.getPluginLogger().warn("Action type: " + type + " doesn't exist.");
return EmptyAction.INSTANCE;
}
return factory.process(args, 1);
}
private void registerBuiltInActions() {
this.registerMessageAction();
this.registerCommandAction();
this.registerActionBarAction();
this.registerCloseInvAction();
this.registerExpAction();
this.registerFoodAction();
this.registerChainAction();
this.registerMoneyAction();
this.registerItemAction();
this.registerPotionAction();
this.registerFishFindAction();
this.registerPluginExpAction();
this.registerSoundAction();
this.registerHologramAction();
this.registerFakeItemAction();
this.registerTitleAction();
}
private void registerMessageAction() {
registerAction("message", (args, chance) -> {
List<String> messages = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
List<String> replaced = plugin.getPlaceholderManager().parse(context.getHolder(), messages, context.placeholderMap());
Audience audience = plugin.getSenderFactory().getAudience(context.getHolder());
for (String text : replaced) {
audience.sendMessage(AdventureHelper.miniMessage(text));
}
};
});
registerAction("random-message", (args, chance) -> {
List<String> messages = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
String random = messages.get(RandomUtils.generateRandomInt(0, messages.size() - 1));
random = BukkitPlaceholderManager.getInstance().parse(context.getHolder(), random, context.placeholderMap());
Audience audience = plugin.getSenderFactory().getAudience(context.getHolder());
audience.sendMessage(AdventureHelper.miniMessage(random));
};
});
registerAction("broadcast", (args, chance) -> {
List<String> messages = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
List<String> replaced = plugin.getPlaceholderManager().parse(context.getHolder(), messages, context.placeholderMap());
for (Player player : Bukkit.getOnlinePlayers()) {
Audience audience = plugin.getSenderFactory().getAudience(player);
for (String text : replaced) {
audience.sendMessage(AdventureHelper.miniMessage(text));
}
}
};
});
registerAction("message-nearby", (args, chance) -> {
if (args instanceof Section section) {
List<String> messages = ListUtils.toList(section.get("message"));
MathValue<Player> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
double realRange = range.evaluate(context);
Player owner = context.getHolder();
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
plugin.getScheduler().sync().run(() -> {
for (Entity player : location.getWorld().getNearbyEntities(location, realRange, realRange, realRange, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= realRange) {
context.arg(ContextKeys.TEMP_NEAR_PLAYER, player.getName());
List<String> replaced = BukkitPlaceholderManager.getInstance().parse(
owner,
messages,
context.placeholderMap()
);
Audience audience = plugin.getSenderFactory().getAudience(player);
for (String text : replaced) {
audience.sendMessage(AdventureHelper.miniMessage(text));
}
}
}
}, location);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at message-nearby action which should be Section");
return EmptyAction.INSTANCE;
}
});
}
private void registerCommandAction() {
registerAction("command", (args, chance) -> {
List<String> commands = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
List<String> replaced = BukkitPlaceholderManager.getInstance().parse(context.getHolder(), commands, context.placeholderMap());
plugin.getScheduler().sync().run(() -> {
for (String text : replaced) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), text);
}
}, context.arg(ContextKeys.LOCATION));
};
});
registerAction("player-command", (args, chance) -> {
List<String> commands = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
List<String> replaced = BukkitPlaceholderManager.getInstance().parse(context.getHolder(), commands, context.placeholderMap());
plugin.getScheduler().sync().run(() -> {
for (String text : replaced) {
context.getHolder().performCommand(text);
}
}, context.arg(ContextKeys.LOCATION));
};
});
registerAction("random-command", (args, chance) -> {
List<String> commands = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
String random = commands.get(ThreadLocalRandom.current().nextInt(commands.size()));
random = BukkitPlaceholderManager.getInstance().parse(context.getHolder(), random, context.placeholderMap());
String finalRandom = random;
plugin.getScheduler().sync().run(() -> {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalRandom);
}, context.arg(ContextKeys.LOCATION));
};
});
registerAction("command-nearby", (args, chance) -> {
if (args instanceof Section section) {
List<String> cmd = ListUtils.toList(section.get("command"));
MathValue<Player> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
Player owner = context.getHolder();
double realRange = range.evaluate(context);
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
plugin.getScheduler().sync().run(() -> {
for (Entity player : location.getWorld().getNearbyEntities(location, realRange, realRange, realRange, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= realRange) {
context.arg(ContextKeys.TEMP_NEAR_PLAYER, player.getName());
List<String> replaced = BukkitPlaceholderManager.getInstance().parse(owner, cmd, context.placeholderMap());
for (String text : replaced) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), text);
}
}
}
}, location);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at command-nearby action which should be Section");
return EmptyAction.INSTANCE;
}
});
}
private void registerCloseInvAction() {
registerAction("close-inv", (args, chance) -> condition -> {
if (Math.random() > chance) return;
condition.getHolder().closeInventory();
});
}
private void registerActionBarAction() {
registerAction("actionbar", (args, chance) -> {
String text = (String) args;
return context -> {
if (Math.random() > chance) return;
Audience audience = plugin.getSenderFactory().getAudience(context.getHolder());
Component component = AdventureHelper.miniMessage(plugin.getPlaceholderManager().parse(context.getHolder(), text, context.placeholderMap()));
audience.sendActionBar(component);
};
});
registerAction("random-actionbar", (args, chance) -> {
List<String> texts = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
String random = texts.get(RandomUtils.generateRandomInt(0, texts.size() - 1));
random = plugin.getPlaceholderManager().parse(context.getHolder(), random, context.placeholderMap());
Audience audience = plugin.getSenderFactory().getAudience(context.getHolder());
audience.sendActionBar(AdventureHelper.miniMessage(random));
};
});
registerAction("actionbar-nearby", (args, chance) -> {
if (args instanceof Section section) {
String actionbar = section.getString("actionbar");
MathValue<Player> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
Player owner = context.getHolder();
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
double realRange = range.evaluate(context);
plugin.getScheduler().sync().run(() -> {
for (Entity player : location.getWorld().getNearbyEntities(location, realRange, realRange, realRange, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= realRange) {
context.arg(ContextKeys.TEMP_NEAR_PLAYER, player.getName());
String replaced = plugin.getPlaceholderManager().parse(owner, actionbar, context.placeholderMap());
Audience audience = plugin.getSenderFactory().getAudience(player);
audience.sendActionBar(AdventureHelper.miniMessage(replaced));
}
}
}, location
);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at actionbar-nearby action which should be Section");
return EmptyAction.INSTANCE;
}
});
}
private void registerExpAction() {
registerAction("mending", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
final Player player = context.getHolder();
player.getLocation().getWorld().spawn(player.getLocation().clone().add(0,0.5,0), ExperienceOrb.class, e -> e.setExperience((int) value.evaluate(context)));
};
});
registerAction("exp", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
final Player player = context.getHolder();
player.giveExp((int) Math.round(value.evaluate(context)));
Audience audience = plugin.getSenderFactory().getAudience(player);
AdventureHelper.playSound(audience, Sound.sound(Key.key("minecraft:entity.experience_orb.pickup"), Sound.Source.PLAYER, 1, 1));
};
});
registerAction("level", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
player.setLevel((int) Math.max(0, player.getLevel() + value.evaluate(context)));
};
});
}
private void registerFoodAction() {
registerAction("food", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
player.setFoodLevel((int) (player.getFoodLevel() + value.evaluate(context)));
};
});
registerAction("saturation", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
player.setSaturation((float) (player.getSaturation() + value.evaluate(context)));
};
});
}
private void registerItemAction() {
registerAction("item-amount", (args, chance) -> {
if (args instanceof Section section) {
boolean mainOrOff = section.getString("hand", "main").equalsIgnoreCase("main");
int amount = section.getInt("amount", 1);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
ItemStack itemStack = mainOrOff ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand();
itemStack.setAmount(Math.max(0, itemStack.getAmount() + amount));
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at item-amount action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
registerAction("durability", (args, chance) -> {
if (args instanceof Section section) {
EquipmentSlot slot = EquipmentSlot.valueOf(section.getString("slot", "hand").toUpperCase(Locale.ENGLISH));
int amount = section.getInt("amount", 1);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
ItemStack itemStack = player.getInventory().getItem(slot);
// if (amount > 0) {
// ItemUtils.increaseDurability(itemStack, amount, true);
// } else {
// ItemUtils.decreaseDurability(context.getHolder(), itemStack, -amount, true);
// }
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at durability action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
registerAction("give-item", (args, chance) -> {
if (args instanceof Section section) {
String id = section.getString("item");
int amount = section.getInt("amount", 1);
return context -> {
if (Math.random() > chance) return;
Player player = context.getHolder();
ItemStack itemStack = plugin.getItemManager().buildAny(context, id);
int maxStack = itemStack.getType().getMaxStackSize();
int amountToGive = amount;
while (amountToGive > 0) {
int perStackSize = Math.min(maxStack, amountToGive);
amountToGive -= perStackSize;
ItemStack more = itemStack.clone();
more.setAmount(perStackSize);
PlayerUtils.dropItem(player, itemStack, true, true, false);
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at give-item action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerChainAction() {
registerAction("chain", (args, chance) -> {
List<Action<Player>> actions = new ArrayList<>();
if (args instanceof Section section) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
actions.add(parseAction(innerSection));
}
}
}
return context -> {
if (Math.random() > chance) return;
for (Action<Player> action : actions) {
action.trigger(context);
}
};
});
registerAction("delay", (args, chance) -> {
List<Action<Player>> actions = new ArrayList<>();
int delay;
boolean async;
if (args instanceof Section section) {
delay = section.getInt("delay", 1);
async = section.getBoolean("async", false);
Section actionSection = section.getSection("actions");
if (actionSection != null)
for (Map.Entry<String, Object> entry : actionSection.getStringRouteMappedValues(false).entrySet())
if (entry.getValue() instanceof Section innerSection)
actions.add(parseAction(innerSection));
} else {
delay = 1;
async = false;
}
return context -> {
if (Math.random() > chance) return;
Location location = context.arg(ContextKeys.LOCATION);
if (async) {
plugin.getScheduler().asyncLater(() -> {
for (Action<Player> action : actions)
action.trigger(context);
}, delay * 50L, TimeUnit.MILLISECONDS);
} else {
plugin.getScheduler().sync().runLater(() -> {
for (Action<Player> action : actions)
action.trigger(context);
}, delay, location);
}
};
});
registerAction("timer", (args, chance) -> {
List<Action<Player>> actions = new ArrayList<>();
int delay, duration, period;
boolean async;
if (args instanceof Section section) {
delay = section.getInt("delay", 2);
duration = section.getInt("duration", 20);
period = section.getInt("period", 2);
async = section.getBoolean("async", false);
Section actionSection = section.getSection("actions");
if (actionSection != null)
for (Map.Entry<String, Object> entry : actionSection.getStringRouteMappedValues(false).entrySet())
if (entry.getValue() instanceof Section innerSection)
actions.add(parseAction(innerSection));
} else {
delay = 1;
period = 1;
async = false;
duration = 20;
}
return context -> {
if (Math.random() > chance) return;
Location location = context.arg(ContextKeys.LOCATION);
SchedulerTask task;
if (async) {
task = plugin.getScheduler().asyncRepeating(() -> {
for (Action<Player> action : actions) {
action.trigger(context);
}
}, delay * 50L, period * 50L, TimeUnit.MILLISECONDS);
} else {
task = plugin.getScheduler().sync().runRepeating(() -> {
for (Action<Player> action : actions) {
action.trigger(context);
}
}, delay, period, location);
}
plugin.getScheduler().asyncLater(task::cancel, duration * 50L, TimeUnit.MILLISECONDS);
};
});
registerAction("conditional", (args, chance) -> {
if (args instanceof Section section) {
Action<Player>[] actions = parseActions(section.getSection("actions"));
Requirement<Player>[] requirements = plugin.getRequirementManager().parseRequirements(section.getSection("conditions"), true);
return condition -> {
if (Math.random() > chance) return;
for (Requirement<Player> requirement : requirements) {
if (!requirement.isSatisfied(condition)) {
return;
}
}
for (Action<Player> action : actions) {
action.trigger(condition);
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at conditional action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
registerAction("priority", (args, chance) -> {
if (args instanceof Section section) {
List<Pair<Requirement<Player>[], Action<Player>[]>> conditionActionPairList = new ArrayList<>();
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
Action<Player>[] actions = parseActions(inner.getSection("actions"));
Requirement<Player>[] requirements = plugin.getRequirementManager().parseRequirements(inner.getSection("conditions"), false);
conditionActionPairList.add(Pair.of(requirements, actions));
}
}
return context -> {
if (Math.random() > chance) return;
outer:
for (Pair<Requirement<Player>[], Action<Player>[]> pair : conditionActionPairList) {
if (pair.left() != null)
for (Requirement<Player> requirement : pair.left()) {
if (!requirement.isSatisfied(context)) {
continue outer;
}
}
if (pair.right() != null)
for (Action<Player> action : pair.right()) {
action.trigger(context);
}
return;
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at priority action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerMoneyAction() {
registerAction("give-money", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
VaultHook.deposit(context.getHolder(), value.evaluate(context));
};
});
registerAction("take-money", (args, chance) -> {
MathValue<Player> value = MathValue.auto(args);
return context -> {
if (Math.random() > chance) return;
VaultHook.withdraw(context.getHolder(), value.evaluate(context));
};
});
}
private void registerPotionAction() {
registerAction("potion-effect", (args, chance) -> {
if (args instanceof Section 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 context -> {
if (Math.random() > chance) return;
context.getHolder().addPotionEffect(potionEffect);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at potion-effect action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerSoundAction() {
registerAction("sound", (args, chance) -> {
if (args instanceof Section section) {
Sound sound = Sound.sound(
Key.key(section.getString("key")),
Sound.Source.valueOf(section.getString("source", "PLAYER").toUpperCase(Locale.ENGLISH)),
section.getDouble("volume", 1.0).floatValue(),
section.getDouble("pitch", 1.0).floatValue()
);
return context -> {
if (Math.random() > chance) return;
Audience audience = plugin.getSenderFactory().getAudience(context.getHolder());
AdventureHelper.playSound(audience, sound);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at sound action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerPluginExpAction() {
registerAction("plugin-exp", (args, chance) -> {
if (args instanceof Section section) {
String pluginName = section.getString("plugin");
MathValue<Player> value = MathValue.auto(section.get("exp"));
String target = section.getString("target");
return context -> {
if (Math.random() > chance) return;
Optional.ofNullable(plugin.getIntegrationManager().getLevelerProvider(pluginName)).ifPresentOrElse(it -> {
it.addXp(context.getHolder(), target, value.evaluate(context));
}, () -> plugin.getPluginLogger().warn("Plugin (" + pluginName + "'s) level is not compatible. Please double check if it's a problem caused by pronunciation."));
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at plugin-exp action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerTitleAction() {
registerAction("title", (args, chance) -> {
if (args instanceof Section section) {
TextValue<Player> title = TextValue.auto(section.getString("title", ""));
TextValue<Player> subtitle = TextValue.auto(section.getString("subtitle", ""));
int fadeIn = section.getInt("fade-in", 20);
int stay = section.getInt("stay", 30);
int fadeOut = section.getInt("fade-out", 10);
return context -> {
if (Math.random() > chance) return;
final Player player = context.getHolder();
Audience audience = plugin.getSenderFactory().getAudience(player);
AdventureHelper.sendTitle(audience,
AdventureHelper.miniMessage(title.render(context)),
AdventureHelper.miniMessage(subtitle.render(context)),
fadeIn, stay, fadeOut
);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at title action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
registerAction("random-title", (args, chance) -> {
if (args instanceof Section section) {
List<String> titles = section.getStringList("titles");
if (titles.isEmpty()) titles.add("");
List<String> subtitles = section.getStringList("subtitles");
if (subtitles.isEmpty()) subtitles.add("");
int fadeIn = section.getInt("fade-in", 20);
int stay = section.getInt("stay", 30);
int fadeOut = section.getInt("fade-out", 10);
return context -> {
if (Math.random() > chance) return;
TextValue<Player> title = TextValue.auto(titles.get(RandomUtils.generateRandomInt(0, titles.size() - 1)));
TextValue<Player> subtitle = TextValue.auto(subtitles.get(RandomUtils.generateRandomInt(0, subtitles.size() - 1)));
final Player player = context.getHolder();
Audience audience = plugin.getSenderFactory().getAudience(player);
AdventureHelper.sendTitle(audience,
AdventureHelper.miniMessage(title.render(context)),
AdventureHelper.miniMessage(subtitle.render(context)),
fadeIn, stay, fadeOut
);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at random-title action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
registerAction("title-nearby", (args, chance) -> {
if (args instanceof Section section) {
TextValue<Player> title = TextValue.auto(section.getString("title"));
TextValue<Player> subtitle = TextValue.auto(section.getString("subtitle"));
int fadeIn = section.getInt("fade-in", 20);
int stay = section.getInt("stay", 30);
int fadeOut = section.getInt("fade-out", 10);
int range = section.getInt("range", 0);
return context -> {
if (Math.random() > chance) return;
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
plugin.getScheduler().sync().run(() -> {
for (Entity player : location.getWorld().getNearbyEntities(location, range, range, range, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= range) {
context.arg(ContextKeys.TEMP_NEAR_PLAYER, player.getName());
Audience audience = plugin.getSenderFactory().getAudience(player);
AdventureHelper.sendTitle(audience,
AdventureHelper.miniMessage(title.render(context)),
AdventureHelper.miniMessage(subtitle.render(context)),
fadeIn, stay, fadeOut
);
}
}
}, location
);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at title-nearby action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
});
}
private void registerFakeItemAction() {
registerAction("fake-item", ((args, chance) -> {
if (args instanceof Section section) {
String itemID = section.getString("item", "");
String[] split = itemID.split(":");
if (split.length >= 2) itemID = split[split.length - 1];
MathValue<Player> duration = MathValue.auto(section.get("duration", 20));
boolean position = !section.getString("position", "player").equals("player");
MathValue<Player> x = MathValue.auto(section.get("x", 0));
MathValue<Player> y = MathValue.auto(section.get("y", 0));
MathValue<Player> z = MathValue.auto(section.get("z", 0));
MathValue<Player> yaw = MathValue.auto(section.get("yaw", 0));
int range = section.getInt("range", 0);
boolean opposite = section.getBoolean("opposite-yaw", false);
String finalItemID = itemID;
return context -> {
if (Math.random() > chance) return;
Player owner = context.getHolder();
Location location = position ? requireNonNull(context.arg(ContextKeys.OTHER_LOCATION)).clone() : owner.getLocation().clone();
location.add(x.evaluate(context), y.evaluate(context) - 1, z.evaluate(context));
if (opposite) location.setYaw(-owner.getLocation().getYaw());
else location.setYaw((float) yaw.evaluate(context));
FakeArmorStand armorStand = SparrowHeart.getInstance().createFakeArmorStand(location);
armorStand.invisible(true);
armorStand.equipment(EquipmentSlot.HEAD, plugin.getItemManager().buildInternal(context, finalItemID));
ArrayList<Player> viewers = new ArrayList<>();
if (range > 0) {
for (Entity player : location.getWorld().getNearbyEntities(location, range, range, range, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= range) {
viewers.add((Player) player);
}
}
} else {
viewers.add(owner);
}
for (Player player : viewers) {
armorStand.spawn(player);
}
plugin.getScheduler().asyncLater(() -> {
for (Player player : viewers) {
if (player.isOnline() && player.isValid()) {
armorStand.destroy(player);
}
}
}, (long) (duration.evaluate(context) * 50), TimeUnit.MILLISECONDS);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at fake-item action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
}));
}
private void registerHologramAction() {
registerAction("hologram", ((args, chance) -> {
if (args instanceof Section section) {
TextValue<Player> text = TextValue.auto(section.getString("text", ""));
MathValue<Player> duration = MathValue.auto(section.get("duration", 20));
boolean position = section.getString("position", "other").equals("other");
MathValue<Player> x = MathValue.auto(section.get("x", 0));
MathValue<Player> y = MathValue.auto(section.get("y", 0));
MathValue<Player> z = MathValue.auto(section.get("z", 0));
int range = section.getInt("range", 16);
return context -> {
if (Math.random() > chance) return;
Player owner = context.getHolder();
Location location = position ? requireNonNull(context.arg(ContextKeys.OTHER_LOCATION)).clone() : owner.getLocation().clone();
location.add(x.evaluate(context), y.evaluate(context), z.evaluate(context));
FakeArmorStand armorStand = SparrowHeart.getInstance().createFakeArmorStand(location);
armorStand.invisible(true);
armorStand.small(true);
armorStand.name(AdventureHelper.miniMessageToJson(text.render(context)));
ArrayList<Player> viewers = new ArrayList<>();
if (range > 0) {
for (Entity player : location.getWorld().getNearbyEntities(location, range, range, range, entity -> entity instanceof Player)) {
double distance = LocationUtils.getDistance(player.getLocation(), location);
if (distance <= range) {
viewers.add((Player) player);
}
}
} else {
viewers.add(owner);
}
for (Player player : viewers) {
armorStand.spawn(player);
}
plugin.getScheduler().asyncLater(() -> {
for (Player player : viewers) {
if (player.isOnline() && player.isValid()) {
armorStand.destroy(player);
}
}
}, (long) (duration.evaluate(context) * 50), TimeUnit.MILLISECONDS);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at hologram action which is expected to be `Section`");
return EmptyAction.INSTANCE;
}
}));
}
private void registerFishFindAction() {
registerAction("fish-finder", (args, chance) -> {
String surrounding = (String) args;
return context -> {
if (Math.random() > chance) return;
String previous = context.arg(ContextKeys.SURROUNDING);
context.arg(ContextKeys.SURROUNDING, surrounding);
Collection<String> loots = plugin.getLootManager().getWeightedLoots(Effect.newInstance(), context).keySet();
StringJoiner stringJoiner = new StringJoiner(TranslationManager.miniMessageTranslation(MessageConstants.COMMAND_FISH_FINDER_SPLIT_CHAR.build().key()));
for (String loot : loots) {
plugin.getLootManager().getLoot(loot).ifPresent(lootIns -> {
if (lootIns.showInFinder()) {
if (!lootIns.nick().equals("UNDEFINED")) {
stringJoiner.add(lootIns.nick());
}
}
});
}
if (previous == null) {
context.remove(ContextKeys.SURROUNDING);
} else {
context.arg(ContextKeys.SURROUNDING, previous);
}
if (loots.isEmpty()) {
plugin.getSenderFactory().wrap(context.getHolder()).sendMessage(TranslationManager.render(MessageConstants.COMMAND_FISH_FINDER_NO_LOOT.build()));
} else {
plugin.getSenderFactory().wrap(context.getHolder()).sendMessage(TranslationManager.render(MessageConstants.COMMAND_FISH_FINDER_POSSIBLE_LOOTS.arguments(AdventureHelper.miniMessage(stringJoiner.toString())).build()));
}
};
});
}
/**
* 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", "unchecked"})
private void loadExpansions() {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);
if (!expansionFolder.exists())
expansionFolder.mkdirs();
List<Class<? extends ActionExpansion<Player>>> classes = new ArrayList<>();
File[] expansionJars = expansionFolder.listFiles();
if (expansionJars == null) return;
for (File expansionJar : expansionJars) {
if (expansionJar.getName().endsWith(".jar")) {
try {
Class<? extends ActionExpansion<Player>> expansionClass = (Class<? extends ActionExpansion<Player>>) ClassUtils.findClass(expansionJar, ActionExpansion.class);
classes.add(expansionClass);
} catch (IOException | ClassNotFoundException e) {
plugin.getPluginLogger().warn("Failed to load expansion: " + expansionJar.getName(), e);
}
}
}
try {
for (Class<? extends ActionExpansion<Player>> expansionClass : classes) {
ActionExpansion<Player> expansion = expansionClass.getDeclaredConstructor().newInstance();
unregisterAction(expansion.getActionType());
registerAction(expansion.getActionType(), expansion.getActionFactory());
plugin.getPluginLogger().info("Loaded action expansion: " + expansion.getActionType() + "[" + expansion.getVersion() + "]" + " by " + expansion.getAuthor() );
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
plugin.getPluginLogger().warn("Error occurred when creating expansion instance.", e);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* This file is part of InvUI, licensed under the MIT License.
*
* Copyright (c) 2021 NichtStudioCode
*/
package net.momirealms.customfishing.bukkit.adventure;
import com.google.gson.stream.JsonReader;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
public class Languages {
private static final Languages INSTANCE = new Languages();
private final Map<String, Map<String, String>> translations = new HashMap<>();
private Function<Player, Locale> languageProvider = Player::locale;
private boolean serverSideTranslations = true;
private Languages() {
}
public static Languages getInstance() {
return INSTANCE;
}
public void addLanguage(@NotNull String lang, @NotNull Map<String, String> translations) {
this.translations.put(lang, translations);
}
public void loadLanguage(@NotNull String lang, @NotNull Reader reader) throws IOException {
var translations = new HashMap<String, String>();
try (var jsonReader = new JsonReader(reader)) {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
var key = jsonReader.nextName();
var value = jsonReader.nextString();
translations.put(key, value);
}
addLanguage(lang, translations);
}
}
public void loadLanguage(@NotNull String lang, @NotNull File file, @NotNull Charset charset) throws IOException {
try (var reader = new FileReader(file, charset)) {
loadLanguage(lang, reader);
}
}
public @Nullable String getFormatString(@NotNull String lang, @NotNull String key) {
var map = translations.get(lang);
if (map == null)
return null;
return map.get(key);
}
public void setLanguageProvider(@NotNull Function<Player, Locale> languageProvider) {
this.languageProvider = languageProvider;
}
public @NotNull Locale getLanguage(@NotNull Player player) {
return languageProvider.apply(player);
}
public void enableServerSideTranslations(boolean enable) {
serverSideTranslations = enable;
}
public boolean doesServerSideTranslations() {
return serverSideTranslations;
}
}

View File

@@ -0,0 +1,28 @@
/*
* This file is part of InvUI, licensed under the MIT License.
*
* Copyright (c) 2021 NichtStudioCode
*/
package net.momirealms.customfishing.bukkit.adventure;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
public class ShadedAdventureComponentUtils {
private static final Style FORMATTING_TEMPLATE = Style.style()
.color(NamedTextColor.WHITE)
.decoration(TextDecoration.ITALIC, false)
.decoration(TextDecoration.BOLD, false)
.decoration(TextDecoration.STRIKETHROUGH, false)
.decoration(TextDecoration.UNDERLINED, false)
.decoration(TextDecoration.OBFUSCATED, false)
.build();
public static Component withoutPreFormatting(Component component) {
return component.style(component.style().merge(FORMATTING_TEMPLATE, Style.Merge.Strategy.IF_ABSENT_ON_TARGET));
}
}

View File

@@ -0,0 +1,50 @@
/*
* This file is part of InvUI, licensed under the MIT License.
*
* Copyright (c) 2021 NichtStudioCode
*/
package net.momirealms.customfishing.bukkit.adventure;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.xenondevs.inventoryaccess.component.ComponentWrapper;
public class ShadedAdventureComponentWrapper implements ComponentWrapper {
public static final ShadedAdventureComponentWrapper EMPTY = new ShadedAdventureComponentWrapper(Component.empty());
private final Component component;
public ShadedAdventureComponentWrapper(Component component) {
this.component = component;
}
@Override
public @NotNull String serializeToJson() {
return GsonComponentSerializer.gson().serialize(component);
}
@Override
public @NotNull ComponentWrapper localized(@NotNull String lang) {
if (!Languages.getInstance().doesServerSideTranslations())
return this;
return new ShadedAdventureComponentWrapper(ShadedAdventureShadedComponentLocalizer.getInstance().localize(lang, component));
}
@Override
public @NotNull ComponentWrapper withoutPreFormatting() {
return new ShadedAdventureComponentWrapper(ShadedAdventureComponentUtils.withoutPreFormatting(component));
}
@Override
public @NotNull ShadedAdventureComponentWrapper clone() {
try {
return (ShadedAdventureComponentWrapper) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* This file is part of InvUI, licensed under the MIT License.
*
* Copyright (c) 2021 NichtStudioCode
*/
package net.momirealms.customfishing.bukkit.adventure;
import net.kyori.adventure.text.*;
public class ShadedAdventureShadedComponentLocalizer extends ShadedComponentLocalizer<Component> {
private static final ShadedAdventureShadedComponentLocalizer INSTANCE = new ShadedAdventureShadedComponentLocalizer();
private ShadedAdventureShadedComponentLocalizer() {
super(Component::text);
}
public static ShadedAdventureShadedComponentLocalizer getInstance() {
return INSTANCE;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Component localize(String lang, Component component) {
if (!(component instanceof BuildableComponent))
throw new IllegalStateException("Component is not a BuildableComponent");
return localize(lang, (BuildableComponent) component);
}
@SuppressWarnings("NonExtendableApiUsage")
private <C extends BuildableComponent<C, B>, B extends ComponentBuilder<C, B>> BuildableComponent<?, ?> localize(String lang, BuildableComponent<C, B> component) {
ComponentBuilder<?, ?> builder;
if (component instanceof TranslatableComponent) {
builder = localizeTranslatable(lang, (TranslatableComponent) component).toBuilder();
} else {
builder = component.toBuilder();
}
builder.mapChildrenDeep(child -> {
if (child instanceof TranslatableComponent)
return localizeTranslatable(lang, (TranslatableComponent) child);
return child;
});
return builder.build();
}
private BuildableComponent<?, ?> localizeTranslatable(String lang, TranslatableComponent component) {
var formatString = Languages.getInstance().getFormatString(lang, component.key());
if (formatString == null)
return component;
var children = decomposeFormatString(lang, formatString, component, component.args());
return Component.textOfChildren(children.toArray(ComponentLike[]::new)).style(component.style());
}
}

View File

@@ -0,0 +1,83 @@
/*
* This file is part of InvUI, licensed under the MIT License.
*
* Copyright (c) 2021 NichtStudioCode
*/
package net.momirealms.customfishing.bukkit.adventure;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;
abstract class ShadedComponentLocalizer<T> {
private static final Pattern FORMAT_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?([A-Za-z%]|$)");
private Function<String, T> componentCreator;
public ShadedComponentLocalizer(Function<String, T> componentCreator) {
this.componentCreator = componentCreator;
}
public void setComponentCreator(Function<String, T> componentCreator) {
this.componentCreator = componentCreator;
}
public abstract T localize(String lang, T component);
protected List<T> decomposeFormatString(String lang, String formatString, T component, List<T> args) {
var matcher = FORMAT_PATTERN.matcher(formatString);
var components = new ArrayList<T>();
var sb = new StringBuilder();
var nextArgIdx = 0;
var i = 0;
while (matcher.find(i)) {
var start = matcher.start();
var end = matcher.end();
// check for escaped %
var matchedStr = formatString.substring(i, start);
if ("%%".equals(matchedStr)) {
sb.append('%');
} else {
// check for invalid format, only %s is supported
var argType = matcher.group(2);
if (!"s".equals(argType)) {
throw new IllegalStateException("Unsupported format: '" + matchedStr + "'");
}
// retrieve argument index
var argIdxStr = matcher.group(1);
var argIdx = argIdxStr == null ? nextArgIdx++ : Integer.parseInt(argIdxStr) - 1;
// validate argument index
if (argIdx < 0)
throw new IllegalStateException("Invalid argument index: " + argIdx);
// append the text before the argument
sb.append(formatString, i, start);
// add text component
components.add(componentCreator.apply(sb.toString()));
// add argument component
components.add(args.size() <= argIdx ? componentCreator.apply("") : localize(lang, args.get(argIdx)));
// clear string builder
sb.setLength(0);
}
// start next search after matcher end index
i = end;
}
// append the text after the last argument
if (i < formatString.length()) {
sb.append(formatString, i, formatString.length());
components.add(componentCreator.apply(sb.toString()));
}
return components;
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.bag;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.event.FishingBagPreCollectEvent;
import net.momirealms.customfishing.api.event.FishingLootSpawnEvent;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.action.ActionManager;
import net.momirealms.customfishing.api.mechanic.bag.BagManager;
import net.momirealms.customfishing.api.mechanic.bag.FishingBagHolder;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.requirement.Requirement;
import net.momirealms.customfishing.api.mechanic.requirement.RequirementManager;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.api.util.EventUtils;
import net.momirealms.customfishing.bukkit.config.BukkitConfigManager;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.sparrow.heart.SparrowHeart;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class BukkitBagManager implements BagManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<UUID, UserData> tempEditMap;
private Action<Player>[] collectLootActions;
private Action<Player>[] bagFullActions;
private boolean bagStoreLoots;
private boolean bagStoreRods;
private boolean bagStoreBaits;
private boolean bagStoreHooks;
private boolean bagStoreUtils;
private final HashSet<MechanicType> storedTypes = new HashSet<>();
private boolean enable;
private String bagTitle;
private List<Material> bagWhiteListItems = new ArrayList<>();
private Requirement<Player>[] collectRequirements;
public BukkitBagManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.tempEditMap = new HashMap<>();
}
@Override
public void load() {
this.loadConfig();
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
storedTypes.clear();
}
@Override
public void disable() {
unload();
this.plugin.getStorageManager().getDataSource().updateManyPlayersData(tempEditMap.values(), true);
}
@EventHandler
public void onLootSpawn(FishingLootSpawnEvent event) {
if (!enable || !bagStoreLoots) {
return;
}
if (!event.summonEntity()) {
return;
}
if (!(event.getEntity() instanceof Item itemEntity)) {
return;
}
Player player = event.getPlayer();
Context<Player> context = event.getContext();
if (!RequirementManager.isSatisfied(context, collectRequirements)) {
return;
}
Optional<UserData> onlineUser = plugin.getStorageManager().getOnlineUser(player.getUniqueId());
if (onlineUser.isEmpty()) {
return;
}
UserData userData = onlineUser.get();
Inventory inventory = userData.holder().getInventory();
ItemStack item = itemEntity.getItemStack();
FishingBagPreCollectEvent preCollectEvent = new FishingBagPreCollectEvent(player, item, inventory);
if (EventUtils.fireAndCheckCancel(preCollectEvent)) {
return;
}
int cannotPut = PlayerUtils.putItemsToInventory(inventory, item, item.getAmount());
// some are put into bag
if (cannotPut != item.getAmount()) {
ActionManager.trigger(context, collectLootActions);
}
// all are put
if (cannotPut == 0) {
event.summonEntity(false);
return;
}
item.setAmount(cannotPut);
itemEntity.setItemStack(item);
ActionManager.trigger(context, bagFullActions);
}
private void loadConfig() {
Section config = BukkitConfigManager.getMainConfig().getSection("mechanics.fishing-bag");
enable = config.getBoolean("enable", true);
bagTitle = config.getString("bag-title", "");
bagStoreLoots = config.getBoolean("can-store-loot", false);
bagStoreRods = config.getBoolean("can-store-rod", true);
bagStoreBaits = config.getBoolean("can-store-bait", true);
bagStoreHooks = config.getBoolean("can-store-hook", true);
bagStoreUtils = config.getBoolean("can-store-util", true);
bagWhiteListItems = config.getStringList("whitelist-items").stream().map(it -> Material.valueOf(it.toUpperCase(Locale.ENGLISH))).toList();
collectLootActions = plugin.getActionManager().parseActions(config.getSection("collect-actions"));
bagFullActions = plugin.getActionManager().parseActions(config.getSection("full-actions"));
collectRequirements = plugin.getRequirementManager().parseRequirements(config.getSection("collect-requirements"), false);
if (bagStoreLoots) storedTypes.add(MechanicType.LOOT);
if (bagStoreRods) storedTypes.add(MechanicType.ROD);
if (bagStoreBaits) storedTypes.add(MechanicType.BAIT);
if (bagStoreHooks) storedTypes.add(MechanicType.HOOK);
if (bagStoreUtils) storedTypes.add(MechanicType.UTIL);
}
@Override
public CompletableFuture<Boolean> openBag(Player viewer, UUID owner) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
if (enable) {
Optional<UserData> onlineUser = plugin.getStorageManager().getOnlineUser(owner);
onlineUser.ifPresentOrElse(data -> {
viewer.openInventory(data.holder().getInventory());
SparrowHeart.getInstance().updateInventoryTitle(viewer, AdventureHelper.componentToJson(AdventureHelper.miniMessage(plugin.getPlaceholderManager().parse(Bukkit.getOfflinePlayer(owner), bagTitle, Map.of("{uuid}", owner.toString(), "{player}", data.name())))));
future.complete(true);
}, () -> plugin.getStorageManager().getOfflineUserData(owner, true).thenAccept(result -> result.ifPresentOrElse(data -> {
if (data.isLocked()) {
future.completeExceptionally(new RuntimeException("Data is locked"));
return;
}
this.tempEditMap.put(viewer.getUniqueId(), data);
viewer.openInventory(data.holder().getInventory());
SparrowHeart.getInstance().updateInventoryTitle(viewer, AdventureHelper.componentToJson(AdventureHelper.miniMessage(plugin.getPlaceholderManager().parse(Bukkit.getOfflinePlayer(owner), bagTitle, Map.of("{uuid}", owner.toString(), "{player}", data.name())))));
future.complete(true);
}, () -> future.complete(false))));
} else {
future.complete(false);
}
return future;
}
/**
* 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))
return;
final Player viewer = (Player) event.getPlayer();
UserData userData = tempEditMap.remove(viewer.getUniqueId());
if (userData == null)
return;
this.plugin.getStorageManager().saveUserData(userData, 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 (ignoreCancelled = true)
public void onInvClick(InventoryClickEvent event) {
if (!(event.getInventory().getHolder() instanceof FishingBagHolder))
return;
ItemStack movedItem = event.getCurrentItem();
Inventory clicked = event.getClickedInventory();
if (clicked != event.getWhoClicked().getInventory()) {
if (event.getAction() != InventoryAction.HOTBAR_SWAP && event.getAction() != InventoryAction.HOTBAR_MOVE_AND_READD) {
return;
}
movedItem = event.getWhoClicked().getInventory().getItem(event.getHotbarButton());
}
if (movedItem == null || movedItem.getType() == Material.AIR || bagWhiteListItems.contains(movedItem.getType()))
return;
String id = plugin.getItemManager().getItemID(movedItem);
List<MechanicType> type = MechanicType.getTypeByID(id);
if (type == null) {
event.setCancelled(true);
return;
}
for (MechanicType mechanicType : type) {
if (storedTypes.contains(mechanicType)) {
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) {
UserData userData = tempEditMap.remove(event.getPlayer().getUniqueId());
if (userData == null)
return;
plugin.getStorageManager().saveUserData(userData, true);
}
}

View File

@@ -0,0 +1,344 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.block;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.integration.BlockProvider;
import net.momirealms.customfishing.api.integration.ExternalProvider;
import net.momirealms.customfishing.api.mechanic.block.*;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.misc.value.MathValue;
import net.momirealms.customfishing.common.util.Pair;
import net.momirealms.customfishing.common.util.RandomUtils;
import net.momirealms.customfishing.common.util.Tuple;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Container;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.block.data.Rotatable;
import org.bukkit.block.data.type.NoteBlock;
import org.bukkit.configuration.ConfigurationSection;
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.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.Objects.requireNonNull;
public class BukkitBlockManager implements BlockManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, BlockProvider> blockProviders = new HashMap<>();
private final HashMap<String, BlockConfig> blocks = new HashMap<>();
private final HashMap<String, BlockDataModifierFactory> dataFactories = new HashMap<>();
private final HashMap<String, BlockStateModifierFactory> stateFactories = new HashMap<>();
private BlockProvider[] blockDetectArray;
public BukkitBlockManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.registerInbuiltProperties();
this.registerBlockProvider(new BlockProvider() {
@Override
public String identifier() {
return "vanilla";
}
@Override
public BlockData blockData(@NotNull Context<Player> context, @NotNull String id, List<BlockDataModifier> modifiers) {
BlockData blockData = Material.valueOf(id.toUpperCase(Locale.ENGLISH)).createBlockData();
for (BlockDataModifier modifier : modifiers)
modifier.apply(context, blockData);
return blockData;
}
@NotNull
@Override
public String blockID(@NotNull Block block) {
return block.getType().name();
}
});
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
this.resetBlockDetectionOrder();
for (BlockProvider provider : blockProviders.values()) {
plugin.debug("Registered BlockProvider: " + provider.identifier());
}
plugin.debug("Loaded " + blocks.size() + " blocks");
plugin.debug("Block order: " + Arrays.toString(Arrays.stream(blockDetectArray).map(ExternalProvider::identifier).toList().toArray(new String[0])));
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
this.blocks.clear();
}
@Override
public void disable() {
this.blockProviders.clear();
}
private void resetBlockDetectionOrder() {
ArrayList<BlockProvider> list = new ArrayList<>();
for (String plugin : ConfigManager.blockDetectOrder()) {
BlockProvider library = blockProviders.get(plugin);
if (library != null)
list.add(library);
}
this.blockDetectArray = list.toArray(new BlockProvider[0]);
}
@Override
public boolean registerBlock(@NotNull BlockConfig block) {
if (blocks.containsKey(block.id())) return false;
blocks.put(block.id(), block);
return true;
}
/**
* 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;
// Retrieve a custom string value stored in the entity's persistent data container.
String temp = event.getEntity().getPersistentDataContainer().get(
requireNonNull(NamespacedKey.fromString("block", plugin.getBoostrap())),
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= blocks.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();
event.getBlock().setType(Material.AIR);
return;
}
Context<Player> context = Context.player(player);
Location location = event.getBlock().getLocation();
// Apply block state modifiers from the BlockConfig to the block 1 tick later.
plugin.getScheduler().sync().runLater(() -> {
BlockState state = location.getBlock().getState();
for (BlockStateModifier modifier : blockConfig.stateModifiers()) {
modifier.apply(context, state);
}
}, 1, location);
}
public boolean registerBlockProvider(BlockProvider blockProvider) {
if (this.blockProviders.containsKey(blockProvider.identifier())) return false;
this.blockProviders.put(blockProvider.identifier(), blockProvider);
this.resetBlockDetectionOrder();
return true;
}
public boolean unregisterBlockProvider(String identification) {
boolean success = blockProviders.remove(identification) != null;
if (success)
this.resetBlockDetectionOrder();
return success;
}
public boolean registerBlockDataModifierBuilder(String type, BlockDataModifierFactory factory) {
if (this.dataFactories.containsKey(type)) return false;
this.dataFactories.put(type, factory);
return true;
}
public boolean registerBlockStateModifierBuilder(String type, BlockStateModifierFactory factory) {
if (stateFactories.containsKey(type)) return false;
this.stateFactories.put(type, factory);
return true;
}
public boolean unregisterBlockDataModifierBuilder(String type) {
return this.dataFactories.remove(type) != null;
}
public boolean unregisterBlockStateModifierBuilder(String type) {
return this.stateFactories.remove(type) != null;
}
private void registerInbuiltProperties() {
this.registerDirectional();
this.registerStorage();
this.registerRotatable();
this.registerNoteBlock();
}
@Override
@NotNull
public FallingBlock summonBlockLoot(@NotNull Context<Player> context) {
String id = context.arg(ContextKeys.ID);
BlockConfig config = requireNonNull(blocks.get(id), "Block " + id + " not found");
String blockID = config.blockID();
BlockData blockData;
if (blockID.contains(":")) {
String[] split = blockID.split(":", 2);
BlockProvider provider = requireNonNull(blockProviders.get(split[0]), "BlockProvider " + split[0] + " doesn't exist");
blockData = requireNonNull(provider.blockData(context, split[1], config.dataModifier()), "Block " + split[1] + " doesn't exist");
} else {
blockData = blockProviders.get("vanilla").blockData(context, blockID, config.dataModifier());
}
Location hookLocation = requireNonNull(context.arg(ContextKeys.OTHER_LOCATION));
Location playerLocation = requireNonNull(context.getHolder()).getLocation();
FallingBlock fallingBlock = hookLocation.getWorld().spawn(hookLocation, FallingBlock.class, (fb -> fb.setBlockData(blockData)));
fallingBlock.getPersistentDataContainer().set(
requireNonNull(NamespacedKey.fromString("block", plugin.getBoostrap())),
PersistentDataType.STRING,
id + ";" + context.getHolder().getName()
);
Vector vector = playerLocation.subtract(hookLocation).toVector().multiply(1.2 - 1);
vector = vector.setY((vector.getY() + 0.2) * 1.2);
fallingBlock.setVelocity(vector);
return fallingBlock;
}
@Override
@NotNull
public String getBlockID(@NotNull Block block) {
for (BlockProvider blockProvider : blockDetectArray) {
String id = blockProvider.blockID(block);
if (id != null) return id;
}
// Should not reach this because vanilla library would always work
return "AIR";
}
private void registerDirectional() {
this.registerBlockDataModifierBuilder("directional-4", (args) -> (context, blockData) -> {
boolean arg = (boolean) args;
if (arg && blockData instanceof Directional directional) {
directional.setFacing(BlockFace.values()[ThreadLocalRandom.current().nextInt(0, 4)]);
}
});
this.registerBlockDataModifierBuilder("directional-6", (args) -> (context, blockData) -> {
boolean arg = (boolean) args;
if (arg && blockData instanceof Directional directional) {
directional.setFacing(BlockFace.values()[ThreadLocalRandom.current().nextInt(0, 6)]);
}
});
}
private void registerRotatable() {
this.registerBlockDataModifierBuilder("rotatable", (args) -> {
boolean arg = (boolean) args;
return (context, blockData) -> {
if (arg && blockData instanceof Rotatable rotatable) {
rotatable.setRotation(BlockFace.values()[ThreadLocalRandom.current().nextInt(BlockFace.values().length)]);
}
};
});
}
private void registerNoteBlock() {
this.registerBlockDataModifierBuilder("noteblock", (args) -> {
if (args instanceof Section section) {
var instrument = Instrument.valueOf(section.getString("instrument"));
var note = new Note(section.getInt("note"));
return (context, blockData) -> {
if (blockData instanceof NoteBlock noteBlock) {
noteBlock.setNote(note);
noteBlock.setInstrument(instrument);
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at noteblock property which should be Section");
return EmptyBlockDataModifier.INSTANCE;
}
});
}
private void registerStorage() {
this.registerBlockStateModifierBuilder("storage", (args) -> {
if (args instanceof Section section) {
List<Tuple<MathValue<Player>, String, Pair<MathValue<Player>, MathValue<Player>>>> contents = new ArrayList<>();
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection inner) {
String item = inner.getString("item");
String[] split = inner.getString("amount","1~1").split("~");
Pair<MathValue<Player>, MathValue<Player>> amountPair = Pair.of(MathValue.auto(split[0]), MathValue.auto(split[1]));
MathValue<Player> chance = MathValue.auto(inner.get("chance", 1d));
contents.add(Tuple.of(chance, item, amountPair));
}
}
return (context, blockState) -> {
if (blockState instanceof Container container) {
setInventoryItems(contents, context, container.getInventory());
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at storage property which should be Section");
return EmptyBlockStateModifier.INSTANCE;
}
});
}
private void setInventoryItems(
List<Tuple<MathValue<Player>, String, Pair<MathValue<Player>, MathValue<Player>>>> contents,
Context<Player> context,
Inventory inventory
) {
LinkedList<Integer> unused = new LinkedList<>();
for (int i = 0; i < inventory.getSize(); i++) {
unused.add(i);
}
Collections.shuffle(unused);
for (Tuple<MathValue<Player>, String, Pair<MathValue<Player>, MathValue<Player>>> tuple : contents) {
if (tuple.left().evaluate(context) > Math.random()) {
ItemStack itemStack = plugin.getItemManager().buildAny(context, tuple.mid());
if (itemStack != null) {
itemStack.setAmount(RandomUtils.generateRandomInt((int) tuple.right().left().evaluate(context), (int) (tuple.right().right().evaluate(context))));
inventory.setItem(unused.pop(), itemStack);
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.common.command.AbstractCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.sender.SenderFactory;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.incendo.cloud.bukkit.data.Selector;
import java.util.Collection;
public abstract class BukkitCommandFeature<C extends CommandSender> extends AbstractCommandFeature<C> {
public BukkitCommandFeature(CustomFishingCommandManager<C> commandManager) {
super(commandManager);
}
@Override
@SuppressWarnings("unchecked")
protected SenderFactory<?, C> getSenderFactory() {
return (SenderFactory<?, C>) BukkitCustomFishingPlugin.getInstance().getSenderFactory();
}
public Pair<TranslatableComponent.Builder, Component> resolveSelector(Selector<? extends Entity> selector, TranslatableComponent.Builder single, TranslatableComponent.Builder multiple) {
Collection<? extends Entity> entities = selector.values();
if (entities.size() == 1) {
return Pair.of(single, Component.text(entities.iterator().next().getName()));
} else {
return Pair.of(multiple, Component.text(entities.size()));
}
}
public Pair<TranslatableComponent.Builder, Component> resolveSelector(Collection<? extends Entity> selector, TranslatableComponent.Builder single, TranslatableComponent.Builder multiple) {
if (selector.size() == 1) {
return Pair.of(single, Component.text(selector.iterator().next().getName()));
} else {
return Pair.of(multiple, Component.text(selector.size()));
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command;
import net.kyori.adventure.util.Index;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.feature.*;
import net.momirealms.customfishing.common.command.AbstractCommandManager;
import net.momirealms.customfishing.common.command.CommandFeature;
import net.momirealms.customfishing.common.sender.Sender;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.paper.LegacyPaperCommandManager;
import org.incendo.cloud.setting.ManagerSetting;
import java.util.List;
public class BukkitCommandManager extends AbstractCommandManager<CommandSender> {
private final List<CommandFeature<CommandSender>> FEATURES = List.of(
new ReloadCommand(this),
new SellFishCommand(this),
new GetItemCommand(this),
new GiveItemCommand(this),
new ImportItemCommand(this),
new EndCompetitionCommand(this),
new StopCompetitionCommand(this),
new StartCompetitionCommand(this),
new OpenMarketCommand(this),
new OpenBagCommand(this),
new FishingBagCommand(this),
new EditOnlineBagCommand(this),
new EditOfflineBagCommand(this),
new UnlockDataCommand(this),
new ImportDataCommand(this),
new ExportDataCommand(this),
new AddStatisticsCommand(this),
new SetStatisticsCommand(this),
new ResetStatisticsCommand(this),
new QueryStatisticsCommand(this)
);
private final Index<String, CommandFeature<CommandSender>> INDEX = Index.create(CommandFeature::getFeatureID, FEATURES);
public BukkitCommandManager(BukkitCustomFishingPlugin plugin) {
super(plugin, new LegacyPaperCommandManager<>(
plugin.getBoostrap(),
ExecutionCoordinator.simpleCoordinator(),
SenderMapper.identity()
));
final LegacyPaperCommandManager<CommandSender> manager = (LegacyPaperCommandManager<CommandSender>) getCommandManager();
manager.settings().set(ManagerSetting.ALLOW_UNSAFE_REGISTRATION, true);
if (manager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
manager.registerBrigadier();
manager.brigadierManager().setNativeNumberSuggestions(true);
} else if (manager.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
manager.registerAsynchronousCompletions();
}
}
@Override
protected Sender wrapSender(CommandSender sender) {
return ((BukkitCustomFishingPlugin) plugin).getSenderFactory().wrap(sender);
}
@Override
public Index<String, CommandFeature<CommandSender>> getFeatures() {
return INDEX;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.statistic.FishingStatistics;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
import org.incendo.cloud.parser.standard.DoubleParser;
import org.incendo.cloud.parser.standard.EnumParser;
import org.incendo.cloud.parser.standard.StringParser;
public class AddStatisticsCommand extends BukkitCommandFeature<CommandSender> {
public AddStatisticsCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.required("player", PlayerParser.playerParser())
.required("id", StringParser.stringParser())
.required("type", EnumParser.enumParser(FishingStatistics.Type.class))
.required("value", DoubleParser.doubleParser(0))
.handler(context -> {
Player player = context.get("player");
String id = context.get("id");
FishingStatistics.Type type = context.get("type");
double value = context.get("value");
BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(player.getUniqueId()).ifPresentOrElse(userData -> {
if (type == FishingStatistics.Type.AMOUNT_OF_FISH_CAUGHT) {
userData.statistics().addAmount(id, (int) value);
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_MODIFY_SUCCESS, Component.text(player.getName()));
} else if (type == FishingStatistics.Type.MAX_SIZE) {
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_FAILURE_UNSUPPORTED);
}
}, () -> handleFeedback(context, MessageConstants.COMMAND_STATISTICS_FAILURE_NOT_LOADED));
});
}
@Override
public String getFeatureID() {
return "statistics_add";
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.parser.standard.UUIDParser;
import java.util.UUID;
public class EditOfflineBagCommand extends BukkitCommandFeature<CommandSender> {
public EditOfflineBagCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.required("uuid", UUIDParser.uuidParser())
.handler(context -> {
Player admin = context.sender();
UUID uuid = context.get("uuid");
BukkitCustomFishingPlugin.getInstance().getBagManager().openBag(admin, uuid).whenComplete((result, throwable) -> {
if (throwable != null) {
handleFeedback(context, MessageConstants.COMMAND_BAG_EDIT_FAILURE_UNSAFE);
return;
}
if (!result) {
handleFeedback(context, MessageConstants.COMMAND_BAG_EDIT_FAILURE_NEVER_PLAYED);
return;
}
});
});
}
@Override
public String getFeatureID() {
return "edit_offline_bag";
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
public class EditOnlineBagCommand extends BukkitCommandFeature<CommandSender> {
public EditOnlineBagCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.required("player", PlayerParser.playerParser())
.handler(context -> {
Player admin = context.sender();
Player online = context.get("player");
BukkitCustomFishingPlugin.getInstance().getBagManager().openBag(admin, online.getUniqueId());
});
}
@Override
public String getFeatureID() {
return "edit_online_bag";
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.FishingCompetition;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public class EndCompetitionCommand extends BukkitCommandFeature<CommandSender> {
public EndCompetitionCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
FishingCompetition competition = BukkitCustomFishingPlugin.getInstance().getCompetitionManager().getOnGoingCompetition();
if (competition == null) {
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_FAILURE_NO_COMPETITION);
} else {
competition.end(true);
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_END_SUCCESS);
}
});
}
@Override
public String getFeatureID() {
return "end_competition";
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.DataStorageProvider;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.util.CompletableFutures;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;
public class ExportDataCommand extends BukkitCommandFeature<CommandSender> {
public ExportDataCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(ConsoleCommandSender.class)
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
if (!Bukkit.getOnlinePlayers().isEmpty()) {
handleFeedback(context, MessageConstants.COMMAND_DATA_EXPORT_FAILURE_PLAYER_ONLINE);
return;
}
BukkitCustomFishingPlugin plugin = BukkitCustomFishingPlugin.getInstance();
handleFeedback(context, MessageConstants.COMMAND_DATA_EXPORT_START);
plugin.getScheduler().async().execute(() -> {
DataStorageProvider storageProvider = plugin.getStorageManager().getDataSource();
Set<UUID> uuids = storageProvider.getUniqueUsers();
Set<CompletableFuture<Void>> futures = new HashSet<>();
AtomicInteger userCount = new AtomicInteger(0);
Map<UUID, String> out = Collections.synchronizedMap(new TreeMap<>());
int amount = uuids.size();
for (UUID uuid : uuids) {
futures.add(storageProvider.getPlayerData(uuid, false).thenAccept(it -> {
if (it.isPresent()) {
out.put(uuid, plugin.getStorageManager().toJson(it.get()));
userCount.incrementAndGet();
}
}));
}
CompletableFuture<Void> overallFuture = CompletableFutures.allOf(futures);
while (true) {
try {
overallFuture.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
break;
} catch (TimeoutException e) {
handleFeedback(context, MessageConstants.COMMAND_DATA_EXPORT_PROGRESS, Component.text(userCount.get()), Component.text(amount));
continue;
}
break;
}
JsonObject outJson = new JsonObject();
for (Map.Entry<UUID, String> entry : out.entrySet()) {
outJson.addProperty(entry.getKey().toString(), entry.getValue());
}
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
String formattedDate = formatter.format(new Date());
File outFile = new File(plugin.getDataFolder(), "exported-" + formattedDate + ".json.gz");
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(outFile.toPath())), StandardCharsets.UTF_8))) {
new GsonBuilder().disableHtmlEscaping().create().toJson(outJson, writer);
} catch (IOException e) {
throw new RuntimeException("Unexpected issue: ", e);
}
handleFeedback(context, MessageConstants.COMMAND_DATA_EXPORT_SUCCESS);
});
});
}
@Override
public String getFeatureID() {
return "data_export";
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public class FishingBagCommand extends BukkitCommandFeature<CommandSender> {
public FishingBagCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.handler(context -> {
BukkitCustomFishingPlugin.getInstance().getBagManager().openBag(context.sender(), context.sender().getUniqueId()).whenComplete((result, e) -> {
if (!result || e != null) {
handleFeedback(context, MessageConstants.COMMAND_DATA_FAILURE_NOT_LOADED);
}
});
});
}
@Override
public String getFeatureID() {
return "fishingbag";
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
@SuppressWarnings("DuplicatedCode")
public class GetItemCommand extends BukkitCommandFeature<CommandSender> {
public GetItemCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.required("id", StringParser.stringComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(BukkitCustomFishingPlugin.getInstance().getItemManager().getItemIDs().stream().map(Suggestion::suggestion).toList());
}
}))
.optional("amount", IntegerParser.integerParser(1, 6400))
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
final int amount = context.getOrDefault("amount", 1);
final String id = context.get("id");
final Player player = context.sender();
try {
ItemStack itemStack = BukkitCustomFishingPlugin.getInstance().getItemManager().buildInternal(Context.player(player).arg(ContextKeys.ID, id), id);
if (itemStack == null) {
throw new RuntimeException("Unrecognized item id: " + id);
}
int amountToGive = amount;
int maxStack = itemStack.getType().getMaxStackSize();
while (amountToGive > 0) {
int perStackSize = Math.min(maxStack, amountToGive);
amountToGive -= perStackSize;
ItemStack more = itemStack.clone();
more.setAmount(perStackSize);
PlayerUtils.dropItem(player, more, false, true, false);
}
handleFeedback(context, MessageConstants.COMMAND_ITEM_GET_SUCCESS, Component.text(amount), Component.text(id));
} catch (NullPointerException e) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_FAILURE_NOT_EXIST, Component.text(id));
}
});
}
@Override
public String getFeatureID() {
return "get_item";
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
@SuppressWarnings("DuplicatedCode")
public class GiveItemCommand extends BukkitCommandFeature<CommandSender> {
public GiveItemCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("player", PlayerParser.playerParser())
.required("id", StringParser.stringComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(BukkitCustomFishingPlugin.getInstance().getItemManager().getItemIDs().stream().map(Suggestion::suggestion).toList());
}
}))
.optional("amount", IntegerParser.integerParser(1, 6400))
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
final Player player = context.get("player");
final int amount = context.getOrDefault("amount", 1);
final String id = context.get("id");
try {
ItemStack itemStack = BukkitCustomFishingPlugin.getInstance().getItemManager().buildInternal(Context.player(player).arg(ContextKeys.ID, id), id);
if (itemStack == null) {
throw new RuntimeException("Unrecognized item id: " + id);
}
int amountToGive = amount;
int maxStack = itemStack.getType().getMaxStackSize();
while (amountToGive > 0) {
int perStackSize = Math.min(maxStack, amountToGive);
amountToGive -= perStackSize;
ItemStack more = itemStack.clone();
more.setAmount(perStackSize);
PlayerUtils.dropItem(player, more, false, true, false);
}
handleFeedback(context, MessageConstants.COMMAND_ITEM_GIVE_SUCCESS, Component.text(player.getName()), Component.text(amount), Component.text(id));
} catch (NullPointerException e) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_FAILURE_NOT_EXIST, Component.text(id));
}
});
}
@Override
public String getFeatureID() {
return "give_item";
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.DataStorageProvider;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.util.CompletableFutures;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.parser.standard.StringParser;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPInputStream;
public class ImportDataCommand extends BukkitCommandFeature<CommandSender> {
public ImportDataCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(ConsoleCommandSender.class)
.flag(manager.flagBuilder("silent").withAliases("s").build())
.required("file", StringParser.greedyFlagYieldingStringParser())
.handler(context -> {
if (!Bukkit.getOnlinePlayers().isEmpty()) {
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_FAILURE_PLAYER_ONLINE);
return;
}
String fileName = context.get("file");
BukkitCustomFishingPlugin plugin = BukkitCustomFishingPlugin.getInstance();
File file = new File(plugin.getDataFolder(), fileName);
if (!file.exists()) {
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_FAILURE_NOT_EXISTS);
return;
}
if (!file.getName().endsWith(".json.gz")) {
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_FAILURE_INVALID_FILE);
return;
}
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_START);
plugin.getScheduler().async().execute(() -> {
JsonObject data;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(Files.newInputStream(file.toPath())), StandardCharsets.UTF_8))) {
data = new GsonBuilder().disableHtmlEscaping().create().fromJson(reader, JsonObject.class);
} catch (IOException e) {
throw new RuntimeException("Unexpected issue: ", e);
}
DataStorageProvider storageProvider = plugin.getStorageManager().getDataSource();
var entrySet = data.entrySet();
int amount = entrySet.size();
AtomicInteger userCount = new AtomicInteger(0);
Set<CompletableFuture<Void>> futures = new HashSet<>();
for (Map.Entry<String, JsonElement> entry : entrySet) {
UUID uuid = UUID.fromString(entry.getKey());
if (entry.getValue() instanceof JsonPrimitive primitive) {
PlayerData playerData = plugin.getStorageManager().fromJson(primitive.getAsString());
futures.add(storageProvider.updateOrInsertPlayerData(uuid, playerData, true).thenAccept(it -> userCount.incrementAndGet()));
}
}
CompletableFuture<Void> overallFuture = CompletableFutures.allOf(futures);
while (true) {
try {
overallFuture.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
break;
} catch (TimeoutException e) {
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_PROGRESS, Component.text(userCount.get()), Component.text(amount));
continue;
}
break;
}
handleFeedback(context, MessageConstants.COMMAND_DATA_IMPORT_SUCCESS);
});
});
}
@Override
public String getFeatureID() {
return "data_import";
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.bukkit.util.ItemStackUtils;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.parser.standard.StringParser;
import java.io.File;
import java.io.IOException;
import java.util.Map;
@SuppressWarnings("DuplicatedCode")
public class ImportItemCommand extends BukkitCommandFeature<CommandSender> {
public ImportItemCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.required("id", StringParser.stringParser())
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
Player player = context.sender();
ItemStack item = player.getInventory().getItemInMainHand();
String id = context.get("id");
if (item.getType() == Material.AIR) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_IMPORT_FAILURE_NO_ITEM);
return;
}
File saved = new File(BukkitCustomFishingPlugin.getInstance().getDataFolder(), "imported_items.yml");
if (!saved.exists()) {
try {
saved.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
YamlDocument document = BukkitCustomFishingPlugin.getInstance().getConfigManager().loadData(saved);
Map<String, Object> map = ItemStackUtils.itemStackToMap(item);
document.set(id, map);
try {
document.save(saved);
handleFeedback(context, MessageConstants.COMMAND_ITEM_IMPORT_SUCCESS, Component.text(id));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
@Override
public String getFeatureID() {
return "import_item";
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
public class OpenBagCommand extends BukkitCommandFeature<CommandSender> {
public OpenBagCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("player", PlayerParser.playerParser())
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
final Player player = context.get("player");
BukkitCustomFishingPlugin.getInstance().getBagManager().openBag(player, player.getUniqueId()).whenComplete((result, e) -> {
if (!result || e != null) {
handleFeedback(context, MessageConstants.COMMAND_BAG_OPEN_FAILURE_NOT_LOADED, Component.text(player.getName()));
} else {
handleFeedback(context, MessageConstants.COMMAND_BAG_OPEN_SUCCESS, Component.text(player.getName()));
}
});
});
}
@Override
public String getFeatureID() {
return "open_bag";
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
public class OpenMarketCommand extends BukkitCommandFeature<CommandSender> {
public OpenMarketCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("player", PlayerParser.playerParser())
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
final Player player = context.get("player");
if (BukkitCustomFishingPlugin.getInstance().getMarketManager().openMarketGUI(player)) {
handleFeedback(context, MessageConstants.COMMAND_MARKET_OPEN_SUCCESS, Component.text(player.getName()));
} else {
handleFeedback(context, MessageConstants.COMMAND_MARKET_OPEN_FAILURE_NOT_LOADED, Component.text(player.getName()));
}
});
}
@Override
public String getFeatureID() {
return "open_market";
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.statistic.FishingStatistics;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
import org.incendo.cloud.parser.standard.EnumParser;
public class QueryStatisticsCommand extends BukkitCommandFeature<CommandSender> {
public QueryStatisticsCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.required("player", PlayerParser.playerParser())
.required("type", EnumParser.enumParser(FishingStatistics.Type.class))
.handler(context -> {
Player player = context.get("player");
FishingStatistics.Type type = context.get("type");
BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(player.getUniqueId()).ifPresentOrElse(userData -> {
if (type == FishingStatistics.Type.AMOUNT_OF_FISH_CAUGHT) {
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_QUERY_AMOUNT, Component.text(userData.statistics().amountMap().toString()));
} else if (type == FishingStatistics.Type.MAX_SIZE) {
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_QUERY_SIZE, Component.text(userData.statistics().sizeMap().toString()));
}
}, () -> handleFeedback(context, MessageConstants.COMMAND_STATISTICS_FAILURE_NOT_LOADED));
});
}
@Override
public String getFeatureID() {
return "statistics_query";
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public class ReloadCommand extends BukkitCommandFeature<CommandSender> {
public ReloadCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.handler(context -> {
long time1 = System.currentTimeMillis();
BukkitCustomFishingPlugin.getInstance().reload();
handleFeedback(context, MessageConstants.COMMAND_RELOAD_SUCCESS, Component.text(System.currentTimeMillis() - time1));
});
}
@Override
public String getFeatureID() {
return "reload";
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
public class ResetStatisticsCommand extends BukkitCommandFeature<CommandSender> {
public ResetStatisticsCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.required("player", PlayerParser.playerParser())
.handler(context -> {
Player player = context.get("player");
BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(player.getUniqueId()).ifPresentOrElse(userData -> {
userData.statistics().reset();
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_RESET_SUCCESS, Component.text(player.getName()));
}, () -> handleFeedback(context, MessageConstants.COMMAND_STATISTICS_FAILURE_NOT_LOADED));
});
}
@Override
public String getFeatureID() {
return "statistics_reset";
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public class SellFishCommand extends BukkitCommandFeature<CommandSender> {
public SellFishCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.handler(context -> {
if (!BukkitCustomFishingPlugin.getInstance().getMarketManager().openMarketGUI(context.sender())) {
handleFeedback(context, MessageConstants.COMMAND_DATA_FAILURE_NOT_LOADED);
}
});
}
@Override
public String getFeatureID() {
return "sellfish";
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.statistic.FishingStatistics;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.PlayerParser;
import org.incendo.cloud.parser.standard.DoubleParser;
import org.incendo.cloud.parser.standard.EnumParser;
import org.incendo.cloud.parser.standard.StringParser;
public class SetStatisticsCommand extends BukkitCommandFeature<CommandSender> {
public SetStatisticsCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.required("player", PlayerParser.playerParser())
.required("id", StringParser.stringParser())
.required("type", EnumParser.enumParser(FishingStatistics.Type.class))
.required("value", DoubleParser.doubleParser(0))
.handler(context -> {
Player player = context.get("player");
String id = context.get("id");
FishingStatistics.Type type = context.get("type");
double value = context.get("value");
BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(player.getUniqueId()).ifPresentOrElse(userData -> {
if (type == FishingStatistics.Type.AMOUNT_OF_FISH_CAUGHT) {
userData.statistics().setAmount(id, (int) value);
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_MODIFY_SUCCESS, Component.text(player.getName()));
} else if (type == FishingStatistics.Type.MAX_SIZE) {
userData.statistics().setMaxSize(id, (float) value);
handleFeedback(context, MessageConstants.COMMAND_STATISTICS_MODIFY_SUCCESS, Component.text(player.getName()));
}
}, () -> handleFeedback(context, MessageConstants.COMMAND_STATISTICS_FAILURE_NOT_LOADED));
});
}
@Override
public String getFeatureID() {
return "statistics_set";
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
public class StartCompetitionCommand extends BukkitCommandFeature<CommandSender> {
public StartCompetitionCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("id", StringParser.stringComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(BukkitCustomFishingPlugin.getInstance().getCompetitionManager().getCompetitionIDs().stream().map(Suggestion::suggestion).toList());
}
}))
.optional("group", StringParser.stringParser())
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
String id = context.get("id");
String group = context.getOrDefault("group", null);
if (BukkitCustomFishingPlugin.getInstance().getCompetitionManager().startCompetition(id, true, group)) {
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_START_SUCCESS);
} else {
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_FAILURE_NOT_EXIST, Component.text(id));
}
});
}
@Override
public String getFeatureID() {
return "start_competition";
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.FishingCompetition;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
public class StopCompetitionCommand extends BukkitCommandFeature<CommandSender> {
public StopCompetitionCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s").build())
.handler(context -> {
FishingCompetition competition = BukkitCustomFishingPlugin.getInstance().getCompetitionManager().getOnGoingCompetition();
if (competition == null) {
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_FAILURE_NO_COMPETITION);
} else {
competition.stop(true);
handleFeedback(context, MessageConstants.COMMAND_COMPETITION_STOP_SUCCESS);
}
});
}
@Override
public String getFeatureID() {
return "stop_competition";
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.command.BukkitCommandFeature;
import net.momirealms.customfishing.common.command.CustomFishingCommandManager;
import net.momirealms.customfishing.common.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.parser.standard.UUIDParser;
import java.util.UUID;
public class UnlockDataCommand extends BukkitCommandFeature<CommandSender> {
public UnlockDataCommand(CustomFishingCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.required("uuid", UUIDParser.uuidParser())
.handler(context -> {
UUID uuid = context.get("uuid");
BukkitCustomFishingPlugin.getInstance().getStorageManager().getDataSource().lockOrUnlockPlayerData(uuid, false);
handleFeedback(context, MessageConstants.COMMAND_DATA_UNLOCK_SUCCESS, Component.text(uuid.toString()));
});
}
@Override
public String getFeatureID() {
return "data_unlock";
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.kyori.adventure.bossbar.BossBar;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.action.ActionManager;
import net.momirealms.customfishing.api.mechanic.competition.CompetitionConfig;
import net.momirealms.customfishing.api.mechanic.competition.CompetitionGoal;
import net.momirealms.customfishing.api.mechanic.competition.CompetitionManager;
import net.momirealms.customfishing.api.mechanic.competition.FishingCompetition;
import net.momirealms.customfishing.api.mechanic.competition.info.ActionBarConfig;
import net.momirealms.customfishing.api.mechanic.competition.info.BossBarConfig;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.bukkit.storage.method.database.nosql.RedisManager;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.Bukkit;
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.TimeUnit;
public class BukkitCompetitionManager implements CompetitionManager {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<CompetitionSchedule, CompetitionConfig> timeConfigMap;
private final HashMap<String, CompetitionConfig> commandConfigMap;
private Competition currentCompetition;
private SchedulerTask timerCheckTask;
private int nextCompetitionSeconds;
public BukkitCompetitionManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.timeConfigMap = new HashMap<>();
this.commandConfigMap = new HashMap<>();
}
public void load() {
loadConfig();
this.timerCheckTask = plugin.getScheduler().asyncRepeating(
this::timerCheck,
1,
1,
TimeUnit.SECONDS
);
plugin.debug("Loaded " + commandConfigMap.size() + " competitions");
}
public void unload() {
if (this.timerCheckTask != null)
this.timerCheckTask.cancel();
if (currentCompetition != null && currentCompetition.isOnGoing())
this.currentCompetition.stop(true);
this.commandConfigMap.clear();
this.timeConfigMap.clear();
}
public void disable() {
if (this.timerCheckTask != null)
this.timerCheckTask.cancel();
if (currentCompetition != null && currentCompetition.isOnGoing())
this.currentCompetition.stop(false);
this.commandConfigMap.clear();
this.timeConfigMap.clear();
}
@SuppressWarnings("DuplicatedCode")
private void loadConfig() {
Deque<File> fileDeque = new ArrayDeque<>();
for (String type : List.of("competition")) {
File typeFolder = new File(plugin.getDataFolder() + File.separator + "contents" + File.separator + type);
if (!typeFolder.exists()) {
if (!typeFolder.mkdirs()) return;
plugin.getBoostrap().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() && subFile.getName().endsWith(".yml")) {
this.loadSingleFileCompetition(subFile);
}
}
}
}
}
private void loadSingleFileCompetition(File file) {
YamlDocument document = plugin.getConfigManager().loadData(file);
for (Map.Entry<String, Object> entry : document.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section section) {
CompetitionConfig.Builder builder = CompetitionConfig.builder()
.key(entry.getKey())
.goal(CompetitionGoal.index().value(section.getString("goal", "TOTAL_SCORE").toLowerCase(Locale.ENGLISH)))
.minPlayers(section.getInt("min-players", 0))
.duration(section.getInt("duration", 300))
.rewards(getPrizeActions(section.getSection("rewards")))
.joinRequirements(plugin.getRequirementManager().parseRequirements(section.getSection("participate-requirements"), false))
.joinActions(plugin.getActionManager().parseActions(section.getSection("participate-actions")))
.startActions(plugin.getActionManager().parseActions(section.getSection("start-actions")))
.endActions(plugin.getActionManager().parseActions(section.getSection("end-actions")))
.skipActions(plugin.getActionManager().parseActions(section.getSection("skip-actions")));;
if (section.getBoolean("bossbar.enable", false)) {
builder.bossBarConfig(
BossBarConfig.builder()
.enable(true)
.color(BossBar.Color.valueOf(section.getString("bossbar.color", "WHITE").toUpperCase(Locale.ENGLISH)))
.overlay(BossBar.Overlay.valueOf(section.getString("bossbar.overlay", "PROGRESS").toUpperCase(Locale.ENGLISH)))
.refreshRate(section.getInt("bossbar.refresh-rate", 20))
.switchInterval(section.getInt("bossbar.switch-interval", 200))
.showToAll(!section.getBoolean("bossbar.only-show-to-participants", true))
.text(section.getStringList("bossbar.text").toArray(new String[0]))
.build()
);
}
if (section.getBoolean("actionbar.enable", false)) {
builder.actionBarConfig(
ActionBarConfig.builder()
.enable(true)
.refreshRate(section.getInt("actionbar.refresh-rate", 5))
.switchInterval(section.getInt("actionbar.switch-interval", 200))
.showToAll(!section.getBoolean("actionbar.only-show-to-participants", true))
.text(section.getStringList("actionbar.text").toArray(new String[0]))
.build()
);
}
CompetitionConfig competitionConfig = builder.build();
List<Pair<Integer, Integer>> timePairs = section.getStringList("start-time")
.stream().map(it -> {
String[] split = it.split(":");
return Pair.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
}).toList();
List<Integer> weekdays = section.getIntList("start-weekday");
if (weekdays.isEmpty()) {
weekdays.addAll(List.of(1,2,3,4,5,6,7));
}
for (Integer weekday : weekdays) {
for (Pair<Integer, Integer> timePair : timePairs) {
CompetitionSchedule schedule = new CompetitionSchedule(weekday, timePair.left(), timePair.right(), 0);
timeConfigMap.put(schedule, competitionConfig);
}
}
commandConfigMap.put(entry.getKey(), competitionConfig);
}
}
}
/**
* Gets prize actions from a configuration section.
*
* @param section The configuration section containing prize actions.
* @return A HashMap where keys are action names and values are arrays of Action objects.
*/
public HashMap<String, Action<Player>[]> getPrizeActions(Section section) {
HashMap<String, Action<Player>[]> map = new HashMap<>();
if (section == null) return map;
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
map.put(entry.getKey(), plugin.getActionManager().parseActions(innerSection));
}
}
return map;
}
/**
* Checks the timer for the next competition and starts it if necessary.
*/
public void timerCheck() {
LocalDateTime now = LocalDateTime.now();
CompetitionSchedule competitionSchedule = new CompetitionSchedule(
now.getDayOfWeek().getValue(),
now.getHour(),
now.getMinute(),
now.getSecond()
);
int seconds = competitionSchedule.getTotalSeconds();
int nextCompetitionTime = 7 * 24 * 60 * 60;
for (CompetitionSchedule schedule : timeConfigMap.keySet()) {
nextCompetitionTime = Math.min(nextCompetitionTime, schedule.getTimeDelta(seconds));
}
this.nextCompetitionSeconds = nextCompetitionTime;
CompetitionConfig config = timeConfigMap.get(competitionSchedule);
if (config != null) {
startCompetition(config, false, null);
}
}
@Override
public boolean startCompetition(String competition, boolean force, String serverGroup) {
CompetitionConfig config = commandConfigMap.get(competition);
if (config == null) {
return false;
}
return startCompetition(config, force, serverGroup);
}
/**
* 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() {
if (currentCompetition == null) return null;
return currentCompetition.isOnGoing() ? currentCompetition : null;
}
@Override
public boolean startCompetition(CompetitionConfig config, boolean force, @Nullable String serverGroup) {
if (!force) {
int players = Bukkit.getOnlinePlayers().size();
if (players < config.minPlayersToStart()) {
ActionManager.trigger(Context.player(null), config.skipActions());
return false;
}
start(config);
return true;
} else if (serverGroup == null) {
start(config);
return true;
} else {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(serverGroup);
out.writeUTF("competition");
out.writeUTF("start");
out.writeUTF(config.key());
RedisManager.getInstance().publishRedisMessage(Arrays.toString(out.toByteArray()));
return true;
}
}
private void start(CompetitionConfig config) {
if (getOnGoingCompetition() != null) {
// END
currentCompetition.end(true);
plugin.getScheduler().asyncLater(() -> {
// start one second later
this.currentCompetition = new Competition(plugin, config);
this.currentCompetition.start(true);
}, 1, TimeUnit.SECONDS);
} else {
// start instantly
plugin.getScheduler().async().execute(() -> {
this.currentCompetition = new Competition(plugin, config);
this.currentCompetition.start(true);
});
}
}
/**
* Gets the number of seconds until the next competition.
*
* @return The number of seconds until the next competition.
*/
@Override
public int getNextCompetitionInSeconds() {
return nextCompetitionSeconds;
}
@Nullable
@Override
public CompetitionConfig getCompetition(String key) {
return commandConfigMap.get(key);
}
@Override
public Collection<String> getCompetitionIDs() {
return commandConfigMap.keySet();
}
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.event.CompetitionEvent;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.action.ActionManager;
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.RankingProvider;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.bukkit.competition.actionbar.ActionBarManager;
import net.momirealms.customfishing.bukkit.competition.bossbar.BossBarManager;
import net.momirealms.customfishing.bukkit.competition.ranking.LocalRankingProvider;
import net.momirealms.customfishing.bukkit.competition.ranking.RedisRankingProvider;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.locale.TranslationManager;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.Pair;
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.Optional;
import java.util.concurrent.TimeUnit;
public class Competition implements FishingCompetition {
private final BukkitCustomFishingPlugin plugin;
private final CompetitionConfig config;
private SchedulerTask competitionTimerTask;
private final CompetitionGoal goal;
private final Context<Player> publicContext;
private final RankingProvider rankingProvider;
private float progress;
private int remainingTime;
private long startTime;
private BossBarManager bossBarManager;
private ActionBarManager actionBarManager;
public Competition(BukkitCustomFishingPlugin plugin, CompetitionConfig config) {
this.config = config;
this.plugin = plugin;
this.goal = config.goal() == CompetitionGoal.RANDOM ? CompetitionGoal.getRandom() : config.goal();
if (ConfigManager.redisRanking()) this.rankingProvider = new RedisRankingProvider();
else this.rankingProvider = new LocalRankingProvider();
this.publicContext = Context.player(null, true);
this.publicContext.arg(ContextKeys.GOAL, goal);
}
@Override
public void start(boolean triggerEvent) {
this.progress = 1;
this.remainingTime = this.config.durationInSeconds();
this.startTime = Instant.now().getEpochSecond();
this.arrangeTimerTask();
if (this.config.bossBarConfig() != null && this.config.bossBarConfig().enabled()) {
this.bossBarManager = new BossBarManager(this.config.bossBarConfig(), this);
this.bossBarManager.load();
}
if (this.config.actionBarConfig() != null && this.config.actionBarConfig().enabled()) {
this.actionBarManager = new ActionBarManager(this.config.actionBarConfig(), this);
this.actionBarManager.load();
}
this.updatePublicPlaceholders();
ActionManager.trigger(this.publicContext, this.config.startActions());
this.rankingProvider.clear();
if (triggerEvent) {
this.plugin.getScheduler().async().execute(() -> {
CompetitionEvent competitionStartEvent = new CompetitionEvent(CompetitionEvent.State.START, this);
Bukkit.getPluginManager().callEvent(competitionStartEvent);
});
}
}
@Override
public void stop(boolean triggerEvent) {
if (this.competitionTimerTask != null)
this.competitionTimerTask.cancel();
if (this.bossBarManager != null)
this.bossBarManager.unload();
if (this.actionBarManager != null)
this.actionBarManager.unload();
this.rankingProvider.clear();
this.remainingTime = 0;
if (triggerEvent) {
plugin.getScheduler().async().execute(() -> {
CompetitionEvent competitionEvent = new CompetitionEvent(CompetitionEvent.State.STOP, this);
Bukkit.getPluginManager().callEvent(competitionEvent);
});
}
}
@Override
public void end(boolean triggerEvent) {
// mark it as ended
this.remainingTime = 0;
// cancel some sub tasks
if (competitionTimerTask != null)
this.competitionTimerTask.cancel();
if (this.bossBarManager != null)
this.bossBarManager.unload();
if (this.actionBarManager != null)
this.actionBarManager.unload();
// give prizes
HashMap<String, Action<Player>[]> rewardsMap = config.rewards();
if (rankingProvider.getSize() != 0 && rewardsMap != null) {
Iterator<Pair<String, Double>> iterator = rankingProvider.getIterator();
int i = 1;
while (iterator.hasNext()) {
Pair<String, Double> competitionPlayer = iterator.next();
this.publicContext.arg(ContextKeys.of(i + "_player", String.class), competitionPlayer.left());
this.publicContext.arg(ContextKeys.of(i + "_score", String.class), String.format("%.2f", competitionPlayer.right()));
if (i < rewardsMap.size()) {
Player player = Bukkit.getPlayer(competitionPlayer.left());
if (player != null) {
ActionManager.trigger(Context.player(player).combine(this.publicContext), rewardsMap.get(String.valueOf(i)));
}
} else {
Action<Player>[] actions = rewardsMap.get("participation");
if (actions != null) {
Player player = Bukkit.getPlayer(competitionPlayer.left()); {
if (player != null) {
ActionManager.trigger(Context.player(player).combine(this.publicContext), actions);
}
}
}
}
i++;
}
}
// end actions
ActionManager.trigger(publicContext, config.endActions());
// call event
if (triggerEvent) {
plugin.getScheduler().async().execute(() -> {
CompetitionEvent competitionEndEvent = new CompetitionEvent(CompetitionEvent.State.END, this);
Bukkit.getPluginManager().callEvent(competitionEndEvent);
});
}
// 1 seconds delay for other servers to read the redis data
plugin.getScheduler().asyncLater(this.rankingProvider::clear, 1, TimeUnit.SECONDS);
}
private void arrangeTimerTask() {
this.competitionTimerTask = this.plugin.getScheduler().asyncRepeating(() -> {
if (decreaseTime()) {
end(true);
return;
}
updatePublicPlaceholders();
}, 1, 1, TimeUnit.SECONDS);
}
private void updatePublicPlaceholders() {
for (int i = 1; i < ConfigManager.placeholderLimit() + 1; i++) {
Optional<String> player = Optional.ofNullable(this.rankingProvider.getPlayerAt(i));
if (player.isPresent()) {
this.publicContext.arg(ContextKeys.of(i + "_player", String.class), player.get());
this.publicContext.arg(ContextKeys.of(i + "_score", String.class), String.format("%.2f", this.rankingProvider.getScoreAt(i)));
} else {
this.publicContext.arg(ContextKeys.of(i + "_player", String.class), TranslationManager.miniMessageTranslation(MessageConstants.COMPETITION_NO_PLAYER.build().key()));
this.publicContext.arg(ContextKeys.of(i + "_score", String.class), TranslationManager.miniMessageTranslation(MessageConstants.COMPETITION_NO_SCORE.build().key()));
}
}
this.publicContext.arg(ContextKeys.HOUR, remainingTime < 3600 ? "" : (remainingTime / 3600) + TranslationManager.miniMessageTranslation(MessageConstants.FORMAT_HOUR.build().key()));
this.publicContext.arg(ContextKeys.MINUTE, remainingTime < 60 ? "" : (remainingTime % 3600) / 60 + TranslationManager.miniMessageTranslation(MessageConstants.FORMAT_MINUTE.build().key()));
this.publicContext.arg(ContextKeys.SECOND, remainingTime == 0 ? "" : remainingTime % 60 + TranslationManager.miniMessageTranslation(MessageConstants.FORMAT_SECOND.build().key()));
this.publicContext.arg(ContextKeys.SECONDS, remainingTime);
}
@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.durationInSeconds();
remainingTime = (int) (duration - (current - startTime));
progress = (float) remainingTime / duration;
return remainingTime <= 0;
}
@Override
public void refreshData(Player player, double score) {
// if player join for the first time, trigger join actions
if (!hasPlayerJoined(player)) {
ActionManager.trigger(Context.player(player).combine(publicContext), config.joinActions());
}
// show competition info
if (this.bossBarManager != null)
this.bossBarManager.showBossBarTo(player);
if (this.actionBarManager != null)
this.actionBarManager.showActionBarTo(player);
// refresh data
this.goal.refreshScore(rankingProvider, player, score);
}
@Override
public boolean hasPlayerJoined(OfflinePlayer player) {
return rankingProvider.getPlayerRank(player.getName()) != -1;
}
@Override
public float getProgress() {
return progress;
}
@Override
public long getRemainingTime() {
return remainingTime;
}
@Override
public long getStartTime() {
return startTime;
}
@NotNull
@Override
public CompetitionConfig getConfig() {
return config;
}
@NotNull
@Override
public CompetitionGoal getGoal() {
return goal;
}
@NotNull
@Override
public RankingProvider getRanking() {
return rankingProvider;
}
@Override
public Context<Player> getPublicContext() {
return publicContext;
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition;
public class CompetitionSchedule {
private final int weekday;
private final int hour;
private final int minute;
private final int second;
public CompetitionSchedule(int weekday, int hour, int minute, int second) {
this.weekday = weekday;
this.hour = hour;
this.minute = minute;
this.second = second;
}
/**
* Gets the weekday associated with this time schedule.
*
* @return The weekday value (e.g., 1 for Monday, 2 for Tuesday, etc.).
*/
public int getWeekday() {
return weekday;
}
/**
* Gets the hour of the day associated with this time schedule.
*
* @return The hour value (0-23).
*/
public int getHour() {
return hour;
}
/**
* Gets the minute of the hour associated with this time schedule.
*
* @return The minute value (0-59).
*/
public int getMinute() {
return minute;
}
/**
* Gets the second of the minute associated with this time schedule.
*
* @return The second value (0-59).
*/
public int getSecond() {
return second;
}
/**
* Calculates the total number of seconds represented by this time schedule.
*
* @return The total number of seconds.
*/
public int getTotalSeconds() {
return second +
minute * 60 +
hour * 60 * 60 +
weekday * 24 * 60 * 60;
}
/**
* Calculates the time difference (delta) in seconds between this time schedule and a given total seconds value.
*
* @param totalSeconds The total seconds value to compare against.
* @return The time difference in seconds.
*/
public int getTimeDelta(int totalSeconds) {
int thisSeconds = getTotalSeconds();
if (thisSeconds >= totalSeconds) {
return thisSeconds - totalSeconds;
} else {
return (7 * 24 * 60 * 60) - (totalSeconds - thisSeconds);
}
}
@Override
public int hashCode() {
final int prime = 7;
int result = 1;
result = prime * result + weekday;
result = prime * result + hour;
result = prime * result + minute;
result = prime * result + second;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (this == obj)
return true;
if (getClass() != obj.getClass())
return false;
CompetitionSchedule other = (CompetitionSchedule) obj;
if (weekday != other.weekday)
return false;
if (hour != other.hour)
return false;
if (minute != other.minute)
return false;
return true;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.actionbar;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.info.ActionBarConfig;
import net.momirealms.customfishing.bukkit.competition.Competition;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class ActionBarManager implements Listener {
private static final ConcurrentHashMap<UUID, ActionBarSender> senderMap = new ConcurrentHashMap<>();
private final ActionBarConfig actionBarConfig;
private final Competition competition;
public ActionBarManager(ActionBarConfig actionBarConfig, Competition competition) {
this.actionBarConfig = actionBarConfig;
this.competition = competition;
}
/**
* Loads the ActionBar manager, registering events and showing ActionBar messages to online players.
*/
public void load() {
Bukkit.getPluginManager().registerEvents(this, BukkitCustomFishingPlugin.getInstance().getBoostrap());
if (actionBarConfig.showToAll()) {
for (Player player : Bukkit.getOnlinePlayers()) {
ActionBarSender sender = new ActionBarSender(player, actionBarConfig, competition);
if (!sender.isVisible()) {
sender.show();
}
senderMap.put(player.getUniqueId(), sender);
}
}
}
/**
* Unloads the ActionBar manager, unregistering events and hiding ActionBar messages for all players.
*/
public void unload() {
HandlerList.unregisterAll(this);
for (ActionBarSender ActionBarSender : senderMap.values()) {
ActionBarSender.hide();
}
senderMap.clear();
}
/**
* Handles the PlayerQuitEvent to hide ActionBar messages for a player when they quit the game.
*
* @param event The PlayerQuitEvent.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
final Player player = event.getPlayer();
ActionBarSender sender = senderMap.remove(player.getUniqueId());
if (sender != null) {
if (sender.isVisible())
sender.hide();
}
}
/**
* Handles the PlayerJoinEvent to show ActionBar messages to players when they join the game.
*
* @param event The PlayerJoinEvent.
*/
@EventHandler
public void onJoin(PlayerJoinEvent event) {
final Player player = event.getPlayer();
BukkitCustomFishingPlugin.getInstance().getScheduler().asyncLater(() -> {
boolean hasJoined = competition.hasPlayerJoined(player);
if ((hasJoined || actionBarConfig.showToAll())
&& !senderMap.containsKey(player.getUniqueId())) {
ActionBarSender sender = new ActionBarSender(player, actionBarConfig, competition);
if (!sender.isVisible()) {
sender.show();
}
senderMap.put(player.getUniqueId(), sender);
}
}, 200, TimeUnit.MILLISECONDS);
}
/**
* Shows an ActionBar message to a specific player.
*
* @param player The player to show the ActionBar message to.
*/
public void showActionBarTo(Player player) {
ActionBarSender sender = senderMap.get(player.getUniqueId());
if (sender == null) {
sender = new ActionBarSender(player, actionBarConfig, competition);
senderMap.put(player.getUniqueId(), sender);
}
if (!sender.isVisible())
sender.show();
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.actionbar;
import net.kyori.adventure.audience.Audience;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.info.ActionBarConfig;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.misc.value.DynamicText;
import net.momirealms.customfishing.bukkit.competition.Competition;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.locale.TranslationManager;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.entity.Player;
import java.util.concurrent.TimeUnit;
public class ActionBarSender {
private final Player player;
private final Audience audience;
private int refreshTimer;
private int switchTimer;
private int counter;
private final DynamicText[] texts;
private SchedulerTask senderTask;
private final ActionBarConfig config;
private boolean isShown;
private final Competition competition;
private final Context<Player> privateContext;
public ActionBarSender(Player player, ActionBarConfig config, Competition competition) {
this.player = player;
this.audience = BukkitCustomFishingPlugin.getInstance().getSenderFactory().getAudience(player);
this.config = config;
this.privateContext = Context.player(player);
this.isShown = false;
this.competition = competition;
this.updatePrivatePlaceholders();
String[] str = config.texts();
texts = new DynamicText[str.length];
for (int i = 0; i < str.length; i++) {
texts[i] = new DynamicText(player, str[i]);
texts[i].update(privateContext.placeholderMap());
}
}
@SuppressWarnings("DuplicatedCode")
private void updatePrivatePlaceholders() {
this.privateContext.arg(ContextKeys.SCORE_FORMATTED, String.format("%.2f", competition.getRanking().getPlayerScore(player.getName())));
int rank = competition.getRanking().getPlayerRank(player.getName());
this.privateContext.arg(ContextKeys.RANK, rank != -1 ? String.valueOf(rank) : TranslationManager.miniMessageTranslation(MessageConstants.COMPETITION_NO_RANK.build().key()));
this.privateContext.combine(competition.getPublicContext());
}
public void show() {
this.isShown = true;
senderTask = BukkitCustomFishingPlugin.getInstance().getScheduler().asyncRepeating(() -> {
switchTimer++;
if (switchTimer > config.switchInterval()) {
switchTimer = 0;
counter++;
}
if (refreshTimer < config.refreshRate()){
refreshTimer++;
} else {
refreshTimer = 0;
DynamicText text = texts[counter % (texts.length)];
updatePrivatePlaceholders();
text.update(this.privateContext.placeholderMap());
audience.sendActionBar(AdventureHelper.miniMessage(text.getLatestValue()));
}
}, 50, 50, TimeUnit.MILLISECONDS);
}
public void hide() {
if (senderTask != null)
senderTask.cancel();
this.isShown = false;
}
public boolean isVisible() {
return this.isShown;
}
public ActionBarConfig getConfig() {
return config;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.bossbar;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.info.BossBarConfig;
import net.momirealms.customfishing.bukkit.competition.Competition;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class BossBarManager implements Listener {
private static final ConcurrentHashMap<UUID, BossBarSender> senderMap = new ConcurrentHashMap<>();
private final BossBarConfig bossBarConfig;
private final Competition competition;
public BossBarManager(BossBarConfig bossBarConfig, Competition competition) {
this.bossBarConfig = bossBarConfig;
this.competition = competition;
}
/**
* Loads the boss bar manager, registering events and showing boss bars to online players.
*/
public void load() {
Bukkit.getPluginManager().registerEvents(this, BukkitCustomFishingPlugin.getInstance().getBoostrap());
if (bossBarConfig.showToAll()) {
for (Player player : Bukkit.getOnlinePlayers()) {
BossBarSender sender = new BossBarSender(player, bossBarConfig, competition);
if (!sender.isVisible()) {
sender.show();
}
senderMap.put(player.getUniqueId(), sender);
}
}
}
/**
* Unloads the boss bar manager, unregistering events and hiding boss bars for all players.
*/
public void unload() {
HandlerList.unregisterAll(this);
for (BossBarSender bossBarSender : senderMap.values()) {
bossBarSender.hide();
}
senderMap.clear();
}
/**
* Handles the PlayerQuitEvent to hide the boss bar for a player when they quit the game.
*
* @param event The PlayerQuitEvent.
*/
@EventHandler
public void onQuit(PlayerQuitEvent event) {
final Player player = event.getPlayer();
BossBarSender sender = senderMap.remove(player.getUniqueId());
if (sender != null) {
if (sender.isVisible())
sender.hide();
}
}
/**
* Handles the PlayerJoinEvent to show boss bars to players when they join the game.
*
* @param event The PlayerJoinEvent.
*/
@EventHandler
public void onJoin(PlayerJoinEvent event) {
final Player player = event.getPlayer();
BukkitCustomFishingPlugin.getInstance().getScheduler().asyncLater(() -> {
boolean hasJoined = competition.hasPlayerJoined(player);
if ((hasJoined || bossBarConfig.showToAll())
&& !senderMap.containsKey(player.getUniqueId())) {
BossBarSender sender = new BossBarSender(player, bossBarConfig, competition);
if (!sender.isVisible()) {
sender.show();
}
senderMap.put(player.getUniqueId(), sender);
}
}, 200, TimeUnit.MILLISECONDS);
}
/**
* Shows a boss bar to a specific player.
*
* @param player The player to show the boss bar to.
*/
public void showBossBarTo(Player player) {
BossBarSender sender = senderMap.get(player.getUniqueId());
if (sender == null) {
sender = new BossBarSender(player, bossBarConfig, competition);
senderMap.put(player.getUniqueId(), sender);
}
if (!sender.isVisible())
sender.show();
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.bossbar;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.bossbar.BossBar;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.competition.info.BossBarConfig;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.misc.value.DynamicText;
import net.momirealms.customfishing.bukkit.competition.Competition;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.locale.MessageConstants;
import net.momirealms.customfishing.common.locale.TranslationManager;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class BossBarSender {
private final Player player;
private final Audience audience;
private int refreshTimer;
private int switchTimer;
private int counter;
private final DynamicText[] texts;
private SchedulerTask senderTask;
private final BossBar bossBar;
private final BossBarConfig config;
private boolean isShown;
private final Competition competition;
private final Context<Player> privateContext;
public BossBarSender(Player player, BossBarConfig config, Competition competition) {
this.player = player;
this.audience = BukkitCustomFishingPlugin.getInstance().getSenderFactory().getAudience(player);
this.config = config;
this.isShown = false;
this.competition = competition;
this.privateContext = Context.player(player);
this.updatePrivatePlaceholders();
String[] str = config.texts();
texts = new DynamicText[str.length];
for (int i = 0; i < str.length; i++) {
texts[i] = new DynamicText(player, str[i]);
texts[i].update(privateContext.placeholderMap());
}
bossBar = BossBar.bossBar(
AdventureHelper.miniMessage(texts[0].getLatestValue()),
competition.getProgress(),
config.color(),
config.overlay(),
Set.of()
);
}
@SuppressWarnings("DuplicatedCode")
private void updatePrivatePlaceholders() {
this.privateContext.arg(ContextKeys.SCORE_FORMATTED, String.format("%.2f", competition.getRanking().getPlayerScore(player.getName())));
int rank = competition.getRanking().getPlayerRank(player.getName());
this.privateContext.arg(ContextKeys.RANK, rank != -1 ? String.valueOf(rank) : TranslationManager.miniMessageTranslation(MessageConstants.COMPETITION_NO_RANK.build().key()));
this.privateContext.combine(competition.getPublicContext());
}
public void show() {
this.isShown = true;
this.bossBar.addViewer(audience);
this.senderTask = BukkitCustomFishingPlugin.getInstance().getScheduler().asyncRepeating(() -> {
switchTimer++;
if (switchTimer > config.switchInterval()) {
switchTimer = 0;
counter++;
}
if (refreshTimer < config.refreshRate()){
refreshTimer++;
} else {
refreshTimer = 0;
DynamicText text = texts[counter % (texts.length)];
updatePrivatePlaceholders();
if (text.update(privateContext.placeholderMap())) {
bossBar.name(AdventureHelper.miniMessage(text.getLatestValue()));
}
bossBar.progress(competition.getProgress());
}
}, 50, 50, TimeUnit.MILLISECONDS);
}
public boolean isVisible() {
return this.isShown;
}
public BossBarConfig getConfig() {
return config;
}
public void hide() {
this.bossBar.removeViewer(audience);
if (senderTask != null) senderTask.cancel();
this.isShown = false;
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.ranking;
import net.momirealms.customfishing.api.mechanic.competition.CompetitionPlayer;
import net.momirealms.customfishing.api.mechanic.competition.RankingProvider;
import net.momirealms.customfishing.common.util.Pair;
import java.util.*;
/**
* Implementation of the Ranking interface that manages the ranking of competition players locally.
*/
public class LocalRankingProvider implements RankingProvider {
private final Set<CompetitionPlayer> competitionPlayers;
public LocalRankingProvider() {
competitionPlayers = Collections.synchronizedSet(new TreeSet<>());
}
/**
* Adds a competition player to the ranking.
*
* @param competitionPlayer The CompetitionPlayer to add.
*/
@Override
public void addPlayer(CompetitionPlayer competitionPlayer) {
competitionPlayers.add(competitionPlayer);
}
/**
* Removes a competition player from the ranking.
*
* @param player player's name
*/
@Override
public void removePlayer(String player) {
competitionPlayers.removeIf(e -> e.getPlayer().equals(player));
}
/**
* Removes a competition player from the ranking.
*
* @param competitionPlayer The CompetitionPlayer to remove.
*/
public void removePlayer(CompetitionPlayer competitionPlayer) {
competitionPlayers.removeIf(e -> e.equals(competitionPlayer));
}
/**
* Clears the list of competition players.
*/
@Override
public void clear() {
competitionPlayers.clear();
}
/**
* Retrieves a competition player by their name.
*
* @param player The name of the player to retrieve.
* @return The CompetitionPlayer object if found, or null if not found.
*/
@Override
public CompetitionPlayer getCompetitionPlayer(String player) {
for (CompetitionPlayer competitionPlayer : competitionPlayers) {
if (competitionPlayer.getPlayer().equals(player)) {
return competitionPlayer;
}
}
return null;
}
@Override
public CompetitionPlayer getCompetitionPlayer(int rank) {
int i = 1;
int size = getSize();
if (rank > size) return null;
for (CompetitionPlayer player : competitionPlayers) {
if (rank == i) {
return player;
}
i++;
}
return null;
}
/**
* Returns an iterator for iterating over items of player names and scores.
*
* @return An iterator for items of player names and scores.
*/
@Override
public Iterator<Pair<String, Double>> getIterator() {
List<Pair<String, Double>> players = new ArrayList<>();
for (CompetitionPlayer competitionPlayer: competitionPlayers){
players.add(Pair.of(competitionPlayer.getPlayer(), competitionPlayer.getScore()));
}
return players.iterator();
}
/**
* Returns the number of competition players.
*
* @return The number of competition players.
*/
@Override
public int getSize() {
return competitionPlayers.size();
}
/**
* Returns the rank of a player based on their name.
*
* @param player The name of the player to get the rank for.
* @return The rank of the player, or -1 if the player is not found.
*/
@Override
public int getPlayerRank(String player) {
int index = 1;
for (CompetitionPlayer competitionPlayer : competitionPlayers) {
if (competitionPlayer.getPlayer().equals(player)) {
return index;
}else {
index++;
}
}
return -1;
}
/**
* Returns the score of a player based on their name.
*
* @param player The name of the player to get the score for.
* @return The score of the player, or 0 if the player is not found.
*/
@Override
public double getPlayerScore(String player) {
for (CompetitionPlayer competitionPlayer : competitionPlayers) {
if (competitionPlayer.getPlayer().equals(player)) {
return competitionPlayer.getScore();
}
}
return 0;
}
/**
* Returns the name of a player at a given index.
*
* @param i The index of the player to retrieve.
* @return The name of the player at the specified index, or null if not found.
*/
@Override
public String getPlayerAt(int i) {
int index = 1;
for (CompetitionPlayer competitionPlayer : competitionPlayers) {
if (index == i) {
return competitionPlayer.getPlayer();
}
index++;
}
return null;
}
/**
* Returns the score of a player at a given index.
*
* @param i The index of the player to retrieve.
* @return The score of the player at the specified index, or 0 if not found.
*/
@Override
public double getScoreAt(int i) {
int index = 1;
for (CompetitionPlayer competitionPlayer : competitionPlayers) {
if (index == i) {
return competitionPlayer.getScore();
}
index++;
}
return 0f;
}
/**
* Refreshes the data for a player by adding a score to their existing score or creating a new player.
*
* @param player The name of the player to update or create.
* @param score The score to add to the player's existing score or set as their initial score.
*/
@Override
public void refreshData(String player, double score) {
CompetitionPlayer competitionPlayer = getCompetitionPlayer(player);
if (competitionPlayer != null) {
removePlayer(competitionPlayer);
competitionPlayer.addScore(score);
addPlayer(competitionPlayer);
} else {
competitionPlayer = new CompetitionPlayer(player, score);
addPlayer(competitionPlayer);
}
}
/**
* Sets the data for a player, updating their score or creating a new player.
*
* @param player The name of the player to update or create.
* @param score The score to set for the player.
*/
@Override
public void setData(String player, double score) {
CompetitionPlayer competitionPlayer = getCompetitionPlayer(player);
if (competitionPlayer != null) {
removePlayer(competitionPlayer);
competitionPlayer.setScore(score);
addPlayer(competitionPlayer);
} else {
competitionPlayer = new CompetitionPlayer(player, score);
addPlayer(competitionPlayer);
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.competition.ranking;
import net.momirealms.customfishing.api.mechanic.competition.CompetitionPlayer;
import net.momirealms.customfishing.api.mechanic.competition.RankingProvider;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.bukkit.storage.method.database.nosql.RedisManager;
import net.momirealms.customfishing.common.util.Pair;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.resps.Tuple;
import java.util.Iterator;
import java.util.List;
public class RedisRankingProvider implements RankingProvider {
/**
* Clears the ranking data by removing all players and scores.
*/
@Override
public void clear() {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
jedis.del("cf_competition_" + ConfigManager.serverGroup());
}
}
/**
* Retrieves a competition player by their name from the Redis ranking.
*
* @param player The name of the player to retrieve.
* @return The CompetitionPlayer object if found, or null if not found.
*/
@Override
public CompetitionPlayer getCompetitionPlayer(String player) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
Double score = jedis.zscore("cf_competition_" + ConfigManager.serverGroup(), player);
if (score == null || score == 0) return null;
return new CompetitionPlayer(player, Float.parseFloat(score.toString()));
}
}
@Override
public CompetitionPlayer getCompetitionPlayer(int rank) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
List<Tuple> player = jedis.zrevrangeWithScores("cf_competition_" + ConfigManager.serverGroup(), rank - 1, rank -1);
if (player == null || player.isEmpty()) return null;
return new CompetitionPlayer(player.get(0).getElement(), player.get(0).getScore());
}
}
@Override
public void addPlayer(CompetitionPlayer competitionPlayer) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
jedis.zincrby("cf_competition_" + ConfigManager.serverGroup(), competitionPlayer.getScore(), competitionPlayer.getPlayer());
}
}
@Override
public void removePlayer(String player) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
jedis.zrem("cf_competition_" + ConfigManager.serverGroup(), player);
}
}
/**
* Returns an iterator for iterating over items of player names and scores in descending order.
*
* @return An iterator for items of player names and scores.
*/
@Override
public Iterator<Pair<String, Double>> getIterator() {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
List<Tuple> players = jedis.zrevrangeWithScores("cf_competition_" + ConfigManager.serverGroup(), 0, -1);
return players.stream().map(it -> Pair.of(it.getElement(), it.getScore())).toList().iterator();
}
}
/**
* Returns the number of players in the Redis ranking.
*
* @return The number of players in the ranking.
*/
@Override
public int getSize() {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
long size = jedis.zcard("cf_competition_" + ConfigManager.serverGroup());
return (int) size;
}
}
/**
* Returns the rank of a player based on their name in descending order (1-based).
*
* @param player The name of the player to get the rank for.
* @return The rank of the player, or -1 if the player is not found.
*/
@Override
public int getPlayerRank(String player) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
Long rank = jedis.zrevrank("cf_competition_" + ConfigManager.serverGroup(), player);
if (rank == null)
return -1;
return (int) (rank + 1);
}
}
/**
* Returns the score of a player based on their name from the Redis ranking.
*
* @param player The name of the player to get the score for.
* @return The score of the player, or 0 if the player is not found.
*/
@Override
public double getPlayerScore(String player) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
Double rank = jedis.zscore("cf_competition_" + ConfigManager.serverGroup(), player);
if (rank == null)
return 0;
return rank.floatValue();
}
}
/**
* Refreshes the data for a player in the Redis ranking by adding a score to their existing score or creating a new player.
*
* @param player The name of the player to update or create.
* @param score The score to add to the player's existing score or set as their initial score.
*/
@Override
public void refreshData(String player, double score) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
jedis.zincrby("cf_competition_" + ConfigManager.serverGroup(), score, player);
}
}
/**
* Sets the data for a player in the Redis ranking, updating their score or creating a new player.
*
* @param player The name of the player to update or create.
* @param score The score to set for the player.
*/
@Override
public void setData(String player, double score) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
jedis.zadd("cf_competition_" + ConfigManager.serverGroup(), score, player);
}
}
/**
* Returns the name of the player at a given rank in descending order.
*
* @param rank The rank of the player to retrieve (1-based).
* @return The name of the player at the specified rank, or null if not found.
*/
@Override
public String getPlayerAt(int rank) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
List<String> player = jedis.zrevrange("cf_competition_" + ConfigManager.serverGroup(), rank - 1, rank -1);
if (player == null || player.isEmpty()) return null;
return player.get(0);
}
}
/**
* Returns the score of the player at a given rank in descending order.
*
* @param rank The rank of the player to retrieve (1-based).
* @return The score of the player at the specified rank, or 0 if not found.
*/
@Override
public double getScoreAt(int rank) {
try (Jedis jedis = RedisManager.getInstance().getJedis()) {
List<Tuple> players = jedis.zrevrangeWithScores("cf_competition_" + ConfigManager.serverGroup(), rank - 1, rank -1);
if (players == null || players.isEmpty()) return 0;
return players.get(0).getScore();
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.effect;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.effect.EffectManager;
import net.momirealms.customfishing.api.mechanic.effect.EffectModifier;
import java.util.HashMap;
import java.util.Optional;
public class BukkitEffectManager implements EffectManager {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, EffectModifier> effectModifiers = new HashMap<>();
public BukkitEffectManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void unload() {
this.effectModifiers.clear();
}
@Override
public void load() {
plugin.debug("Loaded " + effectModifiers.size() + " effects");
}
@Override
public boolean registerEffectModifier(EffectModifier effect, MechanicType type) {
if (effectModifiers.containsKey(effect.id())) return false;
this.effectModifiers.put(type.getType() + ":" + effect.id(), effect);
return true;
}
@Override
public Optional<EffectModifier> getEffectModifier(String id, MechanicType type) {
return Optional.ofNullable(this.effectModifiers.get(type.getType() + ":" + id));
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.entity;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.integration.EntityProvider;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.entity.EntityConfig;
import net.momirealms.customfishing.api.mechanic.entity.EntityManager;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
public class BukkitEntityManager implements EntityManager {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, EntityProvider> entityProviders = new HashMap<>();
private final HashMap<String, EntityConfig> entities = new HashMap<>();
public BukkitEntityManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.registerEntityProvider(new EntityProvider() {
@Override
public String identifier() {
return "vanilla";
}
@NotNull
@Override
public Entity spawn(@NotNull Location location, @NotNull String id, @NotNull Map<String, Object> propertyMap) {
return location.getWorld().spawnEntity(location, EntityType.valueOf(id.toUpperCase(Locale.ENGLISH)));
}
});
}
@Override
public void load() {
for (EntityProvider provider : entityProviders.values()) {
plugin.debug("Registered EntityProvider: " + provider.identifier());
}
plugin.debug("Loaded " + entities.size() + " entities");
}
@Override
public void unload() {
this.entities.clear();
}
@Override
public void disable() {
unload();
this.entityProviders.clear();
}
@Override
public Optional<EntityConfig> getEntity(String id) {
return Optional.ofNullable(this.entities.get(id));
}
@Override
public boolean registerEntity(EntityConfig entity) {
if (entities.containsKey(entity.id())) return false;
this.entities.put(entity.id(), entity);
return true;
}
public boolean registerEntityProvider(EntityProvider entityProvider) {
if (entityProviders.containsKey(entityProvider.identifier())) return false;
else entityProviders.put(entityProvider.identifier(), entityProvider);
return true;
}
public boolean unregisterEntityProvider(String id) {
return entityProviders.remove(id) != null;
}
@NotNull
@Override
public Entity summonEntityLoot(Context<Player> context) {
String id = context.arg(ContextKeys.ID);
EntityConfig config = requireNonNull(entities.get(id), "Entity " + id + " not found");
Location hookLocation = requireNonNull(context.arg(ContextKeys.OTHER_LOCATION));
Location playerLocation = requireNonNull(context.getHolder().getLocation());
String entityID = config.entityID();
Entity entity;
if (entityID.contains(":")) {
String[] split = entityID.split(":", 2);
EntityProvider provider = requireNonNull(entityProviders.get(split[0]), "EntityProvider " + split[0] + " doesn't exist");
entity = requireNonNull(provider.spawn(hookLocation, split[1], config.propertyMap()), "Entity " + entityID + " doesn't exist");
} else {
entity = entityProviders.get("vanilla").spawn(hookLocation, entityID, config.propertyMap());
}
double d0 = playerLocation.getX() - hookLocation.getX();
double d1 = playerLocation.getY() - hookLocation.getY();
double d2 = playerLocation.getZ() - hookLocation.getZ();
double d3 = config.horizontalVector().evaluate(context);
double d4 = config.verticalVector().evaluate(context);
Vector vector = new Vector(d0 * 0.1D * d3, d1 * 0.1D + Math.sqrt(Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2)) * 0.08D * d4, d2 * 0.1D * d3);
entity.setVelocity(vector);
return entity;
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.event;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.action.ActionTrigger;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.event.EventCarrier;
import net.momirealms.customfishing.api.mechanic.event.EventManager;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Optional;
public class BukkitEventManager implements EventManager, Listener {
private final HashMap<String, EventCarrier> carriers = new HashMap<>();
private final BukkitCustomFishingPlugin plugin;
public BukkitEventManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void unload() {
this.carriers.clear();
HandlerList.unregisterAll(this);
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, this.plugin.getBoostrap());
}
@Override
public Optional<EventCarrier> getEventCarrier(String id, MechanicType type) {
return Optional.ofNullable(this.carriers.get(type.getType() + ":" + id));
}
@Override
public boolean registerEventCarrier(EventCarrier carrier) {
if (this.carriers.containsKey(carrier.id())) return false;
this.carriers.put(carrier.type().getType() + ":" + carrier.id(), carrier);
return true;
}
@EventHandler
public void onInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND)
return;
if (event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_AIR && event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK)
return;
ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR || itemStack.getAmount() == 0)
return;
String id = this.plugin.getItemManager().getItemID(itemStack);
Context<Player> context = Context.player(event.getPlayer());
Block clicked = event.getClickedBlock();
context.arg(ContextKeys.OTHER_LOCATION, clicked == null ? event.getPlayer().getLocation() : clicked.getLocation());
trigger(context, id, MechanicType.UTIL, ActionTrigger.INTERACT);
}
@EventHandler (ignoreCancelled = true)
public void onConsumeItem(PlayerItemConsumeEvent event) {
Context<Player> context = Context.player(event.getPlayer());
trigger(context, plugin.getItemManager().getItemID(event.getItem()), MechanicType.LOOT, ActionTrigger.CONSUME);
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.fishing;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.event.FishingHookStateEvent;
import net.momirealms.customfishing.api.event.RodCastEvent;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.fishing.CustomFishingHook;
import net.momirealms.customfishing.api.mechanic.fishing.FishingGears;
import net.momirealms.customfishing.api.mechanic.fishing.FishingManager;
import net.momirealms.customfishing.api.mechanic.fishing.hook.VanillaMechanic;
import net.momirealms.customfishing.api.mechanic.game.AbstractGamingPlayer;
import net.momirealms.customfishing.api.mechanic.game.GamingPlayer;
import net.momirealms.customfishing.api.mechanic.requirement.RequirementManager;
import net.momirealms.customfishing.api.util.EventUtils;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Entity;
import org.bukkit.entity.FishHook;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.*;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class BukkitFishingManager implements FishingManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final ConcurrentHashMap<UUID, CustomFishingHook> castHooks = new ConcurrentHashMap<>();
public BukkitFishingManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
}
@Override
public Optional<CustomFishingHook> getFishHook(Player player) {
return getFishHook(player.getUniqueId());
}
@Override
public Optional<CustomFishingHook> getFishHook(UUID player) {
return Optional.ofNullable(castHooks.get(player));
}
@Override
public Optional<Player> getOwner(FishHook hook) {
if (hook.getOwnerUniqueId() != null) {
return Optional.ofNullable(Bukkit.getPlayer(hook.getOwnerUniqueId()));
}
return Optional.empty();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onFishMONITOR(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.MONITOR) return;
this.selectState(event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onFishHIGHEST(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.HIGHEST) return;
this.selectState(event);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onFishHIGH(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.HIGH) return;
this.selectState(event);
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onFishNORMAL(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.NORMAL) return;
this.selectState(event);
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onFishLOW(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.LOW) return;
this.selectState(event);
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onFishLOWEST(PlayerFishEvent event) {
if (ConfigManager.eventPriority() != EventPriority.LOWEST) return;
this.selectState(event);
}
@EventHandler(ignoreCancelled = true)
public void onItemHeldChange(PlayerItemHeldEvent event) {
if (getFishHook(event.getPlayer()).isPresent()) {
this.destroy(event.getPlayer().getUniqueId());
}
}
@EventHandler(ignoreCancelled = true)
public void onSwapItem(PlayerSwapHandItemsEvent event) {
getFishHook(event.getPlayer()).ifPresent(hook -> {
Optional<GamingPlayer> optionalGamingPlayer = hook.getGamingPlayer();
if (optionalGamingPlayer.isPresent()) {
optionalGamingPlayer.get().handleSwapHand();
event.setCancelled(true);
} else {
this.destroy(event.getPlayer().getUniqueId());
}
});
}
@EventHandler(ignoreCancelled = true)
public void onJump(PlayerMoveEvent event) {
final Player player = event.getPlayer();
if (event.getFrom().getY() < event.getTo().getY() && player.isOnGround()) {
getFishHook(player).flatMap(CustomFishingHook::getGamingPlayer).ifPresent(gamingPlayer -> {
if (gamingPlayer.handleJump()) {
event.setCancelled(true);
}
});
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
this.destroy(event.getPlayer().getUniqueId());
}
@EventHandler (ignoreCancelled = false)
public void onLeftClick(PlayerInteractEvent event) {
if (event.getAction() != Action.LEFT_CLICK_AIR)
return;
if (event.getMaterial() != Material.FISHING_ROD)
return;
if (event.getHand() != EquipmentSlot.HAND)
return;
getFishHook(event.getPlayer()).ifPresent(hook -> {
Optional<GamingPlayer> optionalGamingPlayer = hook.getGamingPlayer();
if (optionalGamingPlayer.isPresent()) {
if (((AbstractGamingPlayer) optionalGamingPlayer.get()).internalLeftClick()) {
event.setCancelled(true);
}
}
});
}
@EventHandler (ignoreCancelled = true)
public void onSneak(PlayerToggleSneakEvent event) {
if (!event.isSneaking()) return;
getFishHook(event.getPlayer()).ifPresent(hook -> {
Optional<GamingPlayer> optionalGamingPlayer = hook.getGamingPlayer();
if (optionalGamingPlayer.isPresent()) {
if (optionalGamingPlayer.get().handleSneak()) {
event.setCancelled(true);
}
}
});
}
@EventHandler
public void onChat(AsyncPlayerChatEvent event) {
if (event.isCancelled()) return;
getFishHook(event.getPlayer()).ifPresent(hook -> {
Optional<GamingPlayer> optionalGamingPlayer = hook.getGamingPlayer();
if (optionalGamingPlayer.isPresent()) {
if (optionalGamingPlayer.get().handleChat(event.getMessage())) {
event.setCancelled(true);
}
}
});
}
private void selectState(PlayerFishEvent event) {
switch (event.getState()) {
case FISHING -> onCastRod(event);
case REEL_IN, CAUGHT_FISH -> onReelIn(event);
case CAUGHT_ENTITY -> onCaughtEntity(event);
//case CAUGHT_FISH -> onCaughtFish(event);
case BITE -> onBite(event);
case IN_GROUND -> onInGround(event);
case FAILED_ATTEMPT -> onFailedAttempt(event);
// case LURED 1.20.5+
}
}
// for vanilla mechanics
private void onFailedAttempt(PlayerFishEvent event) {
Player player = event.getPlayer();
getFishHook(player).ifPresent(hook -> {
if (hook.getCurrentHookMechanic() instanceof VanillaMechanic vanillaMechanic) {
vanillaMechanic.onFailedAttempt();
}
});
}
// for vanilla mechanics
private void onBite(PlayerFishEvent event) {
Player player = event.getPlayer();
getFishHook(player).ifPresent(hook -> {
if (hook.getCurrentHookMechanic() instanceof VanillaMechanic vanillaMechanic) {
vanillaMechanic.onBite();
}
});
}
private void onCaughtEntity(PlayerFishEvent event) {
final Player player = event.getPlayer();
Optional<CustomFishingHook> hook = getFishHook(player);
if (hook.isPresent()) {
Entity entity = event.getCaught();
if (entity != null && entity.getPersistentDataContainer().get(
Objects.requireNonNull(NamespacedKey.fromString("temp-entity", plugin.getBoostrap())),
PersistentDataType.STRING
) != null) {
event.setCancelled(true);
hook.get().onReelIn();
return;
}
}
if (player.getGameMode() != GameMode.CREATIVE) {
ItemStack itemStack = player.getInventory().getItemInMainHand();
if (itemStack.getType() != Material.FISHING_ROD) itemStack = player.getInventory().getItemInOffHand();
if (plugin.getItemManager().hasCustomDurability(itemStack)) {
event.getHook().pullHookedEntity();
event.getHook().remove();
event.setCancelled(true);
plugin.getItemManager().decreaseDurability(player, itemStack, event.getCaught() instanceof Item ? 3 : 5, true);
}
}
}
private void onReelIn(PlayerFishEvent event) {
Player player = event.getPlayer();
getFishHook(player).ifPresent(hook -> {
event.setCancelled(true);
Optional<GamingPlayer> gamingPlayer = hook.getGamingPlayer();
if (gamingPlayer.isPresent()) {
((AbstractGamingPlayer) gamingPlayer.get()).internalRightClick();
return;
}
hook.onReelIn();
});
}
// private void onCaughtFish(PlayerFishEvent event) {
// Player player = event.getPlayer();
// getFishHook(player).ifPresent(hook -> {
// Optional<GamingPlayer> gamingPlayer = hook.getGamingPlayer();
// if (gamingPlayer.isPresent()) {
// if (gamingPlayer.get().handleRightClick()) {
// event.setCancelled(true);
// }
// return;
// }
// event.setCancelled(true);
// hook.onReelIn();
// });
// }
private void onCastRod(PlayerFishEvent event) {
FishHook hook = event.getHook();
Player player = event.getPlayer();
Context<Player> context = Context.player(player);
FishingGears gears = new FishingGears(context);
if (!RequirementManager.isSatisfied(context, ConfigManager.mechanicRequirements())) {
this.destroy(player.getUniqueId());
return;
}
if (!gears.canFish()) {
event.setCancelled(true);
return;
}
if (EventUtils.fireAndCheckCancel(new RodCastEvent(event, gears))) {
return;
}
plugin.debug(context);
CustomFishingHook customHook = new CustomFishingHook(plugin, hook, gears, context);
this.castHooks.put(player.getUniqueId(), customHook);
}
private void onInGround(PlayerFishEvent event) {
final Player player = event.getPlayer();
if (player.getGameMode() != GameMode.CREATIVE) {
ItemStack itemStack = player.getInventory().getItemInMainHand();
if (itemStack.getType() != Material.FISHING_ROD) itemStack = player.getInventory().getItemInOffHand();
if (itemStack.getType() == Material.FISHING_ROD) {
if (plugin.getItemManager().hasCustomDurability(itemStack)) {
event.setCancelled(true);
event.getHook().remove();
plugin.getItemManager().decreaseDurability(player, itemStack, 2, true);
}
}
}
}
@EventHandler
public void onHookStateChange(FishingHookStateEvent event) {
Player player = event.getPlayer();
getFishHook(player).ifPresent(hook -> {
switch (event.getState()) {
case BITE -> hook.onBite();
case LAND -> hook.onLand();
case ESCAPE -> hook.onEscape();
case LURE -> hook.onLure();
}
});
}
@Override
public void destroy(UUID uuid) {
this.getFishHook(uuid).ifPresent(hook -> {
hook.destroy();
this.castHooks.remove(uuid);
});
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.hook;
import com.saicone.rtag.item.ItemTagStream;
import net.jpountz.lz4.LZ4Compressor;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ScoreComponent;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.effect.EffectModifier;
import net.momirealms.customfishing.api.mechanic.hook.HookConfig;
import net.momirealms.customfishing.api.mechanic.hook.HookManager;
import net.momirealms.customfishing.api.mechanic.requirement.RequirementManager;
import net.momirealms.customfishing.bukkit.item.damage.CustomDurabilityItem;
import net.momirealms.customfishing.bukkit.item.damage.DurabilityItem;
import net.momirealms.customfishing.bukkit.item.damage.VanillaDurabilityItem;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.item.Item;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
public class BukkitHookManager implements HookManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, HookConfig> hooks = new HashMap<>();
private LZ4Factory factory;
public BukkitHookManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.factory = LZ4Factory.fastestInstance();
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
plugin.debug("Loaded " + hooks.size() + " hooks");
}
@Override
public boolean registerHook(HookConfig hook) {
if (hooks.containsKey(hook.id())) return false;
hooks.put(hook.id(), hook);
return true;
}
@NotNull
@Override
public Optional<HookConfig> getHook(String id) {
return Optional.ofNullable(hooks.get(id));
}
@Override
public Optional<String> getHookID(ItemStack rod) {
if (rod == null || rod.getType() != Material.FISHING_ROD || rod.getAmount() == 0)
return Optional.empty();
Item<ItemStack> wrapped = plugin.getItemManager().wrap(rod);
return wrapped.getTag("CustomFishing", "hook_id").map(o -> (String) o);
}
@EventHandler (ignoreCancelled = true)
public void onDragDrop(InventoryClickEvent event) {
final Player player = (Player) event.getWhoClicked();
if (event.getClickedInventory() != player.getInventory())
return;
if (player.getGameMode() != GameMode.SURVIVAL)
return;
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() != Material.FISHING_ROD)
return;
if (plugin.getFishingManager().getFishHook(player).isPresent())
return;
ItemStack cursor = event.getCursor();
if (cursor.getType() == Material.AIR) {
if (event.getClick() != ClickType.RIGHT) {
return;
}
Item<ItemStack> wrapped = plugin.getItemManager().wrap(clicked);
if (!wrapped.hasTag("CustomFishing", "hook_id")) {
return;
}
event.setCancelled(true);
String id = (String) wrapped.getTag("CustomFishing", "hook_id").orElseThrow();
byte[] hookItemBase64 = (byte[]) wrapped.getTag("CustomFishing", "hook_stack").orElse(null);
int damage = (int) wrapped.getTag("CustomFishing", "hook_damage").orElse(0);
ItemStack itemStack;
if (hookItemBase64 != null) {
itemStack = bytesToHook(hookItemBase64);
} else {
itemStack = plugin.getItemManager().buildInternal(Context.player(player), id);
}
plugin.getItemManager().setDurability(player, itemStack, damage);
wrapped.removeTag("CustomFishing", "hook_id");
wrapped.removeTag("CustomFishing", "hook_stack");
wrapped.removeTag("CustomFishing", "hook_damage");
wrapped.removeTag("CustomFishing", "hook_max_damage");
event.setCursor(itemStack);
List<String> previousLore = wrapped.lore().orElse(new ArrayList<>());
List<String> newLore = new ArrayList<>();
for (String previous : previousLore) {
Component component = AdventureHelper.jsonToComponent(previous);
if (component instanceof ScoreComponent scoreComponent && scoreComponent.name().equals("cf") && scoreComponent.objective().equals("hook")) {
continue;
}
newLore.add(previous);
}
wrapped.lore(newLore);
wrapped.load();
return;
}
String hookID = plugin.getItemManager().getItemID(cursor);
Optional<HookConfig> setting = getHook(hookID);
if (setting.isEmpty()) {
return;
}
Context<Player> context = Context.player(player);
HookConfig hookConfig = setting.get();
Optional<EffectModifier> modifier = plugin.getEffectManager().getEffectModifier(hookID, MechanicType.HOOK);
if (modifier.isPresent()) {
if (!RequirementManager.isSatisfied(context, modifier.get().requirements())) {
return;
}
}
event.setCancelled(true);
ItemStack clonedHook = cursor.clone();
clonedHook.setAmount(1);
cursor.setAmount(cursor.getAmount() - 1);
Item<ItemStack> wrapped = plugin.getItemManager().wrap(clicked);
String previousHookID = (String) wrapped.getTag("CustomFishing", "hook_id").orElse(null);
if (previousHookID != null) {
int previousHookDamage = (int) wrapped.getTag("CustomFishing", "hook_damage").orElse(0);
ItemStack previousItemStack;
byte[] stackBytes = (byte[]) wrapped.getTag("CustomFishing", "hook_stack").orElse(null);
if (stackBytes != null) {
previousItemStack = bytesToHook(stackBytes);
} else {
previousItemStack = plugin.getItemManager().buildInternal(Context.player(player), previousHookID);
}
if (previousItemStack != null) {
plugin.getItemManager().setDurability(player, previousItemStack, previousHookDamage);
if (cursor.getAmount() == 0) {
event.setCursor(previousItemStack);
} else {
PlayerUtils.giveItem(player, previousItemStack, 1);
}
}
}
Item<ItemStack> wrappedHook = plugin.getItemManager().wrap(clonedHook);
DurabilityItem durabilityItem;
if (wrappedHook.hasTag("CustomFishing", "max_dur")) {
durabilityItem = new CustomDurabilityItem(wrappedHook);
} else {
durabilityItem = new VanillaDurabilityItem(wrappedHook);
}
wrapped.setTag(hookID, "CustomFishing", "hook_id");
wrapped.setTag(hookToBytes(clonedHook), "CustomFishing", "hook_stack");
wrapped.setTag(durabilityItem.damage(), "CustomFishing", "hook_damage");
wrapped.setTag(durabilityItem.maxDamage(), "CustomFishing", "hook_max_damage");
List<String> previousLore = wrapped.lore().orElse(new ArrayList<>());
List<String> newLore = new ArrayList<>();
List<String> durabilityLore = new ArrayList<>();
for (String previous : previousLore) {
Component component = AdventureHelper.jsonToComponent(previous);
if (component instanceof ScoreComponent scoreComponent && scoreComponent.name().equals("cf")) {
if (scoreComponent.objective().equals("hook")) {
continue;
} else if (scoreComponent.objective().equals("durability")) {
durabilityLore.add(previous);
continue;
}
}
newLore.add(previous);
}
for (String lore : hookConfig.lore()) {
ScoreComponent.Builder builder = Component.score().name("cf").objective("hook");
builder.append(AdventureHelper.miniMessage(lore.replace("{dur}", String.valueOf(durabilityItem.maxDamage() - durabilityItem.damage())).replace("{max}", String.valueOf(durabilityItem.maxDamage()))));
newLore.add(AdventureHelper.componentToJson(builder.build()));
}
newLore.addAll(durabilityLore);
wrapped.lore(newLore);
wrapped.load();
}
private byte[] hookToBytes(ItemStack hook) {
try {
byte[] data = ItemTagStream.INSTANCE.toBytes(hook);
int decompressedLength = data.length;
LZ4Compressor compressor = factory.fastCompressor();
int maxCompressedLength = compressor.maxCompressedLength(decompressedLength);
byte[] compressed = new byte[maxCompressedLength];
int compressedLength = compressor.compress(data, 0, decompressedLength, compressed, 0, maxCompressedLength);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream outputStream = new DataOutputStream(byteArrayOutputStream);
outputStream.writeInt(decompressedLength);
outputStream.write(compressed, 0, compressedLength);
outputStream.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private ItemStack bytesToHook(byte[] bytes) {
try {
DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(bytes));
int decompressedLength = inputStream.readInt();
byte[] compressed = new byte[inputStream.available()];
inputStream.readFully(compressed);
LZ4FastDecompressor decompressor = factory.fastDecompressor();
byte[] restored = new byte[decompressedLength];
decompressor.decompress(compressed, 0, restored, 0, decompressedLength);
return ItemTagStream.INSTANCE.fromBytes(restored);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.integration;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.integration.*;
import net.momirealms.customfishing.bukkit.block.BukkitBlockManager;
import net.momirealms.customfishing.bukkit.entity.BukkitEntityManager;
import net.momirealms.customfishing.bukkit.integration.block.ItemsAdderBlockProvider;
import net.momirealms.customfishing.bukkit.integration.block.OraxenBlockProvider;
import net.momirealms.customfishing.bukkit.integration.enchant.AdvancedEnchantmentsProvider;
import net.momirealms.customfishing.bukkit.integration.enchant.VanillaEnchantmentsProvider;
import net.momirealms.customfishing.bukkit.integration.entity.ItemsAdderEntityProvider;
import net.momirealms.customfishing.bukkit.integration.entity.MythicEntityProvider;
import net.momirealms.customfishing.bukkit.integration.item.*;
import net.momirealms.customfishing.bukkit.integration.level.*;
import net.momirealms.customfishing.bukkit.integration.papi.CompetitionPapi;
import net.momirealms.customfishing.bukkit.integration.papi.CustomFishingPapi;
import net.momirealms.customfishing.bukkit.integration.papi.StatisticsPapi;
import net.momirealms.customfishing.bukkit.integration.quest.BattlePassQuest;
import net.momirealms.customfishing.bukkit.integration.quest.BetonQuestQuest;
import net.momirealms.customfishing.bukkit.integration.quest.ClueScrollsQuest;
import net.momirealms.customfishing.bukkit.integration.season.AdvancedSeasonsProvider;
import net.momirealms.customfishing.bukkit.integration.season.CustomCropsSeasonProvider;
import net.momirealms.customfishing.bukkit.integration.season.RealisticSeasonsProvider;
import net.momirealms.customfishing.bukkit.item.BukkitItemManager;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class BukkitIntegrationManager implements IntegrationManager {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, LevelerProvider> levelerProviders = new HashMap<>();
private final HashMap<String, EnchantmentProvider> enchantmentProviders = new HashMap<>();
private SeasonProvider seasonProvider;
public BukkitIntegrationManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.load();
}
@Override
public void disable() {
this.enchantmentProviders.clear();
this.levelerProviders.clear();
}
@Override
public void load() {
registerEnchantmentProvider(new VanillaEnchantmentsProvider());
if (isHooked("ItemsAdder")) {
registerItemProvider(new ItemsAdderItemProvider());
registerBlockProvider(new ItemsAdderBlockProvider());
registerEntityProvider(new ItemsAdderEntityProvider());
}
if (isHooked("MMOItems")) {
registerItemProvider(new MMOItemsItemProvider());
}
if (isHooked("Oraxen")) {
registerItemProvider(new OraxenItemProvider());
registerBlockProvider(new OraxenBlockProvider());
}
if (isHooked("Zaphkiel")) {
registerItemProvider(new ZaphkielItemProvider());
}
if (isHooked("NeigeItems")) {
registerItemProvider(new NeigeItemsItemProvider());
}
if (isHooked("MythicMobs")) {
registerItemProvider(new MythicMobsItemProvider());
registerEntityProvider(new MythicEntityProvider());
}
if (isHooked("EcoJobs")) {
registerLevelerProvider(new EcoJobsLevelerProvider());
}
if (isHooked("EcoSkills")) {
registerLevelerProvider(new EcoSkillsLevelerProvider());
}
if (isHooked("Jobs")) {
registerLevelerProvider(new JobsRebornLevelerProvider());
}
if (isHooked("MMOCore")) {
registerLevelerProvider(new MMOCoreLevelerProvider());
}
if (isHooked("mcMMO")) {
try {
registerItemProvider(new McMMOTreasureProvider());
} catch (ClassNotFoundException | NoSuchMethodException e) {
plugin.getPluginLogger().warn("Failed to initialize mcMMO Treasure");
}
registerLevelerProvider(new McMMOLevelerProvider());
}
if (isHooked("AureliumSkills")) {
registerLevelerProvider(new AureliumSkillsProvider());
}
if (isHooked("AuraSkills")) {
registerLevelerProvider(new AuraSkillsLevelerProvider());
}
if (isHooked("AdvancedEnchantments")) {
registerEnchantmentProvider(new AdvancedEnchantmentsProvider());
}
if (isHooked("RealisticSeasons")) {
registerSeasonProvider(new RealisticSeasonsProvider());
} else if (isHooked("AdvancedSeasons")) {
registerSeasonProvider(new AdvancedSeasonsProvider());
} else if (isHooked("CustomCrops")) {
registerSeasonProvider(new CustomCropsSeasonProvider());
}
if (isHooked("Vault")) {
VaultHook.initialize();
}
if (isHooked("BattlePass")){
BattlePassQuest battlePassQuest = new BattlePassQuest();
battlePassQuest.register();
}
if (isHooked("ClueScrolls")) {
ClueScrollsQuest clueScrollsQuest = new ClueScrollsQuest();
clueScrollsQuest.register();
}
if (isHooked("BetonQuest")) {
BetonQuestQuest.register();
}
if (isHooked("PlaceholderAPI")) {
new CustomFishingPapi(plugin).load();
new CompetitionPapi(plugin).load();
new StatisticsPapi(plugin).load();
}
}
private boolean isHooked(String hooked) {
if (Bukkit.getPluginManager().getPlugin(hooked) != null) {
plugin.getPluginLogger().info(hooked + " hooked!");
return true;
}
return false;
}
@Override
public boolean registerLevelerProvider(@NotNull LevelerProvider leveler) {
if (levelerProviders.containsKey(leveler.identifier())) return false;
levelerProviders.put(leveler.identifier(), leveler);
return true;
}
@Override
public boolean unregisterLevelerProvider(@NotNull String id) {
return levelerProviders.remove(id) != null;
}
@Override
public boolean registerEnchantmentProvider(@NotNull EnchantmentProvider enchantment) {
if (enchantmentProviders.containsKey(enchantment.identifier())) return false;
enchantmentProviders.put(enchantment.identifier(), enchantment);
return true;
}
@Override
public boolean unregisterEnchantmentProvider(@NotNull String id) {
return enchantmentProviders.remove(id) != null;
}
@Override
@Nullable
public LevelerProvider getLevelerProvider(String plugin) {
return levelerProviders.get(plugin);
}
@Override
@Nullable
public EnchantmentProvider getEnchantmentProvider(String id) {
return enchantmentProviders.get(id);
}
@Override
public List<Pair<String, Short>> getEnchantments(ItemStack itemStack) {
ArrayList<Pair<String, Short>> list = new ArrayList<>();
for (EnchantmentProvider enchantmentProvider : enchantmentProviders.values()) {
list.addAll(enchantmentProvider.getEnchants(itemStack));
}
return list;
}
@Nullable
@Override
public SeasonProvider getSeasonProvider() {
return seasonProvider;
}
@Override
public boolean registerSeasonProvider(@NotNull SeasonProvider season) {
if (this.seasonProvider != null) return false;
this.seasonProvider = season;
return true;
}
@Override
public boolean unregisterSeasonProvider() {
if (this.seasonProvider == null) return false;
this.seasonProvider = null;
return true;
}
@Override
public boolean registerEntityProvider(@NotNull EntityProvider entity) {
return ((BukkitEntityManager) plugin.getEntityManager()).registerEntityProvider(entity);
}
@Override
public boolean unregisterEntityProvider(@NotNull String id) {
return ((BukkitEntityManager) plugin.getEntityManager()).unregisterEntityProvider(id);
}
@Override
public boolean registerItemProvider(@NotNull ItemProvider item) {
return ((BukkitItemManager) plugin.getItemManager()).registerItemProvider(item);
}
@Override
public boolean unregisterItemProvider(@NotNull String id) {
return ((BukkitItemManager) plugin.getItemManager()).unregisterItemProvider(id);
}
@Override
public boolean registerBlockProvider(@NotNull BlockProvider block) {
return ((BukkitBlockManager) plugin.getBlockManager()).registerBlockProvider(block);
}
@Override
public boolean unregisterBlockProvider(@NotNull String id) {
return ((BukkitBlockManager) plugin.getBlockManager()).unregisterBlockProvider(id);
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item;
import com.saicone.rtag.RtagItem;
import net.momirealms.customfishing.bukkit.item.impl.ComponentItemFactory;
import net.momirealms.customfishing.bukkit.item.impl.UniversalItemFactory;
import net.momirealms.customfishing.common.item.Item;
import net.momirealms.customfishing.common.item.ItemFactory;
import net.momirealms.customfishing.common.plugin.CustomFishingPlugin;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
import java.util.Optional;
public abstract class BukkitItemFactory extends ItemFactory<CustomFishingPlugin, RtagItem, ItemStack> {
protected BukkitItemFactory(CustomFishingPlugin plugin) {
super(plugin);
}
public static BukkitItemFactory create(CustomFishingPlugin plugin) {
Objects.requireNonNull(plugin, "plugin");
switch (plugin.getServerVersion()) {
case "1.17", "1.17.1",
"1.18", "1.18.1", "1.18.2",
"1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4",
"1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4" -> {
return new UniversalItemFactory(plugin);
}
case "1.20.5", "1.20.6",
"1.21", "1.21.1", "1.21.2" -> {
return new ComponentItemFactory(plugin);
}
default -> throw new IllegalStateException("Unsupported server version: " + plugin.getServerVersion());
}
}
public Item<ItemStack> wrap(ItemStack item) {
Objects.requireNonNull(item, "item");
return wrap(new RtagItem(item));
}
@Override
protected void setTag(RtagItem item, Object value, Object... path) {
item.set(value, path);
}
@Override
protected Optional<Object> getTag(RtagItem item, Object... path) {
return Optional.ofNullable(item.get(path));
}
@Override
protected boolean hasTag(RtagItem item, Object... path) {
return item.hasTag(path);
}
@Override
protected boolean removeTag(RtagItem item, Object... path) {
return item.remove(path);
}
@Override
protected void update(RtagItem item) {
item.update();
}
@Override
protected ItemStack load(RtagItem item) {
return item.load();
}
@Override
protected ItemStack getItem(RtagItem item) {
return item.getItem();
}
@Override
protected ItemStack loadCopy(RtagItem item) {
return item.loadCopy();
}
}

View File

@@ -0,0 +1,484 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.integration.ExternalProvider;
import net.momirealms.customfishing.api.integration.ItemProvider;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.item.CustomFishingItem;
import net.momirealms.customfishing.api.mechanic.item.ItemManager;
import net.momirealms.customfishing.api.util.EventUtils;
import net.momirealms.customfishing.bukkit.integration.item.CustomFishingItemProvider;
import net.momirealms.customfishing.bukkit.item.damage.CustomDurabilityItem;
import net.momirealms.customfishing.bukkit.item.damage.DurabilityItem;
import net.momirealms.customfishing.bukkit.item.damage.VanillaDurabilityItem;
import net.momirealms.customfishing.bukkit.util.ItemStackUtils;
import net.momirealms.customfishing.bukkit.util.LocationUtils;
import net.momirealms.customfishing.common.item.Item;
import net.momirealms.sparrow.heart.SparrowHeart;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.bukkit.block.Skull;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.FishHook;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.inventory.InventoryPickupItemEvent;
import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.event.player.PlayerAttemptPickupItemEvent;
import org.bukkit.event.player.PlayerItemDamageEvent;
import org.bukkit.event.player.PlayerItemMendEvent;
import org.bukkit.inventory.AnvilInventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static java.util.Objects.requireNonNull;
public class BukkitItemManager implements ItemManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, ItemProvider> itemProviders = new HashMap<>();
private final HashMap<String, CustomFishingItem> items = new HashMap<>();
private final BukkitItemFactory factory;
private ItemProvider[] itemDetectArray;
public BukkitItemManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.factory = BukkitItemFactory.create(plugin);
this.registerItemProvider(new ItemProvider() {
@NotNull
@Override
public ItemStack buildItem(@NotNull Player player, @NotNull String id) {
return new ItemStack(Material.valueOf(id.toUpperCase(Locale.ENGLISH)));
}
@NotNull
@Override
public String itemID(@NotNull ItemStack itemStack) {
return itemStack.getType().name();
}
@Override
public String identifier() {
return "vanilla";
}
});
this.registerItemProvider(new CustomFishingItemProvider());
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
this.items.clear();
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
this.resetItemDetectionOrder();
for (ItemProvider provider : itemProviders.values()) {
plugin.debug("Registered ItemProvider: " + provider.identifier());
}
plugin.debug("Loaded " + items.size() + " items");
plugin.debug("Item order: " + Arrays.toString(Arrays.stream(itemDetectArray).map(ExternalProvider::identifier).toList().toArray(new String[0])));
}
@Override
public boolean registerItem(@NotNull CustomFishingItem item) {
if (items.containsKey(item.id())) return false;
items.put(item.id(), item);
return true;
}
@Nullable
@Override
public ItemStack buildInternal(@NotNull Context<Player> context, @NotNull String id) {
// CustomFishingItem item = requireNonNull(items.get(id), () -> "No item found for " + id);
CustomFishingItem item = items.get(id);
if (item == null) return null;
return build(context, item);
}
@NotNull
@Override
public ItemStack build(@NotNull Context<Player> context, @NotNull CustomFishingItem item) {
ItemStack itemStack = getOriginalStack(context.getHolder(), item.material());
if (itemStack.getType() == Material.AIR) return itemStack;
Item<ItemStack> wrappedItemStack = factory.wrap(itemStack);
for (BiConsumer<Item<ItemStack>, Context<Player>> consumer : item.tagConsumers()) {
consumer.accept(wrappedItemStack, context);
}
return wrappedItemStack.load();
}
@Override
public ItemStack buildAny(@NotNull Context<Player> context, @NotNull String item) {
return getOriginalStack(context.getHolder(), item);
}
@NotNull
@Override
public String getItemID(@NotNull ItemStack itemStack) {
if (itemStack.getType() == Material.AIR)
return "AIR";
for (ItemProvider library : itemDetectArray) {
String id = library.itemID(itemStack);
if (id != null)
return id;
}
// should not reach this because vanilla library would always work
return "AIR";
}
@Override
public String getCustomFishingItemID(@NotNull ItemStack itemStack) {
return (String) factory.wrap(itemStack).getTag("CustomFishing", "id").orElse(null);
}
@Nullable
@Override
public org.bukkit.entity.Item dropItemLoot(@NotNull Context<Player> context, ItemStack rod, FishHook hook) {
String id = requireNonNull(context.arg(ContextKeys.ID));
ItemStack itemStack;
if (id.equals("vanilla")) {
itemStack = SparrowHeart.getInstance().getFishingLoot(context.getHolder(), hook, rod).stream().findAny().orElseThrow(() -> new RuntimeException("new EntityItem would throw if for whatever reason (mostly shitty datapacks) the fishing loot turns out to be empty"));
} else {
itemStack = requireNonNull(buildInternal(context, id));
}
if (itemStack.getType() == Material.AIR) {
return null;
}
Player player = context.getHolder();
Location playerLocation = player.getLocation();
Location hookLocation = requireNonNull(context.arg(ContextKeys.OTHER_LOCATION));
double d0 = playerLocation.getX() - hookLocation.getX();
double d1 = playerLocation.getY() - hookLocation.getY();
double d2 = playerLocation.getZ() - hookLocation.getZ();
Vector vector = new Vector(d0 * 0.1D, d1 * 0.1D + Math.sqrt(Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2)) * 0.08D, d2 * 0.1D);
org.bukkit.entity.Item itemEntity = hookLocation.getWorld().dropItem(hookLocation, itemStack);
itemEntity.setInvulnerable(true);
// prevent from being killed by lava
plugin.getScheduler().asyncLater(() -> {
if (itemEntity.isValid())
itemEntity.setInvulnerable(false);
}, 1, TimeUnit.SECONDS);
itemEntity.setVelocity(vector);
return itemEntity;
}
private ItemStack getOriginalStack(Player player, String material) {
if (!material.contains(":")) {
try {
return new ItemStack(Material.valueOf(material.toUpperCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
plugin.getPluginLogger().severe("material " + material + " not exists", e);
return new ItemStack(Material.PAPER);
}
} else {
String[] split = material.split(":", 2);
ItemProvider provider = requireNonNull(itemProviders.get(split[0]), "Item provider: " + split[0] + " not found");
return requireNonNull(provider.buildItem(player, split[0]), "Item: " + split[0] + " not found");
}
}
private void resetItemDetectionOrder() {
ArrayList<ItemProvider> list = new ArrayList<>();
for (String plugin : ConfigManager.itemDetectOrder()) {
ItemProvider provider = itemProviders.get(plugin);
if (provider != null)
list.add(provider);
}
this.itemDetectArray = list.toArray(new ItemProvider[0]);
}
public boolean registerItemProvider(ItemProvider item) {
if (itemProviders.containsKey(item.identifier())) return false;
itemProviders.put(item.identifier(), item);
this.resetItemDetectionOrder();
return true;
}
public boolean unregisterItemProvider(String id) {
boolean success = itemProviders.remove(id) != null;
if (success)
this.resetItemDetectionOrder();
return success;
}
@Override
public boolean hasCustomDurability(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() == 0)
return false;
Item<ItemStack> wrapped = factory.wrap(itemStack);
return wrapped.hasTag("CustomFishing", "max_dur");
}
@Override
public void decreaseDurability(Player player, ItemStack itemStack, int amount, boolean incorrectUsage) {
if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() == 0)
return;
if (!incorrectUsage) {
int unBreakingLevel = itemStack.getEnchantmentLevel(Enchantment.DURABILITY);
if (Math.random() > (double) 1 / (unBreakingLevel + 1)) {
return;
}
}
Item<ItemStack> wrapped = factory.wrap(itemStack);
if (wrapped.unbreakable())
return;
ItemMeta previousMeta = itemStack.getItemMeta().clone();
PlayerItemDamageEvent itemDamageEvent = new PlayerItemDamageEvent(player, itemStack, amount);
if (EventUtils.fireAndCheckCancel(itemDamageEvent)) {
plugin.debug("Another plugin modified the item from `PlayerItemDamageEvent` called by CustomFishing");
return;
}
if (!itemStack.getItemMeta().equals(previousMeta)) {
return;
}
DurabilityItem durabilityItem = wrapDurabilityItem(wrapped);
int damage = durabilityItem.damage();
if (damage + amount >= durabilityItem.maxDamage()) {
plugin.getSenderFactory().getAudience(player).playSound(Sound.sound(Key.key("minecraft:entity.item.break"), Sound.Source.PLAYER, 1, 1));
itemStack.setAmount(0);
return;
}
durabilityItem.damage(damage + amount);
wrapped.load();
}
@Override
public void setDurability(Player player, ItemStack itemStack, int damage) {
if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() == 0)
return;
Item<ItemStack> wrapped = factory.wrap(itemStack);
if (wrapped.unbreakable())
return;
DurabilityItem wrappedDurability = wrapDurabilityItem(wrapped);
if (damage >= wrappedDurability.maxDamage()) {
if (player != null)
plugin.getSenderFactory().getAudience(player).playSound(Sound.sound(Key.key("minecraft:entity.item.break"), Sound.Source.PLAYER, 1, 1));
itemStack.setAmount(0);
return;
}
wrappedDurability.damage(damage);
wrapped.load();
}
public DurabilityItem wrapDurabilityItem(Item<ItemStack> wrapped) {
if (wrapped.hasTag("CustomFishing", "max_dur")) {
return new CustomDurabilityItem(wrapped);
} else {
return new VanillaDurabilityItem(wrapped);
}
}
@EventHandler (ignoreCancelled = true)
public void onMending(PlayerItemMendEvent event) {
ItemStack itemStack = event.getItem();
if (!hasCustomDurability(itemStack)) {
return;
}
event.setCancelled(true);
Item<ItemStack> wrapped = factory.wrap(itemStack);
if (wrapped.unbreakable())
return;
DurabilityItem wrappedDurability = wrapDurabilityItem(wrapped);
setDurability(event.getPlayer(), itemStack, Math.max(wrappedDurability.damage() - event.getRepairAmount(), 0));
}
@EventHandler (ignoreCancelled = true)
public void onAnvil(PrepareAnvilEvent event) {
AnvilInventory anvil = event.getInventory();
ItemStack first = anvil.getFirstItem();
ItemStack second = anvil.getSecondItem();
if (first != null && second != null
&& first.getType() == Material.FISHING_ROD && second.getType() == Material.FISHING_ROD && event.getResult() != null
&& hasCustomDurability(first)) {
Item<ItemStack> wrapped1 = factory.wrap(anvil.getResult());
DurabilityItem wrappedDurability1 = wrapDurabilityItem(wrapped1);
Item<ItemStack> wrapped2 = factory.wrap(second);
DurabilityItem wrappedDurability2 = wrapDurabilityItem(wrapped2);
int durability2 = wrappedDurability2.maxDamage() - wrappedDurability2.damage();
int damage1 = Math.max(wrappedDurability1.damage() - durability2, 0);
wrappedDurability1.damage(damage1);
event.setResult(wrapped1.load());
}
}
@EventHandler(ignoreCancelled = true)
public void onInvPickItem(InventoryPickupItemEvent event) {
ItemStack itemStack = event.getItem().getItemStack();
Item<ItemStack> wrapped = factory.wrap(itemStack);
if (wrapped.hasTag("owner")) {
wrapped.removeTag("owner");
itemStack.setItemMeta(wrapped.getItem().getItemMeta());
}
}
@EventHandler (ignoreCancelled = true)
public void onPlaceBlock(BlockPlaceEvent event) {
ItemStack itemStack = event.getItemInHand();
if (itemStack.getType() == Material.AIR || itemStack.getAmount() == 0 || !itemStack.hasItemMeta()) {
return;
}
Item<ItemStack> wrapped = factory.wrap(itemStack);
if (wrapped.hasTag("CustomFishing")) {
if (!wrapped.hasTag("CustomFishing", "placeable") || ((int) wrapped.getTag("CustomFishing", "placeable").get()) != 1) {
event.setCancelled(true);
return;
}
Block block = event.getBlock();
if (block.getState() instanceof Skull) {
PersistentDataContainer pdc = block.getChunk().getPersistentDataContainer();
ItemStack cloned = itemStack.clone();
cloned.setAmount(1);
pdc.set(new NamespacedKey(plugin.getBoostrap(), LocationUtils.toChunkPosString(block.getLocation())), PersistentDataType.STRING, ItemStackUtils.toBase64(cloned));
} else {
event.setCancelled(true);
}
}
}
@EventHandler (ignoreCancelled = true)
public void onBreakBlock(BlockBreakEvent event) {
final Block block = event.getBlock();
if (block.getState() instanceof Skull) {
PersistentDataContainer pdc = block.getChunk().getPersistentDataContainer();
String base64 = pdc.get(new NamespacedKey(plugin.getBoostrap(), LocationUtils.toChunkPosString(block.getLocation())), PersistentDataType.STRING);
if (base64 != null) {
ItemStack itemStack = ItemStackUtils.fromBase64(base64);
event.setDropItems(false);
block.getLocation().getWorld().dropItemNaturally(block.getLocation(), itemStack);
}
}
}
@EventHandler (ignoreCancelled = true)
public void onPiston(BlockPistonExtendEvent event) {
handlePiston(event, event.getBlocks());
}
@EventHandler (ignoreCancelled = true)
public void onPiston(BlockPistonRetractEvent event) {
handlePiston(event, event.getBlocks());
}
private void handlePiston(Cancellable event, List<Block> blockList) {
for (Block block : blockList) {
if (block.getState() instanceof Skull) {
PersistentDataContainer pdc = block.getChunk().getPersistentDataContainer();
if (pdc.has(new NamespacedKey(plugin.getBoostrap(), LocationUtils.toChunkPosString(block.getLocation())), PersistentDataType.STRING)) {
event.setCancelled(true);
return;
}
}
}
}
@EventHandler (ignoreCancelled = true)
public void onExplosion(BlockExplodeEvent event) {
handleExplosion(event.blockList());
}
@EventHandler (ignoreCancelled = true)
public void onExplosion(EntityExplodeEvent event) {
handleExplosion(event.blockList());
}
@EventHandler (ignoreCancelled = true)
public void onPickUpItem(PlayerAttemptPickupItemEvent event) {
String owner = event.getItem().getPersistentDataContainer().get(requireNonNull(NamespacedKey.fromString("owner", plugin.getBoostrap())), PersistentDataType.STRING);
if (owner != null) {
if (!owner.equals(event.getPlayer().getName())) {
event.setCancelled(true);
}
}
}
private void handleExplosion(List<Block> blocks) {
ArrayList<Block> blockToRemove = new ArrayList<>();
for (Block block : blocks) {
if (block.getState() instanceof Skull) {
PersistentDataContainer pdc = block.getChunk().getPersistentDataContainer();
var nk = new NamespacedKey(plugin.getBoostrap(), LocationUtils.toChunkPosString(block.getLocation()));
String base64 = pdc.get(nk, PersistentDataType.STRING);
if (base64 != null) {
ItemStack itemStack = ItemStackUtils.fromBase64(base64);
block.getLocation().getWorld().dropItemNaturally(block.getLocation(), itemStack);
blockToRemove.add(block);
block.setType(Material.AIR);
pdc.remove(nk);
}
}
}
blocks.removeAll(blockToRemove);
}
@Override
public BukkitItemFactory getFactory() {
return factory;
}
@Override
public ItemProvider[] getItemProviders() {
return itemProviders.values().toArray(new ItemProvider[0]);
}
@Override
public Collection<String> getItemIDs() {
return items.keySet();
}
@Override
public Item<ItemStack> wrap(ItemStack itemStack) {
return factory.wrap(itemStack);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item.damage;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ScoreComponent;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.item.Item;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
public class CustomDurabilityItem implements DurabilityItem {
private final Item<ItemStack> item;
public CustomDurabilityItem(Item<ItemStack> item) {
this.item = item;
}
@Override
public void damage(int value) {
int customMaxDamage = (int) item.getTag("CustomFishing", "max_dur").get();
int maxDamage = item.maxDamage().get();
double ratio = (double) maxDamage / (double) customMaxDamage;
int fakeDamage = (int) (value * ratio);
item.damage(fakeDamage);
item.setTag(customMaxDamage - value, "CustomFishing", "cur_dur");
List<String> durabilityLore = ConfigManager.durabilityLore();
List<String> previousLore = item.lore().orElse(new ArrayList<>());
List<String> newLore = new ArrayList<>();
for (String previous : previousLore) {
Component component = AdventureHelper.jsonToComponent(previous);
if (component instanceof ScoreComponent scoreComponent && scoreComponent.name().equals("cf")) {
if (scoreComponent.objective().equals("durability")) {
continue;
}
}
newLore.add(previous);
}
for (String lore : durabilityLore) {
ScoreComponent.Builder builder = Component.score().name("cf").objective("durability");
builder.append(AdventureHelper.miniMessage(lore.replace("{dur}", String.valueOf(customMaxDamage - value)).replace("{max}", String.valueOf(customMaxDamage))));
newLore.add(AdventureHelper.componentToJson(builder.build()));
}
item.lore(newLore);
}
@Override
public int damage() {
int customMaxDamage = (int) item.getTag("CustomFishing", "max_dur").get();
return customMaxDamage - (int) item.getTag("CustomFishing", "cur_dur").orElse(0);
}
@Override
public int maxDamage() {
return (int) item.getTag("CustomFishing", "max_dur").get();
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item.damage;
public interface DurabilityItem {
void damage(int value);
int damage();
int maxDamage();
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item.damage;
import net.momirealms.customfishing.common.item.Item;
import org.bukkit.inventory.ItemStack;
public class VanillaDurabilityItem implements DurabilityItem {
private final Item<ItemStack> item;
public VanillaDurabilityItem(Item<ItemStack> item) {
this.item = item;
}
@Override
public void damage(int value) {
item.damage(value);
}
@Override
public int damage() {
return item.damage().orElse(0);
}
@Override
public int maxDamage() {
return item.maxDamage().get();
}
}

View File

@@ -0,0 +1,209 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item.impl;
import com.saicone.rtag.RtagItem;
import com.saicone.rtag.data.ComponentType;
import net.momirealms.customfishing.bukkit.item.BukkitItemFactory;
import net.momirealms.customfishing.common.item.ComponentKeys;
import net.momirealms.customfishing.common.plugin.CustomFishingPlugin;
import net.momirealms.customfishing.common.util.Key;
import net.momirealms.sparrow.heart.SparrowHeart;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@SuppressWarnings("UnstableApiUsage")
public class ComponentItemFactory extends BukkitItemFactory {
public ComponentItemFactory(CustomFishingPlugin plugin) {
super(plugin);
}
@Override
protected void customModelData(RtagItem item, Integer data) {
if (data == null) {
item.removeComponent(ComponentKeys.CUSTOM_MODEL_DATA);
} else {
item.setComponent(ComponentKeys.CUSTOM_MODEL_DATA, data);
}
}
@Override
protected Optional<Integer> customModelData(RtagItem item) {
if (!item.hasComponent(ComponentKeys.CUSTOM_MODEL_DATA)) return Optional.empty();
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.CUSTOM_MODEL_DATA,
item.getComponent(ComponentKeys.CUSTOM_MODEL_DATA)
).orElse(null)
);
}
@Override
protected void displayName(RtagItem item, String json) {
if (json == null) {
item.removeComponent(ComponentKeys.CUSTOM_NAME);
} else {
item.setComponent(ComponentKeys.CUSTOM_NAME, json);
}
}
@Override
protected Optional<String> displayName(RtagItem item) {
if (!item.hasComponent(ComponentKeys.CUSTOM_NAME)) return Optional.empty();
return Optional.ofNullable(
(String) ComponentType.encodeJava(
ComponentKeys.CUSTOM_NAME,
item.getComponent(ComponentKeys.CUSTOM_NAME)
).orElse(null)
);
}
@Override
protected void skull(RtagItem item, String skullData) {
final Map<String, Object> profile = Map.of(
"properties", List.of(
Map.of(
"name", "textures",
"value", skullData
)
)
);
item.setComponent("minecraft:profile", profile);
}
@SuppressWarnings("unchecked")
@Override
protected Optional<List<String>> lore(RtagItem item) {
if (!item.hasComponent(ComponentKeys.LORE)) return Optional.empty();
return Optional.ofNullable(
(List<String>) ComponentType.encodeJava(
ComponentKeys.LORE,
item.getComponent(ComponentKeys.LORE)
).orElse(null)
);
}
@Override
protected void lore(RtagItem item, List<String> lore) {
if (lore == null || lore.isEmpty()) {
item.removeComponent(ComponentKeys.LORE);
} else {
item.setComponent(ComponentKeys.LORE, lore);
}
}
@Override
protected boolean unbreakable(RtagItem item) {
return item.isUnbreakable();
}
@Override
protected void unbreakable(RtagItem item, boolean unbreakable) {
item.setUnbreakable(unbreakable);
}
@Override
protected Optional<Boolean> glint(RtagItem item) {
return Optional.ofNullable((Boolean) item.getComponent(ComponentKeys.ENCHANTMENT_GLINT_OVERRIDE));
}
@Override
protected void glint(RtagItem item, Boolean glint) {
item.setComponent(ComponentKeys.ENCHANTMENT_GLINT_OVERRIDE, glint);
}
@Override
protected Optional<Integer> damage(RtagItem item) {
if (!item.hasComponent(ComponentKeys.DAMAGE)) return Optional.empty();
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.DAMAGE,
item.getComponent(ComponentKeys.DAMAGE)
).orElse(null)
);
}
@Override
protected void damage(RtagItem item, Integer damage) {
if (damage == null) damage = 0;
item.setComponent(ComponentKeys.DAMAGE, damage);
}
@Override
protected Optional<Integer> maxDamage(RtagItem item) {
if (!item.hasComponent(ComponentKeys.MAX_DAMAGE)) return Optional.of((int) item.getItem().getType().getMaxDurability());
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.MAX_DAMAGE,
item.getComponent(ComponentKeys.MAX_DAMAGE)
).orElse(null)
);
}
@Override
protected void maxDamage(RtagItem item, Integer damage) {
if (damage == null) {
item.removeComponent(ComponentKeys.MAX_DAMAGE);
} else {
item.setComponent(ComponentKeys.MAX_DAMAGE, damage);
}
}
@Override
protected void enchantments(RtagItem item, Map<Key, Short> enchantments) {
Map<String, Integer> enchants = new HashMap<>();
for (Map.Entry<Key, Short> entry : enchantments.entrySet()) {
enchants.put(entry.getKey().toString(), Integer.valueOf(entry.getValue()));
}
item.setComponent(ComponentKeys.ENCHANTMENTS, enchants);
}
@Override
protected void storedEnchantments(RtagItem item, Map<Key, Short> enchantments) {
Map<String, Integer> enchants = new HashMap<>();
for (Map.Entry<Key, Short> entry : enchantments.entrySet()) {
enchants.put(entry.getKey().toString(), Integer.valueOf(entry.getValue()));
}
item.setComponent(ComponentKeys.STORED_ENCHANTMENTS, enchants);
}
@Override
protected void addEnchantment(RtagItem item, Key enchantment, int level) {
Object enchant = item.getComponent(ComponentKeys.ENCHANTMENTS);
Map<String, Integer> map = SparrowHeart.getInstance().itemEnchantmentsToMap(enchant);
map.put(enchantment.toString(), level);
item.setComponent(ComponentKeys.ENCHANTMENTS, map);
}
@Override
protected void addStoredEnchantment(RtagItem item, Key enchantment, int level) {
Object enchant = item.getComponent(ComponentKeys.STORED_ENCHANTMENTS);
Map<String, Integer> map = SparrowHeart.getInstance().itemEnchantmentsToMap(enchant);
map.put(enchantment.toString(), level);
item.setComponent(ComponentKeys.STORED_ENCHANTMENTS, map);
}
@Override
protected void itemFlags(RtagItem item, List<String> flags) {
throw new UnsupportedOperationException("This feature is not available on 1.20.5+");
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.item.impl;
import com.saicone.rtag.RtagItem;
import com.saicone.rtag.tag.TagBase;
import com.saicone.rtag.tag.TagCompound;
import com.saicone.rtag.tag.TagList;
import net.momirealms.customfishing.bukkit.item.BukkitItemFactory;
import net.momirealms.customfishing.common.plugin.CustomFishingPlugin;
import net.momirealms.customfishing.common.util.Key;
import org.bukkit.inventory.ItemFlag;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class UniversalItemFactory extends BukkitItemFactory {
public UniversalItemFactory(CustomFishingPlugin plugin) {
super(plugin);
}
@Override
protected void displayName(RtagItem item, String json) {
if (json != null) {
item.set(json, "display", "Name");
} else {
item.remove("display", "Name");
}
}
@Override
protected Optional<String> displayName(RtagItem item) {
if (!item.hasTag("display", "Name")) return Optional.empty();
return Optional.of(item.get("display", "Name"));
}
@Override
protected void customModelData(RtagItem item, Integer data) {
if (data == null) {
item.remove("CustomModelData");
} else {
item.set(data, "CustomModelData");
}
}
@Override
protected Optional<Integer> customModelData(RtagItem item) {
if (!item.hasTag("CustomModelData")) return Optional.empty();
return Optional.of(item.get("CustomModelData"));
}
@Override
protected void skull(RtagItem item, String skullData) {
if (skullData == null) {
item.remove("SkullOwner");
} else {
item.set(List.of(Map.of("Value", skullData)), "SkullOwner", "Properties", "textures");
}
}
@Override
protected Optional<List<String>> lore(RtagItem item) {
if (!item.hasTag("display", "Lore")) return Optional.empty();
return Optional.of(item.get("display", "Lore"));
}
@Override
protected void lore(RtagItem item, List<String> lore) {
if (lore == null || lore.isEmpty()) {
item.remove("display", "Lore");
} else {
item.set(lore, "display", "Lore");
}
}
@Override
protected boolean unbreakable(RtagItem item) {
return item.isUnbreakable();
}
@Override
protected void unbreakable(RtagItem item, boolean unbreakable) {
item.setUnbreakable(unbreakable);
}
@Override
protected Optional<Boolean> glint(RtagItem item) {
return Optional.of(false);
}
@Override
protected void glint(RtagItem item, Boolean glint) {
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected Optional<Integer> damage(RtagItem item) {
if (!item.hasTag("Damage")) return Optional.empty();
return Optional.of(item.get("Damage"));
}
@Override
protected void damage(RtagItem item, Integer damage) {
item.set(damage, "Damage");
}
@Override
protected Optional<Integer> maxDamage(RtagItem item) {
// if (!item.hasTag("CustomFishing", "max_dur")) return Optional.empty();
// return Optional.of(item.get("CustomFishing", "max_dur"));
return Optional.of((int) item.getItem().getType().getMaxDurability());
}
@Override
protected void maxDamage(RtagItem item, Integer damage) {
// if (damage == null) {
// item.remove("CustomFishing", "max_dur");
// } else {
// item.set(damage, "CustomFishing", "max_dur");
// }
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected void enchantments(RtagItem item, Map<Key, Short> enchantments) {
ArrayList<Object> tags = new ArrayList<>();
for (Map.Entry<Key, Short> entry : enchantments.entrySet()) {
tags.add((Map.of("id", entry.getKey().toString(), "lvl", entry.getValue())));
}
item.set(tags, "Enchantments");
}
@Override
protected void storedEnchantments(RtagItem item, Map<Key, Short> enchantments) {
ArrayList<Object> tags = new ArrayList<>();
for (Map.Entry<Key, Short> entry : enchantments.entrySet()) {
tags.add((Map.of("id", entry.getKey().toString(), "lvl", entry.getValue())));
}
item.set(tags, "StoredEnchantments");
}
@Override
protected void addEnchantment(RtagItem item, Key enchantment, int level) {
Object enchantments = item.getExact("Enchantments");
if (enchantments != null) {
for (Object enchant : TagList.getValue(enchantments)) {
if (TagBase.getValue(TagCompound.get(enchant, "id")).equals(enchant.toString())) {
TagCompound.set(enchant, "lvl", TagBase.newTag(level));
return;
}
}
item.add(Map.of("id", enchantment.toString(), "lvl", (short) level), "Enchantments");
} else {
item.set(List.of(Map.of("id", enchantment.toString(), "lvl", (short) level)), "Enchantments");
}
}
@Override
protected void addStoredEnchantment(RtagItem item, Key enchantment, int level) {
Object enchantments = item.getExact("StoredEnchantments");
if (enchantments != null) {
for (Object enchant : TagList.getValue(enchantments)) {
if (TagBase.getValue(TagCompound.get(enchant, "id")).equals(enchant.toString())) {
TagCompound.set(enchant, "lvl", TagBase.newTag(level));
return;
}
}
item.add(Map.of("id", enchantment.toString(), "lvl", (short) level), "StoredEnchantments");
} else {
item.set(List.of(Map.of("id", enchantment.toString(), "lvl", (short) level)), "StoredEnchantments");
}
}
@Override
protected void itemFlags(RtagItem item, List<String> flags) {
if (flags == null || flags.isEmpty()) {
item.remove("HideFlags");
return;
}
int f = 0;
for (String flag : flags) {
ItemFlag itemFlag = ItemFlag.valueOf(flag);
f = f | 1 << itemFlag.ordinal();
}
item.set(f, "HideFlags");
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.loot;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.effect.Effect;
import net.momirealms.customfishing.api.mechanic.loot.Loot;
import net.momirealms.customfishing.api.mechanic.loot.LootManager;
import net.momirealms.customfishing.api.mechanic.requirement.ConditionalElement;
import net.momirealms.customfishing.api.mechanic.requirement.RequirementManager;
import net.momirealms.customfishing.common.util.Pair;
import net.momirealms.customfishing.common.util.WeightUtils;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.function.BiFunction;
@SuppressWarnings("DuplicatedCode")
public class BukkitLootManager implements LootManager {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, Loot> lootMap = new HashMap<>();
private final HashMap<String, List<String>> groupMembersMap = new HashMap<>();
private final LinkedHashMap<String, ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player>> lootConditions = new LinkedHashMap<>();
public BukkitLootManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void unload() {
this.lootMap.clear();
this.groupMembersMap.clear();
this.lootConditions.clear();
}
@Override
public void load() {
plugin.debug("Loaded " + lootMap.size() + " loots");
for (Map.Entry<String, List<String>> entry : groupMembersMap.entrySet()) {
plugin.debug("Group: {" + entry.getKey() + "} Members: " + entry.getValue());
}
File file = new File(plugin.getDataFolder(), "loot-conditions.yml");
if (!file.exists()) {
plugin.getBoostrap().saveResource("loot-conditions.yml", false);
}
YamlDocument lootConditionsConfig = plugin.getConfigManager().loadData(file);
for (Map.Entry<String, Object> entry : lootConditionsConfig.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section section) {
lootConditions.put(entry.getKey(), parseLootConditions(section));
}
}
}
private ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player> parseLootConditions(Section section) {
Section subSection = section.getSection("sub-groups");
if (subSection == null) {
return new ConditionalElement<>(
plugin.getConfigManager().parseWeightOperation(section.getStringList("list")),
Map.of(),
plugin.getRequirementManager().parseRequirements(section.getSection("conditions"), false)
);
} else {
HashMap<String, ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player>> subElements = new HashMap<>();
for (Map.Entry<String, Object> entry : subSection.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
subElements.put(entry.getKey(), parseLootConditions(innerSection));
}
}
return new ConditionalElement<>(
plugin.getConfigManager().parseWeightOperation(section.getStringList("list")),
subElements,
plugin.getRequirementManager().parseRequirements(section.getSection("conditions"), false)
);
}
}
@Override
public boolean registerLoot(@NotNull Loot loot) {
if (lootMap.containsKey(loot.id())) return false;
this.lootMap.put(loot.id(), loot);
for (String group : loot.lootGroup()) {
addGroupMember(group, loot.id());
}
return true;
}
private void addGroupMember(String group, String member) {
List<String> members = groupMembersMap.get(group);
if (members == null) {
members = new ArrayList<>(List.of(member));
groupMembersMap.put(group, members);
} else {
members.add(member);
}
}
@NotNull
@Override
public List<String> getGroupMembers(String key) {
return Optional.ofNullable(groupMembersMap.get(key)).orElse(List.of());
}
@NotNull
@Override
public Optional<Loot> getLoot(String key) {
return Optional.ofNullable(lootMap.get(key));
}
@Override
public HashMap<String, Double> getWeightedLoots(Effect effect, Context<Player> context) {
HashMap<String, Double> lootWeightMap = new HashMap<>();
for (ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player> conditionalElement : lootConditions.values()) {
modifyWeightMap(lootWeightMap, context, conditionalElement);
}
for (Pair<String, BiFunction<Context<Player>, Double, Double>> pair : effect.weightOperations()) {
double previous = lootWeightMap.getOrDefault(pair.left(), 0d);
if (previous > 0)
lootWeightMap.put(pair.left(), pair.right().apply(context, previous));
}
for (Pair<String, BiFunction<Context<Player>, Double, Double>> pair : effect.weightOperationsIgnored()) {
double previous = lootWeightMap.getOrDefault(pair.left(), 0d);
lootWeightMap.put(pair.left(), pair.right().apply(context, previous));
}
return lootWeightMap;
}
@Nullable
@Override
public Loot getNextLoot(Effect effect, Context<Player> context) {
HashMap<String, Double> lootWeightMap = new HashMap<>();
for (ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player> conditionalElement : lootConditions.values()) {
modifyWeightMap(lootWeightMap, context, conditionalElement);
}
for (Pair<String, BiFunction<Context<Player>, Double, Double>> pair : effect.weightOperations()) {
double previous = lootWeightMap.getOrDefault(pair.left(), 0d);
if (previous > 0)
lootWeightMap.put(pair.left(), pair.right().apply(context, previous));
}
for (Pair<String, BiFunction<Context<Player>, Double, Double>> pair : effect.weightOperationsIgnored()) {
double previous = lootWeightMap.getOrDefault(pair.left(), 0d);
lootWeightMap.put(pair.left(), pair.right().apply(context, previous));
}
String lootID = WeightUtils.getRandom(lootWeightMap);
return Optional.ofNullable(lootID)
.map(id -> getLoot(lootID).orElseThrow(() -> new RuntimeException("Could not find loot " + lootID)))
.orElseThrow(() -> new RuntimeException("No loot available. " + context));
}
private void modifyWeightMap(Map<String, Double> weightMap, Context<Player> context, ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player> conditionalElement) {
if (conditionalElement == null) return;
if (RequirementManager.isSatisfied(context, conditionalElement.getRequirements())) {
for (Pair<String, BiFunction<Context<Player>, Double, Double>> modifierPair : conditionalElement.getElement()) {
double previous = weightMap.getOrDefault(modifierPair.left(), 0d);
weightMap.put(modifierPair.left(), modifierPair.right().apply(context, previous));
}
for (ConditionalElement<List<Pair<String, BiFunction<Context<Player>, Double, Double>>>, Player> sub : conditionalElement.getSubElements().values()) {
modifyWeightMap(weightMap, context, sub);
}
}
}
}

View File

@@ -0,0 +1,528 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.market;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.action.Action;
import net.momirealms.customfishing.api.mechanic.action.ActionManager;
import net.momirealms.customfishing.api.mechanic.config.GUIItemParser;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.item.CustomFishingItem;
import net.momirealms.customfishing.api.mechanic.market.MarketGUIHolder;
import net.momirealms.customfishing.api.mechanic.market.MarketManager;
import net.momirealms.customfishing.api.mechanic.misc.value.MathValue;
import net.momirealms.customfishing.api.mechanic.misc.value.TextValue;
import net.momirealms.customfishing.api.storage.data.EarningData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.bukkit.config.BukkitConfigManager;
import net.momirealms.customfishing.bukkit.item.BukkitItemFactory;
import net.momirealms.customfishing.common.item.Item;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.ShulkerBox;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.BundleMeta;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@SuppressWarnings("DuplicatedCode")
public class BukkitMarketManager implements MarketManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, MathValue<Player>> priceMap;
private String formula;
private MathValue<Player> earningsLimit;
private boolean allowItemWithNoPrice;
protected boolean sellFishingBag;
protected TextValue<Player> title;
protected String[] layout;
protected final HashMap<Character, CustomFishingItem> decorativeIcons;
protected final ConcurrentHashMap<UUID, MarketGUI> marketGUICache;
protected char itemSlot;
protected char sellSlot;
protected char sellAllSlot;
protected CustomFishingItem sellIconAllowItem;
protected CustomFishingItem sellIconDenyItem;
protected CustomFishingItem sellIconLimitItem;
protected CustomFishingItem sellAllIconAllowItem;
protected CustomFishingItem sellAllIconDenyItem;
protected CustomFishingItem sellAllIconLimitItem;
protected Action<Player>[] sellDenyActions;
protected Action<Player>[] sellAllowActions;
protected Action<Player>[] sellLimitActions;
protected Action<Player>[] sellAllDenyActions;
protected Action<Player>[] sellAllAllowActions;
protected Action<Player>[] sellAllLimitActions;
private SchedulerTask resetEarningsTask;
private int cachedDate;
private boolean allowBundle;
private boolean allowShulkerBox;
public BukkitMarketManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.priceMap = new HashMap<>();
this.decorativeIcons = new HashMap<>();
this.marketGUICache = new ConcurrentHashMap<>();
this.cachedDate = getRealTimeDate();
}
@Override
public void load() {
this.loadConfig();
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
this.resetEarningsTask = plugin.getScheduler().asyncRepeating(() -> {
int now = getRealTimeDate();
if (this.cachedDate != now) {
this.cachedDate = now;
for (UserData userData : plugin.getStorageManager().getOnlineUsers()) {
userData.earningData().refresh();
}
}
}, 1, 1, TimeUnit.SECONDS);
}
private int getRealTimeDate() {
Calendar calendar = Calendar.getInstance();
return (calendar.get(Calendar.MONTH) +1) * 100 + calendar.get(Calendar.DATE);
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
this.priceMap.clear();
this.decorativeIcons.clear();
if (this.resetEarningsTask != null)
this.resetEarningsTask.cancel();
}
private void loadConfig() {
Section config = BukkitConfigManager.getMainConfig().getSection("mechanics.market");
this.formula = config.getString("price-formula", "{base} + {bonus} * {size}");
this.layout = config.getStringList("layout").toArray(new String[0]);
this.title = TextValue.auto(config.getString("title", "market.title"));
this.itemSlot = config.getString("item-slot.symbol", "I").charAt(0);
this.allowItemWithNoPrice = config.getBoolean("item-slot.allow-items-with-no-price", true);
this.allowBundle = config.getBoolean("allow-bundle", true);
this.allowShulkerBox = config.getBoolean("allow-shulker-box", true);
Section sellAllSection = config.getSection("sell-all-icons");
if (sellAllSection != null) {
this.sellAllSlot = sellAllSection.getString("symbol", "S").charAt(0);
this.sellFishingBag = sellAllSection.getBoolean("fishingbag", true);
this.sellAllIconAllowItem = new GUIItemParser("allow", sellAllSection.getSection("allow-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellAllIconDenyItem = new GUIItemParser("deny", sellAllSection.getSection("deny-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellAllIconLimitItem = new GUIItemParser("limit", sellAllSection.getSection("limit-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellAllAllowActions = plugin.getActionManager().parseActions(sellAllSection.getSection("allow-icon.action"));
this.sellAllDenyActions = plugin.getActionManager().parseActions(sellAllSection.getSection("deny-icon.action"));
this.sellAllLimitActions = plugin.getActionManager().parseActions(sellAllSection.getSection("limit-icon.action"));
}
Section sellSection = config.getSection("sell-icons");
if (sellSection == null) {
// for old config compatibility
sellSection = config.getSection("functional-icons");
}
if (sellSection != null) {
this.sellSlot = sellSection.getString("symbol", "B").charAt(0);
this.sellIconAllowItem = new GUIItemParser("allow", sellSection.getSection("allow-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellIconDenyItem = new GUIItemParser("deny", sellSection.getSection("deny-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellIconLimitItem = new GUIItemParser("limit", sellSection.getSection("limit-icon"), plugin.getConfigManager().getFormatFunctions()).getItem();
this.sellAllowActions = plugin.getActionManager().parseActions(sellSection.getSection("allow-icon.action"));
this.sellDenyActions = plugin.getActionManager().parseActions(sellSection.getSection("deny-icon.action"));
this.sellLimitActions = plugin.getActionManager().parseActions(sellSection.getSection("limit-icon.action"));
}
this.earningsLimit = config.getBoolean("limitation.enable", true) ? MathValue.auto(config.getString("limitation.earnings", "10000")) : MathValue.plain(-1);
// Load item prices from the configuration
Section priceSection = config.getSection("item-price");
if (priceSection != null) {
for (Map.Entry<String, Object> entry : priceSection.getStringRouteMappedValues(false).entrySet()) {
this.priceMap.put(entry.getKey(), MathValue.auto(entry.getValue()));
}
}
// Load decorative icons from the configuration
Section decorativeSection = config.getSection("decorative-icons");
if (decorativeSection != null) {
for (Map.Entry<String, Object> entry : decorativeSection.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
char symbol = Objects.requireNonNull(innerSection.getString("symbol")).charAt(0);
decorativeIcons.put(symbol, new GUIItemParser("gui", innerSection, plugin.getConfigManager().getFormatFunctions()).getItem());
}
}
}
}
/**
* Open the market GUI for a player
*
* @param player player
*/
@Override
public boolean openMarketGUI(Player player) {
Optional<UserData> optionalUserData = plugin.getStorageManager().getOnlineUser(player.getUniqueId());
if (optionalUserData.isEmpty()) {
plugin.getPluginLogger().warn("Player " + player.getName() + "'s market data has not been loaded yet.");
return false;
}
Context<Player> context = Context.player(player);
MarketGUI gui = new MarketGUI(this, context, optionalUserData.get().earningData());
gui.addElement(new MarketGUIElement(itemSlot, new ItemStack(Material.AIR)));
gui.addElement(new MarketDynamicGUIElement(sellSlot, new ItemStack(Material.AIR)));
gui.addElement(new MarketDynamicGUIElement(sellAllSlot, new ItemStack(Material.AIR)));
for (Map.Entry<Character, CustomFishingItem> entry : decorativeIcons.entrySet()) {
gui.addElement(new MarketGUIElement(entry.getKey(), entry.getValue().build(context)));
}
gui.build().refresh().show();
marketGUICache.put(player.getUniqueId(), gui);
return true;
}
/**
* 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))
return;
if (!(event.getInventory().getHolder() instanceof MarketGUIHolder))
return;
MarketGUI gui = marketGUICache.remove(player.getUniqueId());
if (gui != null)
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 = marketGUICache.remove(event.getPlayer().getUniqueId());
if (gui != null)
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())
return;
Inventory inventory = event.getInventory();
if (!(inventory.getHolder() instanceof MarketGUIHolder))
return;
Player player = (Player) event.getWhoClicked();
MarketGUI gui = marketGUICache.get(player.getUniqueId());
if (gui == null) {
event.setCancelled(true);
player.closeInventory();
return;
}
MarketGUIElement element = gui.getElement(itemSlot);
if (element == null) {
event.setCancelled(true);
return;
}
List<Integer> slots = element.getSlots();
for (int dragSlot : event.getRawSlots()) {
if (!slots.contains(dragSlot)) {
event.setCancelled(true);
return;
}
}
plugin.getScheduler().sync().runLater(gui::refresh, 1, player.getLocation());
}
/**
* This method handles inventory click events.
*
* @param event The InventoryClickEvent that triggered this method.
*/
@EventHandler (ignoreCancelled = true)
public void onClickInv(InventoryClickEvent event) {
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;
MarketGUI gui = marketGUICache.get(player.getUniqueId());
if (gui == null) {
event.setCancelled(true);
player.closeInventory();
return;
}
EarningData earningData = gui.earningData;
earningData.refresh();
double earningLimit = earningLimit(gui.context);
if (clickedInv != player.getInventory()) {
int slot = event.getSlot();
MarketGUIElement element = gui.getElement(slot);
if (element == null) {
event.setCancelled(true);
return;
}
if (element.getSymbol() == itemSlot) {
if (!allowItemWithNoPrice) {
if (event.getAction() == InventoryAction.HOTBAR_MOVE_AND_READD || event.getAction() == InventoryAction.HOTBAR_SWAP) {
ItemStack moved = player.getInventory().getItem(event.getHotbarButton());
double price = getItemPrice(gui.context, moved);
if (price <= 0) {
event.setCancelled(true);
return;
}
}
}
} else {
event.setCancelled(true);
}
if (element.getSymbol() == sellSlot) {
Pair<Integer, Double> pair = getItemsToSell(gui.context, gui.getItemsInGUI());
double totalWorth = pair.right();
gui.context.arg(ContextKeys.MONEY, money(totalWorth))
.arg(ContextKeys.MONEY_FORMATTED, String.format("%.2f", totalWorth))
.arg(ContextKeys.REST, money(earningLimit - earningData.earnings))
.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)))
.arg(ContextKeys.SOLD_ITEM_AMOUNT, pair.left());
if (totalWorth > 0) {
if (earningLimit != -1 && (earningLimit - earningData.earnings) < totalWorth) {
// Can't earn more money
ActionManager.trigger(gui.context, sellLimitActions);
} else {
// Clear items and update earnings
clearWorthyItems(gui.context, gui.getItemsInGUI());
earningData.earnings += totalWorth;
gui.context.arg(ContextKeys.REST, money(earningLimit - earningData.earnings));
gui.context.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)));
ActionManager.trigger(gui.context, sellAllowActions);
}
} else {
// Nothing to sell
ActionManager.trigger(gui.context, sellDenyActions);
}
} else if (element.getSymbol() == sellAllSlot) {
ArrayList<ItemStack> itemStacksToSell = new ArrayList<>(List.of(gui.context.getHolder().getInventory().getStorageContents()));
if (sellFishingBag) {
Optional<UserData> optionalUserData = BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(gui.context.getHolder().getUniqueId());
optionalUserData.ifPresent(userData -> itemStacksToSell.addAll(List.of(userData.holder().getInventory().getStorageContents())));
}
Pair<Integer, Double> pair = getItemsToSell(gui.context, itemStacksToSell);
double totalWorth = pair.right();
gui.context.arg(ContextKeys.MONEY, money(totalWorth))
.arg(ContextKeys.MONEY_FORMATTED, String.format("%.2f", totalWorth))
.arg(ContextKeys.REST, money(earningLimit - earningData.earnings))
.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)))
.arg(ContextKeys.SOLD_ITEM_AMOUNT, pair.left());
if (totalWorth > 0) {
if (earningLimit != -1 && (earningLimit - earningData.earnings) < totalWorth) {
// Can't earn more money
ActionManager.trigger(gui.context, sellAllLimitActions);
} else {
// Clear items and update earnings
clearWorthyItems(gui.context, itemStacksToSell);
earningData.earnings += totalWorth;
gui.context.arg(ContextKeys.REST, money(earningLimit - earningData.earnings));
gui.context.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)));
ActionManager.trigger(gui.context, sellAllAllowActions);
}
} else {
// Nothing to sell
ActionManager.trigger(gui.context, sellAllDenyActions);
}
}
} else {
// Handle interactions with the player's inventory
ItemStack current = event.getCurrentItem();
if (!allowItemWithNoPrice) {
double price = getItemPrice(gui.context, current);
if (price <= 0) {
event.setCancelled(true);
return;
}
}
if ((event.getClick() == ClickType.SHIFT_LEFT || event.getClick() == ClickType.SHIFT_RIGHT)
&& (current != null && current.getType() != Material.AIR)) {
event.setCancelled(true);
MarketGUIElement element = gui.getElement(itemSlot);
if (element == null) return;
for (int slot : element.getSlots()) {
ItemStack itemStack = gui.inventory.getItem(slot);
if (itemStack != null && itemStack.getType() != Material.AIR) {
if (current.getType() == itemStack.getType()
&& itemStack.getAmount() != itemStack.getType().getMaxStackSize()
&& current.getItemMeta().equals(itemStack.getItemMeta())
) {
int left = itemStack.getType().getMaxStackSize() - itemStack.getAmount();
if (current.getAmount() <= left) {
itemStack.setAmount(itemStack.getAmount() + current.getAmount());
current.setAmount(0);
break;
} else {
current.setAmount(current.getAmount() - left);
itemStack.setAmount(itemStack.getType().getMaxStackSize());
}
}
} else {
gui.inventory.setItem(slot, current.clone());
current.setAmount(0);
break;
}
}
}
}
// Refresh the GUI
plugin.getScheduler().sync().runLater(gui::refresh, 1, player.getLocation());
}
@Override
@SuppressWarnings("UnstableApiUsage")
public double getItemPrice(Context<Player> context, ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return 0;
Item<ItemStack> wrapped = ((BukkitItemFactory) plugin.getItemManager().getFactory()).wrap(itemStack);
double price = (double) wrapped.getTag("Price").orElse(0d);
if (price != 0) {
// If a custom price is defined in the ItemStack's NBT data, use it.
return price * itemStack.getAmount();
}
if (allowBundle && itemStack.getItemMeta() instanceof BundleMeta bundleMeta) {
Pair<Integer, Double> pair = getItemsToSell(context, bundleMeta.getItems());
return pair.right();
}
if (allowShulkerBox && itemStack.getItemMeta() instanceof BlockStateMeta stateMeta) {
if (stateMeta.getBlockState() instanceof ShulkerBox shulkerBox) {
Pair<Integer, Double> pair = getItemsToSell(context, Arrays.stream(shulkerBox.getInventory().getStorageContents()).filter(Objects::nonNull).toList());
return pair.right();
}
}
// If no custom price is defined, attempt to fetch the price from a predefined price map.
String itemID = itemStack.getType().name();
Optional<Integer> optionalCMD = wrapped.customModelData();
if (optionalCMD.isPresent()) {
itemID = itemID + ":" + optionalCMD.get();
}
MathValue<Player> formula = priceMap.get(itemID);
if (formula == null) return 0;
return formula.evaluate(context) * itemStack.getAmount();
}
@Override
public String getFormula() {
return formula;
}
@Override
public double earningLimit(Context<Player> context) {
return earningsLimit.evaluate(context);
}
public Pair<Integer, Double> getItemsToSell(Context<Player> context, List<ItemStack> itemStacks) {
int amount = 0;
double worth = 0d;
for (ItemStack itemStack : itemStacks) {
double price = getItemPrice(context, itemStack);
if (price > 0 && itemStack != null) {
amount += itemStack.getAmount();
worth += price;
}
}
return Pair.of(amount, worth);
}
@SuppressWarnings("UnstableApiUsage")
public void clearWorthyItems(Context<Player> context, List<ItemStack> itemStacks) {
for (ItemStack itemStack : itemStacks) {
double price = getItemPrice(context, itemStack);
if (price > 0 && itemStack != null) {
if (allowBundle && itemStack.getItemMeta() instanceof BundleMeta bundleMeta) {
clearWorthyItems(context, bundleMeta.getItems());
itemStack.setItemMeta(bundleMeta);
continue;
}
if (allowShulkerBox && itemStack.getItemMeta() instanceof BlockStateMeta stateMeta) {
if (stateMeta.getBlockState() instanceof ShulkerBox shulkerBox) {
clearWorthyItems(context, Arrays.stream(shulkerBox.getInventory().getStorageContents()).filter(Objects::nonNull).toList());
stateMeta.setBlockState(shulkerBox);
itemStack.setItemMeta(stateMeta);
continue;
}
}
itemStack.setAmount(0);
}
}
}
protected String money(double money) {
String str = String.format("%.2f", money);
return str.replace(",", ".");
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.market;
import org.bukkit.inventory.ItemStack;
public class MarketDynamicGUIElement extends MarketGUIElement {
public MarketDynamicGUIElement(char symbol, ItemStack itemStack) {
super(symbol, itemStack);
}
public void setItemStack(ItemStack itemStack) {
super.itemStack = itemStack;
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.market;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.market.MarketGUIHolder;
import net.momirealms.customfishing.api.storage.data.EarningData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.bukkit.util.PlayerUtils;
import net.momirealms.customfishing.common.helper.AdventureHelper;
import net.momirealms.customfishing.common.util.Pair;
import net.momirealms.sparrow.heart.SparrowHeart;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@SuppressWarnings("DuplicatedCode")
public class MarketGUI {
private final HashMap<Character, MarketGUIElement> itemsCharMap;
private final HashMap<Integer, MarketGUIElement> itemsSlotMap;
private final BukkitMarketManager manager;
protected final Inventory inventory;
protected final Context<Player> context;
protected final EarningData earningData;
public MarketGUI(BukkitMarketManager manager, Context<Player> context, EarningData earningData) {
this.manager = manager;
this.context = context;
this.earningData = earningData;
this.itemsCharMap = new HashMap<>();
this.itemsSlotMap = new HashMap<>();
var holder = new MarketGUIHolder();
this.inventory = Bukkit.createInventory(holder, manager.layout.length * 9);
holder.setInventory(this.inventory);
}
private void init() {
int line = 0;
for (String content : manager.layout) {
for (int index = 0; index < 9; index++) {
char symbol;
if (index < content.length()) symbol = content.charAt(index);
else symbol = ' ';
MarketGUIElement element = itemsCharMap.get(symbol);
if (element != null) {
element.addSlot(index + line * 9);
itemsSlotMap.put(index + line * 9, element);
}
}
line++;
}
for (Map.Entry<Integer, MarketGUIElement> entry : itemsSlotMap.entrySet()) {
this.inventory.setItem(entry.getKey(), entry.getValue().getItemStack().clone());
}
}
@SuppressWarnings("UnusedReturnValue")
public MarketGUI addElement(MarketGUIElement... elements) {
for (MarketGUIElement element : elements) {
itemsCharMap.put(element.getSymbol(), element);
}
return this;
}
public MarketGUI build() {
init();
return this;
}
public void show() {
context.getHolder().openInventory(inventory);
SparrowHeart.getInstance().updateInventoryTitle(context.getHolder(), AdventureHelper.componentToJson(AdventureHelper.miniMessage(manager.title.render(context))));
}
@Nullable
public MarketGUIElement getElement(int slot) {
return itemsSlotMap.get(slot);
}
@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 earningLimit = manager.earningLimit(context);
MarketDynamicGUIElement sellElement = (MarketDynamicGUIElement) getElement(manager.sellSlot);
if (sellElement != null && !sellElement.getSlots().isEmpty()) {
Pair<Integer, Double> pair = manager.getItemsToSell(context, getItemsInGUI());
double totalWorth = pair.right();
int soldAmount = pair.left();
context.arg(ContextKeys.MONEY, manager.money(totalWorth))
.arg(ContextKeys.MONEY_FORMATTED, String.format("%.2f", totalWorth))
.arg(ContextKeys.REST, manager.money(earningLimit - earningData.earnings))
.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)))
.arg(ContextKeys.SOLD_ITEM_AMOUNT, soldAmount);
if (totalWorth <= 0) {
sellElement.setItemStack(manager.sellIconDenyItem.build(context));
} else if (earningLimit != -1 && (earningLimit - earningData.earnings < totalWorth)) {
sellElement.setItemStack(manager.sellIconLimitItem.build(context));
} else {
sellElement.setItemStack(manager.sellIconAllowItem.build(context));
}
}
MarketDynamicGUIElement sellAllElement = (MarketDynamicGUIElement) getElement(manager.sellAllSlot);
if (sellAllElement != null && !sellAllElement.getSlots().isEmpty()) {
ArrayList<ItemStack> itemStacksToSell = new ArrayList<>(List.of(context.getHolder().getInventory().getStorageContents()));
if (manager.sellFishingBag) {
Optional<UserData> optionalUserData = BukkitCustomFishingPlugin.getInstance().getStorageManager().getOnlineUser(context.getHolder().getUniqueId());
optionalUserData.ifPresent(userData -> itemStacksToSell.addAll(List.of(userData.holder().getInventory().getStorageContents())));
}
Pair<Integer, Double> pair = manager.getItemsToSell(context, itemStacksToSell);
double totalWorth = pair.right();
int soldAmount = pair.left();
context.arg(ContextKeys.MONEY, manager.money(totalWorth))
.arg(ContextKeys.MONEY_FORMATTED, String.format("%.2f", totalWorth))
.arg(ContextKeys.REST, manager.money(earningLimit - earningData.earnings))
.arg(ContextKeys.REST_FORMATTED, String.format("%.2f", (earningLimit - earningData.earnings)))
.arg(ContextKeys.SOLD_ITEM_AMOUNT, soldAmount);
if (totalWorth <= 0) {
sellAllElement.setItemStack(manager.sellAllIconAllowItem.build(context));
} else if (earningLimit != -1 && (earningLimit - earningData.earnings < totalWorth)) {
sellAllElement.setItemStack(manager.sellAllIconLimitItem.build(context));
} else {
sellAllElement.setItemStack(manager.sellAllIconAllowItem.build(context));
}
}
for (Map.Entry<Integer, MarketGUIElement> entry : itemsSlotMap.entrySet()) {
if (entry.getValue() instanceof MarketDynamicGUIElement dynamicGUIElement) {
this.inventory.setItem(entry.getKey(), dynamicGUIElement.getItemStack().clone());
}
}
return this;
}
public List<ItemStack> getItemsInGUI() {
MarketGUIElement itemElement = getElement(manager.itemSlot);
if (itemElement == null) return List.of();
return itemElement.getSlots().stream().map(inventory::getItem).filter(Objects::nonNull).toList();
}
public int getEmptyItemSlot() {
MarketGUIElement itemElement = getElement(manager.itemSlot);
if (itemElement == null) {
return -1;
}
for (int slot : itemElement.getSlots()) {
ItemStack itemStack = inventory.getItem(slot);
if (itemStack == null || itemStack.getType() == Material.AIR) {
return slot;
}
}
return -1;
}
public void returnItems() {
MarketGUIElement itemElement = getElement(manager.itemSlot);
if (itemElement == null) {
return;
}
for (int slot : itemElement.getSlots()) {
ItemStack itemStack = inventory.getItem(slot);
if (itemStack != null && itemStack.getType() != Material.AIR) {
PlayerUtils.giveItem(context.getHolder(), itemStack, itemStack.getAmount());
inventory.setItem(slot, new ItemStack(Material.AIR));
}
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.market;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
public class MarketGUIElement {
private final char symbol;
private final List<Integer> slots;
protected ItemStack itemStack;
public MarketGUIElement(char symbol, ItemStack itemStack) {
this.symbol = symbol;
this.itemStack = itemStack;
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.clone();
}
// Getter method to retrieve the list of slots where this element can appear
public List<Integer> getSlots() {
return slots;
}
}

View File

@@ -0,0 +1,52 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* 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.
*/
package net.momirealms.customfishing.bukkit.scheduler;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.bukkit.scheduler.impl.BukkitExecutor;
import net.momirealms.customfishing.bukkit.scheduler.impl.FoliaExecutor;
import net.momirealms.customfishing.common.helper.VersionHelper;
import net.momirealms.customfishing.common.plugin.scheduler.AbstractJavaScheduler;
import net.momirealms.customfishing.common.plugin.scheduler.RegionExecutor;
import org.bukkit.Location;
public class BukkitSchedulerAdapter extends AbstractJavaScheduler<Location> {
protected RegionExecutor<Location> sync;
public BukkitSchedulerAdapter(BukkitCustomFishingPlugin plugin) {
super(plugin);
if (VersionHelper.isFolia()) {
this.sync = new FoliaExecutor(plugin.getBoostrap());
} else {
this.sync = new BukkitExecutor(plugin.getBoostrap());
}
}
@Override
public RegionExecutor<Location> sync() {
return this.sync;
}
}

View File

@@ -0,0 +1,64 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* 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.
*/
package net.momirealms.customfishing.bukkit.scheduler.impl;
import net.momirealms.customfishing.common.plugin.scheduler.RegionExecutor;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.plugin.Plugin;
public class BukkitExecutor implements RegionExecutor<Location> {
private final Plugin plugin;
public BukkitExecutor(Plugin plugin) {
this.plugin = plugin;
}
@Override
public void run(Runnable r, Location l) {
Bukkit.getScheduler().runTask(plugin, r);
}
@Override
public SchedulerTask runLater(Runnable r, long delayTicks, Location l) {
if (delayTicks == 0) {
if (Bukkit.isPrimaryThread()) {
r.run();
return () -> {};
} else {
return Bukkit.getScheduler().runTask(plugin, r)::cancel;
}
}
return Bukkit.getScheduler().runTaskLater(plugin, r, delayTicks)::cancel;
}
@Override
public SchedulerTask runRepeating(Runnable r, long delayTicks, long period, Location l) {
return Bukkit.getScheduler().runTaskTimer(plugin, r, delayTicks, period)::cancel;
}
}

View File

@@ -0,0 +1,74 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* 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.
*/
package net.momirealms.customfishing.bukkit.scheduler.impl;
import net.momirealms.customfishing.common.plugin.scheduler.RegionExecutor;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.plugin.Plugin;
import java.util.Optional;
public class FoliaExecutor implements RegionExecutor<Location> {
private final Plugin plugin;
public FoliaExecutor(Plugin plugin) {
this.plugin = plugin;
}
@Override
public void run(Runnable r, Location l) {
Optional.ofNullable(l).ifPresentOrElse(loc -> Bukkit.getRegionScheduler().execute(plugin, loc, r), () -> Bukkit.getGlobalRegionScheduler().execute(plugin, r));
}
@Override
public SchedulerTask runLater(Runnable r, long delayTicks, Location l) {
if (l == null) {
if (delayTicks == 0) {
return Bukkit.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> r.run(), delayTicks)::cancel;
} else {
return Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> r.run())::cancel;
}
} else {
if (delayTicks == 0) {
return Bukkit.getRegionScheduler().run(plugin, l, scheduledTask -> r.run())::cancel;
} else {
return Bukkit.getRegionScheduler().runDelayed(plugin, l, scheduledTask -> r.run(), delayTicks)::cancel;
}
}
}
@Override
public SchedulerTask runRepeating(Runnable r, long delayTicks, long period, Location l) {
if (l == null) {
return Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> r.run(), delayTicks, period)::cancel;
} else {
return Bukkit.getRegionScheduler().runAtFixedRate(plugin, l, scheduledTask -> r.run(), delayTicks, period)::cancel;
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* 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.
*/
package net.momirealms.customfishing.bukkit.sender;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.Component;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.common.sender.Sender;
import net.momirealms.customfishing.common.sender.SenderFactory;
import net.momirealms.customfishing.common.util.Tristate;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.RemoteConsoleCommandSender;
import org.bukkit.entity.Player;
import java.util.UUID;
public class BukkitSenderFactory extends SenderFactory<BukkitCustomFishingPlugin, CommandSender> {
private final BukkitAudiences audiences;
public BukkitSenderFactory(BukkitCustomFishingPlugin plugin) {
super(plugin);
this.audiences = BukkitAudiences.create(plugin.getBoostrap());
}
@Override
protected String getName(CommandSender sender) {
if (sender instanceof Player) {
return sender.getName();
}
return Sender.CONSOLE_NAME;
}
@Override
protected UUID getUniqueId(CommandSender sender) {
if (sender instanceof Player) {
return ((Player) sender).getUniqueId();
}
return Sender.CONSOLE_UUID;
}
@Override
public Audience getAudience(CommandSender sender) {
return this.audiences.sender(sender);
}
@Override
protected void sendMessage(CommandSender sender, Component message) {
// we can safely send async for players and the console - otherwise, send it sync
if (sender instanceof Player || sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) {
getAudience(sender).sendMessage(message);
} else {
getPlugin().getScheduler().executeSync(() -> getAudience(sender).sendMessage(message));
}
}
@Override
protected Tristate getPermissionValue(CommandSender sender, String node) {
if (sender.hasPermission(node)) {
return Tristate.TRUE;
} else if (sender.isPermissionSet(node)) {
return Tristate.FALSE;
} else {
return Tristate.UNDEFINED;
}
}
@Override
protected boolean hasPermission(CommandSender sender, String node) {
return sender.hasPermission(node);
}
@Override
protected void performCommand(CommandSender sender, String command) {
getPlugin().getBoostrap().getServer().dispatchCommand(sender, command);
}
@Override
protected boolean isConsole(CommandSender sender) {
return sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender;
}
@Override
public void close() {
super.close();
this.audiences.close();
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.statistic;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.statistic.StatisticsManager;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.*;
public class BukkitStatisticsManager implements StatisticsManager {
private final BukkitCustomFishingPlugin plugin;
private final Map<String, List<String>> categoryMap = new HashMap<>();
public BukkitStatisticsManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void load() {
this.loadCategoriesFromPluginFolder();
for (Map.Entry<String, List<String>> entry : categoryMap.entrySet()) {
plugin.debug("Category: {" + entry.getKey() + "} Members: " + entry.getValue());
}
}
@Override
public void unload() {
this.categoryMap.clear();
}
@SuppressWarnings("DuplicatedCode")
public void loadCategoriesFromPluginFolder() {
Deque<File> fileDeque = new ArrayDeque<>();
for (String type : List.of("category")) {
File typeFolder = new File(plugin.getDataFolder() + File.separator + "contents" + File.separator + type);
if (!typeFolder.exists()) {
if (!typeFolder.mkdirs()) return;
plugin.getBoostrap().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() && subFile.getName().endsWith(".yml")) {
this.loadSingleFile(subFile);
}
}
}
}
}
private void loadSingleFile(File file) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
for (String key : config.getKeys(false)) {
categoryMap.put(key, config.getStringList(key));
}
}
@NotNull
@Override
public List<String> getCategoryMembers(String key) {
return categoryMap.getOrDefault(key, List.of());
}
}

View File

@@ -0,0 +1,365 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage;
import com.google.gson.JsonSyntaxException;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.storage.DataStorageProvider;
import net.momirealms.customfishing.api.storage.StorageManager;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.bukkit.storage.method.database.nosql.MongoDBProvider;
import net.momirealms.customfishing.bukkit.storage.method.database.nosql.RedisManager;
import net.momirealms.customfishing.bukkit.storage.method.database.sql.H2Provider;
import net.momirealms.customfishing.bukkit.storage.method.database.sql.MariaDBProvider;
import net.momirealms.customfishing.bukkit.storage.method.database.sql.MySQLProvider;
import net.momirealms.customfishing.bukkit.storage.method.database.sql.SQLiteProvider;
import net.momirealms.customfishing.bukkit.storage.method.file.JsonProvider;
import net.momirealms.customfishing.bukkit.storage.method.file.YAMLProvider;
import net.momirealms.customfishing.common.helper.GsonHelper;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class BukkitStorageManager implements StorageManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private DataStorageProvider dataSource;
private StorageType previousType;
private final ConcurrentHashMap<UUID, UserData> onlineUserMap;
private final HashSet<UUID> locked;
private boolean hasRedis;
private RedisManager redisManager;
private String serverID;
private SchedulerTask timerSaveTask;
public BukkitStorageManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
this.locked = new HashSet<>();
this.onlineUserMap = new ConcurrentHashMap<>();
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
}
@Override
public void reload() {
YamlDocument config = plugin.getConfigManager().loadConfig("database.yml");
this.serverID = 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();
this.previousType = storageType;
switch (storageType) {
case H2 -> this.dataSource = new H2Provider(plugin);
case JSON -> this.dataSource = new JsonProvider(plugin);
case YAML -> this.dataSource = new YAMLProvider(plugin);
case SQLite -> this.dataSource = new SQLiteProvider(plugin);
case MySQL -> this.dataSource = new MySQLProvider(plugin);
case MariaDB -> this.dataSource = new MariaDBProvider(plugin);
case MongoDB -> this.dataSource = new MongoDBProvider(plugin);
}
if (this.dataSource != null) this.dataSource.initialize(config);
else plugin.getPluginLogger().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(config);
}
// 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.cancel();
}
// Schedule periodic data saving if dataSaveInterval is configured
if (ConfigManager.dataSaveInterval() > 0)
this.timerSaveTask = this.plugin.getScheduler().asyncRepeating(
() -> {
long time1 = System.currentTimeMillis();
this.dataSource.updateManyPlayersData(this.onlineUserMap.values(), !ConfigManager.lockData());
if (ConfigManager.logDataSaving())
plugin.getPluginLogger().info("Data Saved for online players. Took " + (System.currentTimeMillis() - time1) + "ms.");
},
ConfigManager.dataSaveInterval(),
ConfigManager.dataSaveInterval(),
TimeUnit.SECONDS
);
}
/**
* Disables the storage manager and cleans up resources.
*/
@Override
public void disable() {
HandlerList.unregisterAll(this);
this.dataSource.updateManyPlayersData(onlineUserMap.values(), true);
this.onlineUserMap.clear();
if (this.dataSource != null)
this.dataSource.disable();
if (this.redisManager != null)
this.redisManager.disable();
}
@NotNull
@Override
public String getServerID() {
return serverID;
}
@NotNull
@Override
public Optional<UserData> getOnlineUser(UUID uuid) {
return Optional.ofNullable(onlineUserMap.get(uuid));
}
@NotNull
@Override
public Collection<UserData> getOnlineUsers() {
return onlineUserMap.values();
}
@Override
public CompletableFuture<Optional<UserData>> getOfflineUserData(UUID uuid, boolean lock) {
CompletableFuture<Optional<PlayerData>> optionalDataFuture = dataSource.getPlayerData(uuid, lock);
return optionalDataFuture.thenCompose(optionalUser -> {
if (optionalUser.isEmpty()) {
return CompletableFuture.completedFuture(Optional.empty());
}
PlayerData data = optionalUser.get();
return CompletableFuture.completedFuture(Optional.of(UserData.builder()
.data(data)
.build()));
});
}
@Override
public CompletableFuture<Boolean> saveUserData(UserData userData, boolean unlock) {
return dataSource.updatePlayerData(userData.uuid(), userData.toPlayerData(), unlock);
}
@NotNull
@Override
public DataStorageProvider 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();
UUID uuid = player.getUniqueId();
locked.add(uuid);
if (!hasRedis) {
waitLock(uuid, 1);
} else {
plugin.getScheduler().asyncLater(() -> redisManager.getChangeServer(uuid).thenAccept(changeServer -> {
if (!changeServer) {
waitLock(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();
UUID uuid = player.getUniqueId();
if (locked.contains(uuid))
return;
UserData onlineUser = onlineUserMap.remove(uuid);
if (onlineUser == null) return;
PlayerData data = onlineUser.toPlayerData();
if (hasRedis) {
redisManager.setChangeServer(uuid).thenRun(
() -> redisManager.updatePlayerData(uuid, data, true).thenRun(
() -> dataSource.updatePlayerData(uuid, data, true).thenAccept(
result -> {
if (result) locked.remove(uuid);
})));
} else {
dataSource.updatePlayerData(uuid, data, true).thenAccept(
result -> {
if (result) locked.remove(uuid);
});
}
}
/**
* Runnable task for asynchronously retrieving data from Redis.
* Retries up to 6 times and cancels the task if the player is offline.
*/
private class RedisGetDataTask implements Runnable {
private final UUID uuid;
private int triedTimes;
private final SchedulerTask task;
public RedisGetDataTask(UUID uuid) {
this.uuid = uuid;
this.task = plugin.getScheduler().asyncRepeating(this, 0, 333, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
triedTimes++;
Player player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline()) {
// offline
task.cancel();
return;
}
if (triedTimes >= 6) {
waitLock(uuid, 3);
return;
}
redisManager.getPlayerData(uuid, false).thenAccept(optionalData -> {
if (optionalData.isPresent()) {
addOnlineUser(player, optionalData.get());
task.cancel();
if (ConfigManager.lockData()) dataSource.lockOrUnlockPlayerData(uuid, true);
}
});
}
}
/**
* 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.
*/
private void waitLock(UUID uuid, int times) {
plugin.getScheduler().asyncLater(() -> {
var player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline())
return;
if (times > 3) {
plugin.getPluginLogger().warn("Tried 3 times when getting data for " + uuid + ". Giving up.");
return;
}
this.dataSource.getPlayerData(uuid, ConfigManager.lockData()).thenAccept(optionalData -> {
// Data should not be empty
if (optionalData.isEmpty()) {
plugin.getPluginLogger().severe("Unexpected error: Data is null");
return;
}
if (optionalData.get().locked()) {
waitLock(uuid, times + 1);
} else {
try {
addOnlineUser(player, optionalData.get());
} catch (Exception e) {
plugin.getPluginLogger().severe("Unexpected error: " + e.getMessage(), e);
}
}
});
}, 1, TimeUnit.SECONDS);
}
private void addOnlineUser(Player player, PlayerData playerData) {
this.locked.remove(player.getUniqueId());
this.onlineUserMap.put(player.getUniqueId(), UserData.builder()
.data(playerData)
// update the name
.name(player.getName())
.build());
}
@Override
public boolean isRedisEnabled() {
return hasRedis;
}
@Nullable
public RedisManager getRedisManager() {
return redisManager;
}
@NotNull
@Override
public byte[] toBytes(@NotNull PlayerData data) {
return toJson(data).getBytes(StandardCharsets.UTF_8);
}
@Override
@NotNull
public String toJson(@NotNull PlayerData data) {
return GsonHelper.get().toJson(data);
}
@NotNull
@Override
public PlayerData fromJson(String json) {
try {
return GsonHelper.get().fromJson(json, PlayerData.class);
} catch (JsonSyntaxException e) {
plugin.getPluginLogger().severe("Failed to parse PlayerData from json");
plugin.getPluginLogger().info("Json: " + json);
throw new RuntimeException(e);
}
}
@Override
@NotNull
public PlayerData fromBytes(byte[] data) {
return fromJson(new String(data, StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.DataStorageProvider;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.user.UserData;
import java.time.Instant;
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 DataStorageProvider {
protected BukkitCustomFishingPlugin plugin;
public AbstractStorage(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void initialize(YamlDocument config) {
// 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 UserData> users, boolean unlock) {
for (UserData user : users) {
this.updatePlayerData(user.uuid(), user.toPlayerData(), unlock);
}
}
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

@@ -0,0 +1,217 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.nosql;
import com.mongodb.*;
import com.mongodb.client.*;
import com.mongodb.client.model.*;
import com.mongodb.client.result.UpdateResult;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.bukkit.storage.method.AbstractStorage;
import org.bson.Document;
import org.bson.UuidRepresentation;
import org.bson.conversions.Bson;
import org.bson.types.Binary;
import org.bukkit.Bukkit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class MongoDBProvider extends AbstractStorage {
private MongoClient mongoClient;
private MongoDatabase database;
private String collectionPrefix;
public MongoDBProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
}
@Override
public void initialize(YamlDocument config) {
Section section = config.getSection("MongoDB");
if (section == null) {
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
return;
}
collectionPrefix = section.getString("collection-prefix", "customfishing");
var settings = MongoClientSettings.builder().uuidRepresentation(UuidRepresentation.STANDARD);
if (!section.getString("connection-uri", "").equals("")) {
settings.applyConnectionString(new ConnectionString(section.getString("connection-uri", "")));
mongoClient = MongoClients.create(settings.build());
return;
}
if (section.contains("user")) {
MongoCredential credential = MongoCredential.createCredential(
section.getString("user", "root"),
section.getString("database", "minecraft"),
section.getString("password", "password").toCharArray()
);
settings.credential(credential);
}
settings.applyToClusterSettings(builder -> builder.hosts(Collections.singletonList(new ServerAddress(
section.getString("host", "localhost"),
section.getInt("port", 27017)
))));
this.mongoClient = MongoClients.create(settings.build());
this.database = mongoClient.getDatabase(section.getString("database", "minecraft"));
}
@Override
public void disable() {
if (this.mongoClient != null) {
this.mongoClient.close();
}
}
/**
* 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;
}
@Override
public StorageType getStorageType() {
return StorageType.MongoDB;
}
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
var future = new CompletableFuture<Optional<PlayerData>>();
plugin.getScheduler().async().execute(() -> {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
Document doc = collection.find(Filters.eq("uuid", uuid)).first();
if (doc == null) {
if (Bukkit.getPlayer(uuid) != null) {
if (lock) lockOrUnlockPlayerData(uuid, true);
var data = PlayerData.empty();
data.uuid(uuid);
future.complete(Optional.of(data));
} else {
future.complete(Optional.empty());
}
} else {
Binary binary = (Binary) doc.get("data");
PlayerData data = plugin.getStorageManager().fromBytes(binary.getData());
data.uuid(uuid);
if (doc.getInteger("lock") != 0 && getCurrentSeconds() - ConfigManager.dataSaveInterval() <= doc.getInteger("lock")) {
data.locked(true);
future.complete(Optional.of(data));
return;
}
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(data));
}
});
return future;
}
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().async().execute(() -> {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
Document query = new Document("uuid", uuid);
Bson updates = Updates.combine(
Updates.set("lock", unlock ? 0 : getCurrentSeconds()),
Updates.set("data", new Binary(plugin.getStorageManager().toBytes(playerData))));
UpdateOptions options = new UpdateOptions().upsert(true);
UpdateResult result = collection.updateOne(query, updates, options);
future.complete(result.wasAcknowledged());
} catch (MongoException e) {
future.completeExceptionally(e);
}
});
return future;
}
@Override
public void updateManyPlayersData(Collection<? extends UserData> users, boolean unlock) {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
int lock = unlock ? 0 : getCurrentSeconds();
var list = users.stream().map(it -> new UpdateOneModel<Document>(
new Document("uuid", it.uuid()),
Updates.combine(
Updates.set("lock", lock),
Updates.set("data", new Binary(plugin.getStorageManager().toBytes(it.toPlayerData())))
),
new UpdateOptions().upsert(true)
)
).toList();
if (list.isEmpty()) return;
collection.bulkWrite(list);
} catch (MongoException e) {
plugin.getPluginLogger().warn("Failed to update data for online players", e);
}
}
@Override
public void lockOrUnlockPlayerData(UUID uuid, boolean lock) {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
Document query = new Document("uuid", uuid);
Bson updates = Updates.combine(Updates.set("lock", !lock ? 0 : getCurrentSeconds()));
UpdateOptions options = new UpdateOptions().upsert(true);
collection.updateOne(query, updates, options);
} catch (MongoException e) {
plugin.getPluginLogger().warn("Failed to lock data for " + uuid, e);
}
}
@Override
public Set<UUID> getUniqueUsers() {
// no legacy files
Set<UUID> uuids = new HashSet<>();
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
Bson projectionFields = Projections.fields(Projections.include("uuid"));
try (MongoCursor<Document> cursor = collection.find().projection(projectionFields).iterator()) {
while (cursor.hasNext()) {
uuids.add(cursor.next().get("uuid", UUID.class));
}
}
} catch (MongoException e) {
plugin.getPluginLogger().warn("Failed to get unique data.", e);
}
return uuids;
}
}

View File

@@ -0,0 +1,392 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.nosql;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.bukkit.storage.method.AbstractStorage;
import org.jetbrains.annotations.NotNull;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.params.XReadParams;
import redis.clients.jedis.resps.StreamEntry;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class RedisManager extends AbstractStorage {
private static RedisManager instance;
private final static String STREAM = "customfishing";
private JedisPool jedisPool;
private String password;
private int port;
private String host;
private boolean useSSL;
private BlockingThreadTask threadTask;
private boolean isNewerThan5;
public RedisManager(BukkitCustomFishingPlugin plugin) {
super(plugin);
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(YamlDocument config) {
Section section = config.getSection("Redis");
if (section == null) {
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
return;
}
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setTestWhileIdle(true);
jedisPoolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(30000));
jedisPoolConfig.setNumTestsPerEvictionRun(-1);
jedisPoolConfig.setMinEvictableIdleDuration(Duration.ofMillis(section.getInt("MinEvictableIdleTimeMillis", 1800000)));
jedisPoolConfig.setMaxTotal(section.getInt("MaxTotal",8));
jedisPoolConfig.setMaxIdle(section.getInt("MaxIdle",8));
jedisPoolConfig.setMinIdle(section.getInt("MinIdle",1));
jedisPoolConfig.setMaxWait(Duration.ofMillis(section.getInt("MaxWaitMillis")));
password = section.getString("password", "");
port = section.getInt("port", 6379);
host = section.getString("host", "localhost");
useSSL = section.getBoolean("use-ssl", false);
if (password.isBlank()) {
jedisPool = new JedisPool(jedisPoolConfig, host, port, 0, useSSL);
} else {
jedisPool = new JedisPool(jedisPoolConfig, host, port, 0, password, useSSL);
}
String info;
try (Jedis jedis = jedisPool.getResource()) {
info = jedis.info();
plugin.getPluginLogger().info("Redis server connected.");
} catch (JedisException e) {
plugin.getPluginLogger().warn("Failed to connect redis.", e);
return;
}
String version = parseRedisVersion(info);
if (isRedisNewerThan5(version)) {
// For Redis 5.0+
this.threadTask = new BlockingThreadTask();
this.isNewerThan5 = true;
} else {
// For Redis 2.0+
this.subscribe();
this.isNewerThan5 = false;
}
}
/**
* Disable the Redis connection by closing the JedisPool.
*/
@Override
public void disable() {
if (threadTask != null)
threadTask.stop();
if (jedisPool != null && !jedisPool.isClosed())
jedisPool.close();
}
/**
* Send a message to Redis on a specified channel.
*
* @param message The message to send.
*/
public void publishRedisMessage(@NotNull String message) {
if (isNewerThan5) {
try (Jedis jedis = jedisPool.getResource()) {
HashMap<String, String> messages = new HashMap<>();
messages.put("value", message);
jedis.xadd(getStream(), StreamEntryID.NEW_ENTRY, messages);
}
} else {
try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(getStream(), message);
}
}
}
/**
* 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() ?
new Jedis(host, port, 0, useSSL) :
new Jedis(host, port, DefaultJedisClientConfig
.builder()
.password(password)
.timeoutMillis(0)
.ssl(useSSL)
.build())
) {
jedis.connect();
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
if (!channel.equals(getStream())) {
return;
}
handleMessage(message);
}
}, getStream());
}
});
thread.start();
}
private void handleMessage(String message) {
ByteArrayDataInput input = ByteStreams.newDataInput(message.getBytes(StandardCharsets.UTF_8));
String server = input.readUTF();
if (!ConfigManager.serverGroup().equals(server))
return;
String type = input.readUTF();
if (type.equals("competition")) {
String action = input.readUTF();
switch (action) {
case "start" -> {
plugin.getCompetitionManager().startCompetition(input.readUTF(), true, null);
}
case "end" -> {
if (plugin.getCompetitionManager().getOnGoingCompetition() != null)
plugin.getCompetitionManager().getOnGoingCompetition().end(true);
}
case "stop" -> {
if (plugin.getCompetitionManager().getOnGoingCompetition() != null)
plugin.getCompetitionManager().getOnGoingCompetition().stop(true);
}
}
}
}
@Override
public StorageType getStorageType() {
return StorageType.Redis;
}
/**
* 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().async().execute(() -> {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getRedisKey("cf_server", uuid),
10,
new byte[0]
);
}
future.complete(null);
});
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().async().execute(() -> {
try (Jedis jedis = jedisPool.getResource()) {
byte[] key = getRedisKey("cf_server", uuid);
if (jedis.get(key) != null) {
jedis.del(key);
future.complete(true);
} else {
future.complete(false);
}
}
});
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>>();
plugin.getScheduler().async().execute(() -> {
try (Jedis jedis = jedisPool.getResource()) {
byte[] key = getRedisKey("cf_data", uuid);
byte[] data = jedis.get(key);
jedis.del(key);
if (data != null) {
future.complete(Optional.of(plugin.getStorageManager().fromBytes(data)));
} else {
future.complete(Optional.empty());
}
} catch (Exception e) {
future.complete(Optional.empty());
}
});
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>();
plugin.getScheduler().async().execute(() -> {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getRedisKey("cf_data", uuid),
10,
plugin.getStorageManager().toBytes(playerData)
);
future.complete(true);
} catch (Exception e) {
future.complete(false);
}
});
return future;
}
@Override
public Set<UUID> getUniqueUsers() {
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);
}
public static String getStream() {
return STREAM;
}
private boolean isRedisNewerThan5(String version) {
String[] split = version.split("\\.");
int major = Integer.parseInt(split[0]);
if (major < 7) {
plugin.getPluginLogger().warn(String.format("Detected that you are running an outdated Redis server. v%s. ", version));
plugin.getPluginLogger().warn("It's recommended to update to avoid security vulnerabilities!");
}
return major >= 5;
}
private String parseRedisVersion(String info) {
for (String line : info.split("\n")) {
if (line.startsWith("redis_version:")) {
return line.split(":")[1];
}
}
return "Unknown";
}
public class BlockingThreadTask {
private boolean stopped;
public void stop() {
stopped = true;
}
public BlockingThreadTask() {
Thread thread = new Thread(() -> {
var map = new HashMap<String, StreamEntryID>();
map.put(getStream(), StreamEntryID.LAST_ENTRY);
while (!this.stopped) {
try {
var connection = getJedis();
if (connection != null) {
var messages = connection.xread(XReadParams.xReadParams().count(1).block(2000), map);
connection.close();
if (messages != null && !messages.isEmpty()) {
for (Map.Entry<String, List<StreamEntry>> message : messages) {
if (message.getKey().equals(getStream())) {
var value = message.getValue().get(0).getFields().get("value");
handleMessage(value);
}
}
}
} else {
Thread.sleep(2000);
}
} catch (Exception e) {
plugin.getPluginLogger().warn("Failed to connect redis. Try reconnecting 10s later",e);
try {
Thread.sleep(10000);
} catch (InterruptedException ex) {
this.stopped = true;
}
}
}
});
thread.start();
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
public abstract class AbstractHikariDatabase extends AbstractSQLDatabase {
private HikariDataSource dataSource;
private final String driverClass;
private final String sqlBrand;
public AbstractHikariDatabase(BukkitCustomFishingPlugin plugin) {
super(plugin);
this.driverClass = getStorageType() == StorageType.MariaDB ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
this.sqlBrand = getStorageType() == StorageType.MariaDB ? "MariaDB" : "MySQL";
try {
Class.forName(this.driverClass);
} catch (ClassNotFoundException e1) {
if (getStorageType() == StorageType.MariaDB) {
plugin.getPluginLogger().warn("No MariaDB driver is found");
} else if (getStorageType() == StorageType.MySQL) {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e2) {
plugin.getPluginLogger().warn("No MySQL driver is found");
}
}
}
}
@Override
public void initialize(YamlDocument config) {
Section section = config.getSection(sqlBrand);
if (section == null) {
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
return;
}
super.tablePrefix = section.getString("table-prefix", "customfishing");
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setUsername(section.getString("user", "root"));
hikariConfig.setPassword(section.getString("password", "pa55w0rd"));
hikariConfig.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
sqlBrand.toLowerCase(Locale.ENGLISH),
section.getString("host", "localhost"),
section.getString("port", "3306"),
section.getString("database", "minecraft"),
section.getString("connection-parameters")
));
hikariConfig.setDriverClassName(driverClass);
hikariConfig.setMaximumPoolSize(section.getInt("Pool-Settings.max-pool-size", 10));
hikariConfig.setMinimumIdle(section.getInt("Pool-Settings.min-idle", 10));
hikariConfig.setMaxLifetime(section.getLong("Pool-Settings.max-lifetime", 180000L));
hikariConfig.setConnectionTimeout(section.getLong("Pool-Settings.time-out", 20000L));
hikariConfig.setPoolName("CustomFishingHikariPool");
try {
hikariConfig.setKeepaliveTime(section.getLong("Pool-Settings.keep-alive-time", 60000L));
} catch (NoSuchMethodError ignored) {
}
final Properties properties = new Properties();
properties.putAll(
Map.of("cachePrepStmts", "true",
"prepStmtCacheSize", "250",
"prepStmtCacheSqlLimit", "2048",
"useServerPrepStmts", "true",
"useLocalSessionState", "true",
"useLocalTransactionState", "true"
));
properties.putAll(
Map.of(
"rewriteBatchedStatements", "true",
"cacheResultSetMetadata", "true",
"cacheServerConfiguration", "true",
"elideSetAutoCommits", "true",
"maintainTimeStats", "false")
);
hikariConfig.setDataSourceProperties(properties);
dataSource = new HikariDataSource(hikariConfig);
super.createTableIfNotExist();
}
@Override
public void disable() {
if (dataSource != null && !dataSource.isClosed())
dataSource.close();
}
@Override
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.bukkit.storage.method.AbstractStorage;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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;
public AbstractSQLDatabase(BukkitCustomFishingPlugin plugin) {
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));
try (Statement statement = connection.createStatement()) {
for (String tableCreationStatement : databaseSchema) {
statement.execute(tableCreationStatement);
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to create tables", e);
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get sql connection", e);
} catch (IOException e) {
plugin.getPluginLogger().warn("Failed to get schema resource", e);
}
}
/**
* 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.getBoostrap().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;
}
@SuppressWarnings("DuplicatedCode")
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
var future = new CompletableFuture<Optional<PlayerData>>();
plugin.getScheduler().async().execute(() -> {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("data")))
) {
statement.setString(1, uuid.toString());
ResultSet rs = statement.executeQuery();
if (rs.next()) {
final Blob blob = rs.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
PlayerData data = plugin.getStorageManager().fromBytes(dataByteArray);
data.uuid(uuid);
if (lock) {
int lockValue = rs.getInt(2);
if (lockValue != 0 && getCurrentSeconds() - ConfigManager.dataSaveInterval() <= lockValue) {
connection.close();
data.locked(true);
future.complete(Optional.of(data));
plugin.getPluginLogger().warn("Player " + uuid + "'s data is locked. Retrying...");
return;
}
}
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(data));
} else if (Bukkit.getPlayer(uuid) != null) {
// the player is online
var data = PlayerData.empty();
data.uuid(uuid);
insertPlayerData(uuid, data, lock);
future.complete(Optional.of(data));
} else {
future.complete(Optional.empty());
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get " + uuid + "'s data.", e);
future.completeExceptionally(e);
}
});
return future;
}
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().async().execute(() -> {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data")))
) {
statement.setInt(1, unlock ? 0 : getCurrentSeconds());
statement.setBlob(2, new ByteArrayInputStream(plugin.getStorageManager().toBytes(playerData)));
statement.setString(3, uuid.toString());
statement.executeUpdate();
future.complete(true);
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to update " + uuid + "'s data.", e);
future.completeExceptionally(e);
}
});
return future;
}
@Override
public void updateManyPlayersData(Collection<? extends UserData> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
try (Connection connection = getConnection()) {
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (UserData user : users) {
statement.setInt(1, unlock ? 0 : getCurrentSeconds());
statement.setBlob(2, new ByteArrayInputStream(plugin.getStorageManager().toBytes(user.toPlayerData())));
statement.setString(3, user.uuid().toString());
statement.addBatch();
}
statement.executeBatch();
connection.commit();
} catch (SQLException e) {
connection.rollback();
plugin.getPluginLogger().warn("Failed to update data for online players", e);
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get connection when saving online players' data", e);
}
}
public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data")))
) {
statement.setString(1, uuid.toString());
statement.setInt(2, lock ? getCurrentSeconds() : 0);
statement.setBlob(3, new ByteArrayInputStream(plugin.getStorageManager().toBytes(playerData)));
statement.execute();
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to insert " + uuid + "'s data.", e);
}
}
@Override
public void lockOrUnlockPlayerData(UUID uuid, boolean lock) {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_LOCK_BY_UUID, getTableName("data")))
) {
statement.setInt(1, lock ? getCurrentSeconds() : 0);
statement.setString(2, uuid.toString());
statement.execute();
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to lock " + uuid + "'s data.", e);
}
}
@Override
public CompletableFuture<Boolean> updateOrInsertPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().async().execute(() -> {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("data")))
) {
statement.setString(1, uuid.toString());
ResultSet rs = statement.executeQuery();
if (rs.next()) {
updatePlayerData(uuid, playerData, unlock).thenRun(() -> future.complete(true));
} else {
insertPlayerData(uuid, playerData, !unlock);
future.complete(true);
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get " + uuid + "'s data.", e);
}
});
return future;
}
@Override
public Set<UUID> getUniqueUsers() {
Set<UUID> uuids = new HashSet<>();
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_ALL_UUID, getTableName("data")))) {
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
UUID uuid = UUID.fromString(rs.getString("uuid"));
uuids.add(uuid);
}
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get unique data.", e);
}
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`";
public static final String SQL_UPDATE_BY_UUID = "UPDATE `%s` SET `lock` = ?, `data` = ? WHERE `uuid` = ?";
public static final String SQL_LOCK_BY_UUID = "UPDATE `%s` SET `lock` = ? WHERE `uuid` = ?";
public static final String SQL_INSERT_DATA_BY_UUID = "INSERT INTO `%s`(`uuid`, `lock`, `data`) VALUES(?, ?, ?)";
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.common.dependency.Dependency;
import java.io.File;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.EnumSet;
/**
* An implementation of AbstractSQLDatabase that uses the H2 embedded database for player data storage.
*/
public class H2Provider extends AbstractSQLDatabase {
private Object connectionPool;
private Method disposeMethod;
private Method getConnectionMethod;
public H2Provider(BukkitCustomFishingPlugin plugin) {
super(plugin);
}
/**
* Initialize the H2 database and connection pool based on the configuration.
*/
@Override
public void initialize(YamlDocument config) {
File databaseFile = new File(plugin.getDataFolder(), config.getString("H2.file", "data.db"));
super.tablePrefix = config.getString("H2.table-prefix", "customfishing");
final String url = String.format("jdbc:h2:%s", databaseFile.getAbsolutePath());
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER));
try {
Class<?> connectionClass = classLoader.loadClass("org.h2.jdbcx.JdbcConnectionPool");
Method createPoolMethod = connectionClass.getMethod("create", String.class, String.class, String.class);
this.connectionPool = createPoolMethod.invoke(null, url, "sa", "");
this.disposeMethod = connectionClass.getMethod("dispose");
this.getConnectionMethod = connectionClass.getMethod("getConnection");
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
super.createTableIfNotExist();
}
@Override
public void disable() {
if (connectionPool != null) {
try {
disposeMethod.invoke(connectionPool);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
@Override
public StorageType getStorageType() {
return StorageType.H2;
}
@Override
public Connection getConnection() {
try {
return (Connection) getConnectionMethod.invoke(connectionPool);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
public class MariaDBProvider extends AbstractHikariDatabase {
public MariaDBProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
}
@Override
public StorageType getStorageType() {
return StorageType.MariaDB;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
public class MySQLProvider extends AbstractHikariDatabase {
public MySQLProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
}
@Override
public StorageType getStorageType() {
return StorageType.MySQL;
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.database.sql;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.user.UserData;
import net.momirealms.customfishing.common.dependency.Dependency;
import org.bukkit.Bukkit;
import java.io.File;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class SQLiteProvider extends AbstractSQLDatabase {
private Connection connection;
private File databaseFile;
private Constructor<?> connectionConstructor;
public SQLiteProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
}
@Override
public void initialize(YamlDocument config) {
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER, Dependency.SLF4J_SIMPLE, Dependency.SLF4J_API));
try {
Class<?> connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection");
connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
this.databaseFile = new File(plugin.getDataFolder(), config.getString("SQLite.file", "data") + ".db");
super.tablePrefix = config.getString("SQLite.table-prefix", "customfishing");
super.createTableIfNotExist();
}
@Override
public void disable() {
try {
if (connection != null && !connection.isClosed())
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public StorageType getStorageType() {
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()) {
return connection;
}
try {
var properties = new Properties();
properties.setProperty("foreign_keys", Boolean.toString(true));
properties.setProperty("encoding", "'UTF-8'");
properties.setProperty("synchronous", "FULL");
connection = (Connection) this.connectionConstructor.newInstance("jdbc:sqlite:" + databaseFile.toString(), databaseFile.toString(), properties);
return connection;
} catch (ReflectiveOperationException e) {
if (e.getCause() instanceof SQLException) {
throw (SQLException) e.getCause();
}
throw new RuntimeException(e);
}
}
@SuppressWarnings("DuplicatedCode")
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
var future = new CompletableFuture<Optional<PlayerData>>();
plugin.getScheduler().async().execute(() -> {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("data")))
) {
statement.setString(1, uuid.toString());
ResultSet rs = statement.executeQuery();
if (rs.next()) {
final byte[] dataByteArray = rs.getBytes("data");
PlayerData data = plugin.getStorageManager().fromBytes(dataByteArray);
data.uuid(uuid);
int lockValue = rs.getInt(2);
if (lockValue != 0 && getCurrentSeconds() - ConfigManager.dataSaveInterval() <= lockValue) {
connection.close();
data.locked(true);
future.complete(Optional.of(data));
return;
}
if (lock) lockOrUnlockPlayerData(uuid, true);
future.complete(Optional.of(data));
} else if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
data.uuid(uuid);
insertPlayerData(uuid, data, lock);
future.complete(Optional.of(data));
} else {
future.complete(Optional.empty());
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get " + uuid + "'s data.", e);
future.completeExceptionally(e);
}
});
return future;
}
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().async().execute(() -> {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data")))
) {
statement.setInt(1, unlock ? 0 : getCurrentSeconds());
statement.setBytes(2, plugin.getStorageManager().toBytes(playerData));
statement.setString(3, uuid.toString());
statement.executeUpdate();
future.complete(true);
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to update " + uuid + "'s data.", e);
future.completeExceptionally(e);
}
});
return future;
}
@Override
public void updateManyPlayersData(Collection<? extends UserData> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
try (Connection connection = getConnection()) {
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (UserData user : users) {
statement.setInt(1, unlock ? 0 : getCurrentSeconds());
statement.setBytes(2, plugin.getStorageManager().toBytes(user.toPlayerData()));
statement.setString(3, user.uuid().toString());
statement.addBatch();
}
statement.executeBatch();
connection.commit();
} catch (SQLException e) {
connection.rollback();
plugin.getPluginLogger().warn("Failed to update bag data for online players", e);
}
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to get connection when saving online players' data", e);
}
}
@Override
public void insertPlayerData(UUID uuid, PlayerData playerData, boolean lock) {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data")))
) {
statement.setString(1, uuid.toString());
statement.setInt(2, lock ? getCurrentSeconds() : 0);
statement.setBytes(3, plugin.getStorageManager().toBytes(playerData));
statement.execute();
} catch (SQLException e) {
plugin.getPluginLogger().warn("Failed to insert " + uuid + "'s data.", e);
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.file;
import com.google.gson.Gson;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.bukkit.storage.method.AbstractStorage;
import org.bukkit.Bukkit;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Optional;
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 JsonProvider extends AbstractStorage {
@SuppressWarnings("ResultOfMethodCallIgnored")
public JsonProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
File folder = new File(plugin.getDataFolder(), "data");
if (!folder.exists()) folder.mkdirs();
}
@Override
public StorageType getStorageType() {
return StorageType.JSON;
}
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
File file = getPlayerDataFile(uuid);
PlayerData playerData;
if (file.exists()) {
playerData = readFromJsonFile(file, PlayerData.class);
} else if (Bukkit.getPlayer(uuid) != null) {
playerData = PlayerData.empty();
playerData.uuid(uuid);
} else {
playerData = null;
}
return CompletableFuture.completedFuture(Optional.ofNullable(playerData));
}
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
this.saveToJsonFile(playerData, getPlayerDataFile(uuid));
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)) {
gson.toJson(obj, file);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 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)) {
fis.read(fileBytes);
} catch (IOException e) {
e.printStackTrace();
}
return fileBytes;
}
// Retrieve a set of unique user UUIDs based on JSON data files in the 'data' folder.
@Override
public Set<UUID> getUniqueUsers() {
// No legacy files
File folder = new File(plugin.getDataFolder(), "data");
Set<UUID> uuids = new HashSet<>();
if (folder.exists()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
uuids.add(UUID.fromString(file.getName().substring(file.getName().length() - 5)));
}
}
}
return uuids;
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.storage.method.file;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.storage.StorageType;
import net.momirealms.customfishing.api.storage.data.EarningData;
import net.momirealms.customfishing.api.storage.data.InventoryData;
import net.momirealms.customfishing.api.storage.data.PlayerData;
import net.momirealms.customfishing.api.storage.data.StatisticData;
import net.momirealms.customfishing.bukkit.storage.method.AbstractStorage;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public class YAMLProvider extends AbstractStorage {
@SuppressWarnings("ResultOfMethodCallIgnored")
public YAMLProvider(BukkitCustomFishingPlugin plugin) {
super(plugin);
File folder = new File(plugin.getDataFolder(), "data");
if (!folder.exists()) folder.mkdirs();
}
@Override
public StorageType getStorageType() {
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");
}
@Override
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean lock) {
File dataFile = getPlayerDataFile(uuid);
if (!dataFile.exists()) {
if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
data.uuid(uuid);
return CompletableFuture.completedFuture(Optional.of(data));
} else {
return CompletableFuture.completedFuture(Optional.empty());
}
}
YamlDocument data = plugin.getConfigManager().loadData(dataFile);
PlayerData playerData = PlayerData.builder()
.bag(new InventoryData(data.getString("bag", ""), data.getInt("size", 9)))
.earnings(new EarningData(data.getDouble("earnings"), data.getInt("date")))
.statistics(getStatistics(data.getSection("stats")))
.name(data.getString("name", ""))
.build();
return CompletableFuture.completedFuture(Optional.of(playerData));
}
@Override
public CompletableFuture<Boolean> updatePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
YamlConfiguration data = new YamlConfiguration();
data.set("name", playerData.name());
data.set("bag", playerData.bagData().serialized);
data.set("size", playerData.bagData().size);
data.set("date", playerData.earningData().date);
data.set("earnings", playerData.earningData().earnings);
ConfigurationSection section = data.createSection("stats");
ConfigurationSection amountSection = section.createSection("amount");
ConfigurationSection sizeSection = section.createSection("size");
for (Map.Entry<String, Integer> entry : playerData.statistics().amountMap.entrySet()) {
amountSection.set(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, Float> entry : playerData.statistics().sizeMap.entrySet()) {
sizeSection.set(entry.getKey(), entry.getValue());
}
try {
data.save(getPlayerDataFile(uuid));
} catch (IOException e) {
plugin.getPluginLogger().warn("Failed to save player data", e);
}
return CompletableFuture.completedFuture(true);
}
@Override
public Set<UUID> getUniqueUsers() {
File folder = new File(plugin.getDataFolder(), "data");
Set<UUID> uuids = new HashSet<>();
if (folder.exists()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
uuids.add(UUID.fromString(file.getName().substring(0, file.getName().length() - 4)));
}
}
}
return uuids;
}
private StatisticData getStatistics(Section section) {
HashMap<String, Integer> amountMap = new HashMap<>();
HashMap<String, Float> sizeMap = new HashMap<>();
if (section == null) {
return new StatisticData(amountMap, sizeMap);
}
Section amountSection = section.getSection("amount");
if (amountSection != null) {
for (Map.Entry<String, Object> entry : amountSection.getStringRouteMappedValues(false).entrySet()) {
amountMap.put(entry.getKey(), (Integer) entry.getValue());
}
}
Section sizeSection = section.getSection("size");
if (sizeSection != null) {
for (Map.Entry<String, Object> entry : sizeSection.getStringRouteMappedValues(false).entrySet()) {
sizeMap.put(entry.getKey(), ((Double) entry.getValue()).floatValue());
}
}
return new StatisticData(amountMap, sizeMap);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.totem;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.action.ActionTrigger;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.context.ContextKeys;
import net.momirealms.customfishing.api.mechanic.totem.TotemConfig;
import net.momirealms.customfishing.api.mechanic.totem.TotemParticle;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
public class ActivatedTotem {
private final List<SchedulerTask> subTasks;
private final Location coreLocation;
private final TotemConfig totemConfig;
private final long expireTime;
private final Context<Player> context;
private final double radius;
public ActivatedTotem(Player activator, Location coreLocation, TotemConfig config) {
this.context = Context.player(activator, true)
.arg(ContextKeys.LOCATION, coreLocation)
.arg(ContextKeys.X, coreLocation.getBlockX())
.arg(ContextKeys.Y, coreLocation.getBlockY())
.arg(ContextKeys.Z, coreLocation.getBlockZ())
.arg(ContextKeys.ID, config.id());
this.subTasks = new ArrayList<>();
this.expireTime = (long) (System.currentTimeMillis() + config.duration().evaluate(context) * 1000L);
this.coreLocation = coreLocation.clone().add(0.5,0,0.5);
this.totemConfig = config;
this.radius = config.radius().evaluate(context);
for (TotemParticle particleSetting : config.particleSettings()) {
this.subTasks.add(particleSetting.start(coreLocation, radius));
}
}
public TotemConfig getTotemConfig() {
return totemConfig;
}
public Location getCoreLocation() {
return coreLocation;
}
public void cancel() {
for (SchedulerTask task : this.subTasks) {
task.cancel();
}
this.subTasks.clear();
}
public long getExpireTime() {
return this.expireTime;
}
public double getRadius() {
return radius;
}
public void doTimerAction() {
this.context.arg(ContextKeys.TIME_LEFT, String.valueOf((expireTime - System.currentTimeMillis())/1000));
BukkitCustomFishingPlugin.getInstance().getEventManager().getEventCarrier(totemConfig.id(), MechanicType.TOTEM)
.ifPresent(carrier -> carrier.trigger(context, ActionTrigger.TIMER));
}
}

View File

@@ -0,0 +1,224 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.totem;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.event.TotemActivateEvent;
import net.momirealms.customfishing.api.mechanic.MechanicType;
import net.momirealms.customfishing.api.mechanic.action.ActionTrigger;
import net.momirealms.customfishing.api.mechanic.config.ConfigManager;
import net.momirealms.customfishing.api.mechanic.context.Context;
import net.momirealms.customfishing.api.mechanic.effect.EffectModifier;
import net.momirealms.customfishing.api.mechanic.requirement.RequirementManager;
import net.momirealms.customfishing.api.mechanic.totem.TotemConfig;
import net.momirealms.customfishing.api.mechanic.totem.TotemManager;
import net.momirealms.customfishing.api.mechanic.totem.block.TotemBlock;
import net.momirealms.customfishing.api.util.SimpleLocation;
import net.momirealms.customfishing.bukkit.util.LocationUtils;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class BukkitTotemManager implements TotemManager, Listener {
private final BukkitCustomFishingPlugin plugin;
private final HashMap<String, List<TotemConfig>> block2Totem = new HashMap<>();
private final HashMap<String, TotemConfig> id2Totem = new HashMap<>();
private final List<String> allMaterials = Arrays.stream(Material.values()).map(Enum::name).toList();
private final ConcurrentHashMap<SimpleLocation, ActivatedTotem> activatedTotems = new ConcurrentHashMap<>();
private SchedulerTask timerCheckTask;
public BukkitTotemManager(BukkitCustomFishingPlugin plugin) {
this.plugin = plugin;
}
@Override
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin.getBoostrap());
this.timerCheckTask = plugin.getScheduler().asyncRepeating(() -> {
long time = System.currentTimeMillis();
ArrayList<SimpleLocation> removed = new ArrayList<>();
for (Map.Entry<SimpleLocation, ActivatedTotem> entry : activatedTotems.entrySet()) {
if (time > entry.getValue().getExpireTime()) {
removed.add(entry.getKey());
entry.getValue().cancel();
} else {
entry.getValue().doTimerAction();
}
}
for (SimpleLocation simpleLocation : removed) {
activatedTotems.remove(simpleLocation);
}
}, 1, 1, TimeUnit.SECONDS);
plugin.debug("Loaded " + id2Totem.size() + " totems");
}
@Override
public void unload() {
HandlerList.unregisterAll(this);
for (ActivatedTotem activatedTotem : this.activatedTotems.values())
activatedTotem.cancel();
this.activatedTotems.clear();
if (this.timerCheckTask != null)
this.timerCheckTask.cancel();
this.block2Totem.clear();
}
@Override
public Collection<String> getActivatedTotems(Location location) {
Collection<String> activated = new ArrayList<>();
double nearest = Double.MAX_VALUE;
String nearestTotemID = null;
for (ActivatedTotem activatedTotem : activatedTotems.values()) {
double distance = LocationUtils.getDistance(activatedTotem.getCoreLocation(), location);
if (distance < activatedTotem.getRadius()) {
activated.add(activatedTotem.getTotemConfig().id());
if (nearest > distance) {
nearest = distance;
nearestTotemID = activatedTotem.getTotemConfig().id();
}
}
}
if (nearestTotemID == null) return List.of();
if (!ConfigManager.allowMultipleTotemType()) {
if (ConfigManager.allowSameTotemType()) {
String finalNearestTotemID = nearestTotemID;
activated.removeIf(element -> !element.equals(finalNearestTotemID));
return activated;
} else {
return List.of(nearestTotemID);
}
} else {
if (ConfigManager.allowSameTotemType()) {
return activated;
} else {
return new HashSet<>(activated);
}
}
}
@EventHandler
public void onBreakTotemCore(BlockBreakEvent event) {
if (event.isCancelled())
return;
Location location = event.getBlock().getLocation();
SimpleLocation simpleLocation = SimpleLocation.of(location);
ActivatedTotem activatedTotem = activatedTotems.remove(simpleLocation);
if (activatedTotem != null)
activatedTotem.cancel();
}
@EventHandler (ignoreCancelled = true)
public void onInteractBlock(PlayerInteractEvent event) {
if (
event.isBlockInHand() ||
event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK ||
event.getHand() != EquipmentSlot.HAND
)
return;
Block block = event.getClickedBlock();
assert block != null;
String id = plugin.getBlockManager().getBlockID(block);
List<TotemConfig> configs = block2Totem.get(id);
if (configs == null)
return;
TotemConfig config = null;
for (TotemConfig temp : configs) {
if (temp.isRightPattern(block.getLocation())) {
config = temp;
break;
}
}
if (config == null)
return;
String totemID = config.id();
final Player player = event.getPlayer();;
Context<Player> context = Context.player(player);
Optional<EffectModifier> optionalEffectModifier = plugin.getEffectManager().getEffectModifier(totemID, MechanicType.TOTEM);
if (optionalEffectModifier.isPresent()) {
if (!RequirementManager.isSatisfied(context, optionalEffectModifier.get().requirements())) {
return;
}
}
TotemActivateEvent totemActivateEvent = new TotemActivateEvent(player, block.getLocation(), config);
Bukkit.getPluginManager().callEvent(totemActivateEvent);
if (totemActivateEvent.isCancelled()) {
return;
}
plugin.getEventManager().trigger(context, totemID, MechanicType.TOTEM, ActionTrigger.ACTIVATE);
Location location = block.getLocation();
ActivatedTotem activatedTotem = new ActivatedTotem(player, location, config);
SimpleLocation simpleLocation = SimpleLocation.of(location);
ActivatedTotem previous = this.activatedTotems.put(simpleLocation, activatedTotem);
if (previous != null) {
previous.cancel();
}
}
@Override
public boolean registerTotem(TotemConfig totem) {
if (id2Totem.containsKey(totem.id())) {
return false;
}
HashSet<String> coreMaterials = new HashSet<>();
for (TotemBlock totemBlock : totem.totemCore()) {
String text = totemBlock.getTypeCondition().getRawText();
if (text.startsWith("*")) {
String sub = text.substring(1);
coreMaterials.addAll(allMaterials.stream().filter(it -> it.endsWith(sub)).toList());
} else if (text.endsWith("*")) {
String sub = text.substring(0, text.length() - 1);
coreMaterials.addAll(allMaterials.stream().filter(it -> it.startsWith(sub)).toList());
} else {
coreMaterials.add(text);
}
}
for (String material : coreMaterials) {
List<TotemConfig> configs = this.block2Totem.getOrDefault(material, new ArrayList<>());
configs.add(totem);
this.block2Totem.put(material, configs);
}
id2Totem.put(totem.id(), totem);
return true;
}
@NotNull
@Override
public Optional<TotemConfig> getTotem(String id) {
return Optional.ofNullable(id2Totem.get(id));
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.totem.particle;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class DustParticleSetting extends ParticleSetting {
private final Particle.DustOptions dustOptions;
public DustParticleSetting(
String formulaHorizontal,
String formulaVertical,
Particle particle,
double interval,
List<Pair<Double, Double>> ranges,
int delayTicks,
int periodTicks,
Particle.DustOptions dustOptions
) {
super(formulaHorizontal, formulaVertical, particle, interval, ranges, delayTicks, periodTicks);
this.dustOptions = dustOptions;
}
@SuppressWarnings("DuplicatedCode")
public SchedulerTask start(Location location, double radius) {
World world = location.getWorld();
return BukkitCustomFishingPlugin.getInstance().getScheduler().asyncRepeating(() -> {
for (Pair<Double, Double> range : ranges) {
for (double theta = range.left(); theta <= range.right(); theta += interval) {
double r = expressionHorizontal.setVariable("theta", theta).setVariable("radius", radius).evaluate();
double x = r * Math.cos(theta) + 0.5;
double z = r * Math.sin(theta) + 0.5;
double y = expressionVertical.setVariable("theta", theta).setVariable("radius", radius).evaluate();
world.spawnParticle(particle, location.clone().add(x, y, z), 1,0,0,0, 0, dustOptions);
}
}
}, delay * 50L, period * 50L, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.totem.particle;
import net.momirealms.customfishing.api.BukkitCustomFishingPlugin;
import net.momirealms.customfishing.api.mechanic.totem.TotemParticle;
import net.momirealms.customfishing.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customfishing.common.util.Pair;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ParticleSetting implements TotemParticle {
protected final Expression expressionHorizontal;
protected final Expression expressionVertical;
protected final double interval;
protected int delay;
protected int period;
protected final Particle particle;
List<Pair<Double, Double>> ranges;
public ParticleSetting(
String formulaHorizontal,
String formulaVertical,
Particle particle,
double interval,
List<Pair<Double, Double>> ranges,
int delayTicks,
int periodTicks
) {
this.interval = interval * Math.PI / 180;
this.particle = particle;
this.delay = delayTicks;
this.period = periodTicks;
this.ranges = ranges;
this.expressionHorizontal = new ExpressionBuilder(formulaHorizontal)
.variables("theta", "radius")
.build();
this.expressionVertical = new ExpressionBuilder(formulaVertical)
.variables("theta", "radius")
.build();
}
@SuppressWarnings("DuplicatedCode")
public SchedulerTask start(Location location, double radius) {
World world = location.getWorld();
return BukkitCustomFishingPlugin.getInstance().getScheduler().asyncRepeating(() -> {
for (Pair<Double, Double> range : ranges) {
for (double theta = range.left(); theta <= range.right(); theta += interval) {
double r = expressionHorizontal.setVariable("theta", theta).setVariable("radius", radius).evaluate();
double x = r * Math.cos(theta) + 0.5;
double z = r * Math.sin(theta) + 0.5;
double y = expressionVertical.setVariable("theta", theta).setVariable("radius", radius).evaluate();
world.spawnParticle(particle, location.clone().add(x, y, z), 1,0,0,0);
}
}
}, delay * 50L, period * 50L, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,435 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.util;
import com.saicone.rtag.item.ItemTagStream;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customfishing.api.mechanic.item.ItemEditor;
import net.momirealms.customfishing.api.mechanic.item.tag.TagMap;
import net.momirealms.customfishing.api.mechanic.item.tag.TagValueType;
import net.momirealms.customfishing.api.mechanic.misc.value.MathValue;
import net.momirealms.customfishing.api.mechanic.misc.value.TextValue;
import net.momirealms.customfishing.common.util.ArrayUtils;
import net.momirealms.customfishing.common.util.Pair;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import static net.momirealms.customfishing.api.util.TagUtils.toTypeAndData;
import static net.momirealms.customfishing.common.util.ArrayUtils.splitValue;
public class ItemStackUtils {
private ItemStackUtils() {}
public static ItemStack fromBase64(String base64) {
if (base64 == null || base64.isEmpty())
return new ItemStack(Material.AIR);
ByteArrayInputStream inputStream;
try {
inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(base64));
} catch (IllegalArgumentException e) {
return new ItemStack(Material.AIR);
}
ItemStack stack = null;
try (BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream)) {
stack = (ItemStack) dataInput.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return stack;
}
public static String toBase64(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR)
return "";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
dataOutput.writeObject(itemStack);
byte[] byteArr = outputStream.toByteArray();
dataOutput.close();
outputStream.close();
return Base64Coder.encodeLines(byteArr);
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
public static Map<String, Object> itemStackToMap(ItemStack itemStack) {
Map<String, Object> map = ItemTagStream.INSTANCE.toMap(itemStack);
map.remove("rtagDataVersion");
map.remove("count");
map.remove("id");
map.put("material", itemStack.getType().name().toLowerCase(Locale.ENGLISH));
map.put("amount", itemStack.getAmount());
Object tag = map.remove("tags");
if (tag != null) {
map.put("nbt", tag);
}
return map;
}
private static void sectionToMap(Section section, Map<String, Object> outPut) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
HashMap<String, Object> map = new HashMap<>();
outPut.put(entry.getKey(), map);
sectionToMap(inner, map);
} else {
outPut.put(entry.getKey(), entry.getValue());
}
}
}
@SuppressWarnings("UnstableApiUsage")
public static void sectionToComponentEditor(Section section, List<ItemEditor> itemEditors) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
String component = entry.getKey();
Object value = entry.getValue();
if (value instanceof Section inner) {
Map<String, Object> innerMap = new HashMap<>();
sectionToMap(inner, innerMap);
TagMap tagMap = TagMap.of(innerMap);
itemEditors.add(((item, context) -> {
item.setComponent(component, tagMap.apply(context));
}));
} else if (value instanceof List<?> list) {
Object first = list.get(0);
if (first instanceof Map<?,?>) {
ArrayList<TagMap> output = new ArrayList<>();
for (Object o : list) {
Map<String, Object> innerMap = (Map<String, Object>) o;
TagMap tagMap = TagMap.of(innerMap);
output.add(tagMap);
}
itemEditors.add(((item, context) -> {
List<Map<String, Object>> maps = output.stream().map(unparsed -> unparsed.apply(context)).toList();
item.setComponent(component, maps);
}));
} else if (first instanceof String str) {
Pair<TagValueType, String> pair = toTypeAndData(str);
switch (pair.left()) {
case INT -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Integer> integers = values.stream().map(unparsed -> (int) unparsed.evaluate(context)).toList();
item.setComponent(component, integers);
}));
}
case BYTE -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Byte> bytes = values.stream().map(unparsed -> (byte) unparsed.evaluate(context)).toList();
item.setComponent(component, bytes);
}));
}
case LONG -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Long> longs = values.stream().map(unparsed -> (long) unparsed.evaluate(context)).toList();
item.setComponent(component, longs);
}));
}
case FLOAT -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Float> floats = values.stream().map(unparsed -> (float) unparsed.evaluate(context)).toList();
item.setComponent(component, floats);
}));
}
case DOUBLE -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Double> doubles = values.stream().map(unparsed -> (double) unparsed.evaluate(context)).toList();
item.setComponent(component, doubles);
}));
}
case STRING -> {
List<TextValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(TextValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<String> texts = values.stream().map(unparsed -> unparsed.render(context)).toList();
item.setComponent(component, texts);
}));
}
}
} else {
itemEditors.add(((item, context) -> {
item.setComponent(component, list);
}));
}
} else if (value instanceof String str) {
Pair<TagValueType, String> pair = toTypeAndData(str);
switch (pair.left()) {
case INT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (int) mathValue.evaluate(context));
}));
}
case BYTE -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (byte) mathValue.evaluate(context));
}));
}
case FLOAT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (float) mathValue.evaluate(context));
}));
}
case LONG -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (long) mathValue.evaluate(context));
}));
}
case SHORT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (short) mathValue.evaluate(context));
}));
}
case DOUBLE -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, (double) mathValue.evaluate(context));
}));
}
case STRING -> {
TextValue<Player> textValue = TextValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.setComponent(component, textValue.render(context));
}));
}
case INTARRAY -> {
String[] split = splitValue(str);
int[] array = Arrays.stream(split).mapToInt(Integer::parseInt).toArray();
itemEditors.add(((item, context) -> {
item.setComponent(component, array);
}));
}
case BYTEARRAY -> {
String[] split = splitValue(str);
byte[] bytes = new byte[split.length];
for (int i = 0; i < split.length; i++){
bytes[i] = Byte.parseByte(split[i]);
}
itemEditors.add(((item, context) -> {
item.setComponent(component, bytes);
}));
}
}
} else {
itemEditors.add(((item, context) -> {
item.setComponent(component, value);
}));
}
}
}
// ugly codes, remaining improvements
public static void sectionToTagEditor(Section section, List<ItemEditor> itemEditors, String... route) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
Object value = entry.getValue();
String key = entry.getKey();
String[] currentRoute = ArrayUtils.appendElementToArray(route, key);
if (value instanceof Section inner) {
sectionToTagEditor(inner, itemEditors, currentRoute);
} else if (value instanceof List<?> list) {
Object first = list.get(0);
if (first instanceof Map<?, ?>) {
List<TagMap> maps = new ArrayList<>();
for (Object o : list) {
Map<String, Object> map = (Map<String, Object>) o;
maps.add(TagMap.of(map));
}
itemEditors.add(((item, context) -> {
List<Map<String, Object>> parsed = maps.stream().map(render -> render.apply(context)).toList();
item.set(parsed, (Object[]) currentRoute);
}));
} else {
if (first instanceof String str) {
Pair<TagValueType, String> pair = toTypeAndData(str);
switch (pair.left()) {
case INT -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Integer> integers = values.stream().map(unparsed -> (int) unparsed.evaluate(context)).toList();
item.set(integers, (Object[]) currentRoute);
}));
}
case BYTE -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Byte> bytes = values.stream().map(unparsed -> (byte) unparsed.evaluate(context)).toList();
item.set(bytes, (Object[]) currentRoute);
}));
}
case LONG -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Long> longs = values.stream().map(unparsed -> (long) unparsed.evaluate(context)).toList();
item.set(longs, (Object[]) currentRoute);
}));
}
case FLOAT -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Float> floats = values.stream().map(unparsed -> (float) unparsed.evaluate(context)).toList();
item.set(floats, (Object[]) currentRoute);
}));
}
case DOUBLE -> {
List<MathValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(MathValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<Double> doubles = values.stream().map(unparsed -> (double) unparsed.evaluate(context)).toList();
item.set(doubles, (Object[]) currentRoute);
}));
}
case STRING -> {
List<TextValue<Player>> values = new ArrayList<>();
for (Object o : list) {
values.add(TextValue.auto(toTypeAndData((String) o).right()));
}
itemEditors.add(((item, context) -> {
List<String> texts = values.stream().map(unparsed -> unparsed.render(context)).toList();
item.set(texts, (Object[]) currentRoute);
}));
}
}
} else {
itemEditors.add(((item, context) -> {
item.set(list, (Object[]) currentRoute);
}));
}
}
} else if (value instanceof String str) {
Pair<TagValueType, String> pair = toTypeAndData(str);
switch (pair.left()) {
case INT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((int) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case BYTE -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((byte) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case LONG -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((long) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case SHORT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((short) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case DOUBLE -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((double) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case FLOAT -> {
MathValue<Player> mathValue = MathValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set((float) mathValue.evaluate(context), (Object[]) currentRoute);
}));
}
case STRING -> {
TextValue<Player> textValue = TextValue.auto(pair.right());
itemEditors.add(((item, context) -> {
item.set(textValue.render(context), (Object[]) currentRoute);
}));
}
case INTARRAY -> {
String[] split = splitValue(str);
int[] array = Arrays.stream(split).mapToInt(Integer::parseInt).toArray();
itemEditors.add(((item, context) -> {
item.set(array, (Object[]) currentRoute);
}));
}
case BYTEARRAY -> {
String[] split = splitValue(str);
byte[] bytes = new byte[split.length];
for (int i = 0; i < split.length; i++){
bytes[i] = Byte.parseByte(split[i]);
}
itemEditors.add(((item, context) -> {
item.set(bytes, (Object[]) currentRoute);
}));
}
}
} else {
itemEditors.add(((item, context) -> {
item.set(value, (Object[]) currentRoute);
}));
}
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.util;
import org.bukkit.Bukkit;
import org.bukkit.Location;
public class LocationUtils {
private 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) +
Math.pow(location2.getZ() - location1.getZ(), 2)
);
}
public static Location getAnyLocationInstance() {
return new Location(Bukkit.getWorlds().get(0), 0, 64, 0);
}
public static String toChunkPosString(Location location) {
return (location.getBlockX() % 16) + "_" + location.getBlockY() + "_" + (location.getBlockZ() % 16);
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customfishing.bukkit.util;
import net.momirealms.customfishing.common.util.RandomUtils;
import org.bukkit.Location;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import static java.util.Objects.requireNonNull;
public class PlayerUtils {
public static void dropItem(@NotNull Player player, @NotNull ItemStack itemStack, boolean retainOwnership, boolean noPickUpDelay, boolean throwRandomly) {
requireNonNull(player, "player");
requireNonNull(itemStack, "itemStack");
Location location = player.getLocation().clone();
Item item = player.getWorld().dropItem(player.getEyeLocation().clone().subtract(new Vector(0,0.3,0)), itemStack);
item.setPickupDelay(noPickUpDelay ? 0 : 40);
if (retainOwnership) {
item.setThrower(player.getUniqueId());
}
if (throwRandomly) {
double d1 = RandomUtils.generateRandomDouble(0,1) * 0.5f;
double d2 = RandomUtils.generateRandomDouble(0,1) * (Math.PI * 2);
item.setVelocity(new Vector(-Math.sin(d2) * d1, 0.2f, Math.cos(d2) * d1));
} else {
double d1 = Math.sin(location.getPitch() * (Math.PI/180));
double d2 = RandomUtils.generateRandomDouble(0, 0.02);
double d3 = RandomUtils.generateRandomDouble(0,1) * (Math.PI * 2);
Vector vector = location.getDirection().multiply(0.3).setY(-d1 * 0.3 + 0.1 + (RandomUtils.generateRandomDouble(0,1) - RandomUtils.generateRandomDouble(0,1)) * 0.1);
vector.add(new Vector(Math.cos(d3) * d2, 0, Math.sin(d3) * d2));
item.setVelocity(vector);
}
}
public static int putItemsToInventory(Inventory inventory, ItemStack itemStack, int amount) {
ItemMeta meta = itemStack.getItemMeta();
int maxStackSize = itemStack.getMaxStackSize();
for (ItemStack other : inventory.getStorageContents()) {
if (other != null) {
if (other.getType() == itemStack.getType() && other.getItemMeta().equals(meta)) {
if (other.getAmount() < maxStackSize) {
int delta = maxStackSize - other.getAmount();
if (amount > delta) {
other.setAmount(maxStackSize);
amount -= delta;
} else {
other.setAmount(amount + other.getAmount());
return 0;
}
}
}
}
}
if (amount > 0) {
for (ItemStack other : inventory.getStorageContents()) {
if (other == null) {
if (amount > maxStackSize) {
amount -= maxStackSize;
ItemStack cloned = itemStack.clone();
cloned.setAmount(maxStackSize);
inventory.addItem(cloned);
} else {
ItemStack cloned = itemStack.clone();
cloned.setAmount(amount);
inventory.addItem(cloned);
return 0;
}
}
}
}
return amount;
}
public static int giveItem(Player player, ItemStack itemStack, int amount) {
PlayerInventory inventory = player.getInventory();
ItemMeta meta = itemStack.getItemMeta();
int maxStackSize = itemStack.getMaxStackSize();
if (amount > maxStackSize * 100) {
amount = maxStackSize * 100;
}
int actualAmount = amount;
for (ItemStack other : inventory.getStorageContents()) {
if (other != null) {
if (other.getType() == itemStack.getType() && other.getItemMeta().equals(meta)) {
if (other.getAmount() < maxStackSize) {
int delta = maxStackSize - other.getAmount();
if (amount > delta) {
other.setAmount(maxStackSize);
amount -= delta;
} else {
other.setAmount(amount + other.getAmount());
return actualAmount;
}
}
}
}
}
if (amount > 0) {
for (ItemStack other : inventory.getStorageContents()) {
if (other == null) {
if (amount > maxStackSize) {
amount -= maxStackSize;
ItemStack cloned = itemStack.clone();
cloned.setAmount(maxStackSize);
inventory.addItem(cloned);
} else {
ItemStack cloned = itemStack.clone();
cloned.setAmount(amount);
inventory.addItem(cloned);
return actualAmount;
}
}
}
}
if (amount > 0) {
for (int i = 0; i < amount / maxStackSize; i++) {
ItemStack cloned = itemStack.clone();
cloned.setAmount(maxStackSize);
player.getWorld().dropItem(player.getLocation(), cloned);
}
int left = amount % maxStackSize;
if (left != 0) {
ItemStack cloned = itemStack.clone();
cloned.setAmount(left);
player.getWorld().dropItem(player.getLocation(), cloned);
}
}
return actualAmount;
}
}

View File

@@ -0,0 +1,186 @@
#
# Don't change this
#
config-version: "${config_version}"
#
# For safety reasons, editing this file requires a restart to apply
#
# A command to reload the plugin
# Usage: [COMMAND]
reload:
enable: true
permission: customfishing.command.reload
usage:
- /customfishing reload
- /cfishing reload
# A command designed for players to sell fish
# Usage: [COMMAND]
sellfish:
enable: true
permission: customfishing.sellfish
usage:
- /sellfish
# A command designed for players to open the fishing bag
# Usage: [COMMAND]
fishingbag:
enable: true
permission: fishingbag.user
usage:
- /fishingbag
# A command to get items
# Usage: [COMMAND] [id]
get_item:
enable: true
permission: customfishing.command.getitem
usage:
- /customfishing items get
- /cfishing items get
# A command to give items
# Usage: [COMMAND] [id]
give_item:
enable: true
permission: customfishing.command.giveitem
usage:
- /customfishing items give
- /cfishing items give
# A command to import items
# Usage: [COMMAND] [type] [id]
import_item:
enable: true
permission: customfishing.command.importitem
usage:
- /customfishing items import
- /cfishing items import
# A command to stop the competition
# Usage: [COMMAND]
stop_competition:
enable: true
permission: customfishing.command.competition
usage:
- /customfishing competition stop
- /cfishing competition stop
# A command to end the competition
# Usage: [COMMAND] [type] [id]
end_competition:
enable: true
permission: customfishing.command.competition
usage:
- /customfishing competition end
- /cfishing competition end
# A command to start competitions
# Usage: [COMMAND] [id]
start_competition:
enable: true
permission: customfishing.command.competition
usage:
- /customfishing competition start
- /cfishing competition start
# A command to open market for players
# Usage: [COMMAND] [player]
open_market:
enable: true
permission: customfishing.command.open.market
usage:
- /customfishing open market
- /cfishing open market
# A command to open bag for players
# Usage: [COMMAND] [player]
open_bag:
enable: true
permission: customfishing.command.open.bag
usage:
- /customfishing open bag
- /cfishing open bag
# A command to edit bag contents
# Usage: [COMMAND] [player]
edit_online_bag:
enable: true
permission: customfishing.command.edit.bag
usage:
- /customfishing fishingbag edit-online
- /cfishing fishingbag edit-online
# A command to edit bag contents
# Usage: [COMMAND] [uuid]
edit_offline_bag:
enable: true
permission: customfishing.command.edit.bag
usage:
- /customfishing fishingbag edit-offline
- /cfishing fishingbag edit-offline
# A command to unlock those locked data
# Usage: [COMMAND] [uuid]
data_unlock:
enable: true
permission: customfishing.command.data
usage:
- /customfishing data unlock
- /cfishing data unlock
# A command to export the data
# Usage: [COMMAND]
data_export:
enable: true
permission: customfishing.command.data
usage:
- /customfishing data export
- /cfishing data export
# A command to import the data
# Usage: [COMMAND] [file]
data_import:
enable: true
permission: customfishing.command.data
usage:
- /customfishing data import
- /cfishing data import
# A command to set a player's fishing statistics
# Usage: [COMMAND] [player] [id] [type] [value]
statistics_set:
enable: true
permission: customfishing.command.statistics
usage:
- /customfishing statistics set
- /cfishing statistics set
# A command to reset a player's fishing statistics
# Usage: [COMMAND] [player]
statistics_reset:
enable: true
permission: customfishing.command.statistics
usage:
- /customfishing statistics reset
- /cfishing statistics reset
# A command to query a player's fishing statistics
# Usage: [COMMAND] [player] [type]
statistics_query:
enable: true
permission: customfishing.command.statistics
usage:
- /customfishing statistics query
- /cfishing statistics query
# A command to manually add a player's fishing statistics
# Usage: [COMMAND] [player] [id] [type] [value]
statistics_add:
enable: true
permission: customfishing.command.statistics
usage:
- /customfishing statistics add
- /cfishing statistics add

View File

@@ -0,0 +1,451 @@
# Don"t change this
config-version: '${config_version}'
# Debug
debug: false
# BStats
metrics: true
# Check updates
update-checker: true
# Mechanic settings
mechanics:
# Specifies the conditions required for the plugin mechanics to work.
# Here, the type is !world, which implies the plugin won't work in
# the world named 'blacklist_world'.
mechanic-requirements:
world_requirement:
type: '!world'
value:
- blacklist_world
# If you want to let some players skip games, you can set requirements that used to skip games
# We used `impossible` requirement here, so players should play the game if there exists
skip-game-requirements:
impossible_requirement:
type: 'impossible'
# Requirements for enabling auto-fishing
auto-fishing-requirements:
impossible_requirement:
type: 'impossible'
# Configures global effects. This is useful if you want to give all the players certain effects based on certain conditions
global-effects:
effect_1:
type: conditional
conditions:
competition:
ongoing: true
id:
- weekend_competition
effects:
effect_1:
type: wait-time-multiplier
value: 0.85
# Configures global events for hook/bait/rod/loot
# which would help you reduce duplicated lines
global-events:
hook: {}
bait: {}
loot:
new_size_record:
conditional_size_record_action:
type: conditional
value:
conditions:
has-stats: true
actions:
actionbar_action:
type: actionbar
value: '<#FFD700>[New Record]</#FFD700> <#FFFFF0>You caught a(n) {nick} which is <#FFA500>{size_formatted}cm</#FFA500> long!</#FFFFF0>'
sound_action:
type: sound
value:
key: "minecraft:block.note_block.cow_bell"
source: 'player'
volume: 1
pitch: 1
delayed_sound:
type: delay
value:
delay: 2
actions:
sound_action:
type: sound
value:
key: "minecraft:block.note_block.bell"
source: 'player'
volume: 1
pitch: 1
success:
conditional_size_info_action:
type: conditional
value:
conditions:
has-size: true
has-stats: true
actions:
actionbar_action:
type: actionbar
value: '<gray>You caught a(n) {nick} which is <#F5F5F5>{size_formatted}cm</#F5F5F5> long!</gray> <#C0C0C0>(Best record: {record_formatted}cm)</#C0C0C0>'
title_action:
type: random-title
value:
titles:
- '<green>GG!</green>'
- '<green>Good Job!</green>'
subtitles:
- 'You caught a(n) {nick}'
- 'Whoa! Nice catch!'
- 'Oh {nick} here we go!'
- 'Let''s see what it is!'
fade-in: 20
stay: 30
fade-out: 10
chance: 1.0
failure:
title_action:
type: random-title
value:
titles:
- '<red>Be concentrated!</red>'
- '<red>What a pity!</red>'
- '<red>Try next time!</red>'
- '<red>Bad luck</red>'
subtitles:
- 'The fish escaped...'
fade-in: 20
stay: 30
fade-out: 10
chance: 1.0
rod:
land:
priority_action:
type: priority
value:
priority_1:
conditions:
lava-fishing: true
actions:
fake_item_action:
type: fake-item
value:
duration: 35
position: other
item: lava_effect
priority_2:
conditions:
lava-fishing: false
actions:
fake_item_action:
type: fake-item
value:
duration: 35
position: other
item: water_effect
# Global properties which would help you reduce duplicated lines
global-loot-property:
show-in-fishfinder: true
disable-stat: false
disable-game: false
instant-game: false
# Fishing bag is where players can store their baits, utils, hooks and rods (Loot optional)
fishing-bag:
enable: true
# Fishing bag container title
bag-title: '<blue>{player}''s Fishing Bag</blue>'
# Other whitelist-items
whitelist-items:
- fishing_rod
# Decide the items that can be stored in bag
can-store-loot: false
can-store-rod: true
can-store-bait: true
can-store-hook: true
can-store-util: true
# Requirements for automatically collecting
collect-requirements:
permission: fishingbag.collectloot
# Actions to do if fishing loots are automatically collected into bag
collect-actions:
sound_action:
type: sound
value:
key: "minecraft:item.armor.equip_leather"
source: 'player'
volume: 1
pitch: 1
hologram_action:
type: hologram
value:
duration: 40
text: '{nick} <#B0E0E6><b>has been stored into bag</#B0E0E6>'
position: other
y: 1
# Actions to do if the fishing bag is full
full-actions:
conditional_action:
type: conditional
value:
conditions:
condition_1:
type: cooldown
value:
key: fishing_bag_full_notice
time: 60000
actions:
message_action:
type: message
value: "<#EEE8AA>[Fishing Bag]</#EEE8AA> Your fishing bag has been full."
market:
# Market GUI title
title: '<gradient:#A52A2A:#800000:#A52A2A>Fish Market</gradient>'
# Whether to enable limitations
limitation:
enable: true
earnings: '10000' # You can use expressions here
# Market menu layout
layout:
- 'AAAAAAAAA'
- 'AIIIIIIIA'
- 'AIIIIIIIA'
- 'AIIIIIIIA'
- 'AAAABAAAA'
# Price formula (For CustomFishing loots)
price-formula: '{base} + {bonus} * {size}'
# Allow player to sell fish in bundles
allow-bundle: true
# Allow player to sell fish in shulker boxes
allow-shulker-box: true
# Item price (For vanilla items & other plugin items that have CustomModelData)
item-price:
# Vanilla Items
COD: 10
PUFFERFISH: 10
SALMON: 10
TROPICAL_FISH: 10
# PAPER (CustomModelData: 999)
PAPER:999: 5
# Slots to put items in
item-slot:
symbol: 'I'
allow-items-with-no-price: true
# This is an icon that allows players to sell all the fish from their inventory and fishingbag
# You can enable it by putting the symbol into layout
sell-all-icons:
symbol: 'S'
# Should the fish in fishing bag be sold
fishingbag: true
allow-icon:
material: IRON_BLOCK
display:
name: '<#00CED1><b>● <!b>Ship the fish'
lore:
- '<font:uniform><gradient:#E6E6FA:#48D1CC:#E6E6FA>You will get <green>{money_formatted} coins</green> from the fish in inventory and bag</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:block.amethyst_block.place'
source: 'player'
volume: 1
pitch: 1
message_action:
type: message
value: 'You earned {money_formatted} coins from the fish! You can get {rest_formatted} more coins from market today'
command_action:
type: command
value: 'money give {player} {money}'
# Requires Vault and any economy plugin
# money_action:
# type: give-money
# value: '{money}'
deny-icon:
material: REDSTONE_BLOCK
display:
name: '<red><b>● <!b>Denied trade'
lore:
- '<font:uniform><gradient:#E6E6FA:red:#E6E6FA>Nothing to sell!</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:entity.villager.no'
source: 'player'
volume: 1
pitch: 1
limit-icon:
material: REDSTONE_BLOCK
display:
name: '<red><b>● <!b>Denied trade'
lore:
- '<font:uniform><gradient:#E6E6FA:red:#E6E6FA>The worth of items exceeds the money that can be earned for the rest of today!</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:block.anvil.land'
source: 'player'
volume: 1
pitch: 1
# Sell icon
sell-icons:
symbol: 'B'
allow-icon:
material: IRON_BLOCK
display:
name: '<#00CED1><b>● <!b>Ship the fish'
lore:
- '<font:uniform><gradient:#E6E6FA:#48D1CC:#E6E6FA>You will get <green>{money_formatted} coins</green> from the fish</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:block.amethyst_block.place'
source: 'player'
volume: 1
pitch: 1
message_action:
type: message
value: 'You earned {money_formatted} coins from the fish! You can get {rest_formatted} more coins from market today'
command_action:
type: command
value: 'money give {player} {money}'
# Requires Vault and any economy plugin
# money_action:
# type: give-money
# value: '{money}'
deny-icon:
material: REDSTONE_BLOCK
display:
name: '<red><b>● <!b>Denied trade'
lore:
- '<font:uniform><gradient:#E6E6FA:red:#E6E6FA>Nothing to sell!</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:entity.villager.no'
source: 'player'
volume: 1
pitch: 1
limit-icon:
material: REDSTONE_BLOCK
display:
name: '<red><b>● <!b>Denied trade'
lore:
- '<font:uniform><gradient:#E6E6FA:red:#E6E6FA>The worth of items exceeds the money that can be earned for the rest of today!</gradient></font>'
action:
sound_action:
type: sound
value:
key: 'minecraft:block.anvil.land'
source: 'player'
volume: 1
pitch: 1
# Decorative icons
decorative-icons:
glass-pane:
symbol: 'A'
material: BLACK_STAINED_GLASS_PANE
display:
name: ' '
# This section would take effect if you set "override-vanilla" to true
# That also means vanilla mechanics for example lure enchantment
# would no longer take effect, so you have to configure its effect in CustomFishing
fishing-wait-time:
# override vanilla mechanic
override-vanilla: false
# ticks
min-wait-time: 100
max-wait-time: 600
# Lava fishing settings
# To modify vanilla fishing time, you should edit paper-world-defaults.yml where there's a section called fishing-time-range
lava-fishing:
enable: true
# ticks
min-wait-time: 100
max-wait-time: 600
void-fishing:
enable: true
# ticks
min-wait-time: 100
max-wait-time: 600
# Size settings
size:
# Some effects would increase/decrease size so the option decides whether they could ignore the limit
restricted-size-range: true
# Competition settings
competition:
# Use redis for cross server data synchronization
redis-ranking: false
# Server group
server-group: default
# Increase this value would allow you to use more placeholders like {4_player} {5_score} in sacrifice of some performance
placeholder-limit: 3
# If a player could get multiple loots from fishing, should the loots spawn at the same time or have delay for each (measured in ticks)
multiple-loot-spawn-delay: 4
# Totem settings
totem:
# Is it allowed for different types of totems to take effect at the same time
allow-multiple-type: true
# Is it allowed for totems of the same type to take effect cumulatively
allow-same-type: false
# Enable fake bait casting animation
bait-animation: true
# Other settings
other-settings:
# It's recommended to use MiniMessage format. If you insist on using legacy color code "&", enable the support below.
# Disable this would improve performance
legacy-color-code-support: true
# Fishing event priority: MONITOR HIGHEST HIGH NORMAL LOW LOWEST
event-priority: NORMAL
# Save the data from cache to file periodically to minimize the data loss if server crashes
# -1 to disable
data-saving-interval: 600
# Log the consumption of time on data saving
log-data-saving: true
# Lock player's data if a player is playing on a server that connected to database
# If you can ensure low database link latency and fast processing, you can consider disabling this option to improve performance
lock-data: true
# Requires PlaceholderAPI to work
placeholder-register:
# Requires server expansion
'{date}': '%server_time_yyyy-MM-dd-HH:mm:ss%'
# Requires player expansion
'{yaw}': '%player_yaw%'
# CustomFishing supports using items/blocks from other plugins
# If items share the same id, they would inherit the effects
# Check the wiki for examples
item-detection-order:
- CustomFishing
- vanilla
block-detection-order:
- vanilla
# Custom durability format
custom-durability-format:
- ''
- '<gray>Durability</gray><white>: {dur} <gray>/</gray> {max}</white>'
# Offset characters
# Never edit this unless you know what you are doing
offset-characters:
font: customfishing:offset_chars
'1':
'2':
'4':
'8':
'16':
'32':
'64':
'128':
'-1':
'-2':
'-4':
'-8':
'-16':
'-32':
'-64':
'-128':

View File

@@ -0,0 +1,95 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
# use Vanilla items as bait
BOOK:
tag: false
material: book
requirements:
requirement_1:
type: rod
value:
- magical_rod
not-met-actions:
action_message:
type: message
value:
- 'This bait can only be used on <#7B68EE>Magical Fishing Rod'
simple_bait:
material: paper
display:
name: '<b><#00BFFF>Simple lures'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray>Made from natural ingredients, it attracts'
- '<gray>fish in a calm and steady manner. It''s the'
- '<gray>go-to choice for those who prefer a '
- '<gray>straightforward and reliable fishing experience.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Reduce fishing difficulty'
- ''
custom-model-data: 50001
effects:
effect_1:
type: difficulty
value: -10
magnetic_bait:
material: paper
display:
name: '<b><red>Magn<blue>etic <gray>lures'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray>Its radiant shimmer and unique energy pulse'
- '<gray>prove irresistible to curious fish, drawing'
- '<gray>them in with unprecedented speed. This is '
- '<gray>not just a bait, it''s a spectacle that fish'
- '<gray>can''t help but investigate.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Reduce wait time'
- '<gray> - More time for fishing'
- ''
custom-model-data: 50002
effects:
effect_1:
type: wait-time-multiplier
value: 0.9
effect_2:
type: game-time
value: 2
wild_bait:
material: paper
display:
name: '<b><#2E8B57>Wild lures'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray>Crafted for the fearless angler, the Wild '
- '<gray>Attraction Bait is an infusion of potent '
- '<gray>natural ingredients, exuding an irresistible'
- '<gray>aroma for large aquatic beasts.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Increase fishing difficulty'
- '<gray> - Increase the size of fish caught'
- ''
custom-model-data: 50003
effects:
effect_1:
type: difficulty
value: +20
effect_2:
type: size-multiplier
value: 1.5
effect_3:
type: game-time
value: -2

View File

@@ -0,0 +1,92 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
apple_crate:
show-in-fishfinder: false
disable-stat: true
nick: Apple Crate
block: barrel
velocity:
horizontal: 1.07
vertical: 1.5
properties:
directional: true
storage:
apple_1:
item: APPLE
amount: 1~2
chance: 0.8
apple_2:
item: APPLE
amount: 1~2
chance: 0.8
apple_3:
item: APPLE
amount: 1~2
chance: 0.8
apple_4:
item: APPLE
amount: 1~2
chance: 0.8
apple_5:
item: APPLE
amount: 1~2
chance: 0.8
apple_6:
item: APPLE
amount: 1~2
chance: 0.8
apple_7:
item: APPLE
amount: 1~2
chance: 0.8
apple_8:
item: APPLE
amount: 1~2
chance: 0.8
carrot_crate:
show-in-fishfinder: false
disable-stat: true
nick: Carrot Crate
block: barrel
velocity:
horizontal: 1.07
vertical: 1.5
properties:
directional: true
storage:
carrot_1:
item: Carrot
amount: 1~2
chance: 0.8
carrot_2:
item: Carrot
amount: 1~2
chance: 0.8
carrot_3:
item: Carrot
amount: 1~2
chance: 0.8
carrot_4:
item: Carrot
amount: 1~2
chance: 0.8
carrot_5:
item: Carrot
amount: 1~2
chance: 0.8
carrot_6:
item: Carrot
amount: 1~2
chance: 0.8
carrot_7:
item: Carrot
amount: 1~2
chance: 0.8
carrot_8:
item: Carrot
amount: 1~2
chance: 0.8

View File

@@ -0,0 +1,106 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
# The concepts of categories and groups are different.
# Categories are only used to classify fish based on certain rules,
# which only affect the return value of placeholder and do not affect any gameplay
normal_fish:
- tuna_fish
- pike_fish
- gold_fish
- perch_fish
- mullet_fish
- sardine_fish
- carp_fish
- cat_fish
- octopus
- sunfish
- red_snapper_fish
- salmon_void_fish
- woodskip_fish
- sturgeon_fish
sliver_star_fish:
- tuna_fish_silver_star
- pike_fish_silver_star
- gold_fish_silver_star
- perch_fish_silver_star
- mullet_fish_silver_star
- sardine_fish_silver_star
- carp_fish_silver_star
- cat_fish_silver_star
- octopus_silver_star
- sunfish_silver_star
- red_snapper_fish_silver_star
- salmon_void_fish_silver_star
- woodskip_fish_silver_star
- sturgeon_fish_silver_star
golden_star_fish:
- tuna_fish_golden_star
- pike_fish_golden_star
- gold_fish_golden_star
- perch_fish_golden_star
- mullet_fish_golden_star
- sardine_fish_golden_star
- carp_fish_golden_star
- cat_fish_golden_star
- octopus_golden_star
- sunfish_golden_star
- red_snapper_fish_golden_star
- salmon_void_fish_golden_star
- woodskip_fish_golden_star
- sturgeon_fish_golden_star
river_fish:
- gold_fish
- gold_fish_silver_star
- gold_fish_golden_star
- perch_fish
- perch_fish_silver_star
- perch_fish_golden_star
- mullet_fish
- mullet_fish_silver_star
- mullet_fish_golden_star
- carp_fish
- carp_fish_silver_star
- carp_fish_golden_star
- cat_fish
- cat_fish_silver_star
- cat_fish_silver_star
- woodskip_fish
- woodskip_fish_silver_star
- woodskip_fish_golden_star
- sturgeon_fish
- sturgeon_fish_silver_star
- sturgeon_fish_golden_star
ocean_fish:
- tuna_fish
- tuna_fish_silver_star
- tuna_fish_golden_star
- pike_fish
- pike_fish_silver_star
- pike_fish_golden_star
- sardine_fish
- sardine_fish_silver_star
- sardine_fish_golden_star
- octopus
- octopus_silver_star
- octopus_golden_star
- sunfish
- sunfish_silver_star
- sunfish_golden_star
- red_snapper_fish
- red_snapper_fish_silver_star
- red_snapper_fish_golden_star
- blue_jellyfish
- blue_jellyfish_silver_star
- blue_jellyfish_golden_star
- pink_jellyfish
- pink_jellyfish_silver_star
- pink_jellyfish_golden_star

View File

@@ -0,0 +1,133 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
weekend_competition:
# TOTAL_SCORE
# CATCH_AMOUNT
# MAX_SIZE
# TOTAL_SIZE
# RANDOM
goal: CATCH_AMOUNT
# Optional
# 1-7
start-weekday:
- 6
- 7
# Optional
start-time:
- '9:30'
- '14:30'
- '20:00'
# Seconds
duration: 300
# Min players to start the competition
min-players: 2
skip-actions:
broadcast:
type: broadcast
value:
- 'The number of players is not enough for the fishing competition to be started as scheduled.'
bossbar:
enable: true
color: WHITE
overlay: PROGRESS
text:
- '<gray>[<#87CEFA>🎣<gray>] <gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{seconds}s <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Your Rank: <#E6E6FA>{rank} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>No.1 Player: <#E6E6FA>{1_player}'
- '<gray>[<#87CEFA>🎣<gray>] <gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{minute}{second} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Your Score: <#E6E6FA>{score} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>No.1 Score: <#E6E6FA>{1_score}'
- '<gray>[<#87CEFA>🎣<gray>] <gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{minute}{second} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Winning condition: <#E6E6FA>{goal}'
refresh-rate: 20
switch-interval: 200
only-show-to-participants: true
actionbar:
enable: false
text:
- '<gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{seconds}s <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Your Rank: <#E6E6FA>{rank} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>No.1 Player: <#E6E6FA>{1_player}'
- '<gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{minute}{second} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Your Score: <#E6E6FA>{score} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>No.1 Score: <#E6E6FA>{1_score}'
- '<gradient:#F0F8FF:#87CEFA:#F0F8FF>Time Left: <#E6E6FA>{minute}{second} <gray>| <gradient:#F0F8FF:#87CEFA:#F0F8FF>Winning condition: <#E6E6FA>{goal}'
refresh-rate: 5
switch-interval: 200
only-show-to-participants: true
start-actions:
broadcast:
type: broadcast
value:
- '<#D4F2E7>◤─────────────────────────◥'
- ''
- ' <gray>[<#87CEFA>🎣<gray>] <gradient:#F0F8FF:#87CEFA:#F0F8FF>Fishing Competition'
- ''
- ' <#E1FFFF>Objectives:'
- ' <#B0C4DE>Catch as many fish as possible'
- ' <#B0C4DE>Start fishing to participate!'
- ''
- '<#D4F2E7>◣─────────────────────────◢'
end-actions:
broadcast:
type: broadcast
value:
- '<#D4F2E7>◤─────────────────────────◥'
- ''
- ' <gray>[<#87CEFA>🎣<gray>] <gradient:#F0F8FF:#87CEFA:#F0F8FF>Fishing Competition'
- ''
- ' <#E1FFFF>Results:'
- ' <gradient:#FFF8DC:#FFD700:#FFF8DC>No.①: {1_player} - {1_score}'
- ' <gradient:#F5FFFA:#F5F5F5:#F5FFFA>No.②: {2_player} - {2_score}'
- ' <gradient:#D2B48C:#CD853F:#D2B48C>No.③: {3_player} - {3_score}'
- ''
- '<#D4F2E7>◣─────────────────────────◢'
participate-actions:
message:
type: message
value:
- 'You have joined the competition. Good luck!'
# Requirements for participating the competition
participate-requirements: {}
# Rewards
rewards:
1:
command_action:
type: command
value:
- 'money give {player} 200'
messages_action:
type: message
value:
- '<#FF4500>[1st] Congratulations! You got the first prize!'
2:
command_action:
type: command
value:
- 'money give {player} 100'
messages_action:
type: message
value:
- '<#FF4500>[2nd] Just miss the opportunity, try next time!'
3:
command_action:
type: command
value:
- 'money give {player} 100'
messages_action:
type: message
value:
- '<#FF4500>[3rd] Just miss the opportunity, try next time!'
participation:
command_action:
type: command
value:
- 'money give {player} 10'
messages_action:
type: message
value:
- '<#FF4500>Thanks for participation!'

View File

@@ -0,0 +1,71 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
# Vanilla/EcoEnchants: minecraft:enchant:level
# AdvancedEnchantments: AE:enchant:level
minecraft:luck_of_the_sea:1:
requirements: {}
effects:
effect_1:
type: group-mod
value:
- silver_star:+2
- golden_star:+1
minecraft:luck_of_the_sea:2:
requirements: {}
effects:
effect_1:
type: group-mod
value:
- silver_star:+4
- golden_star:+2
minecraft:luck_of_the_sea:3:
requirements: {}
effects:
effect_1:
type: group-mod
value:
- silver_star:+6
- golden_star:+3
minecraft:lure:1:
requirements: {}
effects:
effect_1:
type: conditional
conditions:
"||":
in-lava: true
in-void: true
effects:
effect_1:
type: wait-time
value: -80
minecraft:lure:2:
requirements: {}
effects:
effect_1:
type: conditional
conditions:
"||":
in-lava: true
in-void: true
effects:
effect_1:
type: wait-time
value: -160
minecraft:lure:3:
requirements: {}
effects:
effect_1:
type: conditional
conditions:
"||":
in-lava: true
in-void: true
effects:
effect_1:
type: wait-time
value: -240

View File

@@ -0,0 +1,47 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
skeleton:
show-in-fishfinder: false
disable-stat: true
disable-game: true
entity: skeleton
nick: <white>Skeleton</white>
velocity:
horizontal: 1.1
vertical: 1.3
wither_skeleton:
show-in-fishfinder: false
disable-stat: true
disable-game: true
entity: wither_skeleton
nick: <black>Wither Skeleton</black>
velocity:
horizontal: 1.1
vertical: 1.2
magma_cube:
show-in-fishfinder: false
disable-stat: true
disable-game: true
entity: magma_cube
nick: <red>Magma Cube</black>
velocity:
horizontal: 1.1
vertical: 1.3
skeletalknight:
show-in-fishfinder: false
disable-stat: true
disable-game: true
entity: MythicMobs:SkeletalKnight
nick: Skeletal Knight
velocity:
horizontal: 1.1
vertical: 1.2
properties:
level: 0

View File

@@ -0,0 +1,33 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
delicate_hook:
material: SHEARS
display:
name: '<#1E90FF>Delicate hook'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - An embodiment of craftsmanship and allure, the hook'
- '<gray> - is not your average piece of tackle. Polished to'
- '<gray> - perfection and intricately designed, it gleams in the'
- '<gray> - water, irresistibly drawing high-quality fish closer.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Increase the chance of getting high quality fish'
max-durability: 16
custom-model-data: 50000
# {dur} / {max}
lore-on-rod:
- ''
- '<#7FFFAA>Equipped hook:'
- '<gray> - Delicate hook: <white>{dur}</white> times left'
effects:
effect_1:
type: group-mod
value:
- silver_star:+1
- golden_star:+1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
# Vanilla fishing rod properties
FISHING_ROD:
tag: false
material: fishing_rod
effects:
effect_1:
type: wait-time-multiplier
value: 1.5
# Custom rods
beginner_rod:
material: fishing_rod
display:
name: "<b><#EEE9E9>Beginner's Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Specially designed for those new to the art '
- '<gray> - of fishing, the Beginner''s Fishing Rod is a'
- '<gray> - novice angler''s best friend.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Increase the waiting time'
- '<gray> - Reduces the challenge of fishing'
custom-model-data: 50001
max-durability: 64
effects:
effect_1:
type: wait-time-multiplier
value: 1.5
effect_2:
type: difficulty
value: -8
silver_rod:
material: fishing_rod
display:
name: "<b><#C0C0C0>Silver Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Elegantly crafted with a gleaming finish, the'
- '<gray> - Silver Star Fishing Rod is the dream of every'
- '<gray> - angler seeking silver-star quality fish.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Increase the chance of getting silver star fish'
custom-model-data: 50002
max-durability: 96
effects:
effect_1:
type: group-mod
value:
- silver_star:+8
golden_rod:
material: fishing_rod
display:
name: "<b><#FFD700>Golden Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Glistening under the sunlight and exuding an'
- '<gray> - aura of luxury, the Golden Fishing Rod is the'
- '<gray> - epitome of high-tier angling gear.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Increase the chance of getting golden star fish'
custom-model-data: 50003
max-durability: 80
effects:
effect_1:
type: group-mod
value:
- golden_star:+3
star_rod:
material: fishing_rod
display:
name: "<b><#FFFF00>Star Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Carved from stardust and bathed in cosmic energy,'
- '<gray> - the Star Fishing Rod is a testament to the wonders'
- '<gray> - of the universe. Its endless magical reservoir ensures'
- '<gray> - that time seems to stand still, granting anglers what'
- '<gray> - feels like an eternity to perfect their catch.'
- ''
- '<#FFD700>Effects:'
- '<gray> - +15s Game Time'
custom-model-data: 50004
max-durability: 128
effects:
effect_1:
type: game-time
value: 15
bone_rod:
material: fishing_rod
display:
name: "<b><#CD2626>Bone Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Forged from the bones of age-old skeletons and'
- '<gray> - imbued with dark enchantments, the Skeleton Fishing'
- '<gray> - Rod is not for the faint-hearted. It allows the '
- '<gray> - brave or the mad to fish within the scorching '
- '<gray> - confines of lava, defying the usual limitations of'
- '<gray> - regular rods.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Fish in lava'
- '<gray> - Attract skeletons'
custom-model-data: 50005
max-durability: 32
effects:
effect_1:
type: lava-fishing
magical_rod:
material: fishing_rod
display:
name: '<b><#7B68EE>Magical Fishing Rod'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - A product of arcane innovation, this unique'
- '<gray> - fishing rod uses enchanted books as bait.'
- '<gray> - Its intricate reel mechanism and magical '
- '<gray> - allure demand the exp level of its user,'
- '<gray> - consuming experience levels upon each cast.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Get an enchantment book from fishing.'
- '<gray> - The waiting time is very long.'
- ''
- '<#CD5C5C>Requirements:'
- '<gray> - 1x book bait'
- '<gray> - 10 exp levels'
custom-model-data: 50006
max-durability: 16
requirements:
requirement_1:
type: level
value: 1
not-met-actions:
action_1:
type: message
value:
- 'You don''t have enough levels to control the rod.'
requirement_2:
type: bait
value:
- BOOK
not-met-actions:
action_message:
type: message
value:
- 'You have to hold a book as bait.'
events:
cast:
action_level:
type: level
value: -1
effects:
effect_1:
type: group-mod-ignore-conditions
value:
- enchantments:+10
effect_2:
type: wait-time-multiplier
value: 3
master_rod:
material: fishing_rod
display:
name: "<b><#FFFF00>Master's Fishing Rod"
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Wielded only by the most skilled of anglers,'
- '<gray> - the Master''s Fishing Rod represents the pinnacle'
- '<gray> - of fishing craftsmanship. Meticulously crafted '
- '<gray> - for precision, it significantly reduces the '
- '<gray> - time it takes for a fish to bite.'
- ''
- '<#FFD700>Effects:'
- '<gray> - Reduce the waiting time'
- '<gray> - Increase the challenge of fishing'
- '<gray> - Higher chance of getting quality fish'
custom-model-data: 50007
max-durability: 128
effects:
effect_1:
type: wait-time-multiplier
value: 0.95
effect_2:
type: difficulty
value: +10
effect_3:
type: group-mod
value:
- silver_star:+5
- golden_star:+3

View File

@@ -0,0 +1,299 @@
double_loot_totem:
radius: 8
duration: 300
pattern:
core: 3,1,2 # layer:3 line:1 index:2 -> CRYING_OBSIDIAN
layer:
4:
- '*_STAIRS{face=east;half=top} OBSERVER{face=south} *_STAIRS{face=west;half=top}'
3:
- 'AIR CRYING_OBSIDIAN AIR'
2:
- 'AIR *_LOG{axis=y}||*_PILLAR{axis=y} AIR'
1:
- 'AIR||GRASS||SNOW ANVIL AIR||GRASS||SNOW'
effects:
effect_1:
type: multiple-loot
value: 1.0
requirements:
requirement_1:
type: item-in-hand
value:
hand: main
item: NAUTILUS_SHELL
amount: 1
not-met-actions:
action_1:
type: message
value:
- '<#BA55D3>[TotemActivation]</#BA55D3> <#F5FFFA>Hold a nautilus shell in main hand to activate the totem.'
events:
timer:
actionbar_action:
type: actionbar-nearby
value:
actionbar: '<#BA55D3>[Double Loot Totem]</#BA55D3> Time Left: {time_left} seconds'
range: 16
hologram_action_1:
type: hologram
value:
duration: 20
text: '<#BA55D3>[Double Loot Totem]</#BA55D3>'
position: other
range: 16
y: 3.3
x: 0
z: 0
hologram_action_2:
type: hologram
value:
duration: 20
text: 'Time Left: {time_left} seconds'
position: other
range: 16
y: 2.8
x: 0
z: 0
activate:
remove_item_action:
type: item-amount
value:
hand: main
amount: -1
broadcast_action:
type: message-nearby
value:
message:
- '<#BA55D3>[TotemActivation]</#BA55D3> <#F5FFFA>{player}</#F5FFFA> <#F5F5F5>activated a totem nearby!</#F5F5F5>'
range: 32
particles:
particle_01:
type: REDSTONE
options:
color: 186,85,211
scale: 0.8
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 30
range:
- 0~359
task:
period: 5
delay: 0
particle_02:
type: REDSTONE
options:
color: 148,0,211
scale: 1.1
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 30
range:
- 6~365
task:
period: 5
delay: 1
particle_03:
type: REDSTONE
options:
color: 147,112,219
scale: 1.5
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 30
range:
- 12~371
task:
period: 5
delay: 2
particle_04:
type: REDSTONE
options:
color: 123,104,238
scale: 2
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 30
range:
- 18~377
task:
period: 5
delay: 3
particle_05:
type: REDSTONE
options:
color: 30,144,255
scale: 2.5
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 30
range:
- 24~383
task:
period: 5
delay: 4
particle_06:
type: DRIPPING_OBSIDIAN_TEAR
options:
color: 30,144,255
scale: 3
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 90
range:
- 0~359
task:
period: 40
delay: 0
particle_07:
type: DRIPPING_OBSIDIAN_TEAR
options:
color: 30,144,255
scale: 3
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 90
range:
- 22.5~382
task:
period: 40
delay: 10
particle_08:
type: DRIPPING_OBSIDIAN_TEAR
options:
color: 30,144,255
scale: 3
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 90
range:
- 45~404
task:
period: 40
delay: 20
particle_09:
type: DRIPPING_OBSIDIAN_TEAR
options:
color: 30,144,255
scale: 3
polar-coordinates-formula:
horizontal: '{radius}'
vertical: '3'
theta:
draw-interval: 90
range:
- 67.5~427
task:
period: 40
delay: 20
golden_star_totem:
radius: 10
duration: 120
pattern:
core: 4,2,2 # layer:4 line:2 index:2 -> DAYLIGHT_DETECTOR
layer:
4:
- 'AIR AIR AIR'
- 'AIR DAYLIGHT_DETECTOR AIR'
- 'AIR AIR AIR'
3:
- 'AIR AIR AIR'
- 'AIR LIGHTNING_ROD AIR'
- 'AIR AIR AIR'
2:
- 'AIR AIR AIR'
- 'AIR GOLD_BLOCK AIR'
- 'AIR AIR AIR'
1:
- 'AIR GOLD_BLOCK AIR'
- 'GOLD_BLOCK GOLD_BLOCK GOLD_BLOCK'
- 'AIR GOLD_BLOCK AIR'
particles:
particle_01:
type: REDSTONE
options:
color: 255,215,0
scale: 3
polar-coordinates-formula:
horizontal: '{radius} + sin({theta} * 5) * 2.5'
vertical: '-2.5'
theta:
draw-interval: 5
range:
- 0~360
task:
period: 5
delay: 0
effects:
effect_1:
type: group-mod
value:
- golden_star:+15
requirements:
requirement_1:
type: item-in-hand
value:
hand: main
item: GOLD_INGOT
amount: 1
not-met-actions:
action_1:
type: message
value:
- '<#BA55D3>[TotemActivation]</#BA55D3> <#F5FFFA>Hold a gold ingot in main hand to activate the totem.'
events:
timer:
actionbar_action:
type: actionbar-nearby
value:
actionbar: '<#FFFF00>[Golden Star Totem]</#FFFF00> Time Left: {time_left} seconds'
range: 16
hologram_action_1:
type: hologram
value:
duration: 20
text: '<#FFFF00>[Golden Star Totem]</#FFFF00>'
position: other
range: 16
y: 1
x: 0
z: 0
hologram_action_2:
type: hologram
value:
duration: 20
text: 'Time Left: {time_left} seconds'
position: other
range: 16
y: 0.5
x: 0
z: 0
activate:
remove_item_action:
type: item-amount
value:
hand: main
amount: -1
broadcast_action:
type: message-nearby
value:
message:
- '<#FFFF00>[TotemActivation]</#FFFF00> <#F5FFFA>{player}</#F5FFFA> <#F5F5F5>activated a totem nearby!</#F5F5F5>'
range: 32

View File

@@ -0,0 +1,47 @@
# Note: These are the default configurations of the plugin
# and do not necessarily mean that players can have a good
# gaming experience. We hope that you will create
# customized configurations based on your own ideas,
# allowing players to experience the uniqueness of your server.
fishfinder:
material: PAPER
display:
name: '<#1E90FF>Fish Finder'
lore:
- ''
- '<#7FFFD4>Desciption:'
- '<gray> - Compact yet powerful, it scans the waters'
- '<gray> - with precision, revealing the variety of'
- '<gray> - fish species lurking below the surface.'
- '<gray> - Whether you''re in uncharted territories or'
- '<gray> - familiar waters, this device ensures you''re'
- '<gray> - always informed about your aquatic neighbors.'
- ''
custom-model-data: 50000
events:
interact:
conditional_action:
type: conditional
value:
conditions:
condition_1:
type: cooldown
value:
key: fishfinder
time: 3000
not-met-actions:
action_1:
type: message
value:
- 'Slow down! You are using the fish finder too frequently.'
actions:
action_1:
type: fish-finder
value: water
water_effect:
material: PAPER
custom-model-data: 49998
lava_effect:
material: PAPER
custom-model-data: 49999

Some files were not shown because too many files have changed in this diff Show More