9
0
mirror of https://github.com/Xiao-MoMi/Custom-Crops.git synced 2025-12-28 03:19:15 +00:00
This commit is contained in:
XiaoMoMi
2024-08-31 22:57:45 +08:00
parent 3cbd1f65a6
commit bbde4ebd47
475 changed files with 24733 additions and 24341 deletions

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) <2024> <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.customcrops.api;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.core.AbstractItemManager;
import net.momirealms.customcrops.api.core.ConfigManager;
import net.momirealms.customcrops.api.core.RegistryAccess;
import net.momirealms.customcrops.api.core.world.WorldManager;
import net.momirealms.customcrops.api.integration.IntegrationManager;
import net.momirealms.customcrops.api.misc.cooldown.CoolDownManager;
import net.momirealms.customcrops.api.misc.placeholder.PlaceholderManager;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.common.dependency.DependencyManager;
import net.momirealms.customcrops.common.locale.TranslationManager;
import net.momirealms.customcrops.common.plugin.CustomCropsPlugin;
import net.momirealms.customcrops.common.plugin.scheduler.AbstractJavaScheduler;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerAdapter;
import net.momirealms.customcrops.common.sender.SenderFactory;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public abstract class BukkitCustomCropsPlugin implements CustomCropsPlugin {
private static BukkitCustomCropsPlugin instance;
private final Plugin boostrap;
protected AbstractJavaScheduler<Location, World> scheduler;
protected DependencyManager dependencyManager;
protected TranslationManager translationManager;
protected AbstractItemManager itemManager;
protected PlaceholderManager placeholderManager;
protected CoolDownManager coolDownManager;
protected ConfigManager configManager;
protected IntegrationManager integrationManager;
protected WorldManager worldManager;
protected RegistryAccess registryAccess;
protected SenderFactory<BukkitCustomCropsPlugin, CommandSender> senderFactory;
protected final Map<Class<?>, ActionManager<?>> actionManagers = new HashMap<>();
protected final Map<Class<?>, RequirementManager<?>> requirementManagers = new HashMap<>();
/**
* Constructs a new BukkitCustomCropsPlugin instance.
*
* @param boostrap the plugin instance used to initialize this class
*/
public BukkitCustomCropsPlugin(Plugin boostrap) {
if (!boostrap.getName().equals("CustomCrops")) {
throw new IllegalArgumentException("CustomCrops plugin requires custom crops plugin");
}
this.boostrap = boostrap;
instance = this;
}
/**
* Retrieves the singleton instance of BukkitCustomFishingPlugin.
*
* @return the singleton instance
* @throws IllegalArgumentException if the plugin is not initialized
*/
public static BukkitCustomCropsPlugin getInstance() {
if (instance == null) {
throw new IllegalArgumentException("Plugin not initialized");
}
return instance;
}
/**
* Retrieves the plugin instance used to initialize this class.
*
* @return the {@link Plugin} instance
*/
public Plugin getBoostrap() {
return boostrap;
}
/**
* Retrieves the DependencyManager.
*
* @return the {@link DependencyManager}
*/
@Override
public DependencyManager getDependencyManager() {
return dependencyManager;
}
/**
* Retrieves the TranslationManager.
*
* @return the {@link TranslationManager}
*/
@Override
public TranslationManager getTranslationManager() {
return translationManager;
}
public AbstractItemManager getItemManager() {
return itemManager;
}
@Override
public SchedulerAdapter<Location, World> getScheduler() {
return scheduler;
}
public SenderFactory<BukkitCustomCropsPlugin, CommandSender> getSenderFactory() {
return senderFactory;
}
public WorldManager getWorldManager() {
return worldManager;
}
public IntegrationManager getIntegrationManager() {
return integrationManager;
}
public PlaceholderManager getPlaceholderManager() {
return placeholderManager;
}
/**
* Logs a debug message.
*
* @param message the message to log
*/
public abstract void debug(Object message);
public File getDataFolder() {
return boostrap.getDataFolder();
}
public RegistryAccess getRegistryAccess() {
return registryAccess;
}
@SuppressWarnings("unchecked")
public <T> ActionManager<T> getActionManager(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be null");
}
return (ActionManager<T>) actionManagers.get(type);
}
@SuppressWarnings("unchecked")
public <T> RequirementManager<T> getRequirementManager(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be null");
}
return (RequirementManager<T>) instance.requirementManagers.get(type);
}
public CoolDownManager getCoolDownManager() {
return coolDownManager;
}
}

View File

@@ -1,129 +0,0 @@
/*
* 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.customcrops.api;
import net.momirealms.customcrops.api.manager.*;
import net.momirealms.customcrops.api.scheduler.Scheduler;
import org.bukkit.plugin.java.JavaPlugin;
public abstract class CustomCropsPlugin extends JavaPlugin {
protected static CustomCropsPlugin instance;
protected VersionManager versionManager;
protected ConfigManager configManager;
protected Scheduler scheduler;
protected RequirementManager requirementManager;
protected ActionManager actionManager;
protected IntegrationManager integrationManager;
protected CoolDownManager coolDownManager;
protected WorldManager worldManager;
protected ItemManager itemManager;
protected AdventureManager adventure;
protected MessageManager messageManager;
protected ConditionManager conditionManager;
protected PlaceholderManager placeholderManager;
public CustomCropsPlugin() {
instance = this;
}
public static CustomCropsPlugin getInstance() {
return instance;
}
public static CustomCropsPlugin get() {
return instance;
}
/* Get version manager */
public VersionManager getVersionManager() {
return versionManager;
}
/* Get config manager */
public ConfigManager getConfigManager() {
return configManager;
}
/* Get scheduler */
public Scheduler getScheduler() {
return scheduler;
}
/* Get requirement manager */
public RequirementManager getRequirementManager() {
return requirementManager;
}
/* Get integration manager */
public IntegrationManager getIntegrationManager() {
return integrationManager;
}
/* Get action manager */
public ActionManager getActionManager() {
return actionManager;
}
/* Get cool down manager */
public CoolDownManager getCoolDownManager() {
return coolDownManager;
}
/* Get world data manager */
public WorldManager getWorldManager() {
return worldManager;
}
/* Get item manager */
public ItemManager getItemManager() {
return itemManager;
}
/* Get message manager */
public MessageManager getMessageManager() {
return messageManager;
}
/* Get adventure manager */
public AdventureManager getAdventure() {
return adventure;
}
/* Get condition manager */
public ConditionManager getConditionManager() {
return conditionManager;
}
/* Get placeholder manager */
public PlaceholderManager getPlaceholderManager() {
return placeholderManager;
}
public abstract boolean isHookedPluginEnabled(String plugin);
public abstract void debug(String debug);
public abstract void reload();
public abstract boolean isHookedPluginEnabled(String hooked, String... versionPrefix);
public abstract boolean doesHookedPluginExist(String plugin);
public abstract String getServerVersion();
}

View File

@@ -0,0 +1,891 @@
package net.momirealms.customcrops.api.action;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.kyori.adventure.audience.Audience;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.context.ContextKeys;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.block.*;
import net.momirealms.customcrops.api.core.item.Fertilizer;
import net.momirealms.customcrops.api.core.item.FertilizerConfig;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsChunk;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.event.CropPlantEvent;
import net.momirealms.customcrops.api.misc.placeholder.BukkitPlaceholderManager;
import net.momirealms.customcrops.api.misc.value.MathValue;
import net.momirealms.customcrops.api.misc.value.TextValue;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.api.util.*;
import net.momirealms.customcrops.common.helper.AdventureHelper;
import net.momirealms.customcrops.common.helper.VersionHelper;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customcrops.common.util.*;
import net.momirealms.sparrow.heart.SparrowHeart;
import net.momirealms.sparrow.heart.feature.entity.FakeEntity;
import net.momirealms.sparrow.heart.feature.entity.armorstand.FakeArmorStand;
import net.momirealms.sparrow.heart.feature.entity.display.FakeItemDisplay;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
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 abstract class AbstractActionManager<T> implements ActionManager<T> {
protected final BukkitCustomCropsPlugin plugin;
private final HashMap<String, ActionFactory<T>> actionFactoryMap = new HashMap<>();
private static final String EXPANSION_FOLDER = "expansions/action";
public AbstractActionManager(BukkitCustomCropsPlugin plugin) {
this.plugin = plugin;
this.registerBuiltInActions();
}
protected void registerBuiltInActions() {
this.registerCommandAction();
this.registerBroadcastAction();
this.registerNearbyMessage();
this.registerNearbyActionBar();
this.registerNearbyTitle();
this.registerParticleAction();
this.registerQualityCropsAction();
this.registerDropItemsAction();
this.registerLegacyDropItemsAction();
this.registerFakeItemAction();
this.registerPlantAction();
this.registerBreakAction();
}
@Override
public boolean registerAction(ActionFactory<T> actionFactory, String... types) {
for (String type : types) {
if (this.actionFactoryMap.containsKey(type)) return false;
}
for (String type : types) {
this.actionFactoryMap.put(type, actionFactory);
}
return true;
}
@Override
public boolean unregisterAction(String type) {
return this.actionFactoryMap.remove(type) != null;
}
@Override
public boolean hasAction(@NotNull String type) {
return actionFactoryMap.containsKey(type);
}
@Nullable
@Override
public ActionFactory<T> getActionFactory(@NotNull String type) {
return actionFactoryMap.get(type);
}
@Override
public Action<T> parseAction(Section section) {
if (section == null) return Action.empty();
ActionFactory<T> factory = getActionFactory(section.getString("type"));
if (factory == null) {
plugin.getPluginLogger().warn("Action type: " + section.getString("type") + " doesn't exist.");
return Action.empty();
}
return factory.process(section.get("value"), section.getDouble("chance", 1d));
}
@NotNull
@Override
@SuppressWarnings("unchecked")
public Action<T>[] parseActions(Section section) {
ArrayList<Action<T>> actionList = new ArrayList<>();
if (section != null)
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
Action<T> action = parseAction(innerSection);
if (action != null)
actionList.add(action);
}
}
return actionList.toArray(new Action[0]);
}
@Override
public Action<T> parseAction(@NotNull String type, @NotNull Object args) {
ActionFactory<T> factory = getActionFactory(type);
if (factory == null) {
plugin.getPluginLogger().warn("Action type: " + type + " doesn't exist.");
return Action.empty();
}
return factory.process(args, 1);
}
@SuppressWarnings({"ResultOfMethodCallIgnored", "unchecked"})
protected void loadExpansions(Class<T> tClass) {
File expansionFolder = new File(plugin.getDataFolder(), EXPANSION_FOLDER);
if (!expansionFolder.exists())
expansionFolder.mkdirs();
List<Class<? extends ActionExpansion<T>>> classes = new ArrayList<>();
File[] expansionJars = expansionFolder.listFiles();
if (expansionJars == null) return;
for (File expansionJar : expansionJars) {
if (expansionJar.getName().endsWith(".jar")) {
try {
Class<? extends ActionExpansion<T>> expansionClass = (Class<? extends ActionExpansion<T>>) ClassUtils.findClass(expansionJar, ActionExpansion.class, tClass);
classes.add(expansionClass);
} catch (IOException | ClassNotFoundException e) {
plugin.getPluginLogger().warn("Failed to load expansion: " + expansionJar.getName(), e);
}
}
}
try {
for (Class<? extends ActionExpansion<T>> expansionClass : classes) {
ActionExpansion<T> expansion = expansionClass.getDeclaredConstructor().newInstance();
unregisterAction(expansion.getActionType());
registerAction(expansion.getActionFactory(), expansion.getActionType());
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);
}
}
protected void registerBroadcastAction() {
registerAction((args, chance) -> {
List<String> messages = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
OfflinePlayer offlinePlayer = null;
if (context.holder() instanceof Player player) {
offlinePlayer = player;
}
List<String> replaced = plugin.getPlaceholderManager().parse(offlinePlayer, messages, context.placeholderMap());
for (Player player : Bukkit.getOnlinePlayers()) {
Audience audience = plugin.getSenderFactory().getAudience(player);
for (String text : replaced) {
audience.sendMessage(AdventureHelper.miniMessage(text));
}
}
};
}, "broadcast");
}
protected void registerNearbyMessage() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
List<String> messages = ListUtils.toList(section.get("message"));
MathValue<T> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
double realRange = range.evaluate(context);
OfflinePlayer owner = null;
if (context.holder() instanceof Player player) {
owner = player;
}
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= 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));
}
}
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at message-nearby action which should be Section");
return Action.empty();
}
}, "message-nearby");
}
protected void registerNearbyActionBar() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
String actionbar = section.getString("actionbar");
MathValue<T> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
OfflinePlayer owner = null;
if (context.holder() instanceof Player player) {
owner = player;
}
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
double realRange = range.evaluate(context);
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= 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));
}
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at actionbar-nearby action which should be Section");
return Action.empty();
}
}, "actionbar-nearby");
}
protected void registerCommandAction() {
registerAction((args, chance) -> {
List<String> commands = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
OfflinePlayer owner = null;
if (context.holder() instanceof Player player) {
owner = player;
}
List<String> replaced = BukkitPlaceholderManager.getInstance().parse(owner, commands, context.placeholderMap());
plugin.getScheduler().sync().run(() -> {
for (String text : replaced) {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), text);
}
}, null);
};
}, "command");
registerAction((args, chance) -> {
List<String> commands = ListUtils.toList(args);
return context -> {
if (Math.random() > chance) return;
OfflinePlayer owner = null;
if (context.holder() instanceof Player player) {
owner = player;
}
String random = commands.get(ThreadLocalRandom.current().nextInt(commands.size()));
random = BukkitPlaceholderManager.getInstance().parse(owner, random, context.placeholderMap());
String finalRandom = random;
plugin.getScheduler().sync().run(() -> {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalRandom);
}, null);
};
}, "random-command");
registerAction((args, chance) -> {
if (args instanceof Section section) {
List<String> cmd = ListUtils.toList(section.get("command"));
MathValue<T> range = MathValue.auto(section.get("range"));
return context -> {
if (Math.random() > chance) return;
OfflinePlayer owner = null;
if (context.holder() instanceof Player player) {
owner = player;
}
double realRange = range.evaluate(context);
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= 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);
}
}
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at command-nearby action which should be Section");
return Action.empty();
}
}, "command-nearby");
}
protected void registerBundleAction(Class<T> tClass) {
registerAction((args, chance) -> {
List<Action<T>> 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<T> action : actions) {
action.trigger(context);
}
};
}, "chain");
registerAction((args, chance) -> {
List<Action<T>> 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<T> action : actions)
action.trigger(context);
}, delay * 50L, TimeUnit.MILLISECONDS);
} else {
plugin.getScheduler().sync().runLater(() -> {
for (Action<T> action : actions)
action.trigger(context);
}, delay, location);
}
};
}, "delay");
registerAction((args, chance) -> {
List<Action<T>> 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<T> action : actions) {
action.trigger(context);
}
}, delay * 50L, period * 50L, TimeUnit.MILLISECONDS);
} else {
task = plugin.getScheduler().sync().runRepeating(() -> {
for (Action<T> action : actions) {
action.trigger(context);
}
}, delay, period, location);
}
plugin.getScheduler().asyncLater(task::cancel, duration * 50L, TimeUnit.MILLISECONDS);
};
}, "timer");
registerAction((args, chance) -> {
if (args instanceof Section section) {
Action<T>[] actions = parseActions(section.getSection("actions"));
Requirement<T>[] requirements = plugin.getRequirementManager(tClass).parseRequirements(section.getSection("conditions"), true);
return condition -> {
if (Math.random() > chance) return;
for (Requirement<T> requirement : requirements) {
if (!requirement.isSatisfied(condition)) {
return;
}
}
for (Action<T> 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 Action.empty();
}
}, "conditional");
registerAction((args, chance) -> {
if (args instanceof Section section) {
List<Pair<Requirement<T>[], Action<T>[]>> conditionActionPairList = new ArrayList<>();
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
Action<T>[] actions = parseActions(inner.getSection("actions"));
Requirement<T>[] requirements = plugin.getRequirementManager(tClass).parseRequirements(inner.getSection("conditions"), false);
conditionActionPairList.add(Pair.of(requirements, actions));
}
}
return context -> {
if (Math.random() > chance) return;
outer:
for (Pair<Requirement<T>[], Action<T>[]> pair : conditionActionPairList) {
if (pair.left() != null)
for (Requirement<T> requirement : pair.left()) {
if (!requirement.isSatisfied(context)) {
continue outer;
}
}
if (pair.right() != null)
for (Action<T> 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 Action.empty();
}
}, "priority");
}
protected void registerNearbyTitle() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
TextValue<T> title = TextValue.auto(section.getString("title"));
TextValue<T> 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));
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= 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
);
}
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at title-nearby action which is expected to be `Section`");
return Action.empty();
}
}, "title-nearby");
}
protected void registerParticleAction() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
Particle particleType = ParticleUtils.getParticle(section.getString("particle", "ASH").toUpperCase(Locale.ENGLISH));
double x = section.getDouble("x",0.0);
double y = section.getDouble("y",0.0);
double z = section.getDouble("z",0.0);
double offSetX = section.getDouble("offset-x",0.0);
double offSetY = section.getDouble("offset-y",0.0);
double offSetZ = section.getDouble("offset-z",0.0);
int count = section.getInt("count", 1);
double extra = section.getDouble("extra", 0.0);
float scale = section.getDouble("scale", 1d).floatValue();
ItemStack itemStack;
if (section.contains("itemStack"))
itemStack = BukkitCustomCropsPlugin.getInstance()
.getItemManager()
.build(null, section.getString("itemStack"));
else
itemStack = null;
Color color;
if (section.contains("color")) {
String[] rgb = section.getString("color","255,255,255").split(",");
color = Color.fromRGB(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]));
} else {
color = null;
}
Color toColor;
if (section.contains("color")) {
String[] rgb = section.getString("to-color","255,255,255").split(",");
toColor = Color.fromRGB(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]));
} else {
toColor = null;
}
return context -> {
if (Math.random() > chance) return;
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
location.getWorld().spawnParticle(
particleType,
location.getX() + x, location.getY() + y, location.getZ() + z,
count,
offSetX, offSetY, offSetZ,
extra,
itemStack != null ? itemStack : (color != null && toColor != null ? new Particle.DustTransition(color, toColor, scale) : (color != null ? new Particle.DustOptions(color, scale) : null))
);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at particle action which is expected to be `Section`");
return Action.empty();
}
}, "particle");
}
protected void registerQualityCropsAction() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
MathValue<T> min = MathValue.auto(section.get("min"));
MathValue<T> max = MathValue.auto(section.get("max"));
boolean toInv = section.getBoolean("to-inventory", false);
String[] qualityLoots = new String[ConfigManager.defaultQualityRatio().length];
for (int i = 1; i <= ConfigManager.defaultQualityRatio().length; i++) {
qualityLoots[i-1] = section.getString("items." + i);
if (qualityLoots[i-1] == null) {
plugin.getPluginLogger().warn("items." + i + " should not be null");
qualityLoots[i-1] = "";
}
}
return context -> {
if (Math.random() > chance) return;
double[] ratio = ConfigManager.defaultQualityRatio();
int random = RandomUtils.generateRandomInt((int) min.evaluate(context), (int) max.evaluate(context));
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
Optional<CustomCropsWorld<?>> world = plugin.getWorldManager().getWorld(location.getWorld());
if (world.isEmpty()) {
return;
}
Pos3 pos3 = Pos3.from(location);
Fertilizer[] fertilizers = null;
Player player = null;
if (context.holder() instanceof Player p) {
player = p;
}
Pos3 potLocation = pos3.add(0, -1, 0);
Optional<CustomCropsChunk> chunk = world.get().getChunk(potLocation.toChunkPos());
if (chunk.isPresent()) {
Optional<CustomCropsBlockState> state = chunk.get().getBlockState(potLocation);
if (state.isPresent()) {
if (state.get().type() instanceof PotBlock potBlock) {
fertilizers = potBlock.fertilizers(state.get());
}
}
}
ArrayList<FertilizerConfig> configs = new ArrayList<>();
if (fertilizers != null) {
for (Fertilizer fertilizer : fertilizers) {
Optional.ofNullable(fertilizer.config()).ifPresent(configs::add);
}
}
for (FertilizerConfig config : configs) {
random = config.processDroppedItemAmount(random);
double[] newRatio = config.overrideQualityRatio();
if (newRatio != null) {
ratio = newRatio;
}
}
for (int i = 0; i < random; i++) {
double r1 = Math.random();
for (int j = 0; j < ratio.length; j++) {
if (r1 < ratio[j]) {
ItemStack drop = plugin.getItemManager().build(player, qualityLoots[j]);
if (drop == null || drop.getType() == Material.AIR) return;
if (toInv && player != null) {
PlayerUtils.giveItem(player, drop, 1);
} else {
location.getWorld().dropItemNaturally(location, drop);
}
break;
}
}
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at quality-crops action which is expected to be `Section`");
return Action.empty();
}
}, "quality-crops");
}
protected void registerDropItemsAction() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
boolean ignoreFertilizer = section.getBoolean("ignore-fertilizer", true);
String item = section.getString("item");
MathValue<T> min = MathValue.auto(section.get("min"));
MathValue<T> max = MathValue.auto(section.get("max"));
boolean toInv = section.getBoolean("to-inventory", false);
return context -> {
if (Math.random() > chance) return;
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
Optional<CustomCropsWorld<?>> world = plugin.getWorldManager().getWorld(location.getWorld());
if (world.isEmpty()) {
return;
}
Player player = null;
if (context.holder() instanceof Player p) {
player = p;
}
ItemStack itemStack = plugin.getItemManager().build(player, item);
if (itemStack != null) {
int random = RandomUtils.generateRandomInt((int) min.evaluate(context), (int) max.evaluate(context));
if (!ignoreFertilizer) {
Pos3 pos3 = Pos3.from(location);
Fertilizer[] fertilizers = null;
Pos3 potLocation = pos3.add(0, -1, 0);
Optional<CustomCropsChunk> chunk = world.get().getChunk(potLocation.toChunkPos());
if (chunk.isPresent()) {
Optional<CustomCropsBlockState> state = chunk.get().getBlockState(potLocation);
if (state.isPresent()) {
if (state.get().type() instanceof PotBlock potBlock) {
fertilizers = potBlock.fertilizers(state.get());
}
}
}
ArrayList<FertilizerConfig> configs = new ArrayList<>();
if (fertilizers != null) {
for (Fertilizer fertilizer : fertilizers) {
Optional.ofNullable(fertilizer.config()).ifPresent(configs::add);
}
}
for (FertilizerConfig config : configs) {
random = config.processDroppedItemAmount(random);
}
}
itemStack.setAmount(random);
if (toInv && player != null) {
PlayerUtils.giveItem(player, itemStack, random);
} else {
location.getWorld().dropItemNaturally(location, itemStack);
}
} else {
plugin.getPluginLogger().warn("Item: " + item + " doesn't exist");
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at drop-item action which is expected to be `Section`");
return Action.empty();
}
}, "drop-item");
}
protected void registerLegacyDropItemsAction() {
registerAction((args, chance) -> {
if (args instanceof Section section) {
List<Action<T>> actions = new ArrayList<>();
Section otherItemSection = section.getSection("other-items");
if (otherItemSection != null) {
for (Map.Entry<String, Object> entry : otherItemSection.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
actions.add(requireNonNull(getActionFactory("drop-item")).process(inner, inner.getDouble("chance", 1D)));
}
}
}
Section qualitySection = section.getSection("quality-crops");
if (qualitySection != null) {
actions.add(requireNonNull(getActionFactory("quality-crops")).process(qualitySection, 1));
}
return context -> {
if (Math.random() > chance) return;
for (Action<T> action : actions) {
action.trigger(context);
}
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at drop-items action which is expected to be `Section`");
return Action.empty();
}
}, "drop-items");
}
private void registerFakeItemAction() {
registerAction(((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<T> duration = MathValue.auto(section.get("duration", 20));
boolean position = !section.getString("position", "player").equals("player");
MathValue<T> x = MathValue.auto(section.get("x", 0));
MathValue<T> y = MathValue.auto(section.get("y", 0));
MathValue<T> z = MathValue.auto(section.get("z", 0));
MathValue<T> yaw = MathValue.auto(section.get("yaw", 0));
int range = section.getInt("range", 0);
boolean useItemDisplay = section.getBoolean("use-item-display", false);
boolean onlyShowToOne = !section.getBoolean("visible-to-all", true);
String finalItemID = itemID;
return context -> {
if (Math.random() > chance) return;
if (context.argOrDefault(ContextKeys.OFFLINE, false)) return;
Player owner = null;
if (context.holder() instanceof Player p) {
owner = p;
}
Location location = position ? requireNonNull(context.arg(ContextKeys.LOCATION)).clone() : requireNonNull(owner).getLocation().clone();
location.add(x.evaluate(context), y.evaluate(context) - 1, z.evaluate(context));
location.setPitch(0);
location.setYaw((float) yaw.evaluate(context));
FakeEntity fakeEntity;
if (useItemDisplay && VersionHelper.isVersionNewerThan1_19_4()) {
location.add(0,1.5,0);
FakeItemDisplay itemDisplay = SparrowHeart.getInstance().createFakeItemDisplay(location);
itemDisplay.item(plugin.getItemManager().build(owner, finalItemID));
fakeEntity = itemDisplay;
} else {
FakeArmorStand armorStand = SparrowHeart.getInstance().createFakeArmorStand(location);
armorStand.invisible(true);
armorStand.equipment(EquipmentSlot.HEAD, plugin.getItemManager().build(owner, finalItemID));
fakeEntity = armorStand;
}
ArrayList<Player> viewers = new ArrayList<>();
if (onlyShowToOne) {
viewers.add(owner);
} else {
if (range > 0) {
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= range) {
viewers.add(player);
}
}
} else {
viewers.add(owner);
}
}
for (Player player : viewers) {
fakeEntity.spawn(player);
}
plugin.getScheduler().asyncLater(() -> {
for (Player player : viewers) {
if (player.isOnline() && player.isValid()) {
fakeEntity.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 Action.empty();
}
}), "fake-item");
}
@SuppressWarnings("unchecked")
protected void registerPlantAction() {
this.registerAction((args, chance) -> {
if (args instanceof Section section) {
int point = section.getInt("point", 0);
String key = requireNonNull(section.getString("crop"));
int y = section.getInt("y", 0);
boolean triggerAction = section.getBoolean("trigger-event", false);
return context -> {
if (Math.random() > chance) return;
CropConfig cropConfig = Registries.CROP.get(key);
if (cropConfig == null) {
plugin.getPluginLogger().warn("`plant` action is not executed due to crop[" + key + "] not exists");
return;
}
Location cropLocation = requireNonNull(context.arg(ContextKeys.LOCATION)).clone().add(0,y,0);
Location potLocation = cropLocation.clone().subtract(0,1,0);
Optional<CustomCropsWorld<?>> optionalWorld = plugin.getWorldManager().getWorld(cropLocation.getWorld());
if (optionalWorld.isEmpty()) {
return;
}
CustomCropsWorld<?> world = optionalWorld.get();
PotBlock potBlock = (PotBlock) BuiltInBlockMechanics.POT.mechanic();
Pos3 potPos3 = Pos3.from(potLocation);
String potItemID = plugin.getItemManager().blockID(potLocation);
PotConfig potConfig = Registries.ITEM_TO_POT.get(potItemID);
CustomCropsBlockState potState = potBlock.fixOrGetState(world, potPos3, potConfig, potItemID);
if (potState == null) {
plugin.getPluginLogger().warn("Pot doesn't exist below the crop when executing `plant` action at location[" + world.worldName() + "," + potPos3 + "]");
return;
}
CropBlock cropBlock = (CropBlock) BuiltInBlockMechanics.CROP.mechanic();
CustomCropsBlockState state = BuiltInBlockMechanics.CROP.createBlockState();
cropBlock.id(state, key);
cropBlock.point(state, point);
if (context.holder() instanceof Player player) {
EquipmentSlot slot = requireNonNull(context.arg(ContextKeys.SLOT));
CropPlantEvent plantEvent = new CropPlantEvent(player, player.getInventory().getItem(slot), slot, cropLocation, cropConfig, state, point);
if (EventUtils.fireAndCheckCancel(plantEvent)) {
return;
}
cropBlock.point(state, plantEvent.getPoint());
if (triggerAction) {
ActionManager.trigger((Context<Player>) context, cropConfig.plantActions());
}
}
CropStageConfig stageConfigWithModel = cropConfig.stageWithModelByPoint(cropBlock.point(state));
world.addBlockState(Pos3.from(cropLocation), state);
plugin.getScheduler().sync().run(() -> {
plugin.getItemManager().remove(cropLocation, ExistenceForm.ANY);
plugin.getItemManager().place(cropLocation, stageConfigWithModel.existenceForm(), requireNonNull(stageConfigWithModel.stageID()), cropConfig.rotation() ? FurnitureRotation.random() : FurnitureRotation.NONE);
}, cropLocation);
};
} else {
plugin.getPluginLogger().warn("Invalid value type: " + args.getClass().getSimpleName() + " found at plant action which is expected to be `Section`");
return Action.empty();
}
}, "plant", "replant");
}
protected void registerBreakAction() {
this.registerAction((args, chance) -> {
boolean triggerEvent = (boolean) args;
return context -> {
Location location = requireNonNull(context.arg(ContextKeys.LOCATION));
Optional<CustomCropsWorld<?>> optionalWorld = plugin.getWorldManager().getWorld(location.getWorld());
if (optionalWorld.isEmpty()) {
return;
}
Pos3 pos3 = Pos3.from(location);
CustomCropsWorld<?> world = optionalWorld.get();
Optional<CustomCropsBlockState> optionalState = world.getBlockState(pos3);
if (optionalState.isEmpty()) {
return;
}
CustomCropsBlockState state = optionalState.get();
if (!(state.type() instanceof CropBlock cropBlock)) {
return;
}
CropConfig config = cropBlock.config(state);
if (config == null) {
return;
}
if (triggerEvent) {
CropStageConfig stageConfig = config.stageWithModelByPoint(cropBlock.point(state));
Player player = null;
if (context.holder() instanceof Player p) {
player = p;
}
FakeCancellable fakeCancellable = new FakeCancellable();
if (player != null) {
EquipmentSlot slot = requireNonNull(context.arg(ContextKeys.SLOT));
ItemStack itemStack = player.getInventory().getItem(slot);
state.type().onBreak(new WrappedBreakEvent(player, null, world, location, stageConfig.stageID(), itemStack, plugin.getItemManager().id(itemStack), BreakReason.ACTION, fakeCancellable));
} else {
state.type().onBreak(new WrappedBreakEvent(null, null, world, location, stageConfig.stageID(), null, null, BreakReason.ACTION, fakeCancellable));
}
if (fakeCancellable.isCancelled()) {
return;
}
}
world.removeBlockState(pos3);
plugin.getItemManager().remove(location, ExistenceForm.ANY);
};
}, "break");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <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
@@ -15,16 +15,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item.fertilizer;
package net.momirealms.customcrops.api.action;
import net.momirealms.customcrops.api.mechanic.item.Fertilizer;
import net.momirealms.customcrops.api.context.Context;
public interface Variation extends Fertilizer {
/**
* The Action interface defines a generic action that can be triggered based on a provided context.
*
* @param <T> the type of the object that is used in the context for triggering the action.
*/
public interface Action<T> {
/**
* Get the bonus of variation chance
* Triggers the action based on the provided condition.
*
* @return chance bonus
* @param context the context
*/
double getChanceBonus();
void trigger(Context<T> context);
static <T> Action<T> empty() {
return EmptyAction.instance();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) <2024> <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.customcrops.api.action;
/**
* Abstract class representing an expansion of an action.
* This class should be extended to provide specific implementations of actions.
*
* @param <T> the type parameter for the action factory
*/
public abstract class ActionExpansion<T> {
/**
* Retrieves the version of this action expansion.
*
* @return a String representing the version of the action expansion
*/
public abstract String getVersion();
/**
* Retrieves the author of this action expansion.
*
* @return a String representing the author of the action expansion
*/
public abstract String getAuthor();
/**
* Retrieves the type of this action.
*
* @return a String representing the type of action
*/
public abstract String getActionType();
/**
* Retrieves the action factory associated with this action expansion.
*
* @return an ActionFactory of type T that creates instances of the action
*/
public abstract ActionFactory<T> getActionFactory();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <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
@@ -15,18 +15,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.common.item;
package net.momirealms.customcrops.api.action;
import net.momirealms.customcrops.api.mechanic.action.ActionTrigger;
import net.momirealms.customcrops.api.mechanic.requirement.State;
public interface EventItem {
/**
* Interface representing a factory for creating actions.
*
* @param <T> the type of object that the action will operate on
*/
public interface ActionFactory<T> {
/**
* Trigger events
* Constructs an action based on the provided arguments.
*
* @param actionTrigger trigger
* @param state state
* @param args the args containing the arguments needed to build the action
* @return the constructed action
*/
void trigger(ActionTrigger actionTrigger, State state);
Action<T> process(Object args, double chance);
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) <2024> <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.customcrops.api.action;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.common.plugin.feature.Reloadable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* The ActionManager interface manages custom action types and provides methods for handling actions.
*
* @param <T> the type of the context in which the actions are triggered.
*/
public interface ActionManager<T> extends Reloadable {
/**
* Registers a custom action type with its corresponding factory.
*
* @param actionFactory The factory responsible for creating instances of the action.
* @param alias The type identifier of the action.
* @return True if registration was successful, false if the type is already registered.
*/
boolean registerAction(ActionFactory<T> actionFactory, String... alias);
/**
* Unregisters a custom action type.
*
* @param type The type identifier of the action to unregister.
* @return True if unregistration was successful, false if the type is not registered.
*/
boolean unregisterAction(String type);
/**
* Checks if an action type is registered.
*
* @param type The type identifier of the action.
* @return True if the action type is registered, otherwise false.
*/
boolean hasAction(@NotNull String type);
/**
* Retrieves the action factory for the specified action type.
*
* @param type The type identifier of the action.
* @return The action factory for the specified type, or null if no factory is found.
*/
@Nullable
ActionFactory<T> getActionFactory(@NotNull String type);
/**
* Parses an action from a configuration section.
*
* @param section The configuration section containing the action definition.
* @return The parsed action.
*/
Action<T> parseAction(Section section);
/**
* Parses an array of actions from a configuration section.
*
* @param section The configuration section containing the action definitions.
* @return An array of parsed actions.
*/
@NotNull
Action<T>[] parseActions(Section section);
/**
* Parses an action from the given type and arguments.
*
* @param type The type identifier of the action.
* @param args The arguments for the action.
* @return The parsed action.
*/
Action<T> parseAction(@NotNull String type, @NotNull Object args);
/**
* Generates a map of actions triggered by specific events from a configuration section.
*
* @param section The configuration section containing event-action mappings.
* @return A map where the keys are action triggers and the values are arrays of actions associated with those triggers.
*/
default Map<ActionTrigger, Action<T>[]> parseEventActions(Section section) {
HashMap<ActionTrigger, Action<T>[]> actionMap = new HashMap<>();
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
try {
actionMap.put(
ActionTrigger.valueOf(entry.getKey().toUpperCase(Locale.ENGLISH)),
parseActions(innerSection)
);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
}
}
}
return actionMap;
}
/**
* Parses a configuration section to generate a map of timed actions.
*
* @param section The configuration section containing time-action mappings.
* @return A TreeMap where the keys are time values (in integer form) and the values are arrays of actions associated with those times.
*/
default TreeMap<Integer, Action<T>[]> parseTimesActions(Section section) {
TreeMap<Integer, Action<T>[]> actionMap = new TreeMap<>();
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
actionMap.put(Integer.parseInt(entry.getKey()), parseActions(innerSection));
}
}
return actionMap;
}
/**
* Triggers a list of actions with the given context.
* If the list of actions is not null, each action in the list is triggered.
*
* @param context The context associated with the actions.
* @param actions The list of actions to trigger.
*/
static <T> void trigger(@NotNull Context<T> context, @Nullable List<Action<T>> actions) {
if (actions != null)
for (Action<T> action : actions)
action.trigger(context);
}
/**
* Triggers an array of actions with the given context.
* If the array of actions is not null, each action in the array is triggered.
*
* @param context The context associated with the actions.
* @param actions The array of actions to trigger.
*/
static <T> void trigger(@NotNull Context<T> context, @Nullable Action<T>[] actions) {
if (actions != null)
for (Action<T> action : actions)
action.trigger(context);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) <2022> <XiaoMoMi>
* Copyright (C) <2024> <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
@@ -15,12 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item;
package net.momirealms.customcrops.api.action;
public enum ItemType {
CROP,
POT,
SPRINKLER,
GREENHOUSE,
SCARECROW
public enum ActionTrigger {
PLACE,
BREAK,
WORK, INTERACT
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) <2024> <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.customcrops.api.action;
import net.momirealms.customcrops.api.context.Context;
/**
* An implementation of the Action interface that represents an empty action with no behavior.
* This class serves as a default action to prevent NPE.
*/
public class EmptyAction<T> implements Action<T> {
public static <T> EmptyAction<T> instance() {
return new EmptyAction<>();
}
@Override
public void trigger(Context<T> context) {
}
}

View File

@@ -1,59 +0,0 @@
/*
* 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.customcrops.api.common;
public class Tuple<L, M, R> {
private L left;
private M mid;
private R right;
public Tuple(L left, M mid, R right) {
this.left = left;
this.mid = mid;
this.right = right;
}
public static <L, M, R> Tuple<L, M, R> of(final L left, final M mid, final R right) {
return new Tuple<>(left, mid, right);
}
public L getLeft() {
return left;
}
public void setLeft(L left) {
this.left = left;
}
public M getMid() {
return mid;
}
public void setMid(M mid) {
this.mid = mid;
}
public R getRight() {
return right;
}
public void setRight(R right) {
this.right = right;
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.customcrops.api.common.item;
public interface KeyItem {
/**
* Get item's key
*
* @return key
*/
String getKey();
}

View File

@@ -0,0 +1,64 @@
package net.momirealms.customcrops.api.context;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class AbstractContext<T> implements Context<T> {
private final T holder;
private final Map<ContextKeys<?>, Object> args;
private final Map<String, String> placeholderMap;
public AbstractContext(@Nullable T holder, boolean sync) {
this.holder = holder;
this.args = sync ? new ConcurrentHashMap<>() : new HashMap<>();
this.placeholderMap = sync ? new ConcurrentHashMap<>() : new HashMap<>();
}
@Override
public Map<ContextKeys<?>, Object> args() {
return args;
}
@Override
public Map<String, String> placeholderMap() {
return placeholderMap;
}
@Override
public <C> AbstractContext<T> arg(ContextKeys<C> key, C value) {
if (key == null || value == null) return this;
this.args.put(key, value);
this.placeholderMap.put("{" + key.key() + "}", value.toString());
return this;
}
@Override
public AbstractContext<T> combine(Context<T> other) {
this.args.putAll(other.args());
this.placeholderMap.putAll(other.placeholderMap());
return this;
}
@Override
@SuppressWarnings("unchecked")
public <C> C arg(ContextKeys<C> key) {
return (C) args.get(key);
}
@Nullable
@SuppressWarnings("unchecked")
@Override
public <C> C remove(ContextKeys<C> key) {
placeholderMap.remove("{" + key.key() + "}");
return (C) args.remove(key);
}
@Override
public T holder() {
return holder;
}
}

View File

@@ -0,0 +1,19 @@
package net.momirealms.customcrops.api.context;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import org.jetbrains.annotations.NotNull;
public class BlockContextImpl extends AbstractContext<CustomCropsBlockState> {
public BlockContextImpl(@NotNull CustomCropsBlockState block, boolean sync) {
super(block, sync);
}
@Override
public String toString() {
return "BlockContext{" +
"args=" + args() +
", block=" + holder() +
'}';
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) <2024> <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.customcrops.api.context;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
/**
* The Context interface represents a generic context for custom crops mechanics.
* It allows for storing and retrieving arguments, as well as getting the holder
* of the context. This can be used to maintain state or pass parameters within
* the custom crops mechanics.
*
* @param <T> the type of the holder object for this context
*/
public interface Context<T> {
/**
* Retrieves the map of arguments associated with this context.
*
* @return a map where the keys are argument names and the values are argument values.
*/
Map<ContextKeys<?>, Object> args();
/**
* Converts the context to a map of placeholders
*
* @return a map of placeholders
*/
Map<String, String> placeholderMap();
/**
* Adds or updates an argument in the context.
* This method allows adding a new argument or updating the value of an existing argument.
*
* @param <C> the type of the value being added to the context.
* @param key the ContextKeys key representing the argument to be added or updated.
* @param value the value to be associated with the specified key.
* @return the current context instance, allowing for method chaining.
*/
<C> Context<T> arg(ContextKeys<C> key, C value);
/**
* Combines one context with another
*
* @param other other
* @return this context
*/
Context<T> combine(Context<T> other);
/**
* Retrieves the value of a specific argument from the context.
* This method fetches the value associated with the specified ContextKeys key.
*
* @param <C> the type of the value being retrieved.
* @param key the ContextKeys key representing the argument to be retrieved.
* @return the value associated with the specified key, or null if the key does not exist.
*/
@Nullable
<C> C arg(ContextKeys<C> key);
/**
* Retrieves the value of a specific argument from the context.
* This method fetches the value associated with the specified ContextKeys key.
*
* @param <C> the type of the value being retrieved.
* @param key the ContextKeys key representing the argument to be retrieved.
* @return the value associated with the specified key, or null if the key does not exist.
*/
default <C> C argOrDefault(ContextKeys<C> key, C value) {
C result = arg(key);
return result == null ? value : result;
}
/**
* Remove the key from the context
*
* @param key the ContextKeys key
* @return the removed value
* @param <C> the type of the value being removed.
*/
@Nullable
<C> C remove(ContextKeys<C> key);
/**
* Gets the holder of this context.
*
* @return the holder object of type T.
*/
T holder();
/**
* Creates a player-specific context.
*
* @param player the player to be used as the holder of the context.
* @return a new Context instance with the specified player as the holder.
*/
static Context<Player> player(@Nullable Player player) {
return new PlayerContextImpl(player, false);
}
/**
* Creates a block-specific context.
*
* @param block the block to be used as the holder of the context.
* @return a new Context instance with the specified block as the holder.
*/
static Context<CustomCropsBlockState> block(@NotNull CustomCropsBlockState block) {
return new BlockContextImpl(block, false);
}
/**
* Creates a player-specific context.
*
* @param player the player to be used as the holder of the context.
* @param threadSafe is the created map thread safe
* @return a new Context instance with the specified player as the holder.
*/
static Context<Player> player(@Nullable Player player, boolean threadSafe) {
return new PlayerContextImpl(player, threadSafe);
}
/**
* Creates a block-specific context.
*
* @param block the block to be used as the holder of the context.
* @param threadSafe is the created map thread safe
* @return a new Context instance with the specified block as the holder.
*/
static Context<CustomCropsBlockState> block(@NotNull CustomCropsBlockState block, boolean threadSafe) {
return new BlockContextImpl(block, threadSafe);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) <2024> <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.customcrops.api.context;
import org.bukkit.Location;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Objects;
/**
* Represents keys for accessing context values with specific types.
*
* @param <T> the type of the value associated with the context key.
*/
public class ContextKeys<T> {
public static final ContextKeys<Location> LOCATION = of("location", Location.class);
public static final ContextKeys<Integer> X = of("x", Integer.class);
public static final ContextKeys<Integer> Y = of("y", Integer.class);
public static final ContextKeys<Integer> Z = of("z", Integer.class);
public static final ContextKeys<String> WORLD = of("world", String.class);
public static final ContextKeys<String> PLAYER = of("player", String.class);
public static final ContextKeys<EquipmentSlot> SLOT = of("slot", EquipmentSlot.class);
public static final ContextKeys<String> TEMP_NEAR_PLAYER = of("near", String.class);
public static final ContextKeys<Boolean> OFFLINE = of("offline", Boolean.class);
private final String key;
private final Class<T> type;
protected ContextKeys(String key, Class<T> type) {
this.key = key;
this.type = type;
}
/**
* Gets the key.
*
* @return the key.
*/
public String key() {
return key;
}
/**
* Gets the type associated with the key.
*
* @return the type.
*/
public Class<T> type() {
return type;
}
/**
* Creates a new context key.
*
* @param key the key.
* @param type the type.
* @param <T> the type of the value.
* @return a new ContextKeys instance.
*/
public static <T> ContextKeys<T> of(String key, Class<T> type) {
return new ContextKeys<T>(key, type);
}
@Override
public final boolean equals(final Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
ContextKeys<?> that = (ContextKeys) other;
return Objects.equals(this.key, that.key);
} else {
return false;
}
}
@Override
public final int hashCode() {
return Objects.hashCode(this.key);
}
@Override
public String toString() {
return "ContextKeys{" +
"key='" + key + '\'' +
'}';
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) <2024> <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.customcrops.api.context;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
public final class PlayerContextImpl extends AbstractContext<Player> {
public PlayerContextImpl(@Nullable Player player, boolean sync) {
super(player, sync);
if (player == null) return;
final Location location = player.getLocation();
arg(ContextKeys.PLAYER, player.getName())
.arg(ContextKeys.LOCATION, location)
.arg(ContextKeys.X, location.getBlockX())
.arg(ContextKeys.Y, location.getBlockY())
.arg(ContextKeys.Z, location.getBlockZ())
.arg(ContextKeys.WORLD, location.getWorld().getName());
}
@Override
public String toString() {
return "PlayerContext{" +
"args=" + args() +
", player=" + holder() +
'}';
}
}

View File

@@ -0,0 +1,120 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.helper.VersionHelper;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import java.util.HashSet;
import java.util.List;
public abstract class AbstractCustomEventListener implements Listener {
private final HashSet<EntityType> entities = new HashSet<>();
private final HashSet<Material> blocks = new HashSet<>();
protected final AbstractItemManager itemManager;
public AbstractCustomEventListener(AbstractItemManager itemManager) {
this.itemManager = itemManager;
this.entities.addAll(List.of(EntityType.ITEM_FRAME, EntityType.ARMOR_STAND));
if (VersionHelper.isVersionNewerThan1_19_4()) {
this.entities.addAll(List.of(EntityType.ITEM_DISPLAY, EntityType.INTERACTION));
}
this.blocks.addAll(List.of(
Material.NOTE_BLOCK,
Material.MUSHROOM_STEM, Material.BROWN_MUSHROOM_BLOCK, Material.RED_MUSHROOM_BLOCK,
Material.TRIPWIRE,
Material.CHORUS_PLANT, Material.CHORUS_FLOWER,
Material.ACACIA_LEAVES, Material.BIRCH_LEAVES, Material.JUNGLE_LEAVES, Material.DARK_OAK_LEAVES, Material.AZALEA_LEAVES, Material.FLOWERING_AZALEA_LEAVES, Material.OAK_LEAVES, Material.SPRUCE_LEAVES,
Material.CAVE_VINES, Material.TWISTING_VINES, Material.WEEPING_VINES,
Material.KELP,
Material.CACTUS
));
if (VersionHelper.isVersionNewerThan1_19()) {
this.blocks.add(Material.MANGROVE_LEAVES);
}
if (VersionHelper.isVersionNewerThan1_20()) {
this.blocks.add(Material.CHERRY_LEAVES);
}
}
@EventHandler
public void onInteractAir(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR)
return;
this.itemManager.handlePlayerInteractAir(
event.getPlayer(),
event.getHand(), event.getItem()
);
}
@EventHandler(ignoreCancelled = true)
public void onInteractBlock(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK)
return;
Block block = event.getClickedBlock();
assert block != null;
if (blocks.contains(block.getType())) {
return;
}
this.itemManager.handlePlayerInteractBlock(
event.getPlayer(),
block,
block.getType().name(), event.getBlockFace(),
event.getHand(),
event.getItem(),
event
);
}
@EventHandler(ignoreCancelled = true)
public void onInteractEntity(PlayerInteractAtEntityEvent event) {
EntityType type = event.getRightClicked().getType();
if (entities.contains(type)) {
return;
}
this.itemManager.handlePlayerInteractFurniture(
event.getPlayer(),
event.getRightClicked().getLocation(), type.name(),
event.getHand(), event.getPlayer().getInventory().getItem(event.getHand()),
event
);
}
@EventHandler(ignoreCancelled = true)
public void onPlaceBlock(BlockPlaceEvent event) {
Block block = event.getBlock();
if (blocks.contains(block.getType())) {
return;
}
this.itemManager.handlePlayerPlace(
event.getPlayer(),
block.getLocation(),
block.getType().name(),
event.getHand(),
event.getItemInHand(),
event
);
}
@EventHandler(ignoreCancelled = true)
public void onBreakBlock(BlockBreakEvent event) {
Block block = event.getBlock();
if (blocks.contains(block.getType())) {
return;
}
this.itemManager.handlePlayerBreak(
event.getPlayer(),
block.getLocation(), event.getPlayer().getInventory().getItemInMainHand(), block.getType().name(),
event
);
}
}

View File

@@ -0,0 +1,55 @@
package net.momirealms.customcrops.api.core;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
public abstract class AbstractItemManager implements ItemManager {
public abstract void handlePlayerInteractAir(
Player player,
EquipmentSlot hand,
ItemStack itemInHand
);
public abstract void handlePlayerInteractBlock(
Player player,
Block block,
String blockID,
BlockFace blockFace,
EquipmentSlot hand,
ItemStack itemInHand,
Cancellable event
);
// it's not a good choice to use Entity as parameter because the entity might be fake
public abstract void handlePlayerInteractFurniture(
Player player,
Location location,
String furnitureID,
EquipmentSlot hand,
ItemStack itemInHand,
Cancellable event
);
public abstract void handlePlayerBreak(
Player player,
Location location,
ItemStack itemInHand,
String brokenID,
Cancellable event
);
public abstract void handlePlayerPlace(
Player player,
Location location,
String placedID,
EquipmentSlot hand,
ItemStack itemInHand,
Cancellable event
);
}

View File

@@ -0,0 +1,43 @@
package net.momirealms.customcrops.api.core;
import com.flowpowered.nbt.CompoundMap;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.common.util.Key;
import java.util.Objects;
public class BuiltInBlockMechanics {
public static final BuiltInBlockMechanics CROP = create("crop");
public static final BuiltInBlockMechanics SPRINKLER = create("sprinkler");
public static final BuiltInBlockMechanics GREENHOUSE = create("greenhouse");
public static final BuiltInBlockMechanics POT = create("pot");
public static final BuiltInBlockMechanics SCARECROW = create("scarecrow");
private final Key key;
public BuiltInBlockMechanics(Key key) {
this.key = key;
}
static BuiltInBlockMechanics create(String id) {
return new BuiltInBlockMechanics(Key.key("customcrops", id));
}
public Key key() {
return key;
}
public CustomCropsBlockState createBlockState() {
return mechanic().createBlockState();
}
public CustomCropsBlockState createBlockState(CompoundMap data) {
return mechanic().createBlockState(data);
}
public CustomCropsBlock mechanic() {
return Objects.requireNonNull(Registries.BLOCK.get(key));
}
}

View File

@@ -0,0 +1,30 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.item.CustomCropsItem;
public class BuiltInItemMechanics {
public static final BuiltInItemMechanics WATERING_CAN = create("watering_can");
public static final BuiltInItemMechanics FERTILIZER = create("fertilizer");
public static final BuiltInItemMechanics SEED = create("seed");
public static final BuiltInItemMechanics SPRINKLER_ITEM = create("sprinkler_item");
private final Key key;
public BuiltInItemMechanics(Key key) {
this.key = key;
}
static BuiltInItemMechanics create(String id) {
return new BuiltInItemMechanics(Key.key("customcrops", id));
}
public Key key() {
return key;
}
public CustomCropsItem mechanic() {
return Registries.ITEM.get(key);
}
}

View File

@@ -0,0 +1,17 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
public class ClearableMappedRegistry<K, T> extends MappedRegistry<K, T> implements ClearableRegistry<K, T> {
public ClearableMappedRegistry(Key key) {
super(key);
}
@Override
public void clear() {
super.byID.clear();
super.byKey.clear();
super.byValue.clear();
}
}

View File

@@ -0,0 +1,6 @@
package net.momirealms.customcrops.api.core;
public interface ClearableRegistry<K, T> extends WriteableRegistry<K, T> {
void clear();
}

View File

@@ -0,0 +1,422 @@
package net.momirealms.customcrops.api.core;
import com.google.common.base.Preconditions;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning;
import dev.dejvokep.boostedyaml.libs.org.snakeyaml.engine.v2.common.ScalarStyle;
import dev.dejvokep.boostedyaml.libs.org.snakeyaml.engine.v2.nodes.Tag;
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings;
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings;
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings;
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings;
import dev.dejvokep.boostedyaml.utils.format.NodeRole;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.block.*;
import net.momirealms.customcrops.api.core.item.FertilizerConfig;
import net.momirealms.customcrops.api.core.item.FertilizerType;
import net.momirealms.customcrops.api.core.item.WateringCanConfig;
import net.momirealms.customcrops.api.core.water.FillMethod;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.util.PluginUtils;
import net.momirealms.customcrops.common.config.ConfigLoader;
import net.momirealms.customcrops.common.plugin.feature.Reloadable;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public abstract class ConfigManager implements ConfigLoader, Reloadable {
private static ConfigManager instance;
protected final BukkitCustomCropsPlugin plugin;
protected boolean doubleCheck;
protected boolean metrics;
protected boolean checkUpdate;
protected boolean debug;
protected String absoluteWorldPath;
protected Set<String> scarecrow;
protected ExistenceForm scarecrowExistenceForm;
protected boolean enableScarecrow;
protected boolean protectOriginalLore;
protected int scarecrowRange;
protected boolean scarecrowProtectChunk;
protected Set<String> greenhouse;
protected boolean enableGreenhouse;
protected ExistenceForm greenhouseExistenceForm;
protected int greenhouseRange;
protected boolean syncSeasons;
protected String referenceWorld;
protected String[] itemDetectOrder;
protected double[] defaultQualityRatio;
protected boolean hasNamespace;
public ConfigManager(BukkitCustomCropsPlugin plugin) {
this.plugin = plugin;
instance = this;
}
public static boolean syncSeasons() {
return instance.syncSeasons;
}
public static String referenceWorld() {
return instance.referenceWorld;
}
public static boolean enableGreenhouse() {
return instance.enableGreenhouse;
}
public static ExistenceForm greenhouseExistenceForm() {
return instance.greenhouseExistenceForm;
}
public static int greenhouseRange() {
return instance.greenhouseRange;
}
public static int scarecrowRange() {
return instance.scarecrowRange;
}
public static boolean enableScarecrow() {
return instance.enableScarecrow;
}
public static boolean scarecrowProtectChunk() {
return instance.scarecrowProtectChunk;
}
public static boolean debug() {
return instance.debug;
}
public static boolean metrics() {
return instance.metrics;
}
public static boolean checkUpdate() {
return instance.checkUpdate;
}
public static String absoluteWorldPath() {
return instance.absoluteWorldPath;
}
public static Set<String> greenhouse() {
return instance.greenhouse;
}
public static ExistenceForm scarecrowExistenceForm() {
return instance.scarecrowExistenceForm;
}
public static boolean doubleCheck() {
return instance.doubleCheck;
}
public static Set<String> scarecrow() {
return instance.scarecrow;
}
public static boolean protectOriginalLore() {
return instance.protectOriginalLore;
}
public static String[] itemDetectOrder() {
return instance.itemDetectOrder;
}
public static double[] defaultQualityRatio() {
return instance.defaultQualityRatio;
}
public static boolean hasNamespace() {
return instance.hasNamespace;
}
@Override
public YamlDocument loadConfig(String filePath) {
return loadConfig(filePath, '.');
}
@Override
public YamlDocument loadConfig(String filePath, char routeSeparator) {
try (InputStream inputStream = new FileInputStream(resolveConfig(filePath).toFile())) {
return YamlDocument.create(
inputStream,
plugin.getResourceStream(filePath),
GeneralSettings.builder().setRouteSeparator(routeSeparator).build(),
LoaderSettings
.builder()
.setAutoUpdate(true)
.build(),
DumperSettings.builder()
.setScalarFormatter((tag, value, role, def) -> {
if (role == NodeRole.KEY) {
return ScalarStyle.PLAIN;
} else {
return tag == Tag.STR ? ScalarStyle.DOUBLE_QUOTED : ScalarStyle.PLAIN;
}
})
.build(),
UpdaterSettings
.builder()
.setVersioning(new BasicVersioning("config-version"))
.build()
);
} catch (IOException e) {
plugin.getPluginLogger().severe("Failed to load config " + filePath, e);
throw new RuntimeException(e);
}
}
@Override
public YamlDocument loadData(File file) {
try (InputStream inputStream = new FileInputStream(file)) {
return YamlDocument.create(inputStream);
} catch (IOException e) {
plugin.getPluginLogger().severe("Failed to load config " + file, e);
throw new RuntimeException(e);
}
}
@Override
public YamlDocument loadData(File file, char routeSeparator) {
try (InputStream inputStream = new FileInputStream(file)) {
return YamlDocument.create(inputStream, GeneralSettings.builder()
.setRouteSeparator(routeSeparator)
.build());
} catch (IOException e) {
plugin.getPluginLogger().severe("Failed to load config " + file, e);
throw new RuntimeException(e);
}
}
protected Path resolveConfig(String filePath) {
if (filePath == null || filePath.isEmpty()) {
throw new IllegalArgumentException("ResourcePath cannot be null or empty");
}
filePath = filePath.replace('\\', '/');
Path configFile = plugin.getConfigDirectory().resolve(filePath);
// if the config doesn't exist, create it based on the template in the resources dir
if (!Files.exists(configFile)) {
try {
Files.createDirectories(configFile.getParent());
} catch (IOException e) {
// ignore
}
try (InputStream is = plugin.getResourceStream(filePath)) {
if (is == null) {
throw new IllegalArgumentException("The embedded resource '" + filePath + "' cannot be found");
}
Files.copy(is, configFile);
addDefaultNamespace(configFile.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return configFile;
}
public abstract void registerWateringCanConfig(WateringCanConfig config);
public abstract void registerFertilizerConfig(FertilizerConfig config);
public abstract void registerCropConfig(CropConfig config);
public abstract void registerPotConfig(PotConfig config);
public abstract void registerSprinklerConfig(SprinklerConfig config);
public WateringMethod[] getWateringMethods(Section section) {
ArrayList<WateringMethod> methods = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
WateringMethod fillMethod = new WateringMethod(
Preconditions.checkNotNull(innerSection.getString("item"), "fill-method item should not be null"),
innerSection.getInt("item-amount", 1),
innerSection.getString("return"),
innerSection.getInt("return-amount", 1),
innerSection.getInt("amount", 1),
plugin.getActionManager(Player.class).parseActions(innerSection.getSection("actions")),
plugin.getRequirementManager(Player.class).parseRequirements(innerSection.getSection("requirements"), true)
);
methods.add(fillMethod);
}
}
}
return methods.toArray(new WateringMethod[0]);
}
public FillMethod[] getFillMethods(Section section) {
ArrayList<FillMethod> methods = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
FillMethod fillMethod = new FillMethod(
Preconditions.checkNotNull(innerSection.getString("target"), "fill-method target should not be null"),
innerSection.getInt("amount", 1),
plugin.getActionManager(Player.class).parseActions(innerSection.getSection("actions")),
plugin.getRequirementManager(Player.class).parseRequirements(innerSection.getSection("requirements"), true)
);
methods.add(fillMethod);
}
}
}
return methods.toArray(new FillMethod[0]);
}
public HashMap<Integer, Integer> getInt2IntMap(Section section) {
HashMap<Integer, Integer> map = new HashMap<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
try {
int i1 = Integer.parseInt(entry.getKey());
if (entry.getValue() instanceof Number i2) {
map.put(i1, i2.intValue());
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
}
}
return map;
}
public double[] getQualityRatio(String ratios) {
String[] split = ratios.split("/");
double[] ratio = new double[split.length];
double weightTotal = Arrays.stream(split).mapToInt(Integer::parseInt).sum();
double temp = 0;
for (int i = 0; i < ratio.length; i++) {
temp += Integer.parseInt(split[i]);
ratio[i] = temp / weightTotal;
}
return ratio;
}
public List<Pair<Double, Integer>> getIntChancePair(Section section) {
ArrayList<Pair<Double, Integer>> pairs = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
int point = Integer.parseInt(entry.getKey());
if (entry.getValue() instanceof Number n) {
Pair<Double, Integer> pair = new Pair<>(n.doubleValue(), point);
pairs.add(pair);
}
}
}
return pairs;
}
public HashMap<FertilizerType, Pair<String, String>> getFertilizedPotMap(Section section) {
HashMap<FertilizerType, Pair<String, String>> map = new HashMap<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
FertilizerType type = Registries.FERTILIZER_TYPE.get(entry.getKey().replace("-", "_"));
if (type != null) {
map.put(type, Pair.of(
Preconditions.checkNotNull(innerSection.getString("dry"), entry.getKey() + ".dry should not be null"),
Preconditions.checkNotNull(innerSection.getString("wet"), entry.getKey() + ".wet should not be null")
));
}
}
}
}
return map;
}
public BoneMeal[] getBoneMeals(Section section) {
ArrayList<BoneMeal> boneMeals = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section innerSection) {
BoneMeal boneMeal = new BoneMeal(
Preconditions.checkNotNull(innerSection.getString("item"), "Bone meal item can't be null"),
innerSection.getInt("item-amount",1),
innerSection.getString("return"),
innerSection.getInt("return-amount",1),
innerSection.getBoolean("dispenser",true),
getIntChancePair(innerSection.getSection("chance")),
plugin.getActionManager(Player.class).parseActions(innerSection.getSection("actions"))
);
boneMeals.add(boneMeal);
}
}
}
return boneMeals.toArray(new BoneMeal[0]);
}
public DeathCondition[] getDeathConditions(Section section, ExistenceForm original) {
ArrayList<DeathCondition> conditions = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
DeathCondition deathCondition = new DeathCondition(
plugin.getRequirementManager(CustomCropsBlockState.class).parseRequirements(inner.getSection("conditions"), false),
inner.getString("model"),
Optional.ofNullable(inner.getString("type")).map(ExistenceForm::valueOf).orElse(original),
inner.getInt("delay", 0)
);
conditions.add(deathCondition);
}
}
}
return conditions.toArray(new DeathCondition[0]);
}
public GrowCondition[] getGrowConditions(Section section) {
ArrayList<GrowCondition> conditions = new ArrayList<>();
if (section != null) {
for (Map.Entry<String, Object> entry : section.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof Section inner) {
GrowCondition growCondition = new GrowCondition(
plugin.getRequirementManager(CustomCropsBlockState.class).parseRequirements(inner.getSection("conditions"), false),
inner.getInt("point", 1)
);
conditions.add(growCondition);
}
}
}
return conditions.toArray(new GrowCondition[0]);
}
protected void addDefaultNamespace(File file) {
boolean hasNamespace = PluginUtils.isEnabled("ItemsAdder");
String line;
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
while ((line = reader.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
String finalStr = sb.toString();
if (!hasNamespace) {
finalStr = finalStr.replace("CHORUS", "TRIPWIRE").replace("<font:customcrops:default>", "<font:minecraft:customcrops>");
}
writer.write(finalStr.replace("{0}", hasNamespace ? "customcrops:" : ""));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,22 @@
package net.momirealms.customcrops.api.core;
public enum CustomForm {
TRIPWIRE(ExistenceForm.BLOCK),
NOTE_BLOCK(ExistenceForm.BLOCK),
MUSHROOM(ExistenceForm.BLOCK),
CHORUS(ExistenceForm.BLOCK),
ITEM_FRAME(ExistenceForm.FURNITURE),
ITEM_DISPLAY(ExistenceForm.FURNITURE),
ARMOR_STAND(ExistenceForm.FURNITURE);
private final ExistenceForm form;
CustomForm(ExistenceForm form) {
this.form = form;
}
public ExistenceForm existenceForm() {
return form;
}
}

View File

@@ -0,0 +1,34 @@
package net.momirealms.customcrops.api.core;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.Nullable;
public interface CustomItemProvider {
boolean removeCustomBlock(Location location);
boolean placeCustomBlock(Location location, String id);
@Nullable
Entity placeFurniture(Location location, String id);
boolean removeFurniture(Entity entity);
@Nullable
String blockID(Block block);
@Nullable
String itemID(ItemStack itemStack);
@Nullable
ItemStack itemStack(Player player, String id);
@Nullable
String furnitureID(Entity entity);
boolean isFurniture(Entity entity);
}

View File

@@ -0,0 +1,8 @@
package net.momirealms.customcrops.api.core;
public enum ExistenceForm {
BLOCK,
FURNITURE,
ANY
}

View File

@@ -0,0 +1,79 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.RandomUtils;
import org.bukkit.Rotation;
public enum FurnitureRotation {
NONE(0f),
EAST(-90f),
SOUTH(0f),
WEST(90f),
NORTH(180f);
private final float yaw;
FurnitureRotation(float yaw) {
this.yaw = yaw;
}
public float getYaw() {
return yaw;
}
public static FurnitureRotation random() {
return FurnitureRotation.values()[RandomUtils.generateRandomInt(1, 4)];
}
public static FurnitureRotation getByRotation(Rotation rotation) {
switch (rotation) {
default -> {
return FurnitureRotation.SOUTH;
}
case CLOCKWISE -> {
return FurnitureRotation.WEST;
}
case COUNTER_CLOCKWISE -> {
return FurnitureRotation.EAST;
}
case FLIPPED -> {
return FurnitureRotation.NORTH;
}
}
}
public static FurnitureRotation getByYaw(float yaw) {
yaw = (Math.abs(yaw + 180) % 360);
switch ((int) (yaw/90)) {
case 1 -> {
return FurnitureRotation.WEST;
}
case 2 -> {
return FurnitureRotation.NORTH;
}
case 3 -> {
return FurnitureRotation.EAST;
}
default -> {
return FurnitureRotation.SOUTH;
}
}
}
public Rotation getBukkitRotation() {
switch (this) {
case EAST -> {
return Rotation.COUNTER_CLOCKWISE;
}
case WEST -> {
return Rotation.CLOCKWISE;
}
case NORTH -> {
return Rotation.FLIPPED;
}
default -> {
return Rotation.NONE;
}
}
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.customcrops.api.core;
import javax.annotation.Nullable;
public interface IdMap<T> extends Iterable<T> {
int DEFAULT = -1;
int getId(T value);
@Nullable
T byId(int index);
default T byIdOrThrow(int index) {
T object = this.byId(index);
if (object == null) {
throw new IllegalArgumentException("No value with id " + index);
} else {
return object;
}
}
default int getIdOrThrow(T value) {
int i = this.getId(value);
if (i == -1) {
throw new IllegalArgumentException("Can't find id for '" + value + "' in map " + this);
} else {
return i;
}
}
int size();
}

View File

@@ -0,0 +1,8 @@
package net.momirealms.customcrops.api.core;
public enum InteractionResult {
SUCCESS,
FAIL,
PASS
}

View File

@@ -0,0 +1,63 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.item.Item;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface ItemManager {
void place(@NotNull Location location, @NotNull ExistenceForm form, @NotNull String id, FurnitureRotation rotation);
FurnitureRotation remove(@NotNull Location location, @NotNull ExistenceForm form);
void placeBlock(@NotNull Location location, @NotNull String id);
void placeFurniture(@NotNull Location location, @NotNull String id, FurnitureRotation rotation);
void removeBlock(@NotNull Location location);
@Nullable
FurnitureRotation removeFurniture(@NotNull Location location);
@NotNull
default String blockID(@NotNull Location location) {
return blockID(location.getBlock());
}
@NotNull
String blockID(@NotNull Block block);
@Nullable
String furnitureID(@NotNull Entity entity);
@NotNull String entityID(@NotNull Entity entity);
@Nullable
String furnitureID(Location location);
@NotNull
String anyID(Location location);
@Nullable
String id(Location location, ExistenceForm form);
void setCustomEventListener(@NotNull AbstractCustomEventListener listener);
void setCustomItemProvider(@NotNull CustomItemProvider provider);
String id(ItemStack itemStack);
@Nullable
ItemStack build(Player player, String id);
Item<ItemStack> wrap(ItemStack itemStack);
void decreaseDamage(Player player, ItemStack itemStack, int amount);
void increaseDamage(Player holder, ItemStack itemStack, int amount);
}

View File

@@ -0,0 +1,58 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class MappedRegistry<K, T> implements WriteableRegistry<K, T> {
protected final Map<K, T> byKey = new HashMap<>(1024);
protected final Map<T, K> byValue = new IdentityHashMap<>(1024);
protected final ArrayList<T> byID = new ArrayList<>(1024);
private final Key key;
public MappedRegistry(Key key) {
this.key = key;
}
@Override
public void register(K key, T value) {
byKey.put(key, value);
byValue.put(value, key);
}
@Override
public Key key() {
return key;
}
@Override
public int getId(@Nullable T value) {
return byID.indexOf(value);
}
@Nullable
@Override
public T byId(int index) {
return byID.get(index);
}
@Override
public int size() {
return byKey.size();
}
@Nullable
@Override
public T get(@Nullable K key) {
return byKey.get(key);
}
@NotNull
@Override
public Iterator<T> iterator() {
return this.byKey.values().iterator();
}
}

View File

@@ -0,0 +1,42 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.api.core.block.CropConfig;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.block.PotConfig;
import net.momirealms.customcrops.api.core.block.SprinklerConfig;
import net.momirealms.customcrops.api.core.item.CustomCropsItem;
import net.momirealms.customcrops.api.core.item.FertilizerConfig;
import net.momirealms.customcrops.api.core.item.FertilizerType;
import net.momirealms.customcrops.api.core.item.WateringCanConfig;
import net.momirealms.customcrops.common.annotation.DoNotUse;
import net.momirealms.customcrops.common.util.Key;
import org.jetbrains.annotations.ApiStatus;
import java.util.List;
@ApiStatus.Internal
public class Registries {
@DoNotUse
public static final WriteableRegistry<Key, CustomCropsBlock> BLOCK = new MappedRegistry<>(Key.key("mechanic", "block"));
@DoNotUse
public static final WriteableRegistry<Key, CustomCropsItem> ITEM = new MappedRegistry<>(Key.key("mechanic", "item"));
@DoNotUse
public static final WriteableRegistry<String, FertilizerType> FERTILIZER_TYPE = new ClearableMappedRegistry<>(Key.key("mechanic", "fertilizer_type"));
@DoNotUse
public static final ClearableRegistry<String, CustomCropsBlock> BLOCKS = new ClearableMappedRegistry<>(Key.key("internal", "blocks"));
@DoNotUse
public static final ClearableRegistry<String, CustomCropsItem> ITEMS = new ClearableMappedRegistry<>(Key.key("internal", "items"));
public static final ClearableRegistry<String, SprinklerConfig> SPRINKLER = new ClearableMappedRegistry<>(Key.key("config", "sprinkler"));
public static final ClearableRegistry<String, PotConfig> POT = new ClearableMappedRegistry<>(Key.key("config", "pot"));
public static final ClearableRegistry<String, CropConfig> CROP = new ClearableMappedRegistry<>(Key.key("config", "crop"));
public static final ClearableRegistry<String, FertilizerConfig> FERTILIZER = new ClearableMappedRegistry<>(Key.key("config", "fertilizer"));
public static final ClearableRegistry<String, WateringCanConfig> WATERING_CAN = new ClearableMappedRegistry<>(Key.key("config", "watering_can"));
public static final ClearableRegistry<String, CropConfig> SEED_TO_CROP = new ClearableMappedRegistry<>(Key.key("fast_lookup", "seed_to_crop"));
public static final ClearableRegistry<String, List<CropConfig>> STAGE_TO_CROP_UNSAFE = new ClearableMappedRegistry<>(Key.key("fast_lookup", "stage_to_crop"));
public static final ClearableRegistry<String, FertilizerConfig> ITEM_TO_FERTILIZER = new ClearableMappedRegistry<>(Key.key("fast_lookup", "item_to_fertilizer"));
public static final ClearableRegistry<String, PotConfig> ITEM_TO_POT = new ClearableMappedRegistry<>(Key.key("fast_lookup", "item_to_pot"));
public static final ClearableRegistry<String, SprinklerConfig> ITEM_TO_SPRINKLER = new ClearableMappedRegistry<>(Key.key("fast_lookup", "item_to_sprinkler"));
}

View File

@@ -0,0 +1,16 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
import javax.annotation.Nullable;
public interface Registry<K, T> extends IdMap<T> {
Key key();
@Override
int getId(@Nullable T value);
@Nullable
T get(@Nullable K key);
}

View File

@@ -0,0 +1,21 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.item.CustomCropsItem;
import net.momirealms.customcrops.api.core.item.FertilizerType;
public interface RegistryAccess {
void registerBlockMechanic(CustomCropsBlock block);
void registerItemMechanic(CustomCropsItem item);
void registerFertilizerType(FertilizerType type);
Registry<Key, CustomCropsBlock> getBlockRegistry();
Registry<Key, CustomCropsItem> getItemRegistry();
Registry<String, FertilizerType> getFertilizerTypeRegistry();
}

View File

@@ -0,0 +1,54 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.item.CustomCropsItem;
import net.momirealms.customcrops.api.core.item.FertilizerType;
public class SimpleRegistryAccess implements RegistryAccess {
private BukkitCustomCropsPlugin plugin;
private boolean frozen;
public SimpleRegistryAccess(BukkitCustomCropsPlugin plugin) {
this.plugin = plugin;
}
public void freeze() {
this.frozen = true;
}
@Override
public void registerBlockMechanic(CustomCropsBlock block) {
if (frozen) throw new RuntimeException("Registries are frozen");
Registries.BLOCK.register(block.type(), block);
}
@Override
public void registerItemMechanic(CustomCropsItem item) {
if (frozen) throw new RuntimeException("Registries are frozen");
Registries.ITEM.register(item.type(), item);
}
@Override
public void registerFertilizerType(FertilizerType type) {
if (frozen) throw new RuntimeException("Registries are frozen");
Registries.FERTILIZER_TYPE.register(type.id(), type);
}
@Override
public Registry<Key, CustomCropsBlock> getBlockRegistry() {
return Registries.BLOCK;
}
@Override
public Registry<Key, CustomCropsItem> getItemRegistry() {
return Registries.ITEM;
}
@Override
public Registry<String, FertilizerType> getFertilizerTypeRegistry() {
return Registries.FERTILIZER_TYPE;
}
}

View File

@@ -0,0 +1,9 @@
package net.momirealms.customcrops.api.core;
import net.momirealms.customcrops.api.context.Context;
@FunctionalInterface
public interface StatedItem<T> {
String currentState(Context<T> context);
}

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.world;
package net.momirealms.customcrops.api.core;
import com.flowpowered.nbt.CompoundMap;
import com.flowpowered.nbt.Tag;
@@ -37,7 +37,7 @@ public class SynchronizedCompoundMap {
this.compoundMap = compoundMap;
}
public CompoundMap getOriginalMap() {
public CompoundMap originalMap() {
return compoundMap;
}
@@ -59,6 +59,15 @@ public class SynchronizedCompoundMap {
}
}
public Tag<?> remove(String key) {
writeLock.lock();
try {
return compoundMap.remove(key);
} finally {
writeLock.unlock();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -78,16 +87,16 @@ public class SynchronizedCompoundMap {
Tag<?> tag = entry.getValue();
String tagValue;
switch (tag.getType()) {
case TAG_STRING, TAG_BYTE, TAG_DOUBLE, TAG_FLOAT, TAG_INT, TAG_INT_ARRAY, TAG_LONG, TAG_SHORT, TAG_SHORT_ARRAY, TAG_LONG_ARRAY, TAG_BYTE_ARRAY ->
case TAG_STRING, TAG_BYTE, TAG_DOUBLE, TAG_FLOAT, TAG_INT, TAG_INT_ARRAY, TAG_LONG, TAG_SHORT, TAG_SHORT_ARRAY, TAG_LONG_ARRAY, TAG_BYTE_ARRAY,
TAG_LIST ->
tagValue = tag.getValue().toString();
case TAG_COMPOUND -> tagValue = compoundMapToString(tag.getAsCompoundTag().get().getValue());
case TAG_LIST -> tagValue = tag.getAsListTag().get().getValue().toString();
case TAG_COMPOUND -> tagValue = compoundMapToString((CompoundMap) tag.getValue());
default -> {
continue;
}
}
joiner.add("\"" + entry.getKey() + "\":\"" + tagValue + "\"");
}
return "{" + joiner + "}";
return "BlockData{" + joiner + "}";
}
}

View File

@@ -0,0 +1,6 @@
package net.momirealms.customcrops.api.core;
public interface WriteableRegistry<K, T> extends Registry<K, T> {
void register(K key, T value);
}

View File

@@ -0,0 +1,82 @@
package net.momirealms.customcrops.api.core.block;
import com.flowpowered.nbt.CompoundMap;
import com.flowpowered.nbt.IntTag;
import com.flowpowered.nbt.StringTag;
import com.flowpowered.nbt.Tag;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
public abstract class AbstractCustomCropsBlock implements CustomCropsBlock {
private final Key type;
public AbstractCustomCropsBlock(Key type) {
this.type = type;
}
@Override
public Key type() {
return type;
}
@Override
public CustomCropsBlockState createBlockState() {
return CustomCropsBlockState.create(this, new CompoundMap());
}
@Override
public CustomCropsBlockState createBlockState(CompoundMap compoundMap) {
return CustomCropsBlockState.create(this, compoundMap);
}
public String id(CustomCropsBlockState state) {
return state.get("key").getAsStringTag()
.map(StringTag::getValue)
.orElse("");
}
public void id(CustomCropsBlockState state, String id) {
state.set("key", new StringTag("key", id));
}
protected boolean canTick(CustomCropsBlockState state, int interval) {
if (interval <= 0) return false;
if (interval == 1) return true;
Tag<?> tag = state.get("tick");
int tick = 0;
if (tag != null) tick = tag.getAsIntTag().map(IntTag::getValue).orElse(0);
if (++tick >= interval) {
state.set("tick", new IntTag("tick", 0));
return true;
} else {
state.set("tick", new IntTag("tick", tick));
return false;
}
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
}
@Override
public void onInteract(WrappedInteractEvent event) {
}
@Override
public void onBreak(WrappedBreakEvent event) {
}
@Override
public void onPlace(WrappedPlaceEvent event) {
}
}

View File

@@ -15,67 +15,53 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item;
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.common.Pair;
import net.momirealms.customcrops.api.manager.ActionManager;
import net.momirealms.customcrops.api.mechanic.action.Action;
import net.momirealms.customcrops.api.mechanic.requirement.State;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.List;
public class BoneMeal {
private final String item;
private final int usedAmount;
private final int requiredAmount;
private final String returned;
private final int returnedAmount;
private final List<Pair<Double, Integer>> pointGainList;
private final Action[] actions;
private final Action<Player>[] actions;
private final boolean dispenserAllowed;
public BoneMeal(
String item,
int usedAmount,
int requiredAmount,
String returned,
int returnedAmount,
boolean dispenserAllowed,
List<Pair<Double, Integer>> pointGainList,
Action[] actions
Action<Player>[] actions
) {
this.item = item;
this.returned = returned;
this.pointGainList = pointGainList;
this.actions = actions;
this.usedAmount = usedAmount;
this.requiredAmount = requiredAmount;
this.returnedAmount = returnedAmount;
this.dispenserAllowed = dispenserAllowed;
}
/**
* Get the ID of the bone meal item
*
* @return bonemeal item id
*/
public String getItem() {
public String requiredItem() {
return item;
}
/**
* Get the returned item's ID
*
* @return returned item ID
*/
public String getReturned() {
public String returnedItem() {
return returned;
}
/**
* Get the points to gain in one try
*
* @return points
*/
public int getPoint() {
public int rollPoint() {
for (Pair<Double, Integer> pair : pointGainList) {
if (Math.random() < pair.left()) {
return pair.right();
@@ -84,38 +70,18 @@ public class BoneMeal {
return 0;
}
/**
* Trigger the actions of using the bone meal
*
* @param state player state
*/
public void trigger(State state) {
ActionManager.triggerActions(state, actions);
public void triggerActions(Context<Player> context) {
ActionManager.trigger(context, actions);
}
/**
* Get the amount to consume
*
* @return amount to consume
*/
public int getUsedAmount() {
return usedAmount;
public int amountOfRequiredItem() {
return requiredAmount;
}
/**
* Get the amount of the returned items
*
* @return amount of the returned items
*/
public int getReturnedAmount() {
public int amountOfReturnItem() {
return returnedAmount;
}
/**
* If the bone meal can be used with a dispenser
*
* @return can be used or not
*/
public boolean isDispenserAllowed() {
return dispenserAllowed;
}

View File

@@ -0,0 +1,12 @@
package net.momirealms.customcrops.api.core.block;
public enum BreakReason {
// Crop was broken by a player
BREAK,
// Crop was trampled
TRAMPLE,
// Crop was broken due to an explosion (block or entity)
EXPLODE,
// Crop was broken due to a specific action
ACTION
}

View File

@@ -0,0 +1,398 @@
package net.momirealms.customcrops.api.core.block;
import com.flowpowered.nbt.IntTag;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.item.Fertilizer;
import net.momirealms.customcrops.api.core.item.FertilizerConfig;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
import net.momirealms.customcrops.api.event.BoneMealUseEvent;
import net.momirealms.customcrops.api.event.CropBreakEvent;
import net.momirealms.customcrops.api.event.CropInteractEvent;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.api.util.PlayerUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class CropBlock extends AbstractCustomCropsBlock {
public CropBlock() {
super(BuiltInBlockMechanics.CROP.key());
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (!world.setting().randomTickCrop() && canTick(state, world.setting().tickCropInterval())) {
tickCrop(state, world, location);
}
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (world.setting().randomTickCrop() && canTick(state, world.setting().tickCropInterval())) {
tickCrop(state, world, location);
}
}
@Override
public void onBreak(WrappedBreakEvent event) {
List<CropConfig> configs = Registries.STAGE_TO_CROP_UNSAFE.get(event.brokenID());
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
if (configs == null || configs.isEmpty()) {
world.removeBlockState(pos3);
return;
}
CustomCropsBlockState state = fixOrGetState(world, pos3, event.brokenID());
if (state == null) {
return;
}
// check data for precise data
CropConfig cropConfig = config(state);
// the config not exists or it's a wrong one
if (cropConfig == null || !configs.contains(cropConfig)) {
if (configs.size() != 1) {
return;
}
cropConfig = configs.get(0);
}
CropStageConfig stageConfig = cropConfig.stageByID(event.brokenID());
assert stageConfig != null;
final Player player = event.playerBreaker();
Context<Player> context = Context.player(player);
// check requirements
if (!RequirementManager.isSatisfied(context, cropConfig.breakRequirements())) {
event.setCancelled(true);
return;
}
if (!RequirementManager.isSatisfied(context, stageConfig.breakRequirements())) {
event.setCancelled(true);
return;
}
CropBreakEvent breakEvent = new CropBreakEvent(event.entityBreaker(), event.blockBreaker(), cropConfig, event.brokenID(), event.location(),
state, BreakReason.BREAK);
if (EventUtils.fireAndCheckCancel(breakEvent)) {
event.setCancelled(true);
return;
}
ActionManager.trigger(context, stageConfig.breakActions());
ActionManager.trigger(context, cropConfig.breakActions());
world.removeBlockState(pos3);
}
/**
* This can only be triggered admins because players shouldn't get the stages
*/
@Override
public void onPlace(WrappedPlaceEvent event) {
List<CropConfig> configs = Registries.STAGE_TO_CROP_UNSAFE.get(event.placedID());
if (configs == null || configs.size() != 1) {
return;
}
CropConfig cropConfig = configs.get(0);
Context<Player> context = Context.player(event.player());
// if (!RequirementManager.isSatisfied(context, cropConfig.plantRequirements())) {
// event.setCancelled(true);
// return;
// }
Pos3 pos3 = Pos3.from(event.location());
CustomCropsWorld<?> world = event.world();
if (world.setting().cropPerChunk() >= 0) {
if (world.testChunkLimitation(pos3, this.getClass(), world.setting().cropPerChunk())) {
ActionManager.trigger(context, cropConfig.reachLimitActions());
event.setCancelled(true);
return;
}
}
fixOrGetState(world, pos3, event.placedID());
}
@Override
public void onInteract(WrappedInteractEvent event) {
final Player player = event.player();
Context<Player> context = Context.player(player);
// data first
CustomCropsWorld<?> world = event.world();
Location location = event.location();
Pos3 pos3 = Pos3.from(location);
// fix if possible
CustomCropsBlockState state = fixOrGetState(world, pos3, event.relatedID());
if (state == null) return;
CropConfig cropConfig = config(state);
if (!RequirementManager.isSatisfied(context, cropConfig.interactRequirements())) {
return;
}
int point = point(state);
CropStageConfig stageConfig = cropConfig.getFloorStageEntry(point).getValue();
if (!RequirementManager.isSatisfied(context, stageConfig.interactRequirements())) {
return;
}
final ItemStack itemInHand = event.itemInHand();
// trigger event
CropInteractEvent interactEvent = new CropInteractEvent(player, itemInHand, location, state, event.hand(), cropConfig, event.relatedID());
if (EventUtils.fireAndCheckCancel(interactEvent)) {
return;
}
Location potLocation = location.clone().subtract(0,1,0);
String blockBelowID = BukkitCustomCropsPlugin.getInstance().getItemManager().blockID(potLocation.getBlock());
PotConfig potConfig = Registries.ITEM_TO_POT.get(blockBelowID);
if (potConfig != null) {
PotBlock potBlock = (PotBlock) BuiltInBlockMechanics.POT.mechanic();
assert potBlock != null;
// fix or get data
CustomCropsBlockState potState = potBlock.fixOrGetState(world, Pos3.from(potLocation), potConfig, event.relatedID());
if (potBlock.tryWateringPot(player, context, potState, event.hand(), event.itemID(), potConfig, potLocation, itemInHand))
return;
}
if (point < cropConfig.maxPoints()) {
for (BoneMeal boneMeal : cropConfig.boneMeals()) {
if (boneMeal.requiredItem().equals(event.itemID()) && boneMeal.amountOfRequiredItem() <= itemInHand.getAmount()) {
BoneMealUseEvent useEvent = new BoneMealUseEvent(player, itemInHand, location, boneMeal, state, event.hand(), cropConfig);
if (EventUtils.fireAndCheckCancel(useEvent))
return;
if (player.getGameMode() != GameMode.CREATIVE) {
itemInHand.setAmount(itemInHand.getAmount() - boneMeal.amountOfRequiredItem());
if (boneMeal.returnedItem() != null) {
ItemStack returned = BukkitCustomCropsPlugin.getInstance().getItemManager().build(player, boneMeal.returnedItem());
if (returned != null) {
PlayerUtils.giveItem(player, returned, boneMeal.amountOfReturnItem());
}
}
}
boneMeal.triggerActions(context);
int afterPoints = Math.min(point + boneMeal.rollPoint(), cropConfig.maxPoints());
point(state, afterPoints);
String afterStage = null;
ExistenceForm afterForm = null;
int tempPoints = afterPoints;
while (tempPoints >= 0) {
Map.Entry<Integer, CropStageConfig> afterEntry = cropConfig.getFloorStageEntry(tempPoints);
CropStageConfig after = afterEntry.getValue();
if (after.stageID() != null) {
afterStage = after.stageID();
afterForm = after.existenceForm();
break;
}
tempPoints = after.point() - 1;
}
Objects.requireNonNull(afterForm);
Objects.requireNonNull(afterStage);
Context<CustomCropsBlockState> blockContext = Context.block(state);
for (int i = point + 1; i <= afterPoints; i++) {
CropStageConfig stage = cropConfig.stageByPoint(i);
if (stage != null) {
ActionManager.trigger(blockContext, stage.growActions());
}
}
if (Objects.equals(afterStage, event.relatedID())) return;
Location bukkitLocation = location.toLocation(world.bukkitWorld());
FurnitureRotation rotation = BukkitCustomCropsPlugin.getInstance().getItemManager().remove(bukkitLocation, ExistenceForm.ANY);
if (rotation == FurnitureRotation.NONE && cropConfig.rotation()) {
rotation = FurnitureRotation.random();
}
BukkitCustomCropsPlugin.getInstance().getItemManager().place(bukkitLocation, afterForm, afterStage, rotation);
return;
}
}
}
ActionManager.trigger(context, cropConfig.interactActions());
ActionManager.trigger(context, stageConfig.interactActions());
}
public CustomCropsBlockState fixOrGetState(CustomCropsWorld<?> world, Pos3 pos3, String stageID) {
List<CropConfig> configList = Registries.STAGE_TO_CROP_UNSAFE.get(stageID);
if (configList == null) return null;
Optional<CustomCropsBlockState> optionalPotState = world.getBlockState(pos3);
if (optionalPotState.isPresent()) {
CustomCropsBlockState potState = optionalPotState.get();
if (potState.type() instanceof CropBlock cropBlock) {
if (configList.stream().map(CropConfig::id).toList().contains(cropBlock.id(potState))) {
return potState;
}
}
}
if (configList.size() != 1) {
return null;
}
CropConfig cropConfig = configList.get(0);
CropStageConfig stageConfig = cropConfig.stageByID(stageID);
int point = stageConfig.point();
CustomCropsBlockState state = BuiltInBlockMechanics.CROP.createBlockState();
point(state, point);
id(state, cropConfig.id());
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
return state;
}
private void tickCrop(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
CropConfig config = config(state);
if (config == null) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Crop data is removed at location[" + world.worldName() + "," + location + "] because the crop config[" + id(state) + "] has been removed.");
world.removeBlockState(location);
return;
}
int previousPoint = point(state);
World bukkitWorld = world.bukkitWorld();
if (ConfigManager.doubleCheck()) {
Map.Entry<Integer, CropStageConfig> nearest = config.getFloorStageEntry(previousPoint);
String blockID = BukkitCustomCropsPlugin.getInstance().getItemManager().id(location.toLocation(bukkitWorld), nearest.getValue().existenceForm());
if (!config.stageIDs().contains(blockID)) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Crop[" + config.id() + "] is removed at location[" + world.worldName() + "," + location + "] because the id of the block is [" + blockID + "]");
world.removeBlockState(location);
return;
}
}
Context<CustomCropsBlockState> context = Context.block(state);
for (DeathCondition deathCondition : config.deathConditions()) {
if (deathCondition.isMet(context)) {
Location bukkitLocation = location.toLocation(bukkitWorld);
BukkitCustomCropsPlugin.getInstance().getScheduler().sync().runLater(() -> {
FurnitureRotation rotation = BukkitCustomCropsPlugin.getInstance().getItemManager().remove(bukkitLocation, ExistenceForm.ANY);
world.removeBlockState(location);
Optional.ofNullable(deathCondition.deathStage()).ifPresent(it -> {
BukkitCustomCropsPlugin.getInstance().getItemManager().place(bukkitLocation, deathCondition.existenceForm(), it, rotation);
});
}, deathCondition.deathDelay(), bukkitLocation);
return;
}
}
if (previousPoint >= config.maxPoints()) {
return;
}
int pointToAdd = 1;
for (GrowCondition growCondition : config.growConditions()) {
if (growCondition.isMet(context)) {
pointToAdd = growCondition.pointToAdd();
break;
}
}
Optional<CustomCropsBlockState> optionalState = world.getBlockState(location.add(0,-1,0));
if (optionalState.isPresent()) {
CustomCropsBlockState belowState = optionalState.get();
if (belowState.type() instanceof PotBlock potBlock) {
for (Fertilizer fertilizer : potBlock.fertilizers(belowState)) {
FertilizerConfig fertilizerConfig = fertilizer.config();
if (fertilizerConfig != null) {
pointToAdd = fertilizerConfig.processGainPoints(pointToAdd);
}
}
}
}
int afterPoints = Math.min(previousPoint + pointToAdd, config.maxPoints());
point(state, afterPoints);
int tempPoints = previousPoint;
String preStage = null;
while (tempPoints >= 0) {
Map.Entry<Integer, CropStageConfig> preEntry = config.getFloorStageEntry(tempPoints);
CropStageConfig pre = preEntry.getValue();
if (pre.stageID() != null) {
preStage = pre.stageID();
break;
}
tempPoints = pre.point() - 1;
}
String afterStage = null;
ExistenceForm afterForm = null;
tempPoints = afterPoints;
while (tempPoints >= 0) {
Map.Entry<Integer, CropStageConfig> afterEntry = config.getFloorStageEntry(tempPoints);
CropStageConfig after = afterEntry.getValue();
if (after.stageID() != null) {
afterStage = after.stageID();
afterForm = after.existenceForm();
break;
}
tempPoints = after.point() - 1;
}
Location bukkitLocation = location.toLocation(bukkitWorld);
final String finalPreStage = preStage;
final String finalAfterStage = afterStage;
final ExistenceForm finalAfterForm = afterForm;
Objects.requireNonNull(finalAfterStage);
Objects.requireNonNull(finalPreStage);
Objects.requireNonNull(finalAfterForm);
BukkitCustomCropsPlugin.getInstance().getScheduler().sync().run(() -> {
for (int i = previousPoint + 1; i <= afterPoints; i++) {
CropStageConfig stage = config.stageByPoint(i);
if (stage != null) {
ActionManager.trigger(context, stage.growActions());
}
}
if (Objects.equals(finalAfterStage, finalPreStage)) return;
FurnitureRotation rotation = BukkitCustomCropsPlugin.getInstance().getItemManager().remove(bukkitLocation, ExistenceForm.ANY);
if (rotation == FurnitureRotation.NONE && config.rotation()) {
rotation = FurnitureRotation.random();
}
BukkitCustomCropsPlugin.getInstance().getItemManager().place(bukkitLocation, finalAfterForm, finalAfterStage, rotation);
}, bukkitLocation);
}
public int point(CustomCropsBlockState state) {
return state.get("point").getAsIntTag().map(IntTag::getValue).orElse(0);
}
public void point(CustomCropsBlockState state, int point) {
state.set("point", new IntTag("point", point));
}
public CropConfig config(CustomCropsBlockState state) {
return Registries.CROP.get(id(state));
}
}

View File

@@ -0,0 +1,104 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public interface CropConfig {
String id();
String seed();
int maxPoints();
Requirement<Player>[] plantRequirements();
Requirement<Player>[] breakRequirements();
Requirement<Player>[] interactRequirements();
GrowCondition[] growConditions();
Action<Player>[] wrongPotActions();
Action<Player>[] interactActions();
Action<Player>[] breakActions();
Action<Player>[] plantActions();
Action<Player>[] reachLimitActions();
Action<CustomCropsBlockState>[] deathActions();
DeathCondition[] deathConditions();
BoneMeal[] boneMeals();
boolean rotation();
Set<String> potWhitelist();
CropStageConfig stageByPoint(int point);
@Nullable
CropStageConfig stageByID(String stageModel);
CropStageConfig stageWithModelByPoint(int point);
Collection<CropStageConfig> stages();
Collection<String> stageIDs();
Map.Entry<Integer, CropStageConfig> getFloorStageEntry(int previousPoint);
static Builder builder() {
return new CropConfigImpl.BuilderImpl();
}
interface Builder {
CropConfig build();
Builder id(String id);
Builder seed(String seed);
Builder maxPoints(int maxPoints);
Builder wrongPotActions(Action<Player>[] wrongPotActions);
Builder interactActions(Action<Player>[] interactActions);
Builder breakActions(Action<Player>[] breakActions);
Builder plantActions(Action<Player>[] plantActions);
Builder reachLimitActions(Action<Player>[] reachLimitActions);
Builder plantRequirements(Requirement<Player>[] plantRequirements);
Builder breakRequirements(Requirement<Player>[] breakRequirements);
Builder interactRequirements(Requirement<Player>[] interactRequirements);
Builder growConditions(GrowCondition[] growConditions);
Builder deathConditions(DeathCondition[] deathConditions);
Builder boneMeals(BoneMeal[] boneMeals);
Builder rotation(boolean rotation);
Builder potWhitelist(Set<String> whitelist);
Builder stages(Collection<CropStageConfig.Builder> stages);
}
}

View File

@@ -0,0 +1,345 @@
package net.momirealms.customcrops.api.core.block;
import com.google.common.base.Preconditions;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.*;
public class CropConfigImpl implements CropConfig {
private final String id;
private final String seed;
private final int maxPoints;
private final Action<Player>[] wrongPotActions;
private final Action<Player>[] interactActions;
private final Action<Player>[] breakActions;
private final Action<Player>[] plantActions;
private final Action<Player>[] reachLimitActions;
private final Action<CustomCropsBlockState>[] deathActions;
private final Requirement<Player>[] plantRequirements;
private final Requirement<Player>[] breakRequirements;
private final Requirement<Player>[] interactRequirements;
private final GrowCondition[] growConditions;
private final DeathCondition[] deathConditions;
private final BoneMeal[] boneMeals;
private final boolean rotation;
private final Set<String> potWhitelist;
private final HashMap<Integer, CropStageConfig> point2Stages = new HashMap<>();
private final NavigableMap<Integer, CropStageConfig> navigablePoint2Stages = new TreeMap<>();
private final HashMap<String, CropStageConfig> id2Stages = new HashMap<>();
private final Set<String> stageIDs = new HashSet<>();
private final HashMap<Integer, CropStageConfig> cropStageWithModelMap = new HashMap<>();
public CropConfigImpl(
String id,
String seed,
int maxPoints,
Action<Player>[] wrongPotActions,
Action<Player>[] interactActions,
Action<Player>[] breakActions,
Action<Player>[] plantActions,
Action<Player>[] reachLimitActions,
Action<CustomCropsBlockState>[] deathActions,
Requirement<Player>[] plantRequirements,
Requirement<Player>[] breakRequirements,
Requirement<Player>[] interactRequirements,
GrowCondition[] growConditions,
DeathCondition[] deathConditions,
BoneMeal[] boneMeals,
boolean rotation,
Set<String> potWhitelist,
Collection<CropStageConfig.Builder> stageBuilders
) {
this.id = id;
this.seed = seed;
this.maxPoints = maxPoints;
this.wrongPotActions = wrongPotActions;
this.interactActions = interactActions;
this.breakActions = breakActions;
this.plantActions = plantActions;
this.reachLimitActions = reachLimitActions;
this.deathActions = deathActions;
this.plantRequirements = plantRequirements;
this.breakRequirements = breakRequirements;
this.interactRequirements = interactRequirements;
this.growConditions = growConditions;
this.deathConditions = deathConditions;
this.boneMeals = boneMeals;
this.rotation = rotation;
this.potWhitelist = potWhitelist;
for (CropStageConfig.Builder builder : stageBuilders) {
CropStageConfig config = builder.crop(this).build();
point2Stages.put(config.point(), config);
navigablePoint2Stages.put(config.point(), config);
String stageID = config.stageID();
id2Stages.put(stageID, config);
stageIDs.add(stageID);
}
CropStageConfig tempConfig = null;
for (int i = 0; i <= maxPoints; i++) {
CropStageConfig config = point2Stages.get(i);
if (config != null) {
String stageModel = config.stageID();
if (stageModel != null) {
tempConfig = config;
}
}
cropStageWithModelMap.put(i, tempConfig);
}
}
@Override
public String id() {
return id;
}
@Override
public String seed() {
return seed;
}
@Override
public int maxPoints() {
return maxPoints;
}
@Override
public Requirement<Player>[] plantRequirements() {
return plantRequirements;
}
@Override
public Requirement<Player>[] breakRequirements() {
return breakRequirements;
}
@Override
public Requirement<Player>[] interactRequirements() {
return interactRequirements;
}
@Override
public GrowCondition[] growConditions() {
return growConditions;
}
@Override
public Action<Player>[] wrongPotActions() {
return wrongPotActions;
}
@Override
public Action<Player>[] interactActions() {
return interactActions;
}
@Override
public Action<Player>[] breakActions() {
return breakActions;
}
@Override
public Action<Player>[] plantActions() {
return plantActions;
}
@Override
public Action<Player>[] reachLimitActions() {
return reachLimitActions;
}
@Override
public Action<CustomCropsBlockState>[] deathActions() {
return deathActions;
}
@Override
public DeathCondition[] deathConditions() {
return deathConditions;
}
@Override
public BoneMeal[] boneMeals() {
return boneMeals;
}
@Override
public boolean rotation() {
return rotation;
}
@Override
public Set<String> potWhitelist() {
return potWhitelist;
}
@Override
public CropStageConfig stageByPoint(int id) {
return point2Stages.get(id);
}
@Override
public CropStageConfig stageByID(String stageModel) {
return id2Stages.get(stageModel);
}
@Override
public CropStageConfig stageWithModelByPoint(int point) {
return cropStageWithModelMap.get(Math.min(maxPoints, point));
}
@Override
public Collection<CropStageConfig> stages() {
return point2Stages.values();
}
@Override
public Collection<String> stageIDs() {
return stageIDs;
}
@Override
public Map.Entry<Integer, CropStageConfig> getFloorStageEntry(int point) {
Preconditions.checkArgument(point >= 0, "Point should be no lower than " + point);
return navigablePoint2Stages.floorEntry(point);
}
public static class BuilderImpl implements Builder {
private String id;
private String seed;
private ExistenceForm existenceForm;
private int maxPoints;
private Action<Player>[] wrongPotActions;
private Action<Player>[] interactActions;
private Action<Player>[] breakActions;
private Action<Player>[] plantActions;
private Action<Player>[] reachLimitActions;
private Action<CustomCropsBlockState>[] deathActions;
private Requirement<Player>[] plantRequirements;
private Requirement<Player>[] breakRequirements;
private Requirement<Player>[] interactRequirements;
private GrowCondition[] growConditions;
private DeathCondition[] deathConditions;
private BoneMeal[] boneMeals;
private boolean rotation;
private Set<String> potWhitelist;
private Collection<CropStageConfig.Builder> stages;
@Override
public CropConfig build() {
return new CropConfigImpl(id, seed, maxPoints, wrongPotActions, interactActions, breakActions, plantActions, reachLimitActions, deathActions, plantRequirements, breakRequirements, interactRequirements, growConditions, deathConditions, boneMeals, rotation, potWhitelist, stages);
}
@Override
public Builder id(String id) {
this.id = id;
return this;
}
@Override
public Builder seed(String seed) {
this.seed = seed;
return this;
}
@Override
public Builder maxPoints(int maxPoints) {
this.maxPoints = maxPoints;
return this;
}
@Override
public Builder wrongPotActions(Action<Player>[] wrongPotActions) {
this.wrongPotActions = wrongPotActions;
return this;
}
@Override
public Builder interactActions(Action<Player>[] interactActions) {
this.interactActions = interactActions;
return this;
}
@Override
public Builder breakActions(Action<Player>[] breakActions) {
this.breakActions = breakActions;
return this;
}
@Override
public Builder plantActions(Action<Player>[] plantActions) {
this.plantActions = plantActions;
return this;
}
public Builder deathActions(Action<CustomCropsBlockState>[] deathActions) {
this.deathActions = deathActions;
return this;
}
@Override
public Builder reachLimitActions(Action<Player>[] reachLimitActions) {
this.reachLimitActions = reachLimitActions;
return this;
}
@Override
public Builder plantRequirements(Requirement<Player>[] plantRequirements) {
this.plantRequirements = plantRequirements;
return this;
}
@Override
public Builder breakRequirements(Requirement<Player>[] breakRequirements) {
this.breakRequirements = breakRequirements;
return this;
}
@Override
public Builder interactRequirements(Requirement<Player>[] interactRequirements) {
this.interactRequirements = interactRequirements;
return this;
}
@Override
public Builder growConditions(GrowCondition[] growConditions) {
this.growConditions = growConditions;
return this;
}
@Override
public Builder deathConditions(DeathCondition[] deathConditions) {
this.deathConditions = deathConditions;
return this;
}
@Override
public Builder boneMeals(BoneMeal[] boneMeals) {
this.boneMeals = boneMeals;
return this;
}
@Override
public Builder rotation(boolean rotation) {
this.rotation = rotation;
return this;
}
@Override
public Builder potWhitelist(Set<String> potWhitelist) {
this.potWhitelist = new HashSet<>(potWhitelist);
return this;
}
@Override
public Builder stages(Collection<CropStageConfig.Builder> stages) {
this.stages = new HashSet<>(stages);
return this;
}
}
}

View File

@@ -0,0 +1,61 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
public interface CropStageConfig {
CropConfig crop();
double displayInfoOffset();
@Nullable
String stageID();
int point();
Requirement<Player>[] interactRequirements();
Requirement<Player>[] breakRequirements();
Action<Player>[] interactActions();
Action<Player>[] breakActions();
Action<CustomCropsBlockState>[] growActions();
ExistenceForm existenceForm();
static Builder builder() {
return new CropStageConfigImpl.BuilderImpl();
}
interface Builder {
CropStageConfig build();
Builder crop(CropConfig crop);
Builder displayInfoOffset(double offset);
Builder stageID(String id);
Builder point(int i);
Builder interactRequirements(Requirement<Player>[] requirements);
Builder breakRequirements(Requirement<Player>[] requirements);
Builder interactActions(Action<Player>[] actions);
Builder breakActions(Action<Player>[] actions);
Builder growActions(Action<CustomCropsBlockState>[] actions);
Builder existenceForm(ExistenceForm existenceForm);
}
}

View File

@@ -0,0 +1,176 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
public class CropStageConfigImpl implements CropStageConfig {
private final CropConfig crop;
private final ExistenceForm existenceForm;
private final double offset;
private final String stageID;
private final int point;
private final Requirement<Player>[] interactRequirements;
private final Requirement<Player>[] breakRequirements;
private final Action<Player>[] interactActions;
private final Action<Player>[] breakActions;
private final Action<CustomCropsBlockState>[] growActions;
public CropStageConfigImpl(
CropConfig crop,
ExistenceForm existenceForm,
double offset,
String stageID,
int point,
Requirement<Player>[] interactRequirements,
Requirement<Player>[] breakRequirements,
Action<Player>[] interactActions,
Action<Player>[] breakActions,
Action<CustomCropsBlockState>[] growActions
) {
this.crop = crop;
this.existenceForm = existenceForm;
this.offset = offset;
this.stageID = stageID;
this.point = point;
this.interactRequirements = interactRequirements;
this.breakRequirements = breakRequirements;
this.interactActions = interactActions;
this.breakActions = breakActions;
this.growActions = growActions;
}
@Override
public CropConfig crop() {
return crop;
}
@Override
public double displayInfoOffset() {
return offset;
}
@Nullable
@Override
public String stageID() {
return stageID;
}
@Override
public int point() {
return point;
}
@Override
public Requirement<Player>[] interactRequirements() {
return interactRequirements;
}
@Override
public Requirement<Player>[] breakRequirements() {
return breakRequirements;
}
@Override
public Action<Player>[] interactActions() {
return interactActions;
}
@Override
public Action<Player>[] breakActions() {
return breakActions;
}
@Override
public Action<CustomCropsBlockState>[] growActions() {
return growActions;
}
@Override
public ExistenceForm existenceForm() {
return existenceForm;
}
public static class BuilderImpl implements Builder {
private CropConfig crop;
private ExistenceForm existenceForm;
private double offset;
private String stageID;
private int point;
private Requirement<Player>[] interactRequirements;
private Requirement<Player>[] breakRequirements;
private Action<Player>[] interactActions;
private Action<Player>[] breakActions;
private Action<CustomCropsBlockState>[] growActions;
@Override
public CropStageConfig build() {
return new CropStageConfigImpl(crop, existenceForm, offset, stageID, point, interactRequirements, breakRequirements, interactActions, breakActions, growActions);
}
@Override
public Builder crop(CropConfig crop) {
this.crop = crop;
return this;
}
@Override
public Builder displayInfoOffset(double offset) {
this.offset = offset;
return this;
}
@Override
public Builder stageID(String id) {
this.stageID = id;
return this;
}
@Override
public Builder point(int i) {
this.point = i;
return this;
}
@Override
public Builder interactRequirements(Requirement<Player>[] requirements) {
this.interactRequirements = requirements;
return this;
}
@Override
public Builder breakRequirements(Requirement<Player>[] requirements) {
this.breakRequirements = requirements;
return this;
}
@Override
public Builder interactActions(Action<Player>[] actions) {
this.interactActions = actions;
return this;
}
@Override
public Builder breakActions(Action<Player>[] actions) {
this.breakActions = actions;
return this;
}
@Override
public Builder growActions(Action<CustomCropsBlockState>[] actions) {
this.growActions = actions;
return this;
}
@Override
public Builder existenceForm(ExistenceForm existenceForm) {
this.existenceForm = existenceForm;
return this;
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.customcrops.api.core.block;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.util.LocationUtils;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerTask;
import net.momirealms.customcrops.common.util.RandomUtils;
import net.momirealms.sparrow.heart.SparrowHeart;
import net.momirealms.sparrow.heart.feature.entity.armorstand.FakeArmorStand;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
public class CrowAttack {
private SchedulerTask task;
private final Location dynamicLocation;
private final Location cropLocation;
private final Vector vectorDown;
private final Vector vectorUp;
private final Player[] viewers;
private int timer;
private final ItemStack flyModel;
private final ItemStack standModel;
public CrowAttack(Location location, ItemStack flyModel, ItemStack standModel) {
ArrayList<Player> viewers = new ArrayList<>();
for (Player player : location.getWorld().getPlayers()) {
if (LocationUtils.getDistance(player.getLocation(), location) <= 48) {
viewers.add(player);
}
}
this.viewers = viewers.toArray(new Player[0]);
this.cropLocation = location.clone().add(RandomUtils.generateRandomDouble(-0.25, 0.25), 0, RandomUtils.generateRandomDouble(-0.25, 0.25));
float yaw = RandomUtils.generateRandomInt(-180, 180);
this.cropLocation.setYaw(yaw);
this.flyModel = flyModel;
this.standModel = standModel;
this.dynamicLocation = cropLocation.clone().add((10 * Math.sin((Math.PI * yaw) / 180)), 10, (- 10 * Math.cos((Math.PI * yaw) / 180)));
this.dynamicLocation.setYaw(yaw);
Location relative = cropLocation.clone().subtract(dynamicLocation);
this.vectorDown = new Vector(relative.getX() / 100, -0.1, relative.getZ() / 100);
this.vectorUp = new Vector(relative.getX() / 100, 0.1, relative.getZ() / 100);
}
public void start() {
if (this.viewers.length == 0) return;
FakeArmorStand fake1 = SparrowHeart.getInstance().createFakeArmorStand(dynamicLocation);
fake1.invisible(true);
fake1.small(true);
fake1.equipment(EquipmentSlot.HEAD, flyModel);
FakeArmorStand fake2 = SparrowHeart.getInstance().createFakeArmorStand(cropLocation);
fake1.invisible(true);
fake1.small(true);
fake1.equipment(EquipmentSlot.HEAD, standModel);
for (Player player : this.viewers) {
fake1.spawn(player);
}
this.task = BukkitCustomCropsPlugin.getInstance().getScheduler().asyncRepeating(() -> {
timer++;
if (timer < 100) {
dynamicLocation.add(vectorDown);
for (Player player : this.viewers) {
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake1.entityID());
}
} else if (timer == 100){
for (Player player : this.viewers) {
fake1.destroy(player);
}
for (Player player : this.viewers) {
fake2.spawn(player);
}
} else if (timer == 150) {
for (Player player : this.viewers) {
fake2.destroy(player);
}
for (Player player : this.viewers) {
fake1.spawn(player);
}
} else if (timer > 150) {
dynamicLocation.add(vectorUp);
for (Player player : this.viewers) {
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake1.entityID());
}
}
if (timer > 300) {
for (Player player : this.viewers) {
fake1.destroy(player);
}
task.cancel();
}
}, 50, 50, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,29 @@
package net.momirealms.customcrops.api.core.block;
import com.flowpowered.nbt.CompoundMap;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
public interface CustomCropsBlock {
Key type();
CustomCropsBlockState createBlockState();
CustomCropsBlockState createBlockState(CompoundMap data);
void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location);
void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location);
void onInteract(WrappedInteractEvent event);
void onBreak(WrappedBreakEvent event);
void onPlace(WrappedPlaceEvent event);
}

View File

@@ -0,0 +1,40 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import org.jetbrains.annotations.Nullable;
public class DeathCondition {
private final Requirement<CustomCropsBlockState>[] requirements;
private final String deathStage;
private final ExistenceForm existenceForm;
private final int deathDelay;
public DeathCondition(Requirement<CustomCropsBlockState>[] requirements, String deathStage, ExistenceForm existenceForm, int deathDelay) {
this.requirements = requirements;
this.deathStage = deathStage;
this.existenceForm = existenceForm;
this.deathDelay = deathDelay;
}
@Nullable
public String deathStage() {
return deathStage;
}
public int deathDelay() {
return deathDelay;
}
public boolean isMet(Context<CustomCropsBlockState> context) {
return RequirementManager.isSatisfied(context, requirements);
}
public ExistenceForm existenceForm() {
return existenceForm;
}
}

View File

@@ -0,0 +1,96 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.BuiltInBlockMechanics;
import net.momirealms.customcrops.api.core.ConfigManager;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
import net.momirealms.customcrops.api.event.GreenhouseGlassBreakEvent;
import net.momirealms.customcrops.api.event.GreenhouseGlassInteractEvent;
import net.momirealms.customcrops.api.event.GreenhouseGlassPlaceEvent;
import net.momirealms.customcrops.api.util.EventUtils;
import java.util.Optional;
public class GreenhouseBlock extends AbstractCustomCropsBlock {
public GreenhouseBlock() {
super(BuiltInBlockMechanics.GREENHOUSE.key());
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
//tickGreenhouse(world, location);
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
tickGreenhouse(world, location);
}
private void tickGreenhouse(CustomCropsWorld<?> world, Pos3 location) {
if (!ConfigManager.doubleCheck()) return;
String id = BukkitCustomCropsPlugin.getInstance().getItemManager().id(location.toLocation(world.bukkitWorld()), ConfigManager.greenhouseExistenceForm());
if (ConfigManager.greenhouse().contains(id)) return;
// remove outdated data
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Greenhouse is removed at location[" + world.worldName() + "," + location + "] because the id of the block/furniture is [" + id + "]");
world.removeBlockState(location);
}
@Override
public void onInteract(WrappedInteractEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
CustomCropsBlockState state = getOrFixState(world, pos3);
GreenhouseGlassInteractEvent interactEvent = new GreenhouseGlassInteractEvent(event.player(), event.itemInHand(), event.location(), event.relatedID(), state, event.hand());
EventUtils.fireAndForget(interactEvent);
}
@Override
public void onBreak(WrappedBreakEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
CustomCropsBlockState state = getOrFixState(world, pos3);
GreenhouseGlassBreakEvent breakEvent = new GreenhouseGlassBreakEvent(event.entityBreaker(), event.blockBreaker(), event.location(), event.brokenID(), state, event.reason());
if (EventUtils.fireAndCheckCancel(breakEvent)) {
return;
}
world.removeBlockState(pos3);
}
@Override
public void onPlace(WrappedPlaceEvent event) {
CustomCropsBlockState state = createBlockState();
GreenhouseGlassPlaceEvent placeEvent = new GreenhouseGlassPlaceEvent(event.player(), event.location(), event.itemID(), state);
if (EventUtils.fireAndCheckCancel(placeEvent)) {
return;
}
Pos3 pos3 = Pos3.from(event.location());
CustomCropsWorld<?> world = event.world();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
}
public CustomCropsBlockState getOrFixState(CustomCropsWorld<?> world, Pos3 pos3) {
Optional<CustomCropsBlockState> optional = world.getBlockState(pos3);
if (optional.isPresent() && optional.get().type() instanceof GreenhouseBlock) {
return optional.get();
}
CustomCropsBlockState state = createBlockState();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
return state;
}
}

View File

@@ -0,0 +1,25 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.api.requirement.RequirementManager;
public class GrowCondition {
private final Requirement<CustomCropsBlockState>[] requirements;
private final int pointToAdd;
public GrowCondition(Requirement<CustomCropsBlockState>[] requirements, int pointToAdd) {
this.requirements = requirements;
this.pointToAdd = pointToAdd;
}
public int pointToAdd() {
return pointToAdd;
}
public boolean isMet(Context<CustomCropsBlockState> context) {
return RequirementManager.isSatisfied(context, requirements);
}
}

View File

@@ -0,0 +1,574 @@
package net.momirealms.customcrops.api.core.block;
import com.flowpowered.nbt.*;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.item.Fertilizer;
import net.momirealms.customcrops.api.core.item.FertilizerConfig;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
import net.momirealms.customcrops.api.event.*;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.api.util.PlayerUtils;
import net.momirealms.customcrops.api.util.StringUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.block.data.type.Farmland;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class PotBlock extends AbstractCustomCropsBlock {
public PotBlock() {
super(BuiltInBlockMechanics.POT.key());
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (!world.setting().randomTickPot() && canTick(state, world.setting().tickPotInterval())) {
tickPot(state, world, location);
}
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (world.setting().randomTickPot() && canTick(state, world.setting().tickPotInterval())) {
tickPot(state, world, location);
}
}
@Override
public void onBreak(WrappedBreakEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
PotConfig config = Registries.ITEM_TO_POT.get(event.brokenID());
if (config == null) {
world.removeBlockState(pos3);
return;
}
final Player player = event.playerBreaker();
Context<Player> context = Context.player(player);
if (!RequirementManager.isSatisfied(context, config.breakRequirements())) {
event.setCancelled(true);
return;
}
Location upperLocation = event.location().clone().add(0,1,0);
String upperID = BukkitCustomCropsPlugin.getInstance().getItemManager().anyID(upperLocation);
List<CropConfig> cropConfigs = Registries.STAGE_TO_CROP_UNSAFE.get(upperID);
CropConfig cropConfig = null;
CropStageConfig stageConfig = null;
outer: {
if (cropConfigs != null && !cropConfigs.isEmpty()) {
CropBlock cropBlock = (CropBlock) BuiltInBlockMechanics.CROP.mechanic();
CustomCropsBlockState state = cropBlock.fixOrGetState(world, pos3.add(0,1,0), upperID);
if (state == null) {
// remove ambiguous stage
BukkitCustomCropsPlugin.getInstance().getItemManager().remove(upperLocation, ExistenceForm.ANY);
break outer;
}
cropConfig = cropBlock.config(state);
if (cropConfig == null || !cropConfigs.contains(cropConfig)) {
if (cropConfigs.size() != 1) {
// remove ambiguous stage
BukkitCustomCropsPlugin.getInstance().getItemManager().remove(upperLocation, ExistenceForm.ANY);
world.removeBlockState(pos3.add(0,1,0));
break outer;
}
cropConfig = cropConfigs.get(0);
}
if (!RequirementManager.isSatisfied(context, cropConfig.breakRequirements())) {
event.setCancelled(true);
return;
}
stageConfig = cropConfig.stageByID(upperID);
// should not be null
assert stageConfig != null;
if (!RequirementManager.isSatisfied(context, stageConfig.breakRequirements())) {
event.setCancelled(true);
return;
}
CropBreakEvent breakEvent = new CropBreakEvent(event.entityBreaker(), event.blockBreaker(), cropConfig, upperID, upperLocation, state, event.reason());
if (EventUtils.fireAndCheckCancel(breakEvent)) {
event.setCancelled(true);
return;
}
}
}
CustomCropsBlockState potState = fixOrGetState(world, pos3, config, event.brokenID());
PotBreakEvent breakEvent = new PotBreakEvent(event.entityBreaker(), event.blockBreaker(), event.location(), config, potState, event.reason());
if (EventUtils.fireAndCheckCancel(breakEvent)) {
event.setCancelled(true);
return;
}
ActionManager.trigger(context, config.breakActions());
if (stageConfig != null) {
ActionManager.trigger(context, stageConfig.breakActions());
}
if (cropConfig != null) {
ActionManager.trigger(context, cropConfig.breakActions());
world.removeBlockState(pos3.add(0,1,0));
BukkitCustomCropsPlugin.getInstance().getItemManager().remove(upperLocation, ExistenceForm.ANY);
}
world.removeBlockState(pos3);
}
@Override
public void onPlace(WrappedPlaceEvent event) {
PotConfig config = Registries.ITEM_TO_POT.get(event.placedID());
if (config == null) {
event.setCancelled(true);
return;
}
Context<Player> context = Context.player(event.player());
if (!RequirementManager.isSatisfied(context, config.placeRequirements())) {
event.setCancelled(true);
return;
}
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
if (world.setting().potPerChunk() >= 0) {
if (world.testChunkLimitation(pos3, this.getClass(), world.setting().potPerChunk())) {
event.setCancelled(true);
ActionManager.trigger(context, config.reachLimitActions());
return;
}
}
CustomCropsBlockState state = BuiltInBlockMechanics.POT.createBlockState();
id(state, config.id());
water(state, config.isWet(event.placedID()) ? 1 : 0);
PotPlaceEvent placeEvent = new PotPlaceEvent(event.player(), event.location(), config, state, event.item(), event.hand());
if (EventUtils.fireAndCheckCancel(placeEvent)) {
event.setCancelled(true);
return;
}
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
ActionManager.trigger(context, config.placeActions());
}
@Override
public void onInteract(WrappedInteractEvent event) {
PotConfig potConfig = Registries.ITEM_TO_POT.get(event.relatedID());
if (potConfig == null) {
return;
}
Location location = event.location();
Pos3 pos3 = Pos3.from(location);
CustomCropsWorld<?> world = event.world();
// fix or get data
CustomCropsBlockState state = fixOrGetState(world, pos3, potConfig, event.relatedID());
final Player player = event.player();
Context<Player> context = Context.player(player);
// check use requirements
if (!RequirementManager.isSatisfied(context, potConfig.useRequirements())) {
return;
}
final ItemStack itemInHand = event.itemInHand();
// trigger event
PotInteractEvent interactEvent = new PotInteractEvent(player, event.hand(), itemInHand, potConfig, location, state);
if (EventUtils.fireAndCheckCancel(interactEvent)) {
return;
}
if (tryWateringPot(player, context, state, event.hand(), event.itemID(), potConfig, location, itemInHand))
return;
ActionManager.trigger(context, potConfig.interactActions());
}
protected boolean tryWateringPot(Player player, Context<Player> context, CustomCropsBlockState state, EquipmentSlot hand, String itemID, PotConfig potConfig, Location potLocation, ItemStack itemInHand) {
int waterInPot = water(state);
for (WateringMethod method : potConfig.wateringMethods()) {
if (method.getUsed().equals(itemID) && method.getUsedAmount() <= itemInHand.getAmount()) {
if (method.checkRequirements(context)) {
if (waterInPot >= potConfig.storage()) {
ActionManager.trigger(context, potConfig.fullWaterActions());
} else {
PotFillEvent waterEvent = new PotFillEvent(player, itemInHand, hand, potLocation, method, state, potConfig);
if (EventUtils.fireAndCheckCancel(waterEvent))
return true;
if (player.getGameMode() != GameMode.CREATIVE) {
itemInHand.setAmount(Math.max(0, itemInHand.getAmount() - method.getUsedAmount()));
if (method.getReturned() != null) {
ItemStack returned = BukkitCustomCropsPlugin.getInstance().getItemManager().build(player, method.getReturned());
if (returned != null) {
PlayerUtils.giveItem(player, returned, method.getReturnedAmount());
}
}
}
method.triggerActions(context);
ActionManager.trigger(context, potConfig.addWaterActions());
}
}
return true;
}
}
return false;
}
public CustomCropsBlockState fixOrGetState(CustomCropsWorld<?> world, Pos3 pos3, PotConfig potConfig, String blockID) {
Optional<CustomCropsBlockState> optionalPotState = world.getBlockState(pos3);
if (optionalPotState.isPresent()) {
CustomCropsBlockState potState = optionalPotState.get();
if (potState.type() instanceof PotBlock potBlock) {
if (potBlock.id(potState).equals(potConfig.id())) {
return potState;
}
}
}
CustomCropsBlockState state = BuiltInBlockMechanics.POT.createBlockState();
id(state, potConfig.id());
water(state, potConfig.isWet(blockID) ? 1 : 0);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
return state;
}
private void tickPot(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
PotConfig config = config(state);
if (config == null) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Pot data is removed at location[" + world.worldName() + "," + location + "] because the pot config[" + id(state) + "] has been removed.");
world.removeBlockState(location);
return;
}
World bukkitWorld = world.bukkitWorld();
if (ConfigManager.doubleCheck()) {
String blockID = BukkitCustomCropsPlugin.getInstance().getItemManager().blockID(location.toLocation(bukkitWorld));
if (!config.blocks().contains(blockID)) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Pot[" + config.id() + "] is removed at location[" + world.worldName() + "," + location + "] because the id of the block is [" + blockID + "]");
world.removeBlockState(location);
return;
}
}
boolean hasNaturalWater = false;
boolean waterChanged = false;
if (config.isRainDropAccepted()) {
if (bukkitWorld.hasStorm() || (!bukkitWorld.isClearWeather() && !bukkitWorld.isThundering())) {
double temperature = bukkitWorld.getTemperature(location.x(), location.y(), location.z());
if (temperature > 0.15 && temperature < 0.85) {
int y = bukkitWorld.getHighestBlockYAt(location.x(), location.z());
if (y == location.y()) {
if (addWater(state, 1)) {
waterChanged = true;
}
hasNaturalWater = true;
}
}
}
}
if (!hasNaturalWater && config.isNearbyWaterAccepted()) {
for (int i = -4; i <= 4; i++) {
for (int j = -4; j <= 4; j++) {
for (int k : new int[]{0, 1}) {
BlockData block = bukkitWorld.getBlockData(location.x() + i, location.y() + j, location.z() + k);
if (block.getMaterial() == Material.WATER || (block instanceof Waterlogged waterlogged && waterlogged.isWaterlogged())) {
if (addWater(state, 1)) {
waterChanged = true;
}
hasNaturalWater = true;
}
}
}
}
}
if (!hasNaturalWater) {
int waterToLose = 1;
Fertilizer[] fertilizers = fertilizers(state);
for (Fertilizer fertilizer : fertilizers) {
FertilizerConfig fertilizerConfig = fertilizer.config();
if (fertilizerConfig != null) {
waterToLose = fertilizerConfig.processWaterToLose(waterToLose);
}
}
if (waterToLose > 0) {
if (addWater(state, -waterToLose)) {
waterChanged = true;
}
}
}
boolean fertilizerChanged = tickFertilizer(state);
if (fertilizerChanged || waterChanged) {
updateBlockAppearance(location.toLocation(bukkitWorld), config, hasNaturalWater, fertilizers(state));
}
}
public int water(CustomCropsBlockState state) {
Tag<?> tag = state.get("water");
if (tag == null) {
return 0;
}
return tag.getAsIntTag().map(IntTag::getValue).orElse(0);
}
public boolean addWater(CustomCropsBlockState state, int water) {
return water(state, water + water(state));
}
public boolean addWater(CustomCropsBlockState state, PotConfig config, int water) {
return water(state, config, water + water(state));
}
public boolean consumeWater(CustomCropsBlockState state, int water) {
return water(state, water(state) - water);
}
public boolean consumeWater(CustomCropsBlockState state, PotConfig config, int water) {
return water(state, config, water(state) - water);
}
public boolean water(CustomCropsBlockState state, int water) {
return water(state, config(state), water);
}
/**
* Set the water for a pot
*
* @param state the block state
* @param config the pot config
* @param water the amount of water
* @return whether the moisture state has been changed
*/
public boolean water(CustomCropsBlockState state, PotConfig config, int water) {
if (water < 0) water = 0;
int current = Math.min(water, config.storage());
int previous = water(state);
if (water == previous) return false;
state.set("water", new IntTag("water", current));
return previous == 0 ^ current == 0;
}
public PotConfig config(CustomCropsBlockState state) {
return Registries.POT.get(id(state));
}
/**
* Get the fertilizers in the pot
*
* @param state the block state
* @return applied fertilizers
*/
@SuppressWarnings("unchecked")
@NotNull
public Fertilizer[] fertilizers(CustomCropsBlockState state) {
Tag<?> fertilizerTag = state.get("fertilizers");
if (fertilizerTag == null) return new Fertilizer[0];
List<CompoundTag> tags = ((ListTag<CompoundTag>) fertilizerTag.getValue()).getValue();
Fertilizer[] fertilizers = new Fertilizer[tags.size()];
for (int i = 0; i < tags.size(); i++) {
CompoundTag tag = tags.get(i);
fertilizers[i] = tagToFertilizer(tag.getValue());
}
return fertilizers;
}
/**
* Check if the fertilizer can be applied to this pot
*
* @param state the block state
* @param fertilizer the fertilizer to apply
* @return can be applied or not
*/
public boolean canApplyFertilizer(CustomCropsBlockState state, Fertilizer fertilizer) {
Fertilizer[] fertilizers = fertilizers(state);
boolean hasSameTypeFertilizer = false;
for (Fertilizer applied : fertilizers) {
if (fertilizer.id().equals(applied.id())) {
return true;
}
if (fertilizer.type() == applied.type()) {
return false;
}
}
PotConfig config = config(state);
return config.maxFertilizers() > fertilizers.length;
}
/**
* Add fertilizer to the pot
* If the pot contains the fertilizer, the times would be reset.
*
* @param state the block state
* @param fertilizer the fertilizer to apply
* @return whether to update pot appearance
*/
@SuppressWarnings("unchecked")
public boolean addFertilizer(CustomCropsBlockState state, Fertilizer fertilizer) {
Tag<?> fertilizerTag = state.get("fertilizers");
if (fertilizerTag == null) {
fertilizerTag = new ListTag<CompoundTag>("", TagType.TAG_COMPOUND, new ArrayList<>());
state.set("fertilizers", fertilizerTag);
}
List<CompoundTag> tags = ((ListTag<CompoundTag>) fertilizerTag.getValue()).getValue();
for (CompoundTag tag : tags) {
CompoundMap map = tag.getValue();
Fertilizer applied = tagToFertilizer(map);
if (fertilizer.id().equals(applied.id())) {
map.put(new IntTag("times", fertilizer.times()));
return false;
}
if (fertilizer.type() == applied.type()) {
return false;
}
}
PotConfig config = config(state);
if (config.maxFertilizers() <= tags.size()) {
return false;
}
tags.add(new CompoundTag("", fertilizerToTag(fertilizer)));
return true;
}
@SuppressWarnings("unchecked")
private boolean tickFertilizer(CustomCropsBlockState state) {
// no fertilizers applied
Tag<?> fertilizerTag = state.get("fertilizers");
if (fertilizerTag == null) {
return false;
}
List<CompoundTag> tags = ((ListTag<CompoundTag>) fertilizerTag.getValue()).getValue();
if (tags.isEmpty()) {
return false;
}
List<Integer> fertilizerToRemove = new ArrayList<>();
for (int i = 0; i < tags.size(); i++) {
CompoundMap map = tags.get(i).getValue();
Fertilizer applied = tagToFertilizer(map);
if (applied.reduceTimes()) {
fertilizerToRemove.add(i);
} else {
tags.get(i).setValue(fertilizerToTag(applied));
}
}
// no fertilizer is used up
if (fertilizerToRemove.isEmpty()) {
return false;
}
CompoundTag lastEntry = tags.get(tags.size() - 1);
Collections.reverse(fertilizerToRemove);
for (int i : fertilizerToRemove) {
tags.remove(i);
}
// all the fertilizers are used up
if (tags.isEmpty()) {
return true;
}
// if the most recent applied fertilizer is the same
CompoundTag newLastEntry = tags.get(tags.size() - 1);
return lastEntry != newLastEntry;
}
public void updateBlockAppearance(Location location, CustomCropsBlockState state) {
updateBlockAppearance(location, state, fertilizers(state));
}
public void updateBlockAppearance(Location location, CustomCropsBlockState state, Fertilizer[] fertilizers) {
updateBlockAppearance(location, state, water(state) != 0, fertilizers);
}
public void updateBlockAppearance(Location location, CustomCropsBlockState state, boolean hasWater, Fertilizer[] fertilizers) {
updateBlockAppearance(
location,
config(state),
hasWater,
// always using the latest fertilizer as appearance
fertilizers.length == 0 ? null : fertilizers[fertilizers.length - 1]
);
}
public void updateBlockAppearance(Location location, PotConfig config, boolean hasWater, Fertilizer[] fertilizers) {
updateBlockAppearance(
location,
config,
hasWater,
// always using the latest fertilizer as appearance
fertilizers.length == 0 ? null : fertilizers[fertilizers.length - 1]
);
}
public void updateBlockAppearance(Location location, PotConfig config, boolean hasWater, @Nullable Fertilizer fertilizer) {
String appearance = config.getPotAppearance(hasWater, fertilizer == null ? null : fertilizer.type());
if (StringUtils.isCapitalLetter(appearance)) {
Block block = location.getBlock();
Material type = Material.valueOf(appearance);
if (type == Material.FARMLAND) {
Farmland data = ((Farmland) Material.FARMLAND.createBlockData());
data.setMoisture(hasWater ? 7 : 0);
block.setBlockData(data, false);
} else {
block.setType(type, false);
}
} else {
BukkitCustomCropsPlugin.getInstance().getItemManager().place(location, ExistenceForm.BLOCK, appearance, FurnitureRotation.NONE);
}
}
private Fertilizer tagToFertilizer(CompoundMap tag) {
return Fertilizer.builder()
.id(((StringTag) tag.get("id")).getValue())
.times(((IntTag) tag.get("times")).getValue())
.build();
}
private CompoundMap fertilizerToTag(Fertilizer fertilizer) {
CompoundMap tag = new CompoundMap();
tag.put(new IntTag("times", fertilizer.times()));
tag.put(new StringTag("id", fertilizer.id()));
return tag;
}
}

View File

@@ -0,0 +1,103 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.item.FertilizerType;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Set;
public interface PotConfig {
String id();
int storage();
boolean isRainDropAccepted();
boolean isNearbyWaterAccepted();
WateringMethod[] wateringMethods();
Set<String> blocks();
boolean isWet(String blockID);
WaterBar waterBar();
int maxFertilizers();
String getPotAppearance(boolean watered, FertilizerType type);
Requirement<Player>[] placeRequirements();
Requirement<Player>[] breakRequirements();
Requirement<Player>[] useRequirements();
Action<CustomCropsBlockState>[] tickActions();
Action<Player>[] reachLimitActions();
Action<Player>[] interactActions();
Action<Player>[] placeActions();
Action<Player>[] breakActions();
Action<Player>[] addWaterActions();
Action<Player>[] fullWaterActions();
static Builder builder() {
return new PotConfigImpl.BuilderImpl();
}
interface Builder {
PotConfig build();
Builder id(String id);
Builder storage(int storage);
Builder isRainDropAccepted(boolean isRainDropAccepted);
Builder isNearbyWaterAccepted(boolean isNearbyWaterAccepted);
Builder wateringMethods(WateringMethod[] wateringMethods);
Builder waterBar(WaterBar waterBar);
Builder maxFertilizers(int maxFertilizers);
Builder placeRequirements(Requirement<Player>[] requirements);
Builder breakRequirements(Requirement<Player>[] requirements);
Builder useRequirements(Requirement<Player>[] requirements);
Builder tickActions(Action<CustomCropsBlockState>[] tickActions);
Builder reachLimitActions(Action<Player>[] reachLimitActions);
Builder interactActions(Action<Player>[] interactActions);
Builder placeActions(Action<Player>[] placeActions);
Builder breakActions(Action<Player>[] breakActions);
Builder addWaterActions(Action<Player>[] addWaterActions);
Builder fullWaterActions(Action<Player>[] fullWaterActions);
Builder basicAppearance(Pair<String, String> basicAppearance);
Builder potAppearanceMap(HashMap<FertilizerType, Pair<String, String>> potAppearanceMap);
}
}

View File

@@ -0,0 +1,339 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.item.FertilizerType;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
public class PotConfigImpl implements PotConfig {
private final String id;
private final Pair<String, String> basicAppearance;
private final HashMap<FertilizerType, Pair<String, String>> potAppearanceMap;
private final Set<String> blocks = new HashSet<>();
private final Set<String> wetBlocks = new HashSet<>();
private final int storage;
private final boolean isRainDropAccepted;
private final boolean isNearbyWaterAccepted;
private final WateringMethod[] wateringMethods;
private final WaterBar waterBar;
private final int maxFertilizers;
private final Requirement<Player>[] placeRequirements;
private final Requirement<Player>[] breakRequirements;
private final Requirement<Player>[] useRequirements;
private final Action<CustomCropsBlockState>[] tickActions;
private final Action<Player>[] reachLimitActions;
private final Action<Player>[] interactActions;
private final Action<Player>[] placeActions;
private final Action<Player>[] breakActions;
private final Action<Player>[] addWaterActions;
private final Action<Player>[] fullWaterActions;
public PotConfigImpl(
String id,
Pair<String, String> basicAppearance,
HashMap<FertilizerType, Pair<String, String>> potAppearanceMap,
int storage,
boolean isRainDropAccepted,
boolean isNearbyWaterAccepted,
WateringMethod[] wateringMethods,
WaterBar waterBar,
int maxFertilizers,
Requirement<Player>[] placeRequirements,
Requirement<Player>[] breakRequirements,
Requirement<Player>[] useRequirements,
Action<CustomCropsBlockState>[] tickActions,
Action<Player>[] reachLimitActions,
Action<Player>[] interactActions,
Action<Player>[] placeActions,
Action<Player>[] breakActions,
Action<Player>[] addWaterActions,
Action<Player>[] fullWaterActions
) {
this.id = id;
this.basicAppearance = basicAppearance;
this.potAppearanceMap = potAppearanceMap;
this.storage = storage;
this.isRainDropAccepted = isRainDropAccepted;
this.isNearbyWaterAccepted = isNearbyWaterAccepted;
this.wateringMethods = wateringMethods;
this.waterBar = waterBar;
this.maxFertilizers = maxFertilizers;
this.placeRequirements = placeRequirements;
this.breakRequirements = breakRequirements;
this.useRequirements = useRequirements;
this.tickActions = tickActions;
this.reachLimitActions = reachLimitActions;
this.interactActions = interactActions;
this.placeActions = placeActions;
this.breakActions = breakActions;
this.addWaterActions = addWaterActions;
this.fullWaterActions = fullWaterActions;
this.blocks.add(basicAppearance.left());
this.blocks.add(basicAppearance.right());
this.wetBlocks.add(basicAppearance.right());
for (Pair<String, String> pair : potAppearanceMap.values()) {
this.blocks.add(pair.left());
this.blocks.add(pair.right());
this.wetBlocks.add(pair.right());
}
}
@Override
public String id() {
return id;
}
@Override
public int storage() {
return storage;
}
@Override
public boolean isRainDropAccepted() {
return isRainDropAccepted;
}
@Override
public boolean isNearbyWaterAccepted() {
return isNearbyWaterAccepted;
}
@Override
public WateringMethod[] wateringMethods() {
return wateringMethods;
}
@Override
public Set<String> blocks() {
return blocks;
}
@Override
public boolean isWet(String blockID) {
return wetBlocks.contains(blockID);
}
@Override
public WaterBar waterBar() {
return waterBar;
}
@Override
public int maxFertilizers() {
return maxFertilizers;
}
@Override
public String getPotAppearance(boolean watered, FertilizerType type) {
if (type != null) {
Pair<String, String> appearance = potAppearanceMap.get(type);
if (appearance != null) {
return watered ? appearance.right() : appearance.left();
}
}
return watered ? basicAppearance.right() : basicAppearance.left();
}
@Override
public Requirement<Player>[] placeRequirements() {
return placeRequirements;
}
@Override
public Requirement<Player>[] breakRequirements() {
return breakRequirements;
}
@Override
public Requirement<Player>[] useRequirements() {
return useRequirements;
}
@Override
public Action<CustomCropsBlockState>[] tickActions() {
return tickActions;
}
@Override
public Action<Player>[] reachLimitActions() {
return reachLimitActions;
}
@Override
public Action<Player>[] interactActions() {
return interactActions;
}
@Override
public Action<Player>[] placeActions() {
return placeActions;
}
@Override
public Action<Player>[] breakActions() {
return breakActions;
}
@Override
public Action<Player>[] addWaterActions() {
return addWaterActions;
}
@Override
public Action<Player>[] fullWaterActions() {
return fullWaterActions;
}
public static class BuilderImpl implements Builder {
private String id;
private ExistenceForm existenceForm;
private Pair<String, String> basicAppearance;
private HashMap<FertilizerType, Pair<String, String>> potAppearanceMap;
private int storage;
private boolean isRainDropAccepted;
private boolean isNearbyWaterAccepted;
private WateringMethod[] wateringMethods;
private WaterBar waterBar;
private int maxFertilizers;
private Requirement<Player>[] placeRequirements;
private Requirement<Player>[] breakRequirements;
private Requirement<Player>[] useRequirements;
private Action<CustomCropsBlockState>[] tickActions;
private Action<Player>[] reachLimitActions;
private Action<Player>[] interactActions;
private Action<Player>[] placeActions;
private Action<Player>[] breakActions;
private Action<Player>[] addWaterActions;
private Action<Player>[] fullWaterActions;
@Override
public PotConfig build() {
return new PotConfigImpl(id, basicAppearance, potAppearanceMap, storage, isRainDropAccepted, isNearbyWaterAccepted, wateringMethods, waterBar, maxFertilizers, placeRequirements, breakRequirements, useRequirements, tickActions, reachLimitActions, interactActions, placeActions, breakActions, addWaterActions, fullWaterActions);
}
@Override
public Builder id(String id) {
this.id = id;
return this;
}
@Override
public Builder storage(int storage) {
this.storage = storage;
return this;
}
@Override
public Builder isRainDropAccepted(boolean isRainDropAccepted) {
this.isRainDropAccepted = isRainDropAccepted;
return this;
}
@Override
public Builder isNearbyWaterAccepted(boolean isNearbyWaterAccepted) {
this.isNearbyWaterAccepted = isNearbyWaterAccepted;
return this;
}
@Override
public Builder wateringMethods(WateringMethod[] wateringMethods) {
this.wateringMethods = wateringMethods;
return this;
}
@Override
public Builder waterBar(WaterBar waterBar) {
this.waterBar = waterBar;
return this;
}
@Override
public Builder maxFertilizers(int maxFertilizers) {
this.maxFertilizers = maxFertilizers;
return this;
}
@Override
public Builder placeRequirements(Requirement<Player>[] requirements) {
this.placeRequirements = requirements;
return this;
}
@Override
public Builder breakRequirements(Requirement<Player>[] requirements) {
this.breakRequirements = requirements;
return this;
}
@Override
public Builder useRequirements(Requirement<Player>[] requirements) {
this.useRequirements = requirements;
return this;
}
@Override
public Builder tickActions(Action<CustomCropsBlockState>[] tickActions) {
this.tickActions = tickActions;
return this;
}
@Override
public Builder reachLimitActions(Action<Player>[] reachLimitActions) {
this.reachLimitActions = reachLimitActions;
return this;
}
@Override
public Builder interactActions(Action<Player>[] interactActions) {
this.interactActions = interactActions;
return this;
}
@Override
public Builder placeActions(Action<Player>[] placeActions) {
this.placeActions = placeActions;
return this;
}
@Override
public Builder breakActions(Action<Player>[] breakActions) {
this.breakActions = breakActions;
return this;
}
@Override
public Builder addWaterActions(Action<Player>[] addWaterActions) {
this.addWaterActions = addWaterActions;
return this;
}
@Override
public Builder fullWaterActions(Action<Player>[] fullWaterActions) {
this.fullWaterActions = fullWaterActions;
return this;
}
@Override
public Builder basicAppearance(Pair<String, String> basicAppearance) {
this.basicAppearance = basicAppearance;
return this;
}
@Override
public Builder potAppearanceMap(HashMap<FertilizerType, Pair<String, String>> potAppearanceMap) {
this.potAppearanceMap = potAppearanceMap;
return this;
}
}
}

View File

@@ -0,0 +1,94 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.BuiltInBlockMechanics;
import net.momirealms.customcrops.api.core.ConfigManager;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
import net.momirealms.customcrops.api.event.*;
import net.momirealms.customcrops.api.util.EventUtils;
import java.util.Optional;
public class ScarecrowBlock extends AbstractCustomCropsBlock {
public ScarecrowBlock() {
super(BuiltInBlockMechanics.SCARECROW.key());
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
//tickScarecrow(world, location);
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
tickScarecrow(world, location);
}
private void tickScarecrow(CustomCropsWorld<?> world, Pos3 location) {
if (!ConfigManager.doubleCheck()) return;
String id = BukkitCustomCropsPlugin.getInstance().getItemManager().id(location.toLocation(world.bukkitWorld()), ConfigManager.scarecrowExistenceForm());
if (ConfigManager.scarecrow().contains(id)) return;
// remove outdated data
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Scarecrow is removed at location[" + world.worldName() + "," + location + "] because the id of the block/furniture is [" + id + "]");
world.removeBlockState(location);
}
@Override
public void onInteract(WrappedInteractEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
CustomCropsBlockState state = getOrFixState(world, pos3);
ScarecrowInteractEvent interactEvent = new ScarecrowInteractEvent(event.player(), event.itemInHand(), event.location(), event.relatedID(), state, event.hand());
EventUtils.fireAndForget(interactEvent);
}
@Override
public void onBreak(WrappedBreakEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
CustomCropsBlockState state = getOrFixState(world, pos3);
ScarecrowBreakEvent breakEvent = new ScarecrowBreakEvent(event.entityBreaker(), event.blockBreaker(), event.location(), event.brokenID(), state, event.reason());
if (EventUtils.fireAndCheckCancel(breakEvent)) {
return;
}
world.removeBlockState(pos3);
}
@Override
public void onPlace(WrappedPlaceEvent event) {
CustomCropsBlockState state = createBlockState();
ScarecrowPlaceEvent placeEvent = new ScarecrowPlaceEvent(event.player(), event.location(), event.itemID(), state);
if (EventUtils.fireAndCheckCancel(placeEvent)) {
return;
}
Pos3 pos3 = Pos3.from(event.location());
CustomCropsWorld<?> world = event.world();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
}
public CustomCropsBlockState getOrFixState(CustomCropsWorld<?> world, Pos3 pos3) {
Optional<CustomCropsBlockState> optional = world.getBlockState(pos3);
if (optional.isPresent() && optional.get().type() instanceof ScarecrowBlock) {
return optional.get();
}
CustomCropsBlockState state = createBlockState();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
return state;
}
}

View File

@@ -0,0 +1,310 @@
package net.momirealms.customcrops.api.core.block;
import com.flowpowered.nbt.IntTag;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedPlaceEvent;
import net.momirealms.customcrops.api.event.SprinklerBreakEvent;
import net.momirealms.customcrops.api.event.SprinklerFillEvent;
import net.momirealms.customcrops.api.event.SprinklerInteractEvent;
import net.momirealms.customcrops.api.event.SprinklerPlaceEvent;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.api.util.PlayerUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class SprinklerBlock extends AbstractCustomCropsBlock {
public SprinklerBlock() {
super(BuiltInBlockMechanics.SPRINKLER.key());
}
@Override
public void randomTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (!world.setting().randomTickSprinkler() && canTick(state, world.setting().tickSprinklerInterval())) {
tickSprinkler(state, world, location);
}
}
@Override
public void scheduledTick(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
if (world.setting().randomTickSprinkler() && canTick(state, world.setting().tickSprinklerInterval())) {
tickSprinkler(state, world, location);
}
}
@Override
public void onBreak(WrappedBreakEvent event) {
CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(event.location());
SprinklerConfig config = Registries.ITEM_TO_SPRINKLER.get(event.brokenID());
if (config == null) {
world.removeBlockState(pos3);
return;
}
final Player player = event.playerBreaker();
Context<Player> context = Context.player(player);
CustomCropsBlockState state = fixOrGetState(world, pos3, config, event.brokenID());
if (!RequirementManager.isSatisfied(context, config.breakRequirements())) {
event.setCancelled(true);
return;
}
SprinklerBreakEvent breakEvent = new SprinklerBreakEvent(event.entityBreaker(), event.blockBreaker(), event.location(), state, config, event.reason());
if (EventUtils.fireAndCheckCancel(breakEvent)) {
event.setCancelled(true);
return;
}
world.removeBlockState(pos3);
ActionManager.trigger(context, config.breakActions());
}
@Override
public void onPlace(WrappedPlaceEvent event) {
SprinklerConfig config = Registries.ITEM_TO_SPRINKLER.get(event.placedID());
if (config == null) {
event.setCancelled(true);
return;
}
final Player player = event.player();
Context<Player> context = Context.player(player);
if (!RequirementManager.isSatisfied(context, config.placeRequirements())) {
event.setCancelled(true);
return;
}
Pos3 pos3 = Pos3.from(event.location());
CustomCropsWorld<?> world = event.world();
if (world.setting().sprinklerPerChunk() >= 0) {
if (world.testChunkLimitation(pos3, this.getClass(), world.setting().sprinklerPerChunk())) {
event.setCancelled(true);
ActionManager.trigger(context, config.reachLimitActions());
return;
}
}
CustomCropsBlockState state = createBlockState();
id(state, config.id());
water(state, config.threeDItemWithWater().equals(event.placedID()) ? 1 : 0);
SprinklerPlaceEvent placeEvent = new SprinklerPlaceEvent(player, event.item(), event.hand(), event.location(), config, state);
if (EventUtils.fireAndCheckCancel(placeEvent)) {
event.setCancelled(true);
return;
}
world.addBlockState(pos3, state);
ActionManager.trigger(context, config.placeActions());
}
@Override
public void onInteract(WrappedInteractEvent event) {
SprinklerConfig config = Registries.ITEM_TO_SPRINKLER.get(event.relatedID());
if (config == null) {
return;
}
final Player player = event.player();
Context<Player> context = Context.player(player);
CustomCropsBlockState state = fixOrGetState(event.world(), Pos3.from(event.location()), config, event.relatedID());
if (!RequirementManager.isSatisfied(context, config.useRequirements())) {
return;
}
int waterInSprinkler = water(state);
String itemID = event.itemID();
ItemStack itemInHand = event.itemInHand();
if (!config.infinite()) {
for (WateringMethod method : config.wateringMethods()) {
if (method.getUsed().equals(itemID) && method.getUsedAmount() <= itemInHand.getAmount()) {
if (method.checkRequirements(context)) {
if (waterInSprinkler >= config.storage()) {
ActionManager.trigger(context, config.fullWaterActions());
} else {
SprinklerFillEvent waterEvent = new SprinklerFillEvent(player, itemInHand, event.hand(), event.location(), method, state, config);
if (EventUtils.fireAndCheckCancel(waterEvent))
return;
if (player.getGameMode() != GameMode.CREATIVE) {
itemInHand.setAmount(Math.max(0, itemInHand.getAmount() - method.getUsedAmount()));
if (method.getReturned() != null) {
ItemStack returned = BukkitCustomCropsPlugin.getInstance().getItemManager().build(player, method.getReturned());
if (returned != null) {
PlayerUtils.giveItem(player, returned, method.getReturnedAmount());
}
}
}
method.triggerActions(context);
ActionManager.trigger(context, config.addWaterActions());
}
}
return;
}
}
}
SprinklerInteractEvent interactEvent = new SprinklerInteractEvent(player, event.itemInHand(), event.location(), config, state, event.hand());
if (EventUtils.fireAndCheckCancel(interactEvent)) {
return;
}
ActionManager.trigger(context, config.interactActions());
}
public CustomCropsBlockState fixOrGetState(CustomCropsWorld<?> world, Pos3 pos3, SprinklerConfig sprinklerConfig, String blockID) {
Optional<CustomCropsBlockState> optionalPotState = world.getBlockState(pos3);
if (optionalPotState.isPresent()) {
CustomCropsBlockState potState = optionalPotState.get();
if (potState.type() instanceof SprinklerBlock sprinklerBlock) {
if (sprinklerBlock.id(potState).equals(sprinklerConfig.id())) {
return potState;
}
}
}
CustomCropsBlockState state = BuiltInBlockMechanics.SPRINKLER.createBlockState();
id(state, sprinklerConfig.id());
water(state, blockID.equals(sprinklerConfig.threeDItemWithWater()) ? 1 : 0);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
return state;
}
private void tickSprinkler(CustomCropsBlockState state, CustomCropsWorld<?> world, Pos3 location) {
SprinklerConfig config = config(state);
if (config == null) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Sprinkler data is removed at location[" + world.worldName() + "," + location + "] because the sprinkler config[" + id(state) + "] has been removed.");
world.removeBlockState(location);
return;
}
boolean updateState;
if (!config.infinite()) {
int water = water(state);
if (water <= 0) {
return;
}
water(state, --water);
updateState = water == 0;
} else {
updateState = false;
}
Context<CustomCropsBlockState> context = Context.block(state);
World bukkitWorld = world.bukkitWorld();
Location bukkitLocation = location.toLocation(bukkitWorld);
CompletableFuture<Boolean> syncCheck = new CompletableFuture<>();
// place/remove entities on main thread
BukkitCustomCropsPlugin.getInstance().getScheduler().sync().run(() -> {
if (ConfigManager.doubleCheck()) {
String modelID = BukkitCustomCropsPlugin.getInstance().getItemManager().id(bukkitLocation, config.existenceForm());
if (modelID == null || !config.modelIDs().contains(modelID)) {
world.removeBlockState(location);
BukkitCustomCropsPlugin.getInstance().getPluginLogger().warn("Sprinkler[" + config.id() + "] is removed at Location[" + world.worldName() + "," + location + "] because the id of the block/furniture is " + modelID);
syncCheck.complete(false);
return;
}
}
ActionManager.trigger(context, config.workActions());
if (updateState && !config.threeDItem().equals(config.threeDItemWithWater())) {
updateBlockAppearance(bukkitLocation, config, false);
}
syncCheck.complete(true);
}, bukkitLocation);
syncCheck.thenAccept(result -> {
if (result) {
int[][] range = config.range();
Pos3[] pos3s = new Pos3[range.length * 2];
for (int i = 0; i < range.length; i++) {
int x = range[i][0];
int z = range[i][1];
pos3s[i] = location.add(x, 0, z);
pos3s[i] = location.add(x, -1, z);
}
for (Pos3 pos3 : pos3s) {
Optional<CustomCropsBlockState> optionalState = world.getBlockState(pos3);
if (optionalState.isPresent()) {
CustomCropsBlockState anotherState = optionalState.get();
if (anotherState.type() instanceof PotBlock potBlock) {
PotConfig potConfig = potBlock.config(anotherState);
if (config.potWhitelist().contains(potConfig.id())) {
if (potBlock.addWater(anotherState, potConfig, config.sprinklingAmount())) {
BukkitCustomCropsPlugin.getInstance().getScheduler().sync().run(
() -> potBlock.updateBlockAppearance(
pos3.toLocation(world.bukkitWorld()),
potConfig,
true,
potBlock.fertilizers(anotherState)
),
bukkitWorld,
pos3.chunkX(), pos3.chunkZ()
);
}
}
}
}
}
}
});
}
public boolean addWater(CustomCropsBlockState state, int water) {
return water(state, water + water(state));
}
public boolean addWater(CustomCropsBlockState state, SprinklerConfig config, int water) {
return water(state, config, water + water(state));
}
public int water(CustomCropsBlockState state) {
return state.get("water").getAsIntTag().map(IntTag::getValue).orElse(0);
}
public boolean water(CustomCropsBlockState state, int water) {
return water(state, config(state), water);
}
public boolean water(CustomCropsBlockState state, SprinklerConfig config, int water) {
if (water < 0) water = 0;
int current = Math.min(water, config.storage());
int previous = water(state);
if (water == previous) return false;
state.set("water", new IntTag("water", current));
return previous == 0 ^ current == 0;
}
public SprinklerConfig config(CustomCropsBlockState state) {
return Registries.SPRINKLER.get(id(state));
}
public void updateBlockAppearance(Location location, SprinklerConfig config, boolean hasWater) {
FurnitureRotation rotation = BukkitCustomCropsPlugin.getInstance().getItemManager().remove(location, ExistenceForm.ANY);
BukkitCustomCropsPlugin.getInstance().getItemManager().place(location, config.existenceForm(), hasWater ? config.threeDItemWithWater() : config.threeDItem(), rotation);
}
}

View File

@@ -0,0 +1,148 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
public interface SprinklerConfig {
String id();
int storage();
int[][] range();
boolean infinite();
int sprinklingAmount();
@Nullable
String twoDItem();
@NotNull
String threeDItem();
@NotNull
String threeDItemWithWater();
@NotNull
Set<String> potWhitelist();
@NotNull
Set<String> modelIDs();
@Nullable
WaterBar waterBar();
@NotNull
ExistenceForm existenceForm();
/**
* Get the requirements for placement
*
* @return requirements for placement
*/
@Nullable
Requirement<Player>[] placeRequirements();
/**
* Get the requirements for breaking
*
* @return requirements for breaking
*/
@Nullable
Requirement<Player>[] breakRequirements();
/**
* Get the requirements for using
*
* @return requirements for using
*/
@Nullable
Requirement<Player>[] useRequirements();
@Nullable
Action<CustomCropsBlockState>[] workActions();
@Nullable
Action<Player>[] interactActions();
@Nullable
Action<Player>[] placeActions();
@Nullable
Action<Player>[] breakActions();
@Nullable
Action<Player>[] addWaterActions();
@Nullable
Action<Player>[] reachLimitActions();
@Nullable
Action<Player>[] fullWaterActions();
@NotNull
WateringMethod[] wateringMethods();
static Builder builder() {
return new SprinklerConfigImpl.BuilderImpl();
}
interface Builder {
SprinklerConfig build();
Builder id(String id);
Builder existenceForm(ExistenceForm existenceForm);
Builder storage(int storage);
Builder range(int[][] range);
Builder infinite(boolean infinite);
Builder sprinklingAmount(int sprinklingAmount);
Builder potWhitelist(Set<String> potWhitelist);
Builder waterBar(WaterBar waterBar);
Builder twoDItem(@Nullable String twoDItem);
Builder threeDItem(String threeDItem);
Builder threeDItemWithWater(String threeDItemWithWater);
Builder placeRequirements(Requirement<Player>[] placeRequirements);
Builder breakRequirements(Requirement<Player>[] breakRequirements);
Builder useRequirements(Requirement<Player>[] useRequirements);
Builder workActions(Action<CustomCropsBlockState>[] workActions);
Builder interactActions(Action<Player>[] interactActions);
Builder addWaterActions(Action<Player>[] addWaterActions);
Builder reachLimitActions(Action<Player>[] reachLimitActions);
Builder placeActions(Action<Player>[] placeActions);
Builder breakActions(Action<Player>[] breakActions);
Builder fullWaterActions(Action<Player>[] fullWaterActions);
Builder wateringMethods(WateringMethod[] wateringMethods);
}
}

View File

@@ -0,0 +1,375 @@
package net.momirealms.customcrops.api.core.block;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.ExistenceForm;
import net.momirealms.customcrops.api.core.water.WateringMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class SprinklerConfigImpl implements SprinklerConfig {
private final String id;
private final ExistenceForm existenceForm;
private final int storage;
private final int[][] range;
private final boolean infinite;
private final int sprinklingAmount;
private final Set<String> potWhitelist;
private final WaterBar waterBar;
private final String twoDItem;
private final String threeDItem;
private final String threeDItemWithWater;
private final Requirement<Player>[] placeRequirements;
private final Requirement<Player>[] breakRequirements;
private final Requirement<Player>[] useRequirements;
private final Action<CustomCropsBlockState>[] workActions;
private final Action<Player>[] interactActions;
private final Action<Player>[] reachLimitActions;
private final Action<Player>[] addWaterActions;
private final Action<Player>[] placeActions;
private final Action<Player>[] breakActions;
private final Action<Player>[] fullWaterActions;
private final WateringMethod[] wateringMethods;
private final Set<String> modelIDs = new HashSet<>();
public SprinklerConfigImpl(
String id,
ExistenceForm existenceForm,
int storage,
int[][] range,
boolean infinite,
int sprinklingAmount,
Set<String> potWhitelist,
WaterBar waterBar,
String twoDItem,
String threeDItem,
String threeDItemWithWater,
Requirement<Player>[] placeRequirements,
Requirement<Player>[] breakRequirements,
Requirement<Player>[] useRequirements,
Action<CustomCropsBlockState>[] workActions,
Action<Player>[] interactActions,
Action<Player>[] reachLimitActions,
Action<Player>[] addWaterActions,
Action<Player>[] placeActions,
Action<Player>[] breakActions,
Action<Player>[] fullWaterActions,
WateringMethod[] wateringMethods
) {
this.id = id;
this.existenceForm = existenceForm;
this.storage = storage;
this.range = range;
this.infinite = infinite;
this.sprinklingAmount = sprinklingAmount;
this.potWhitelist = potWhitelist;
this.waterBar = waterBar;
this.twoDItem = twoDItem;
this.threeDItem = Objects.requireNonNull(threeDItem);
this.threeDItemWithWater = Objects.requireNonNullElse(threeDItemWithWater, threeDItem);
this.placeRequirements = placeRequirements;
this.breakRequirements = breakRequirements;
this.useRequirements = useRequirements;
this.workActions = workActions;
this.interactActions = interactActions;
this.reachLimitActions = reachLimitActions;
this.addWaterActions = addWaterActions;
this.placeActions = placeActions;
this.breakActions = breakActions;
this.fullWaterActions = fullWaterActions;
this.modelIDs.add(twoDItem);
this.modelIDs.add(threeDItem);
this.modelIDs.add(threeDItemWithWater);
this.wateringMethods = wateringMethods;
}
@Override
public String id() {
return id;
}
@Override
public int storage() {
return storage;
}
@Override
public int[][] range() {
return range;
}
@Override
public boolean infinite() {
return infinite;
}
@Override
public int sprinklingAmount() {
return sprinklingAmount;
}
@Override
public String twoDItem() {
return twoDItem;
}
@NotNull
@Override
public String threeDItem() {
return threeDItem;
}
@NotNull
@Override
public String threeDItemWithWater() {
return threeDItemWithWater;
}
@NotNull
@Override
public Set<String> potWhitelist() {
return potWhitelist;
}
@NotNull
@Override
public Set<String> modelIDs() {
return modelIDs;
}
@Override
public WaterBar waterBar() {
return waterBar;
}
@NotNull
@Override
public ExistenceForm existenceForm() {
return existenceForm;
}
@Override
public Requirement<Player>[] placeRequirements() {
return placeRequirements;
}
@Override
public Requirement<Player>[] breakRequirements() {
return breakRequirements;
}
@Override
public Requirement<Player>[] useRequirements() {
return useRequirements;
}
@Override
public Action<CustomCropsBlockState>[] workActions() {
return workActions;
}
@Override
public Action<Player>[] interactActions() {
return interactActions;
}
@Override
public Action<Player>[] placeActions() {
return placeActions;
}
@Override
public Action<Player>[] breakActions() {
return breakActions;
}
@Override
public Action<Player>[] addWaterActions() {
return addWaterActions;
}
@Override
public Action<Player>[] reachLimitActions() {
return reachLimitActions;
}
@Override
public Action<Player>[] fullWaterActions() {
return fullWaterActions;
}
@NotNull
@Override
public WateringMethod[] wateringMethods() {
return wateringMethods == null ? new WateringMethod[0] : wateringMethods;
}
public static class BuilderImpl implements Builder {
private String id;
private ExistenceForm existenceForm;
private int storage;
private int[][] range;
private boolean infinite;
private int sprinklingAmount;
private Set<String> potWhitelist;
private WaterBar waterBar;
private Requirement<Player>[] placeRequirements;
private Requirement<Player>[] breakRequirements;
private Requirement<Player>[] useRequirements;
private Action<CustomCropsBlockState>[] workActions;
private Action<Player>[] interactActions;
private Action<Player>[] reachLimitActions;
private Action<Player>[] addWaterActions;
private Action<Player>[] placeActions;
private Action<Player>[] breakActions;
private Action<Player>[] fullWaterActions;
private String twoDItem;
private String threeDItem;
private String threeDItemWithWater;
private WateringMethod[] wateringMethods;
@Override
public SprinklerConfig build() {
return new SprinklerConfigImpl(id, existenceForm, storage, range, infinite, sprinklingAmount, potWhitelist, waterBar, twoDItem, threeDItem, threeDItemWithWater,
placeRequirements, breakRequirements, useRequirements, workActions, interactActions, reachLimitActions, addWaterActions, placeActions, breakActions, fullWaterActions, wateringMethods);
}
@Override
public Builder id(String id) {
this.id = id;
return this;
}
@Override
public Builder existenceForm(ExistenceForm existenceForm) {
this.existenceForm = existenceForm;
return this;
}
@Override
public Builder storage(int storage) {
this.storage = storage;
return this;
}
@Override
public Builder range(int[][] range) {
this.range = range;
return this;
}
@Override
public Builder infinite(boolean infinite) {
this.infinite = infinite;
return this;
}
@Override
public Builder sprinklingAmount(int sprinklingAmount) {
this.sprinklingAmount = sprinklingAmount;
return this;
}
@Override
public Builder potWhitelist(Set<String> potWhitelist) {
this.potWhitelist = new HashSet<>(potWhitelist);
return this;
}
@Override
public Builder waterBar(WaterBar waterBar) {
this.waterBar = waterBar;
return this;
}
@Override
public Builder twoDItem(String twoDItem) {
this.twoDItem = twoDItem;
return this;
}
@Override
public Builder threeDItem(String threeDItem) {
this.threeDItem = threeDItem;
return this;
}
@Override
public Builder threeDItemWithWater(String threeDItemWithWater) {
this.threeDItemWithWater = threeDItemWithWater;
return this;
}
@Override
public Builder placeRequirements(Requirement<Player>[] placeRequirements) {
this.placeRequirements = placeRequirements;
return this;
}
@Override
public Builder breakRequirements(Requirement<Player>[] breakRequirements) {
this.breakRequirements = breakRequirements;
return this;
}
@Override
public Builder useRequirements(Requirement<Player>[] useRequirements) {
this.useRequirements = useRequirements;
return this;
}
@Override
public Builder workActions(Action<CustomCropsBlockState>[] workActions) {
this.workActions = workActions;
return this;
}
@Override
public Builder interactActions(Action<Player>[] interactActions) {
this.interactActions = interactActions;
return this;
}
@Override
public Builder addWaterActions(Action<Player>[] addWaterActions) {
this.addWaterActions = addWaterActions;
return this;
}
@Override
public Builder reachLimitActions(Action<Player>[] reachLimitActions) {
this.reachLimitActions = reachLimitActions;
return this;
}
@Override
public Builder placeActions(Action<Player>[] placeActions) {
this.placeActions = placeActions;
return this;
}
@Override
public Builder breakActions(Action<Player>[] breakActions) {
this.breakActions = breakActions;
return this;
}
@Override
public Builder fullWaterActions(Action<Player>[] fullWaterActions) {
this.fullWaterActions = fullWaterActions;
return this;
}
@Override
public Builder wateringMethods(WateringMethod[] wateringMethods) {
this.wateringMethods = wateringMethods;
return this;
}
}
}

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.customcrops.api.core.block;
import net.momirealms.customcrops.api.core.ExistenceForm;
public class VariationData {
private final String id;
private final ExistenceForm form;
private final double chance;
public VariationData(String id, ExistenceForm form, double chance) {
this.id = id;
this.form = form;
this.chance = chance;
}
public String id() {
return id;
}
public ExistenceForm existenceForm() {
return form;
}
public double chance() {
return chance;
}
}

View File

@@ -0,0 +1,29 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.InteractionResult;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractAirEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
public abstract class AbstractCustomCropsItem implements CustomCropsItem {
private final Key type;
public AbstractCustomCropsItem(Key type) {
this.type = type;
}
@Override
public Key type() {
return type;
}
@Override
public InteractionResult interactAt(WrappedInteractEvent wrapped) {
return InteractionResult.PASS;
}
@Override
public void interactAir(WrappedInteractAirEvent event) {
}
}

View File

@@ -0,0 +1,121 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Objects;
import java.util.Set;
public abstract class AbstractFertilizerConfig implements FertilizerConfig {
protected String id;
protected String itemID;
protected String icon;
protected int times;
protected boolean beforePlant;
protected Set<String> whitelistPots;
protected Requirement<Player>[] requirements;
protected Action<Player>[] beforePlantActions;
protected Action<Player>[] useActions;
protected Action<Player>[] wrongPotActions;
public AbstractFertilizerConfig(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions
) {
this.id = Objects.requireNonNull(id);
this.itemID = Objects.requireNonNull(itemID);
this.beforePlant = beforePlant;
this.times = times;
this.icon = icon;
this.requirements = requirements;
this.whitelistPots = whitelistPots;
this.beforePlantActions = beforePlantActions;
this.useActions = useActions;
this.wrongPotActions = wrongPotActions;
}
@Override
public Action<Player>[] beforePlantActions() {
return beforePlantActions;
}
@Override
public Action<Player>[] useActions() {
return useActions;
}
@Override
public Action<Player>[] wrongPotActions() {
return wrongPotActions;
}
@Override
public boolean beforePlant() {
return beforePlant;
}
@Override
public Set<String> whitelistPots() {
return whitelistPots;
}
@Override
public String icon() {
return icon;
}
@Override
public int times() {
return times;
}
@Override
public String id() {
return id;
}
@Override
public Requirement<Player>[] requirements() {
return requirements;
}
@Override
public String itemID() {
return itemID;
}
@Override
public int processGainPoints(int previousPoints) {
return previousPoints;
}
@Override
public int processWaterToLose(int waterToLose) {
return waterToLose;
}
@Override
public double processVariationChance(double previousChance) {
return previousChance;
}
@Override
public int processDroppedItemAmount(int amount) {
return amount;
}
@Override
public double[] overrideQualityRatio() {
return null;
}
}

View File

@@ -0,0 +1,15 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.api.core.InteractionResult;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractAirEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
public interface CustomCropsItem {
Key type();
InteractionResult interactAt(WrappedInteractEvent event);
void interactAir(WrappedInteractAirEvent event);
}

View File

@@ -0,0 +1,40 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.core.Registries;
import org.jetbrains.annotations.Nullable;
public interface Fertilizer {
String id();
int times();
boolean reduceTimes();
// Flexibility matters more than performance
default FertilizerType type() {
FertilizerConfig config = Registries.FERTILIZER.get(id());
if (config == null) {
return FertilizerType.INVALID;
}
return config.type();
}
@Nullable
default FertilizerConfig config() {
return Registries.FERTILIZER.get(id());
}
static Builder builder() {
return new FertilizerImpl.BuilderImpl();
}
interface Builder {
Fertilizer build();
Builder id(String id);
Builder times(int times);
}
}

View File

@@ -0,0 +1,44 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
public interface FertilizerConfig {
String id();
FertilizerType type();
boolean beforePlant();
String icon();
Requirement<Player>[] requirements();
String itemID();
int times();
Set<String> whitelistPots();
Action<Player>[] beforePlantActions();
Action<Player>[] useActions();
Action<Player>[] wrongPotActions();
int processGainPoints(int previousPoints);
int processWaterToLose(int waterToLose);
double processVariationChance(double previousChance);
int processDroppedItemAmount(int amount);
@Nullable
double[] overrideQualityRatio();
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.customcrops.api.core.item;
public class FertilizerImpl implements Fertilizer {
private final String id;
private int times;
public FertilizerImpl(String id, int times) {
this.id = id;
this.times = times;
}
@Override
public String id() {
return id;
}
@Override
public int times() {
return times;
}
@Override
public boolean reduceTimes() {
times--;
return times <= 0;
}
public static class BuilderImpl implements Fertilizer.Builder {
private String id;
private int times;
@Override
public Fertilizer build() {
return new FertilizerImpl(id, times);
}
@Override
public Builder id(String id) {
this.id = id;
return this;
}
@Override
public Builder times(int times) {
this.times = times;
return this;
}
}
}

View File

@@ -0,0 +1,113 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.BuiltInBlockMechanics;
import net.momirealms.customcrops.api.core.BuiltInItemMechanics;
import net.momirealms.customcrops.api.core.InteractionResult;
import net.momirealms.customcrops.api.core.Registries;
import net.momirealms.customcrops.api.core.block.CropBlock;
import net.momirealms.customcrops.api.core.block.CropConfig;
import net.momirealms.customcrops.api.core.block.PotBlock;
import net.momirealms.customcrops.api.core.block.PotConfig;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.event.FertilizerUseEvent;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Optional;
public class FertilizerItem extends AbstractCustomCropsItem {
public FertilizerItem() {
super(BuiltInItemMechanics.FERTILIZER.key());
}
@Override
public InteractionResult interactAt(WrappedInteractEvent event) {
FertilizerConfig fertilizerConfig = Registries.FERTILIZER.get(event.itemID());
if (fertilizerConfig == null) {
return InteractionResult.FAIL;
}
final Player player = event.player();
final Context<Player> context = Context.player(player);
final CustomCropsWorld<?> world = event.world();
final ItemStack itemInHand = event.itemInHand();
String targetBlockID = event.relatedID();
Location targetLocation = event.location();
// if the clicked block is a crop, correct the target block
List<CropConfig> cropConfigs = Registries.STAGE_TO_CROP_UNSAFE.get(event.relatedID());
if (cropConfigs != null) {
// is a crop
targetLocation = targetLocation.subtract(0,1,0);
targetBlockID = BukkitCustomCropsPlugin.getInstance().getItemManager().blockID(targetLocation);
}
// if the clicked block is a pot
PotConfig potConfig = Registries.ITEM_TO_POT.get(targetBlockID);
if (potConfig != null) {
// check pot whitelist
if (!fertilizerConfig.whitelistPots().contains(potConfig.id())) {
ActionManager.trigger(context, fertilizerConfig.wrongPotActions());
return InteractionResult.FAIL;
}
// check requirements
if (!RequirementManager.isSatisfied(context, fertilizerConfig.requirements())) {
return InteractionResult.FAIL;
}
if (!RequirementManager.isSatisfied(context, potConfig.useRequirements())) {
return InteractionResult.FAIL;
}
// check "before-plant"
if (fertilizerConfig.beforePlant()) {
Location cropLocation = targetLocation.clone().add(0,1,0);
Optional<CustomCropsBlockState> state = world.getBlockState(Pos3.from(cropLocation));
if (state.isPresent()) {
CustomCropsBlockState blockState = state.get();
if (blockState.type() instanceof CropBlock) {
ActionManager.trigger(context, fertilizerConfig.beforePlantActions());
return InteractionResult.FAIL;
}
}
}
PotBlock potBlock = (PotBlock) BuiltInBlockMechanics.POT.mechanic();
assert potBlock != null;
// fix or get data
Fertilizer fertilizer = Fertilizer.builder()
.times(fertilizerConfig.times())
.id(fertilizerConfig.id())
.build();
CustomCropsBlockState potState = potBlock.fixOrGetState(world, Pos3.from(targetLocation), potConfig, event.relatedID());
if (!potBlock.canApplyFertilizer(potState,fertilizer)) {
return InteractionResult.FAIL;
}
// trigger event
FertilizerUseEvent useEvent = new FertilizerUseEvent(player, itemInHand, fertilizer, targetLocation, potState, event.hand(), potConfig);
if (EventUtils.fireAndCheckCancel(useEvent))
return InteractionResult.FAIL;
// add the fertilizer
if (potBlock.addFertilizer(potState, fertilizer)) {
potBlock.updateBlockAppearance(targetLocation, potState, potBlock.fertilizers(potState));
}
if (player.getGameMode() != GameMode.CREATIVE) {
itemInHand.setAmount(itemInHand.getAmount() - 1);
}
ActionManager.trigger(context, fertilizerConfig.useActions());
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.customcrops.api.core.item;
import dev.dejvokep.boostedyaml.block.implementation.Section;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.core.ConfigManager;
import net.momirealms.customcrops.common.util.TriFunction;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.Objects;
public class FertilizerType {
public static final FertilizerType SPEED_GROW = of("speed_grow",
(manager, id, section) -> {
ActionManager<Player> pam = BukkitCustomCropsPlugin.getInstance().getActionManager(Player.class);
return SpeedGrow.create(
id, section.getString("item"),
section.getInt("times", 14), section.getString("icon", ""),
section.getBoolean("before-plant", false),
new HashSet<>(section.getStringList("pot-whitelist")),
BukkitCustomCropsPlugin.getInstance().getRequirementManager(Player.class).parseRequirements(section.getSection("requirements"), true),
pam.parseActions(section.getSection("events.before_plant")),
pam.parseActions(section.getSection("events.use")),
pam.parseActions(section.getSection("events.wrong_pot")),
manager.getIntChancePair(section.getSection("chance"))
);
}
);
public static final FertilizerType QUALITY = of("quality",
(manager, id, section) -> {
ActionManager<Player> pam = BukkitCustomCropsPlugin.getInstance().getActionManager(Player.class);
return Quality.create(
id, section.getString("item"),
section.getInt("times", 14), section.getString("icon", ""),
section.getBoolean("before-plant", false),
new HashSet<>(section.getStringList("pot-whitelist")),
BukkitCustomCropsPlugin.getInstance().getRequirementManager(Player.class).parseRequirements(section.getSection("requirements"), true),
pam.parseActions(section.getSection("events.before_plant")),
pam.parseActions(section.getSection("events.use")),
pam.parseActions(section.getSection("events.wrong_pot")),
section.getDouble("chance", 1d),
manager.getQualityRatio(section.getString("ratio"))
);
}
);
public static final FertilizerType SOIL_RETAIN = of("soil_retain",
(manager, id, section) -> {
ActionManager<Player> pam = BukkitCustomCropsPlugin.getInstance().getActionManager(Player.class);
return SoilRetain.create(
id, section.getString("item"),
section.getInt("times", 14), section.getString("icon", ""),
section.getBoolean("before-plant", false),
new HashSet<>(section.getStringList("pot-whitelist")),
BukkitCustomCropsPlugin.getInstance().getRequirementManager(Player.class).parseRequirements(section.getSection("requirements"), true),
pam.parseActions(section.getSection("events.before_plant")),
pam.parseActions(section.getSection("events.use")),
pam.parseActions(section.getSection("events.wrong_pot")),
section.getDouble("chance", 1d)
);
}
);
public static final FertilizerType VARIATION = of("variation",
(manager, id, section) -> {
ActionManager<Player> pam = BukkitCustomCropsPlugin.getInstance().getActionManager(Player.class);
return Variation.create(
id, section.getString("item"),
section.getInt("times", 14), section.getString("icon", ""),
section.getBoolean("before-plant", false),
new HashSet<>(section.getStringList("pot-whitelist")),
BukkitCustomCropsPlugin.getInstance().getRequirementManager(Player.class).parseRequirements(section.getSection("requirements"), true),
pam.parseActions(section.getSection("events.before_plant")),
pam.parseActions(section.getSection("events.use")),
pam.parseActions(section.getSection("events.wrong_pot")),
section.getBoolean("addOrMultiply", true),
section.getDouble("chance", 0.01d)
);
}
);
public static final FertilizerType YIELD_INCREASE = of("yield_increase",
(manager, id, section) -> {
ActionManager<Player> pam = BukkitCustomCropsPlugin.getInstance().getActionManager(Player.class);
return YieldIncrease.create(
id, section.getString("item"),
section.getInt("times", 14), section.getString("icon", ""),
section.getBoolean("before-plant", false),
new HashSet<>(section.getStringList("pot-whitelist")),
BukkitCustomCropsPlugin.getInstance().getRequirementManager(Player.class).parseRequirements(section.getSection("requirements"), true),
pam.parseActions(section.getSection("events.before_plant")),
pam.parseActions(section.getSection("events.use")),
pam.parseActions(section.getSection("events.wrong_pot")),
manager.getIntChancePair(section.getSection("chance"))
);
}
);
public static final FertilizerType INVALID = of("invalid",
(manager, id, section) -> null
);
private final String id;
private final TriFunction<ConfigManager, String, Section, FertilizerConfig> argumentConsumer;
public FertilizerType(String id, TriFunction<ConfigManager, String, Section, FertilizerConfig> argumentConsumer) {
this.id = id;
this.argumentConsumer = argumentConsumer;
}
public String id() {
return id;
}
public static FertilizerType of(String id, TriFunction<ConfigManager, String, Section, FertilizerConfig> argumentConsumer) {
return new FertilizerType(id, argumentConsumer);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FertilizerType that = (FertilizerType) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
public FertilizerConfig parse(ConfigManager manager, String id, Section section) {
return argumentConsumer.apply(manager, id, section);
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public interface Quality extends FertilizerConfig {
double chance();
double[] ratio();
static Quality create(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
double chance,
double[] ratio
) {
return new QualityImpl(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions, chance, ratio);
}
}

View File

@@ -0,0 +1,52 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public class QualityImpl extends AbstractFertilizerConfig implements Quality {
private final double chance;
private final double[] ratio;
protected QualityImpl(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
double chance,
double[] ratio
) {
super(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions);
this.chance = chance;
this.ratio = ratio;
}
@Override
public double chance() {
return chance;
}
@Override
public double[] ratio() {
return ratio;
}
@Override
public FertilizerType type() {
return FertilizerType.QUALITY;
}
@Override
public double[] overrideQualityRatio() {
return ratio();
}
}

View File

@@ -0,0 +1,130 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.context.ContextKeys;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.block.CropBlock;
import net.momirealms.customcrops.api.core.block.CropConfig;
import net.momirealms.customcrops.api.core.block.CropStageConfig;
import net.momirealms.customcrops.api.core.block.PotConfig;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.event.CropPlantEvent;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.api.util.LocationUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collection;
import java.util.Map;
public class SeedItem extends AbstractCustomCropsItem {
public SeedItem() {
super(BuiltInBlockMechanics.CROP.key());
}
@Override
public InteractionResult interactAt(WrappedInteractEvent event) {
// check if it's a pot
PotConfig potConfig = Registries.ITEM_TO_POT.get(event.relatedID());
if (potConfig == null) return InteractionResult.PASS;
// check if the crop exists
CropConfig cropConfig = Registries.SEED_TO_CROP.get(event.itemID());
if (cropConfig == null) return InteractionResult.FAIL;
// check the block face
if (event.clickedBlockFace() != BlockFace.UP)
return InteractionResult.PASS;
final Player player = event.player();
Context<Player> context = Context.player(player);
// check pot whitelist
if (!cropConfig.potWhitelist().contains(potConfig.id())) {
ActionManager.trigger(context, cropConfig.wrongPotActions());
}
// check plant requirements
if (!RequirementManager.isSatisfied(context, cropConfig.plantRequirements())) {
return InteractionResult.FAIL;
}
// check if the block is empty
if (!suitableForSeed(event.location())) {
return InteractionResult.FAIL;
}
CustomCropsWorld<?> world = event.world();
Location seedLocation = event.location().add(0, 1, 0);
Pos3 pos3 = Pos3.from(seedLocation);
// check limitation
if (world.setting().cropPerChunk() >= 0) {
if (world.testChunkLimitation(pos3, CropBlock.class, world.setting().cropPerChunk())) {
ActionManager.trigger(context, cropConfig.reachLimitActions());
return InteractionResult.FAIL;
}
}
final ItemStack itemInHand = event.itemInHand();
CustomCropsBlockState state = BuiltInBlockMechanics.CROP.createBlockState();
CropBlock cropBlock = (CropBlock) state.type();
cropBlock.id(state, cropConfig.id());
// trigger event
CropPlantEvent plantEvent = new CropPlantEvent(player, itemInHand, event.hand(), seedLocation, cropConfig, state, 0);
if (EventUtils.fireAndCheckCancel(plantEvent)) {
return InteractionResult.FAIL;
}
int point = plantEvent.getPoint();
int temp = point;
ExistenceForm form = null;
String stageID = null;
while (temp >= 0) {
Map.Entry<Integer, CropStageConfig> entry = cropConfig.getFloorStageEntry(temp);
CropStageConfig stageConfig = entry.getValue();
if (stageConfig.stageID() != null) {
form = stageConfig.existenceForm();
stageID = stageConfig.stageID();
break;
}
temp = stageConfig.point() - 1;
}
if (stageID == null || form == null) {
return InteractionResult.FAIL;
}
// reduce item
if (player.getGameMode() != GameMode.CREATIVE)
itemInHand.setAmount(itemInHand.getAmount() - 1);
// place model
BukkitCustomCropsPlugin.getInstance().getItemManager().place(seedLocation, form, stageID, cropConfig.rotation() ? FurnitureRotation.random() : FurnitureRotation.NONE);
cropBlock.point(state, point);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
// set slot arg
context.arg(ContextKeys.SLOT, event.hand());
// trigger plant actions
ActionManager.trigger(context, cropConfig.plantActions());
return InteractionResult.SUCCESS;
}
private boolean suitableForSeed(Location location) {
Block block = location.getBlock();
if (block.getType() != Material.AIR) return false;
Location center = LocationUtils.toBlockCenterLocation(location);
Collection<Entity> entities = center.getWorld().getNearbyEntities(center, 0.5,0.51,0.5);
entities.removeIf(entity -> (entity instanceof Player || entity instanceof Item));
return entities.isEmpty();
}
}

View File

@@ -0,0 +1,28 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public interface SoilRetain extends FertilizerConfig {
double chance();
static SoilRetain create(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
double chance
) {
return new SoilRetainImpl(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions, chance);
}
}

View File

@@ -0,0 +1,44 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public class SoilRetainImpl extends AbstractFertilizerConfig implements SoilRetain {
private final double chance;
public SoilRetainImpl(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
double chance
) {
super(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions);
this.chance = chance;
}
@Override
public double chance() {
return chance;
}
@Override
public FertilizerType type() {
return FertilizerType.SOIL_RETAIN;
}
@Override
public int processWaterToLose(int waterToLose) {
return Math.min(waterToLose, 0);
}
}

View File

@@ -0,0 +1,30 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public interface SpeedGrow extends FertilizerConfig {
int pointBonus();
static SpeedGrow create(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
List<Pair<Double, Integer>> chances
) {
return new SpeedGrowImpl(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions, chances);
}
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public class SpeedGrowImpl extends AbstractFertilizerConfig implements SpeedGrow {
private final List<Pair<Double, Integer>> chances;
public SpeedGrowImpl(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
List<Pair<Double, Integer>> chances
) {
super(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions);
this.chances = chances;
}
@Override
public int pointBonus() {
for (Pair<Double, Integer> pair : chances) {
if (Math.random() < pair.left()) {
return pair.right();
}
}
return 0;
}
@Override
public FertilizerType type() {
return FertilizerType.SPEED_GROW;
}
@Override
public int processGainPoints(int previousPoints) {
return pointBonus() + previousPoints;
}
}

View File

@@ -0,0 +1,111 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.block.SprinklerBlock;
import net.momirealms.customcrops.api.core.block.SprinklerConfig;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.event.SprinklerPlaceEvent;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.api.util.LocationUtils;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Collection;
public class SprinklerItem extends AbstractCustomCropsItem {
public SprinklerItem() {
super(BuiltInItemMechanics.SPRINKLER_ITEM.key());
}
@Override
public InteractionResult interactAt(WrappedInteractEvent event) {
// should be place on block
if (event.existenceForm() != ExistenceForm.BLOCK)
return InteractionResult.PASS;
SprinklerConfig config = Registries.SPRINKLER.get(event.itemID());
if (config == null) {
return InteractionResult.FAIL;
}
Block clicked = event.location().getBlock();
Location targetLocation;
if (clicked.isReplaceable()) {
targetLocation = event.location();
} else {
if (event.clickedBlockFace() != BlockFace.UP)
return InteractionResult.PASS;
if (!clicked.isSolid())
return InteractionResult.PASS;
targetLocation = event.location().clone().add(0,1,0);
if (!suitableForSprinkler(targetLocation)) {
return InteractionResult.PASS;
}
}
final Player player = event.player();
final ItemStack itemInHand = event.itemInHand();
Context<Player> context = Context.player(player);
// check requirements
if (!RequirementManager.isSatisfied(context, config.placeRequirements())) {
return InteractionResult.FAIL;
}
final CustomCropsWorld<?> world = event.world();
Pos3 pos3 = Pos3.from(targetLocation);
// check limitation
if (world.setting().sprinklerPerChunk() >= 0) {
if (world.testChunkLimitation(pos3, SprinklerBlock.class, world.setting().sprinklerPerChunk())) {
ActionManager.trigger(context, config.reachLimitActions());
return InteractionResult.FAIL;
}
}
// generate state
CustomCropsBlockState state = BuiltInBlockMechanics.SPRINKLER.createBlockState();
SprinklerBlock sprinklerBlock = (SprinklerBlock) BuiltInBlockMechanics.SPRINKLER.mechanic();
sprinklerBlock.id(state, config.id());
sprinklerBlock.water(state, 0);
// trigger event
SprinklerPlaceEvent placeEvent = new SprinklerPlaceEvent(player, itemInHand, event.hand(), targetLocation.clone(), config, state);
if (EventUtils.fireAndCheckCancel(placeEvent))
return InteractionResult.FAIL;
// clear replaceable block
targetLocation.getBlock().setType(Material.AIR, false);
if (player.getGameMode() != GameMode.CREATIVE)
itemInHand.setAmount(itemInHand.getAmount() - 1);
// place the sprinkler
BukkitCustomCropsPlugin.getInstance().getItemManager().place(LocationUtils.toSurfaceCenterLocation(targetLocation), config.existenceForm(), config.threeDItem(), FurnitureRotation.NONE);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
);
});
ActionManager.trigger(context, config.placeActions());
return InteractionResult.SUCCESS;
}
private boolean suitableForSprinkler(Location location) {
Block block = location.getBlock();
if (block.getType() != Material.AIR) return false;
Location center = LocationUtils.toBlockCenterLocation(location);
Collection<Entity> entities = center.getWorld().getNearbyEntities(center, 0.5,0.51,0.5);
entities.removeIf(entity -> (entity instanceof Player || entity instanceof Item));
return entities.isEmpty();
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public interface Variation extends FertilizerConfig {
double chanceBonus();
boolean addOrMultiply();
static Variation create(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
boolean addOrMultiply,
double chance
) {
return new VariationImpl(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions, addOrMultiply, chance);
}
}

View File

@@ -0,0 +1,56 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.Set;
public class VariationImpl extends AbstractFertilizerConfig implements Variation {
private final double chance;
private final boolean addOrMultiply;
public VariationImpl(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
boolean addOrMultiply,
double chance
) {
super(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions);
this.chance = chance;
this.addOrMultiply = addOrMultiply;
}
@Override
public double chanceBonus() {
return chance;
}
@Override
public boolean addOrMultiply() {
return addOrMultiply;
}
@Override
public FertilizerType type() {
return FertilizerType.VARIATION;
}
@Override
public double processVariationChance(double previousChance) {
if (addOrMultiply()) {
return previousChance + chanceBonus();
} else {
return previousChance * chanceBonus();
}
}
}

View File

@@ -0,0 +1,108 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.water.FillMethod;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.misc.value.TextValue;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface WateringCanConfig {
String id();
String itemID();
int width();
int length();
int storage();
int wateringAmount();
boolean dynamicLore();
Set<String> whitelistPots();
Set<String> whitelistSprinklers();
List<TextValue<Player>> lore();
WaterBar waterBar();
Requirement<Player>[] requirements();
boolean infinite();
Integer appearance(int water);
Action<Player>[] fullActions();
Action<Player>[] addWaterActions();
Action<Player>[] consumeWaterActions();
Action<Player>[] runOutOfWaterActions();
Action<Player>[] wrongPotActions();
Action<Player>[] wrongSprinklerActions();
FillMethod[] fillMethods();
static Builder builder() {
return new WateringCanConfigImpl.BuilderImpl();
}
interface Builder {
WateringCanConfig build();
Builder id(String id);
Builder itemID(String itemID);
Builder width(int width);
Builder length(int length);
Builder storage(int storage);
Builder wateringAmount(int wateringAmount);
Builder dynamicLore(boolean dynamicLore);
Builder potWhitelist(Set<String> whitelistPots);
Builder sprinklerWhitelist(Set<String> whitelistSprinklers);
Builder lore(List<TextValue<Player>> lore);
Builder waterBar(WaterBar waterBar);
Builder requirements(Requirement<Player>[] requirements);
Builder infinite(boolean infinite);
Builder appearances(Map<Integer, Integer> appearances);
Builder fullActions(Action<Player>[] fullActions);
Builder addWaterActions(Action<Player>[] addWaterActions);
Builder consumeWaterActions(Action<Player>[] consumeWaterActions);
Builder runOutOfWaterActions(Action<Player>[] runOutOfWaterActions);
Builder wrongPotActions(Action<Player>[] wrongPotActions);
Builder wrongSprinklerActions(Action<Player>[] wrongSprinklerActions);
Builder fillMethods(FillMethod[] fillMethods);
}
}

View File

@@ -0,0 +1,342 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.core.water.FillMethod;
import net.momirealms.customcrops.api.misc.WaterBar;
import net.momirealms.customcrops.api.misc.value.TextValue;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import java.util.*;
public class WateringCanConfigImpl implements WateringCanConfig {
private final String id;
private final String itemID;
private final int width;
private final int length;
private final int storage;
private final int wateringAmount;
private final boolean dynamicLore;
private final Set<String> whitelistPots;
private final Set<String> whitelistSprinklers;
private final List<TextValue<Player>> lore;
private final WaterBar waterBar;
private final Requirement<Player>[] requirements;
private final boolean infinite;
private final HashMap<Integer, Integer> appearances;
private final Action<Player>[] fullActions;
private final Action<Player>[] addWaterActions;
private final Action<Player>[] consumeWaterActions;
private final Action<Player>[] runOutOfWaterActions;
private final Action<Player>[] wrongPotActions;
private final Action<Player>[] wrongSprinklerActions;
private final FillMethod[] fillMethods;
public WateringCanConfigImpl(
String id,
String itemID,
int width,
int length,
int storage,
int wateringAmount,
boolean dynamicLore,
Set<String> whitelistPots,
Set<String> whitelistSprinklers,
List<TextValue<Player>> lore,
WaterBar waterBar,
Requirement<Player>[] requirements,
boolean infinite,
HashMap<Integer, Integer> appearances,
Action<Player>[] fullActions,
Action<Player>[] addWaterActions,
Action<Player>[] consumeWaterActions,
Action<Player>[] runOutOfWaterActions,
Action<Player>[] wrongPotActions,
Action<Player>[] wrongSprinklerActions,
FillMethod[] fillMethods
) {
this.id = id;
this.itemID = itemID;
this.width = width;
this.length = length;
this.storage = storage;
this.wateringAmount = wateringAmount;
this.dynamicLore = dynamicLore;
this.whitelistPots = whitelistPots;
this.whitelistSprinklers = whitelistSprinklers;
this.lore = lore;
this.waterBar = waterBar;
this.requirements = requirements;
this.infinite = infinite;
this.appearances = appearances;
this.fullActions = fullActions;
this.addWaterActions = addWaterActions;
this.consumeWaterActions = consumeWaterActions;
this.runOutOfWaterActions = runOutOfWaterActions;
this.wrongPotActions = wrongPotActions;
this.wrongSprinklerActions = wrongSprinklerActions;
this.fillMethods = fillMethods;
}
@Override
public String id() {
return id;
}
@Override
public String itemID() {
return itemID;
}
@Override
public int width() {
return width;
}
@Override
public int length() {
return length;
}
@Override
public int storage() {
return storage;
}
@Override
public int wateringAmount() {
return wateringAmount;
}
@Override
public boolean dynamicLore() {
return dynamicLore;
}
@Override
public Set<String> whitelistPots() {
return whitelistPots;
}
@Override
public Set<String> whitelistSprinklers() {
return whitelistSprinklers;
}
@Override
public List<TextValue<Player>> lore() {
return lore;
}
@Override
public WaterBar waterBar() {
return waterBar;
}
@Override
public Requirement<Player>[] requirements() {
return requirements;
}
@Override
public boolean infinite() {
return infinite;
}
@Override
public Integer appearance(int water) {
return appearances.get(water);
}
@Override
public Action<Player>[] fullActions() {
return fullActions;
}
@Override
public Action<Player>[] addWaterActions() {
return addWaterActions;
}
@Override
public Action<Player>[] consumeWaterActions() {
return consumeWaterActions;
}
@Override
public Action<Player>[] runOutOfWaterActions() {
return runOutOfWaterActions;
}
@Override
public Action<Player>[] wrongPotActions() {
return wrongPotActions;
}
@Override
public Action<Player>[] wrongSprinklerActions() {
return wrongSprinklerActions;
}
@Override
public FillMethod[] fillMethods() {
return fillMethods;
}
static class BuilderImpl implements Builder {
private String id;
private String itemID;
private int width;
private int length;
private int storage;
private int wateringAmount;
private boolean dynamicLore;
private Set<String> whitelistPots;
private Set<String> whitelistSprinklers;
private List<TextValue<Player>> lore;
private WaterBar waterBar;
private Requirement<Player>[] requirements;
private boolean infinite;
private HashMap<Integer, Integer> appearances;
private Action<Player>[] fullActions;
private Action<Player>[] addWaterActions;
private Action<Player>[] consumeWaterActions;
private Action<Player>[] runOutOfWaterActions;
private Action<Player>[] wrongPotActions;
private Action<Player>[] wrongSprinklerActions;
private FillMethod[] fillMethods;
@Override
public WateringCanConfig build() {
return new WateringCanConfigImpl(id, itemID, width, length, storage, wateringAmount, dynamicLore, whitelistPots, whitelistSprinklers, lore, waterBar, requirements, infinite, appearances, fullActions, addWaterActions, consumeWaterActions, runOutOfWaterActions, wrongPotActions, wrongSprinklerActions, fillMethods);
}
@Override
public Builder id(String id) {
this.id = id;
return this;
}
@Override
public Builder itemID(String itemID) {
this.itemID = itemID;
return this;
}
@Override
public Builder width(int width) {
this.width = width;
return this;
}
@Override
public Builder length(int length) {
this.length = length;
return this;
}
@Override
public Builder storage(int storage) {
this.storage = storage;
return this;
}
@Override
public Builder wateringAmount(int wateringAmount) {
this.wateringAmount = wateringAmount;
return this;
}
@Override
public Builder dynamicLore(boolean dynamicLore) {
this.dynamicLore = dynamicLore;
return this;
}
@Override
public Builder potWhitelist(Set<String> whitelistPots) {
this.whitelistPots = new HashSet<>(whitelistPots);
return this;
}
@Override
public Builder sprinklerWhitelist(Set<String> whitelistSprinklers) {
this.whitelistSprinklers = new HashSet<>(whitelistSprinklers);
return this;
}
@Override
public Builder lore(List<TextValue<Player>> lore) {
this.lore = new ArrayList<>(lore);
return this;
}
@Override
public Builder waterBar(WaterBar waterBar) {
this.waterBar = waterBar;
return this;
}
@Override
public Builder requirements(Requirement<Player>[] requirements) {
this.requirements = requirements;
return this;
}
@Override
public Builder infinite(boolean infinite) {
this.infinite = infinite;
return this;
}
@Override
public Builder appearances(Map<Integer, Integer> appearances) {
this.appearances = new HashMap<>(appearances);
return this;
}
@Override
public Builder fullActions(Action<Player>[] fullActions) {
this.fullActions = fullActions;
return this;
}
@Override
public Builder addWaterActions(Action<Player>[] addWaterActions) {
this.addWaterActions = addWaterActions;
return this;
}
@Override
public Builder consumeWaterActions(Action<Player>[] consumeWaterActions) {
this.consumeWaterActions = consumeWaterActions;
return this;
}
@Override
public Builder runOutOfWaterActions(Action<Player>[] runOutOfWaterActions) {
this.runOutOfWaterActions = runOutOfWaterActions;
return this;
}
@Override
public Builder wrongPotActions(Action<Player>[] wrongPotActions) {
this.wrongPotActions = wrongPotActions;
return this;
}
@Override
public Builder wrongSprinklerActions(Action<Player>[] wrongSprinklerActions) {
this.wrongSprinklerActions = wrongSprinklerActions;
return this;
}
@Override
public Builder fillMethods(FillMethod[] fillMethods) {
this.fillMethods = fillMethods;
return this;
}
}
}

View File

@@ -0,0 +1,358 @@
package net.momirealms.customcrops.api.core.item;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ScoreComponent;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.context.ContextKeys;
import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.block.*;
import net.momirealms.customcrops.api.core.water.FillMethod;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractAirEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
import net.momirealms.customcrops.api.event.WateringCanFillEvent;
import net.momirealms.customcrops.api.event.WateringCanWaterPotEvent;
import net.momirealms.customcrops.api.event.WateringCanWaterSprinklerEvent;
import net.momirealms.customcrops.api.misc.value.TextValue;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import net.momirealms.customcrops.api.util.EventUtils;
import net.momirealms.customcrops.common.helper.AdventureHelper;
import net.momirealms.customcrops.common.item.Item;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class WateringCanItem extends AbstractCustomCropsItem {
public WateringCanItem() {
super(BuiltInItemMechanics.WATERING_CAN.key());
}
public int getCurrentWater(ItemStack itemStack) {
if (itemStack == null || itemStack.getType() == Material.AIR) return 0;
Item<ItemStack> wrapped = BukkitCustomCropsPlugin.getInstance().getItemManager().wrap(itemStack);
return (int) wrapped.getTag("CustomCrops", "water").orElse(0);
}
public void setCurrentWater(ItemStack itemStack, WateringCanConfig config, int water, Context<Player> context) {
if (itemStack == null || itemStack.getType() == Material.AIR) return;
Item<ItemStack> wrapped = BukkitCustomCropsPlugin.getInstance().getItemManager().wrap(itemStack);
int realWater = Math.min(config.storage(), water);
if (!config.infinite()) {
wrapped.setTag("CustomCrops", "water", realWater);
wrapped.maxDamage().ifPresent(max -> {
if (max <= 0) return;
int damage = (int) (max * (((double) config.storage() - realWater) / config.storage()));
wrapped.damage(damage);
});
// set appearance
Optional.ofNullable(config.appearance(realWater)).ifPresent(wrapped::customModelData);
}
if (config.dynamicLore()) {
List<String> lore = new ArrayList<>(wrapped.lore().orElse(List.of()));
if (ConfigManager.protectOriginalLore()) {
lore.removeIf(line -> {
Component component = AdventureHelper.jsonToComponent(line);
return component instanceof ScoreComponent scoreComponent
&& scoreComponent.objective().equals("water")
&& scoreComponent.name().equals("cc");
});
} else {
lore.clear();
}
for (TextValue<Player> newLore : config.lore()) {
ScoreComponent.Builder builder = Component.score().name("cc").objective("water");
builder.append(AdventureHelper.miniMessage(newLore.render(context)));
lore.add(AdventureHelper.componentToJson(builder.build()));
}
wrapped.lore(lore);
}
wrapped.load();
}
@Override
public void interactAir(WrappedInteractAirEvent event) {
WateringCanConfig config = Registries.WATERING_CAN.get(event.itemID());
if (config == null)
return;
final Player player = event.player();;
Context<Player> context = Context.player(player);
// check requirements
if (!RequirementManager.isSatisfied(context, config.requirements())) {
return;
}
// ignore infinite
if (config.infinite())
return;
// get target block
Block targetBlock = player.getTargetBlockExact(5, FluidCollisionMode.ALWAYS);
if (targetBlock == null)
return;
final ItemStack itemInHand = event.itemInHand();
int water = getCurrentWater(itemInHand);
String blockID = BukkitCustomCropsPlugin.getInstance().getItemManager().blockID(targetBlock);
if (targetBlock.getBlockData() instanceof Waterlogged waterlogged && waterlogged.isWaterlogged()) {
blockID = "WATER";
}
for (FillMethod method : config.fillMethods()) {
if (method.getID().equals(blockID)) {
if (method.checkRequirements(context)) {
if (water >= config.storage()) {
ActionManager.trigger(context, config.fullActions());
return;
}
WateringCanFillEvent fillEvent = new WateringCanFillEvent(player, event.hand(), itemInHand, targetBlock.getLocation(), config, method);
if (EventUtils.fireAndCheckCancel(fillEvent))
return;
setCurrentWater(itemInHand, config, water + method.amountOfWater(), context);
method.triggerActions(context);
ActionManager.trigger(context, config.addWaterActions());
}
return;
}
}
}
@Override
public InteractionResult interactAt(WrappedInteractEvent event) {
WateringCanConfig wateringCanConfig = Registries.WATERING_CAN.get(event.itemID());
if (wateringCanConfig == null)
return InteractionResult.FAIL;
final Player player = event.player();
final Context<Player> context = Context.player(player);
// check watering can requirements
if (!RequirementManager.isSatisfied(context, wateringCanConfig.requirements())) {
return InteractionResult.FAIL;
}
final CustomCropsWorld<?> world = event.world();
final ItemStack itemInHand = event.itemInHand();
String targetBlockID = event.relatedID();
Location targetLocation = event.location();
BlockFace blockFace = event.clickedBlockFace();
int waterInCan = getCurrentWater(itemInHand);
SprinklerConfig sprinklerConfig = Registries.SPRINKLER.get(targetBlockID);
if (sprinklerConfig != null) {
// ignore infinite sprinkler
if (sprinklerConfig.infinite()) {
return InteractionResult.FAIL;
}
// check requirements
if (!RequirementManager.isSatisfied(context, sprinklerConfig.useRequirements())) {
return InteractionResult.FAIL;
}
// check water
if (waterInCan <= 0 && !wateringCanConfig.infinite()) {
ActionManager.trigger(context, wateringCanConfig.runOutOfWaterActions());
return InteractionResult.FAIL;
}
// check whitelist
if (!wateringCanConfig.whitelistSprinklers().contains(sprinklerConfig.id())) {
ActionManager.trigger(context, wateringCanConfig.wrongSprinklerActions());
return InteractionResult.FAIL;
}
SprinklerBlock sprinklerBlock = (SprinklerBlock) BuiltInBlockMechanics.SPRINKLER.mechanic();
CustomCropsBlockState sprinklerState = sprinklerBlock.fixOrGetState(world, Pos3.from(targetLocation), sprinklerConfig, targetBlockID);
// check full
if (sprinklerBlock.water(sprinklerState) >= sprinklerConfig.storage()) {
ActionManager.trigger(context, sprinklerConfig.fullWaterActions());
return InteractionResult.FAIL;
}
// trigger event
WateringCanWaterSprinklerEvent waterSprinklerEvent = new WateringCanWaterSprinklerEvent(player, itemInHand, event.hand(), wateringCanConfig, sprinklerConfig, sprinklerState, targetLocation);
if (EventUtils.fireAndCheckCancel(waterSprinklerEvent)) {
return InteractionResult.FAIL;
}
// add water
if (sprinklerBlock.addWater(sprinklerState, sprinklerConfig, wateringCanConfig.wateringAmount())) {
if (!sprinklerConfig.threeDItem().equals(sprinklerConfig.threeDItemWithWater())) {
sprinklerBlock.updateBlockAppearance(targetLocation, sprinklerConfig, true);
}
}
ActionManager.trigger(context, wateringCanConfig.consumeWaterActions());
setCurrentWater(itemInHand, wateringCanConfig, waterInCan - 1, context);
return InteractionResult.SUCCESS;
}
// try filling the watering can
for (FillMethod method : wateringCanConfig.fillMethods()) {
if (method.getID().equals(event.relatedID())) {
if (method.checkRequirements(context)) {
if (waterInCan >= wateringCanConfig.storage()) {
ActionManager.trigger(context, wateringCanConfig.fullActions());
return InteractionResult.FAIL;
}
WateringCanFillEvent fillEvent = new WateringCanFillEvent(player, event.hand(), itemInHand, targetLocation, wateringCanConfig, method);
if (EventUtils.fireAndCheckCancel(fillEvent))
return InteractionResult.FAIL;
setCurrentWater(itemInHand, wateringCanConfig, waterInCan + method.amountOfWater(), context);
method.triggerActions(context);
ActionManager.trigger(context, wateringCanConfig.addWaterActions());
}
return InteractionResult.SUCCESS;
}
}
// if the clicked block is a crop, correct the target block
List<CropConfig> cropConfigs = Registries.STAGE_TO_CROP_UNSAFE.get(event.relatedID());
if (cropConfigs != null) {
// is a crop
targetLocation = targetLocation.subtract(0,1,0);
targetBlockID = BukkitCustomCropsPlugin.getInstance().getItemManager().blockID(targetLocation);
blockFace = BlockFace.UP;
}
PotConfig potConfig = Registries.ITEM_TO_POT.get(targetBlockID);
if (potConfig != null) {
// need to click the upper face
if (blockFace != BlockFace.UP)
return InteractionResult.PASS;
// check whitelist
if (!wateringCanConfig.whitelistPots().contains(potConfig.id())) {
ActionManager.trigger(context, wateringCanConfig.wrongPotActions());
return InteractionResult.FAIL;
}
// check water
if (waterInCan <= 0 && !wateringCanConfig.infinite()) {
ActionManager.trigger(context, wateringCanConfig.runOutOfWaterActions());
return InteractionResult.FAIL;
}
World bukkitWorld = targetLocation.getWorld();
ArrayList<Pair<Pos3, String>> pots = potInRange(bukkitWorld, Pos3.from(targetLocation), wateringCanConfig.width(), wateringCanConfig.length(), player.getLocation().getYaw(), potConfig);
WateringCanWaterPotEvent waterPotEvent = new WateringCanWaterPotEvent(player, itemInHand, event.hand(), wateringCanConfig, potConfig, pots);
if (EventUtils.fireAndCheckCancel(waterPotEvent)) {
return InteractionResult.FAIL;
}
PotBlock potBlock = (PotBlock) BuiltInBlockMechanics.POT.mechanic();
for (Pair<Pos3, String> pair : waterPotEvent.getPotWithIDs()) {
CustomCropsBlockState potState = potBlock.fixOrGetState(world,pair.left(), potConfig, pair.right());
if (potBlock.addWater(potState, potConfig, wateringCanConfig.wateringAmount())) {
Location temp = pair.left().toLocation(bukkitWorld);
potBlock.updateBlockAppearance(temp, potConfig, true, potBlock.fertilizers(potState));
context.arg(ContextKeys.LOCATION, temp);
ActionManager.trigger(context, potConfig.addWaterActions());
}
}
ActionManager.trigger(context, wateringCanConfig.consumeWaterActions());
setCurrentWater(itemInHand, wateringCanConfig, waterInCan - 1, context);
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
public ArrayList<Pair<Pos3, String>> potInRange(World world, Pos3 pos3, int width, int length, float yaw, PotConfig config) {
ArrayList<Pos3> potPos = new ArrayList<>();
int extend = (width-1) / 2;
int extra = (width-1) % 2;
switch ((int) ((yaw + 180) / 45)) {
case 0 -> {
// -180 ~ -135
for (int i = -extend; i <= extend + extra; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(i, 0, -j));
}
}
}
case 1 -> {
// -135 ~ -90
for (int i = -extend - extra; i <= extend; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(j, 0, i));
}
}
}
case 2 -> {
// -90 ~ -45
for (int i = -extend; i <= extend + extra; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(j, 0, i));
}
}
}
case 3 -> {
// -45 ~ 0
for (int i = -extend; i <= extend + extra; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(i, 0, j));
}
}
}
case 4 -> {
// 0 ~ 45
for (int i = -extend - extra; i <= extend; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(i, 0, j));
}
}
}
case 5 -> {
// 45 ~ 90
for (int i = -extend; i <= extend + extra; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(-j, 0, i));
}
}
}
case 6 -> {
// 90 ~ 135
for (int i = -extend - extra; i <= extend; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(-j, 0, i));
}
}
}
case 7 -> {
// 135 ~ 180
for (int i = -extend - extra; i <= extend; i++) {
for (int j = 0; j < length; j++) {
potPos.add(pos3.add(i, 0, -j));
}
}
}
default -> potPos.add(pos3);
}
ItemManager itemManager = BukkitCustomCropsPlugin.getInstance().getItemManager();
ArrayList<Pair<Pos3, String>> pots = new ArrayList<>();
for (Pos3 loc : potPos) {
Block block = world.getBlockAt(loc.x(), loc.y(), loc.z());
String blockID = itemManager.blockID(block);
PotConfig potConfig = Registries.ITEM_TO_POT.get(blockID);
if (potConfig == config) {
pots.add(Pair.of(loc, blockID));
}
}
return pots;
}
}

View File

@@ -0,0 +1,30 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public interface YieldIncrease extends FertilizerConfig {
int amountBonus();
static YieldIncrease create(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
List<Pair<Double, Integer>> chances
) {
return new YieldIncreaseImpl(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions, chances);
}
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.customcrops.api.core.item;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.common.util.Pair;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Set;
public class YieldIncreaseImpl extends AbstractFertilizerConfig implements YieldIncrease {
private final List<Pair<Double, Integer>> chances;
public YieldIncreaseImpl(
String id,
String itemID,
int times,
String icon,
boolean beforePlant,
Set<String> whitelistPots,
Requirement<Player>[] requirements,
Action<Player>[] beforePlantActions,
Action<Player>[] useActions,
Action<Player>[] wrongPotActions,
List<Pair<Double, Integer>> chances
) {
super(id, itemID, times, icon, beforePlant, whitelistPots, requirements, beforePlantActions, useActions, wrongPotActions);
this.chances = chances;
}
@Override
public int amountBonus() {
for (Pair<Double, Integer> pair : chances) {
if (Math.random() < pair.left()) {
return pair.right();
}
}
return 0;
}
@Override
public FertilizerType type() {
return FertilizerType.YIELD_INCREASE;
}
@Override
public int processDroppedItemAmount(int amount) {
return amount + amountBonus();
}
}

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.customcrops.api.core.water;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.action.ActionManager;
import net.momirealms.customcrops.api.context.Context;
import net.momirealms.customcrops.api.requirement.Requirement;
import net.momirealms.customcrops.api.requirement.RequirementManager;
import org.bukkit.entity.Player;
public abstract class AbstractMethod {
protected int amount;
private final Action<Player>[] actions;
private final Requirement<Player>[] requirements;
protected AbstractMethod(int amount, Action<Player>[] actions, Requirement<Player>[] requirements) {
this.amount = amount;
this.actions = actions;
this.requirements = requirements;
}
public int amountOfWater() {
return amount;
}
public void triggerActions(Context<Player> context) {
ActionManager.trigger(context, actions);
}
public boolean checkRequirements(Context<Player> context) {
return RequirementManager.isSatisfied(context, requirements);
}
}

View File

@@ -15,16 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item.water;
package net.momirealms.customcrops.api.core.water;
import net.momirealms.customcrops.api.mechanic.action.Action;
import net.momirealms.customcrops.api.mechanic.requirement.Requirement;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
public class PositiveFillMethod extends AbstractFillMethod {
public class FillMethod extends AbstractMethod {
private final String id;
public PositiveFillMethod(String id, int amount, Action[] actions, Requirement[] requirements) {
public FillMethod(String id, int amount, Action<Player>[] actions, Requirement<Player>[] requirements) {
super(amount, actions, requirements);
this.id = id;
}

View File

@@ -15,20 +15,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item.water;
package net.momirealms.customcrops.api.core.water;
import net.momirealms.customcrops.api.mechanic.action.Action;
import net.momirealms.customcrops.api.mechanic.requirement.Requirement;
import net.momirealms.customcrops.api.action.Action;
import net.momirealms.customcrops.api.requirement.Requirement;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
public class PassiveFillMethod extends AbstractFillMethod {
public class WateringMethod extends AbstractMethod {
private final String used;
private final int usedAmount;
private final String returned;
private final int returnedAmount;
public PassiveFillMethod(String used, int usedAmount, @Nullable String returned, int returnedAmount, int amount, Action[] actions, Requirement[] requirements) {
public WateringMethod(
String used,
int usedAmount,
@Nullable String returned,
int returnedAmount,
int amount,
Action<Player>[] actions,
Requirement<Player>[] requirements
) {
super(amount, actions, requirements);
this.used = used;
this.returned = returned;

View File

@@ -15,9 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.world;
import java.util.Objects;
package net.momirealms.customcrops.api.core.world;
public class BlockPos {
@@ -31,31 +29,27 @@ public class BlockPos {
this.position = ((x & 0xF) << 28) | ((z & 0xF) << 24) | (y & 0xFFFFFF);
}
public static BlockPos getByLocation(SimpleLocation location) {
return new BlockPos(location.getX() % 16, location.getY(), location.getZ() % 16);
public static BlockPos fromPos3(Pos3 location) {
return new BlockPos(location.x() % 16, location.y(), location.z() % 16);
}
public SimpleLocation getLocation(String world, ChunkPos coordinate) {
return new SimpleLocation(world, coordinate.x() * 16 + getX(), getY(), coordinate.z() * 16 + getZ());
public Pos3 toPos3(ChunkPos coordinate) {
return new Pos3(coordinate.x() * 16 + x(), y(), coordinate.z() * 16 + z());
}
public int getPosition() {
public int position() {
return position;
}
public int getX() {
public int x() {
return (position >> 28) & 0xF;
}
public int getZ() {
public int z() {
return (position >> 24) & 0xF;
}
public int getSectionID() {
return (int) Math.floor((double) getY() / 16);
}
public int getY() {
public int y() {
int y = position & 0xFFFFFF;
if ((y & 0x800000) != 0) {
y |= 0xFF000000;
@@ -63,6 +57,10 @@ public class BlockPos {
return y;
}
public int sectionID() {
return (int) Math.floor((double) y() / 16);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -73,15 +71,15 @@ public class BlockPos {
@Override
public int hashCode() {
return Objects.hash(position);
return Math.abs(position);
}
@Override
public String toString() {
return "BlockPos{" +
"x=" + getX() +
"y=" + getY() +
"z=" + getZ() +
"x=" + x() +
"y=" + y() +
"z=" + z() +
'}';
}
}

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.world;
package net.momirealms.customcrops.api.core.world;
import org.bukkit.Chunk;
import org.jetbrains.annotations.NotNull;
@@ -26,17 +26,21 @@ public record ChunkPos(int x, int z) {
return new ChunkPos(x, z);
}
public static ChunkPos getByString(String coordinate) {
public static ChunkPos fromString(String coordinate) {
String[] split = coordinate.split(",", 2);
try {
int x = Integer.parseInt(split[0]);
int z = Integer.parseInt(split[1]);
return new ChunkPos(x, z);
} catch (NumberFormatException e) {
throw new RuntimeException(e);
}
catch (NumberFormatException e) {
e.printStackTrace();
return null;
}
}
public static ChunkPos fromPos3(Pos3 pos3) {
int chunkX = (int) Math.floor((double) pos3.x() / 16.0);
int chunkZ = (int) Math.floor((double) pos3.z() / 16.0);
return ChunkPos.of(chunkX, chunkZ);
}
@Override
@@ -64,16 +68,16 @@ public record ChunkPos(int x, int z) {
}
@NotNull
public RegionPos getRegionPos() {
public RegionPos toRegionPos() {
return RegionPos.getByChunkPos(this);
}
@NotNull
public static ChunkPos getByBukkitChunk(@NotNull Chunk chunk) {
public static ChunkPos fromBukkitChunk(@NotNull Chunk chunk) {
return new ChunkPos(chunk.getX(), chunk.getZ());
}
public String getAsString() {
public String asString() {
return x + "," + z;
}

View File

@@ -0,0 +1,15 @@
package net.momirealms.customcrops.api.core.world;
import com.flowpowered.nbt.CompoundMap;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import org.jetbrains.annotations.NotNull;
public interface CustomCropsBlockState extends DataBlock {
@NotNull
CustomCropsBlock type();
static CustomCropsBlockState create(CustomCropsBlock owner, CompoundMap compoundMap) {
return new CustomCropsBlockStateImpl(owner, compoundMap);
}
}

View File

@@ -0,0 +1,52 @@
package net.momirealms.customcrops.api.core.world;
import com.flowpowered.nbt.CompoundMap;
import com.flowpowered.nbt.Tag;
import net.momirealms.customcrops.api.core.SynchronizedCompoundMap;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import org.jetbrains.annotations.NotNull;
public class CustomCropsBlockStateImpl implements CustomCropsBlockState {
private final SynchronizedCompoundMap compoundMap;
private final CustomCropsBlock owner;
protected CustomCropsBlockStateImpl(CustomCropsBlock owner, CompoundMap compoundMap) {
this.compoundMap = new SynchronizedCompoundMap(compoundMap);
this.owner = owner;
}
@NotNull
@Override
public CustomCropsBlock type() {
return owner;
}
@Override
public Tag<?> set(String key, Tag<?> tag) {
return compoundMap.put(key, tag);
}
@Override
public Tag<?> get(String key) {
return compoundMap.get(key);
}
@Override
public Tag<?> remove(String key) {
return compoundMap.remove(key);
}
@Override
public SynchronizedCompoundMap compoundMap() {
return compoundMap;
}
@Override
public String toString() {
return "CustomCropsBlock{" +
"Type{" + owner.type().asString() +
"}, " + compoundMap +
'}';
}
}

View File

@@ -0,0 +1,195 @@
/*
* 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.customcrops.api.core.world;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.stream.Stream;
public interface CustomCropsChunk {
/**
* Set if the chunk can be force loaded.
* If a chunk is force loaded, no one can unload it unless you set force load to false.
* This can prevent CustomCrops from unloading the chunks on {@link org.bukkit.event.world.ChunkUnloadEvent}
*
* This value will not be persistently stored. Please use {@link org.bukkit.World#setChunkForceLoaded(int, int, boolean)}
* if you want to force a chunk loaded.
*
* @param forceLoad force loaded
*/
void setForceLoaded(boolean forceLoad);
/**
* Indicates whether the chunk is force loaded
*
* @return force loaded or not
*/
boolean isForceLoaded();
/**
* Loads the chunk to cache and participate in the mechanism of the plugin.
*
* @param loadBukkitChunk whether to load Bukkit chunks temporarily if it's not loaded
*/
void load(boolean loadBukkitChunk);
/**
* Unloads the chunk. Lazy refer to those chunks that will be delayed for unloading.
* Recently unloaded chunks are likely to be loaded again soon.
*
* @param lazy delay unload or not
*/
void unload(boolean lazy);
/**
* Unloads the chunk if it is a lazy chunk
*/
void unloadLazy();
/**
* Indicates whether the chunk is in lazy state
*
* @return lazy or not
*/
boolean isLazy();
/**
* Indicates whether the chunk is loaded
*
* @return loaded or not
*/
boolean isLoaded();
/**
* Get the world associated with the chunk
*
* @return CustomCrops world
*/
CustomCropsWorld<?> getWorld();
/**
* Get the position of the chunk
*
* @return chunk position
*/
ChunkPos chunkPos();
/**
* Do second timer
*/
void timer();
/**
* Get the unloaded time in seconds
* This value would increase if the chunk is lazy
*
* @return the unloaded time
*/
int unloadedSeconds();
/**
* Set the unloaded seconds
*
* @param unloadedSeconds unloadedSeconds
*/
void unloadedSeconds(int unloadedSeconds);
/**
* Get the last loaded time
*
* @return last loaded time
*/
long lastLoadedTime();
/**
* Set the last loaded time to current time
*/
void updateLastLoadedTime();
/**
* Get the loaded time in seconds
*
* @return loaded time
*/
int loadedMilliSeconds();
/**
* Get block data at a certain location
*
* @param location location
* @return block data
*/
@NotNull
Optional<CustomCropsBlockState> getBlockState(Pos3 location);
/**
* Remove any block data from a certain location
*
* @param location location
* @return block data
*/
@NotNull
Optional<CustomCropsBlockState> removeBlockState(Pos3 location);
/**
* Add a custom block data at a certain location
*
* @param block block to add
* @return the previous block data
*/
@NotNull
Optional<CustomCropsBlockState> addBlockState(Pos3 location, CustomCropsBlockState block);
/**
* Get CustomCrops sections
*
* @return sections
*/
@NotNull
Stream<CustomCropsSection> sectionsToSave();
/**
* Get section by ID
*
* @param sectionID id
* @return section
*/
@NotNull
Optional<CustomCropsSection> getLoadedSection(int sectionID);
CustomCropsSection getSection(int sectionID);
Collection<CustomCropsSection> sections();
Optional<CustomCropsSection> removeSection(int sectionID);
void resetUnloadedSeconds();
boolean canPrune();
boolean isOfflineTaskNotified();
PriorityQueue<DelayedTickTask> tickTaskQueue();
Set<BlockPos> tickedBlocks();
}

View File

@@ -0,0 +1,288 @@
package net.momirealms.customcrops.api.core.world;
import net.momirealms.customcrops.common.util.RandomUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream;
public class CustomCropsChunkImpl implements CustomCropsChunk {
private final CustomCropsWorld<?> world;
private final ChunkPos chunkPos;
private final ConcurrentHashMap<Integer, CustomCropsSection> loadedSections;
private final PriorityQueue<DelayedTickTask> queue;
private final Set<BlockPos> tickedBlocks;
private long lastLoadedTime;
private int loadedSeconds;
private int unloadedSeconds;
private boolean notified;
private boolean isLoaded;
private boolean forceLoad;
protected CustomCropsChunkImpl(CustomCropsWorld<?> world, ChunkPos chunkPos) {
this.world = world;
this.chunkPos = chunkPos;
this.loadedSections = new ConcurrentHashMap<>(16);
this.queue = new PriorityQueue<>();
this.unloadedSeconds = 0;
this.tickedBlocks = Collections.synchronizedSet(new HashSet<>());
this.updateLastLoadedTime();
this.notified = true;
this.isLoaded = false;
}
protected CustomCropsChunkImpl(
CustomCropsWorld<?> world,
ChunkPos chunkPos,
int loadedSeconds,
long lastLoadedTime,
ConcurrentHashMap<Integer, CustomCropsSection> loadedSections,
PriorityQueue<DelayedTickTask> queue,
HashSet<BlockPos> tickedBlocks
) {
this.world = world;
this.chunkPos = chunkPos;
this.loadedSections = loadedSections;
this.lastLoadedTime = lastLoadedTime;
this.loadedSeconds = loadedSeconds;
this.queue = queue;
this.unloadedSeconds = 0;
this.tickedBlocks = Collections.synchronizedSet(tickedBlocks);
}
@Override
public void setForceLoaded(boolean forceLoad) {
this.forceLoad = forceLoad;
}
@Override
public boolean isForceLoaded() {
return this.forceLoad;
}
@Override
public void load(boolean loadBukkitChunk) {
if (!isLoaded()) {
if (((CustomCropsWorldImpl<?>) world).loadChunk(this)) {
this.isLoaded = true;
}
if (loadBukkitChunk && !this.world.bukkitWorld().isChunkLoaded(chunkPos.x(), chunkPos.z())) {
this.world.bukkitWorld().getChunkAt(chunkPos.x(), chunkPos.z());
}
}
}
@Override
public void unload(boolean lazy) {
if (isLoaded() && !isForceLoaded()) {
if (((CustomCropsWorldImpl<?>) world).unloadChunk(this, lazy)) {
this.isLoaded = false;
}
}
}
@Override
public void unloadLazy() {
if (!isLoaded() && isLazy()) {
((CustomCropsWorldImpl<?>) world).unloadLazyChunk(chunkPos);
}
}
@Override
public boolean isLazy() {
return ((CustomCropsWorldImpl<?>) world).getLazyChunk(chunkPos) == this;
}
@Override
public boolean isLoaded() {
return this.isLoaded;
}
@Override
public CustomCropsWorld<?> getWorld() {
return world;
}
@Override
public ChunkPos chunkPos() {
return chunkPos;
}
@Override
public void timer() {
WorldSetting setting = world.setting();
int interval = setting.minTickUnit();
this.loadedSeconds++;
// if loadedSeconds reach another recycle, rearrange the tasks
if (this.loadedSeconds >= interval) {
this.loadedSeconds = 0;
this.tickedBlocks.clear();
this.queue.clear();
this.arrangeTasks(interval);
}
scheduledTick();
randomTick(setting.randomTickSpeed());
}
private void arrangeTasks(int unit) {
ThreadLocalRandom random = ThreadLocalRandom.current();
for (CustomCropsSection section : loadedSections.values()) {
for (Map.Entry<BlockPos, CustomCropsBlockState> entry : section.blockMap().entrySet()) {
this.queue.add(new DelayedTickTask(
random.nextInt(0, unit),
entry.getKey()
));
this.tickedBlocks.add(entry.getKey());
}
}
}
private void scheduledTick() {
while (!queue.isEmpty() && queue.peek().getTime() <= loadedSeconds) {
DelayedTickTask task = queue.poll();
if (task != null) {
BlockPos pos = task.blockPos();
CustomCropsSection section = loadedSections.get(pos.sectionID());
if (section != null) {
Optional<CustomCropsBlockState> block = section.getBlockState(pos);
block.ifPresent(state -> state.type().scheduledTick(state, world, pos.toPos3(chunkPos)));
}
}
}
}
private void randomTick(int randomTickSpeed) {
ThreadLocalRandom random = ThreadLocalRandom.current();
for (CustomCropsSection section : loadedSections.values()) {
int sectionID = section.getSectionID();
int baseY = sectionID * 16;
for (int i = 0; i < randomTickSpeed; i++) {
int x = random.nextInt(16);
int y = random.nextInt(16) + baseY;
int z = random.nextInt(16);
BlockPos pos = new BlockPos(x,y,z);
Optional<CustomCropsBlockState> block = section.getBlockState(pos);
block.ifPresent(state -> state.type().randomTick(state, world, pos.toPos3(chunkPos)));
}
}
}
@Override
public int unloadedSeconds() {
return unloadedSeconds;
}
@Override
public void unloadedSeconds(int unloadedSeconds) {
this.unloadedSeconds = unloadedSeconds;
}
@Override
public long lastLoadedTime() {
return lastLoadedTime;
}
@Override
public void updateLastLoadedTime() {
this.lastLoadedTime = System.currentTimeMillis();
}
@Override
public int loadedMilliSeconds() {
return (int) (System.currentTimeMillis() - lastLoadedTime);
}
@NotNull
@Override
public Optional<CustomCropsBlockState> getBlockState(Pos3 location) {
BlockPos pos = BlockPos.fromPos3(location);
return getLoadedSection(pos.sectionID()).flatMap(section -> section.getBlockState(pos));
}
@NotNull
@Override
public Optional<CustomCropsBlockState> removeBlockState(Pos3 location) {
BlockPos pos = BlockPos.fromPos3(location);
return getLoadedSection(pos.sectionID()).flatMap(section -> section.removeBlockState(pos));
}
@NotNull
@Override
public Optional<CustomCropsBlockState> addBlockState(Pos3 location, CustomCropsBlockState block) {
BlockPos pos = BlockPos.fromPos3(location);
CustomCropsSection section = getSection(pos.sectionID());
this.arrangeScheduledTickTaskForNewBlock(pos);
return section.addBlockState(pos, block);
}
@NotNull
@Override
public Stream<CustomCropsSection> sectionsToSave() {
return loadedSections.values().stream().filter(section -> !section.canPrune());
}
@NotNull
@Override
public Optional<CustomCropsSection> getLoadedSection(int sectionID) {
return Optional.ofNullable(loadedSections.get(sectionID));
}
@Override
public CustomCropsSection getSection(int sectionID) {
return getLoadedSection(sectionID).orElseGet(() -> {
CustomCropsSection section = new CustomCropsSectionImpl(sectionID);
this.loadedSections.put(sectionID, section);
return section;
});
}
@Override
public Collection<CustomCropsSection> sections() {
return loadedSections.values();
}
@Override
public Optional<CustomCropsSection> removeSection(int sectionID) {
return Optional.ofNullable(loadedSections.remove(sectionID));
}
@Override
public void resetUnloadedSeconds() {
this.unloadedSeconds = 0;
this.notified = false;
}
@Override
public boolean canPrune() {
return loadedSections.isEmpty();
}
@Override
public boolean isOfflineTaskNotified() {
return notified;
}
@Override
public PriorityQueue<DelayedTickTask> tickTaskQueue() {
return queue;
}
@Override
public Set<BlockPos> tickedBlocks() {
return tickedBlocks;
}
private void arrangeScheduledTickTaskForNewBlock(BlockPos pos) {
WorldSetting setting = world.setting();
if (!tickedBlocks.contains(pos)) {
tickedBlocks.add(pos);
int random = RandomUtils.generateRandomInt(0, setting.minTickUnit() - 1);
if (random > loadedSeconds) {
queue.add(new DelayedTickTask(random, pos));
}
}
}
}

View File

@@ -15,23 +15,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item.fertilizer;
package net.momirealms.customcrops.api.core.world;
import net.momirealms.customcrops.api.mechanic.item.Fertilizer;
import org.jetbrains.annotations.NotNull;
public interface QualityCrop extends Fertilizer {
import java.util.Map;
/**
* Get the chance of taking effect
*
* @return chance
*/
double getChance();
public interface CustomCropsRegion {
/**
* Get the modified quality ratio
*
* @return the modified quality ratio
*/
double[] getRatio();
boolean isLoaded();
void unload();
void load();
@NotNull
CustomCropsWorld<?> getWorld();
byte[] getCachedChunkBytes(ChunkPos pos);
RegionPos regionPos();
boolean removeCachedChunk(ChunkPos pos);
void setCachedChunk(ChunkPos pos, byte[] data);
Map<ChunkPos, byte[]> dataToSave();
boolean canPrune();
}

View File

@@ -0,0 +1,86 @@
package net.momirealms.customcrops.api.core.world;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CustomCropsRegionImpl implements CustomCropsRegion {
private final CustomCropsWorld<?> world;
private final RegionPos regionPos;
private final ConcurrentHashMap<ChunkPos, byte[]> cachedChunks;
private boolean isLoaded = false;
protected CustomCropsRegionImpl(CustomCropsWorld<?> world, RegionPos regionPos) {
this.world = world;
this.cachedChunks = new ConcurrentHashMap<>();
this.regionPos = regionPos;
}
protected CustomCropsRegionImpl(CustomCropsWorld<?> world, RegionPos regionPos, ConcurrentHashMap<ChunkPos, byte[]> cachedChunks) {
this.world = world;
this.regionPos = regionPos;
this.cachedChunks = cachedChunks;
}
@Override
public boolean isLoaded() {
return isLoaded;
}
@Override
public void unload() {
if (this.isLoaded) {
if (((CustomCropsWorldImpl<?>) world).unloadRegion(this)) {
this.isLoaded = false;
}
}
}
@Override
public void load() {
if (!this.isLoaded) {
if (((CustomCropsWorldImpl<?>) world).loadRegion(this)) {
this.isLoaded = true;
}
}
}
@NotNull
@Override
public CustomCropsWorld<?> getWorld() {
return this.world;
}
@Override
public byte[] getCachedChunkBytes(ChunkPos pos) {
return this.cachedChunks.get(pos);
}
@Override
public RegionPos regionPos() {
return this.regionPos;
}
@Override
public boolean removeCachedChunk(ChunkPos pos) {
return cachedChunks.remove(pos) != null;
}
@Override
public void setCachedChunk(ChunkPos pos, byte[] data) {
this.cachedChunks.put(pos, data);
}
@Override
public Map<ChunkPos, byte[]> dataToSave() {
return new HashMap<>(cachedChunks);
}
@Override
public boolean canPrune() {
return cachedChunks.isEmpty();
}
}

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.customcrops.api.core.world;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public interface CustomCropsSection {
static CustomCropsSection create(int sectionID) {
return new CustomCropsSectionImpl(sectionID);
}
static CustomCropsSection restore(int sectionID, ConcurrentHashMap<BlockPos, CustomCropsBlockState> blocks) {
return new CustomCropsSectionImpl(sectionID, blocks);
}
int getSectionID();
@NotNull
Optional<CustomCropsBlockState> getBlockState(BlockPos pos);
@NotNull
Optional<CustomCropsBlockState> removeBlockState(BlockPos pos);
@NotNull
Optional<CustomCropsBlockState> addBlockState(BlockPos pos, CustomCropsBlockState block);
boolean canPrune();
CustomCropsBlockState[] blocks();
Map<BlockPos, CustomCropsBlockState> blockMap();
}

View File

@@ -0,0 +1,61 @@
package net.momirealms.customcrops.api.core.world;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class CustomCropsSectionImpl implements CustomCropsSection {
private final int sectionID;
private final ConcurrentHashMap<BlockPos, CustomCropsBlockState> blocks;
protected CustomCropsSectionImpl(int sectionID) {
this.sectionID = sectionID;
this.blocks = new ConcurrentHashMap<>();
}
protected CustomCropsSectionImpl(int sectionID, ConcurrentHashMap<BlockPos, CustomCropsBlockState> blocks) {
this.sectionID = sectionID;
this.blocks = blocks;
}
@Override
public int getSectionID() {
return sectionID;
}
@NotNull
@Override
public Optional<CustomCropsBlockState> getBlockState(BlockPos pos) {
return Optional.ofNullable(blocks.get(pos));
}
@NotNull
@Override
public Optional<CustomCropsBlockState> removeBlockState(BlockPos pos) {
return Optional.ofNullable(blocks.remove(pos));
}
@NotNull
@Override
public Optional<CustomCropsBlockState> addBlockState(BlockPos pos, CustomCropsBlockState block) {
return Optional.ofNullable(blocks.put(pos, block));
}
@Override
public boolean canPrune() {
return blocks.isEmpty();
}
@Override
public CustomCropsBlockState[] blocks() {
return blocks.values().toArray(new CustomCropsBlockState[0]);
}
@Override
public Map<BlockPos, CustomCropsBlockState> blockMap() {
return blocks;
}
}

View File

@@ -0,0 +1,215 @@
/*
* 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.customcrops.api.core.world;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.world.adaptor.WorldAdaptor;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
public interface CustomCropsWorld<W> {
/**
* Create a CustomCrops world
*
* @param world world instance
* @param adaptor the world adaptor
* @return CustomCrops world
* @param <W> world type
*/
static <W> CustomCropsWorld<W> create(W world, WorldAdaptor<W> adaptor) {
return new CustomCropsWorldImpl<>(world, adaptor);
}
/**
* Create a new CustomCropsChunk associated with this world
*
* @param pos the position of the chunk
* @return the created chunk
*/
default CustomCropsChunk createChunk(ChunkPos pos) {
return new CustomCropsChunkImpl(this, pos);
}
default CustomCropsChunk restoreChunk(
ChunkPos pos,
int loadedSeconds,
long lastLoadedTime,
ConcurrentHashMap<Integer, CustomCropsSection> loadedSections,
PriorityQueue<DelayedTickTask> queue,
HashSet<BlockPos> tickedBlocks
) {
return new CustomCropsChunkImpl(this, pos, loadedSeconds, lastLoadedTime, loadedSections, queue, tickedBlocks);
}
default CustomCropsRegion createRegion(RegionPos pos) {
return new CustomCropsRegionImpl(this, pos);
}
default CustomCropsRegion restoreRegion(RegionPos pos, ConcurrentHashMap<ChunkPos, byte[]> cachedChunks) {
return new CustomCropsRegionImpl(this, pos, cachedChunks);
}
WorldAdaptor<W> adaptor();
WorldExtraData extraData();
boolean testChunkLimitation(Pos3 pos3, Class<? extends CustomCropsBlock> clazz, int amount);
boolean doesChunkHaveBlock(Pos3 pos3, Class<? extends CustomCropsBlock> clazz);
int getChunkBlockAmount(Pos3 pos3, Class<? extends CustomCropsBlock> clazz);
/**
* Get the state of the block at a certain location
*
* @param location location of the block state
* @return the optional block state
*/
@NotNull
Optional<CustomCropsBlockState> getBlockState(Pos3 location);
/**
* Remove the block state from a certain location
*
* @param location the location of the block state
* @return the optional removed state
*/
@NotNull
Optional<CustomCropsBlockState> removeBlockState(Pos3 location);
/**
* Add block state at the certain location
*
* @param location location of the state
* @param block block state to add
* @return the optional previous state
*/
@NotNull
Optional<CustomCropsBlockState> addBlockState(Pos3 location, CustomCropsBlockState block);
/**
* Save the world to file
*/
void save();
/**
* Set if the ticking task is ongoing
*
* @param tick ongoing or not
*/
void setTicking(boolean tick);
/**
* Get the world associated with this world
*
* @return Bukkit world
*/
W world();
World bukkitWorld();
String worldName();
/**
* Get the settings of the world
*
* @return the setting
*/
@NotNull
WorldSetting setting();
/**
* Set the settings of the world
*
* @param setting setting
*/
void setting(WorldSetting setting);
/*
* Chunks
*/
boolean isChunkLoaded(ChunkPos pos);
/**
* Get loaded chunk from cache
*
* @param chunkPos the position of the chunk
* @return the optional loaded chunk
*/
@NotNull
Optional<CustomCropsChunk> getLoadedChunk(ChunkPos chunkPos);
/**
* Get chunk from cache or file
*
* @param chunkPos the position of the chunk
* @return the optional chunk
*/
@NotNull
Optional<CustomCropsChunk> getChunk(ChunkPos chunkPos);
/**
* Get chunk from cache or file, create if not found
*
* @param chunkPos the position of the chunk
* @return the chunk
*/
@NotNull
CustomCropsChunk getOrCreateChunk(ChunkPos chunkPos);
/**
* Check if a region is loaded
*
* @param regionPos the position of the region
* @return loaded or not
*/
boolean isRegionLoaded(RegionPos regionPos);
/**
* Get the loaded region, empty if not loaded
*
* @param regionPos position of the region
* @return the optional loaded region
*/
@NotNull
Optional<CustomCropsRegion> getLoadedRegion(RegionPos regionPos);
/**
* Get the region from cache or file
*
* @param regionPos position of the region
* @return the optional region
*/
@NotNull
Optional<CustomCropsRegion> getRegion(RegionPos regionPos);
/**
* Get the region from cache or file, create if not found
*
* @param regionPos position of the region
* @return the region
*/
@NotNull
CustomCropsRegion getOrCreateRegion(RegionPos regionPos);
}

View File

@@ -0,0 +1,474 @@
package net.momirealms.customcrops.api.core.world;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.world.adaptor.WorldAdaptor;
import net.momirealms.customcrops.common.helper.VersionHelper;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerAdapter;
import net.momirealms.customcrops.common.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class CustomCropsWorldImpl<W> implements CustomCropsWorld<W> {
private final ConcurrentHashMap<ChunkPos, CustomCropsChunk> loadedChunks = new ConcurrentHashMap<>(512);
private final ConcurrentHashMap<ChunkPos, CustomCropsChunk> lazyChunks = new ConcurrentHashMap<>(128);
private final ConcurrentHashMap<RegionPos, CustomCropsRegion> loadedRegions = new ConcurrentHashMap<>(128);
private final WeakReference<W> world;
private final WeakReference<World> bukkitWorld;
private final String worldName;
private long currentMinecraftDay;
private int regionTimer;
private SchedulerTask tickTask;
private WorldSetting setting;
private final WorldAdaptor<W> adaptor;
private final WorldExtraData extraData;
public CustomCropsWorldImpl(W world, WorldAdaptor<W> adaptor) {
this.world = new WeakReference<>(world);
this.worldName = adaptor.getName(world);
this.bukkitWorld = new WeakReference<>(Bukkit.getWorld(worldName));
this.regionTimer = 0;
this.adaptor = adaptor;
this.extraData = adaptor.loadExtraData(world);
this.currentMinecraftDay = (int) (adaptor.getWorldFullTime(world) / 24_000);
}
@Override
public WorldAdaptor<W> adaptor() {
return adaptor;
}
@NotNull
@Override
public WorldExtraData extraData() {
return extraData;
}
@Override
public boolean testChunkLimitation(Pos3 pos3, Class<? extends CustomCropsBlock> clazz, int amount) {
Optional<CustomCropsChunk> optional = getChunk(pos3.toChunkPos());
if (optional.isPresent()) {
int i = 0;
CustomCropsChunk chunk = optional.get();
for (CustomCropsSection section : chunk.sections()) {
for (CustomCropsBlockState state : section.blockMap().values()) {
if (clazz.isAssignableFrom(state.type().getClass())) {
i++;
if (i >= amount) {
return false;
}
}
}
}
return true;
} else {
return false;
}
}
@Override
public boolean doesChunkHaveBlock(Pos3 pos3, Class<? extends CustomCropsBlock> clazz) {
Optional<CustomCropsChunk> optional = getChunk(pos3.toChunkPos());
if (optional.isPresent()) {
CustomCropsChunk chunk = optional.get();
for (CustomCropsSection section : chunk.sections()) {
for (CustomCropsBlockState state : section.blockMap().values()) {
if (clazz.isAssignableFrom(state.type().getClass())) {
return true;
}
}
}
}
return false;
}
@Override
public int getChunkBlockAmount(Pos3 pos3, Class<? extends CustomCropsBlock> clazz) {
Optional<CustomCropsChunk> optional = getChunk(pos3.toChunkPos());
if (optional.isPresent()) {
int i = 0;
CustomCropsChunk chunk = optional.get();
for (CustomCropsSection section : chunk.sections()) {
for (CustomCropsBlockState state : section.blockMap().values()) {
if (clazz.isAssignableFrom(state.type().getClass())) {
i++;
}
}
}
return i;
} else {
return 0;
}
}
@NotNull
@Override
public Optional<CustomCropsBlockState> getBlockState(Pos3 location) {
ChunkPos pos = location.toChunkPos();
Optional<CustomCropsChunk> chunk = getChunk(pos);
if (chunk.isEmpty()) {
return Optional.empty();
} else {
CustomCropsChunk customChunk = chunk.get();
// to let the bukkit system trigger the ChunkUnloadEvent later
customChunk.load(true);
return customChunk.getBlockState(location);
}
}
@NotNull
@Override
public Optional<CustomCropsBlockState> removeBlockState(Pos3 location) {
ChunkPos pos = location.toChunkPos();
Optional<CustomCropsChunk> chunk = getChunk(pos);
if (chunk.isEmpty()) {
return Optional.empty();
} else {
CustomCropsChunk customChunk = chunk.get();
// to let the bukkit system trigger the ChunkUnloadEvent later
customChunk.load(true);
return customChunk.removeBlockState(location);
}
}
@NotNull
@Override
public Optional<CustomCropsBlockState> addBlockState(Pos3 location, CustomCropsBlockState block) {
ChunkPos pos = location.toChunkPos();
CustomCropsChunk chunk = getOrCreateChunk(pos);
// to let the bukkit system trigger the ChunkUnloadEvent later
chunk.load(true);
return chunk.addBlockState(location, block);
}
@Override
public void save() {
long time1 = System.currentTimeMillis();
this.adaptor.saveExtraData(this);
for (CustomCropsChunk chunk : loadedChunks.values()) {
this.adaptor.saveChunk(this, chunk);
}
for (CustomCropsChunk chunk : lazyChunks.values()) {
this.adaptor.saveChunk(this, chunk);
}
for (CustomCropsRegion region : loadedRegions.values()) {
this.adaptor.saveRegion(this, region);
}
long time2 = System.currentTimeMillis();
BukkitCustomCropsPlugin.getInstance().debug("Took " + (time2-time1) + "ms to save world " + worldName + ". Saved " + (lazyChunks.size() + loadedChunks.size()) + " chunks.");
}
@Override
public void setTicking(boolean tick) {
if (tick) {
if (this.tickTask == null || this.tickTask.isCancelled())
this.tickTask = BukkitCustomCropsPlugin.getInstance().getScheduler().asyncRepeating(this::timer, 1, 1, TimeUnit.SECONDS);
} else {
if (this.tickTask != null && !this.tickTask.isCancelled())
this.tickTask.cancel();
}
}
private void timer() {
saveLazyChunks();
saveLazyRegions();
if (isANewDay()) {
updateSeasonAndDate();
}
if (setting().enableScheduler()) {
tickChunks();
}
}
private void tickChunks() {
if (VersionHelper.isFolia()) {
SchedulerAdapter<Location, World> scheduler = BukkitCustomCropsPlugin.getInstance().getScheduler();
for (CustomCropsChunk chunk : loadedChunks.values()) {
scheduler.sync().run(chunk::timer, bukkitWorld(), chunk.chunkPos().x(), chunk.chunkPos().z());
}
} else {
for (CustomCropsChunk chunk : loadedChunks.values()) {
chunk.timer();
}
}
}
private void updateSeasonAndDate() {
int date = extraData().getDate();
date++;
if (date > setting().seasonDuration()) {
Season season = extraData().getSeason().getNextSeason();
extraData().setSeason(season);
extraData().setDate(1);
} else {
extraData().setDate(date);
}
}
private boolean isANewDay() {
long currentDay = bukkitWorld().getFullTime() / 24_000;
if (currentDay != currentMinecraftDay) {
currentMinecraftDay = currentDay;
return true;
}
return false;
}
private void saveLazyRegions() {
this.regionTimer++;
if (this.regionTimer >= 600) {
this.regionTimer = 0;
ArrayList<CustomCropsRegion> removed = new ArrayList<>();
for (Map.Entry<RegionPos, CustomCropsRegion> entry : loadedRegions.entrySet()) {
if (shouldUnloadRegion(entry.getKey())) {
removed.add(entry.getValue());
}
}
for (CustomCropsRegion region : removed) {
region.unload();
}
}
}
private void saveLazyChunks() {
ArrayList<CustomCropsChunk> chunksToSave = new ArrayList<>();
for (Map.Entry<ChunkPos, CustomCropsChunk> lazyEntry : this.lazyChunks.entrySet()) {
CustomCropsChunk chunk = lazyEntry.getValue();
int sec = chunk.unloadedSeconds() + 1;
if (sec >= 30) {
chunksToSave.add(chunk);
} else {
chunk.unloadedSeconds(sec);
}
}
for (CustomCropsChunk chunk : chunksToSave) {
unloadLazyChunk(chunk.chunkPos());
}
}
@Override
public W world() {
return world.get();
}
@Override
public World bukkitWorld() {
return bukkitWorld.get();
}
@Override
public String worldName() {
return worldName;
}
@Override
public @NotNull WorldSetting setting() {
return setting;
}
@Override
public void setting(WorldSetting setting) {
this.setting = setting;
}
/*
* Chunks
*/
@Nullable
public CustomCropsChunk removeLazyChunk(ChunkPos chunkPos) {
return this.lazyChunks.remove(chunkPos);
}
@Nullable
public CustomCropsChunk getLazyChunk(ChunkPos chunkPos) {
return this.lazyChunks.get(chunkPos);
}
@Override
public boolean isChunkLoaded(ChunkPos pos) {
return this.loadedChunks.containsKey(pos);
}
public boolean loadChunk(CustomCropsChunk chunk) {
Optional<CustomCropsChunk> previousChunk = getLoadedChunk(chunk.chunkPos());
if (previousChunk.isPresent()) {
BukkitCustomCropsPlugin.getInstance().debug("Chunk " + chunk.chunkPos() + " already loaded.");
if (previousChunk.get() != chunk) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().severe("Failed to load the chunk. There is already a different chunk instance with the same coordinates in the cache. " + chunk.chunkPos());
return false;
}
return true;
}
this.loadedChunks.put(chunk.chunkPos(), chunk);
this.lazyChunks.remove(chunk.chunkPos());
return true;
}
@ApiStatus.Internal
public boolean unloadChunk(CustomCropsChunk chunk, boolean lazy) {
ChunkPos pos = chunk.chunkPos();
Optional<CustomCropsChunk> previousChunk = getLoadedChunk(chunk.chunkPos());
if (previousChunk.isPresent()) {
if (previousChunk.get() != chunk) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().severe("Failed to remove the chunk. The provided chunk instance is inconsistent with the one in the cache. " + chunk.chunkPos());
return false;
}
} else {
return false;
}
this.loadedChunks.remove(chunk.chunkPos());
chunk.updateLastLoadedTime();
if (lazy) {
this.lazyChunks.put(pos, chunk);
} else {
this.adaptor.saveChunk(this, chunk);
}
return true;
}
@ApiStatus.Internal
public boolean unloadChunk(ChunkPos pos, boolean lazy) {
CustomCropsChunk removed = this.loadedChunks.remove(pos);
if (removed != null) {
removed.updateLastLoadedTime();
if (lazy) {
this.lazyChunks.put(pos, removed);
} else {
this.adaptor.saveChunk(this, removed);
}
return true;
}
return false;
}
@ApiStatus.Internal
public boolean unloadLazyChunk(ChunkPos pos) {
CustomCropsChunk removed = this.lazyChunks.remove(pos);
if (removed != null) {
this.adaptor.saveChunk(this, removed);
return true;
}
return false;
}
@NotNull
@Override
public Optional<CustomCropsChunk> getLoadedChunk(ChunkPos chunkPos) {
return Optional.ofNullable(this.loadedChunks.get(chunkPos));
}
@NotNull
@Override
public Optional<CustomCropsChunk> getChunk(ChunkPos chunkPos) {
return Optional.ofNullable(getLoadedChunk(chunkPos).orElseGet(() -> {
CustomCropsChunk chunk = getLazyChunk(chunkPos);
if (chunk != null) {
return chunk;
}
return this.adaptor.loadChunk(this, chunkPos, false);
}));
}
@NotNull
@Override
public CustomCropsChunk getOrCreateChunk(ChunkPos chunkPos) {
return Objects.requireNonNull(getLoadedChunk(chunkPos).orElseGet(() -> {
CustomCropsChunk chunk = getLazyChunk(chunkPos);
if (chunk != null) {
return chunk;
}
return this.adaptor.loadChunk(this, chunkPos, true);
}));
}
/*
* Regions
*/
@Override
public boolean isRegionLoaded(RegionPos pos) {
return this.loadedRegions.containsKey(pos);
}
@ApiStatus.Internal
public boolean loadRegion(CustomCropsRegion region) {
Optional<CustomCropsRegion> previousRegion = getLoadedRegion(region.regionPos());
if (previousRegion.isPresent()) {
BukkitCustomCropsPlugin.getInstance().debug("Region " + region.regionPos() + " already loaded.");
if (previousRegion.get() != region) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().severe("Failed to load the region. There is already a different region instance with the same coordinates in the cache. " + region.regionPos());
return false;
}
return true;
}
this.loadedRegions.put(region.regionPos(), region);
return true;
}
@NotNull
@Override
public Optional<CustomCropsRegion> getLoadedRegion(RegionPos regionPos) {
return Optional.ofNullable(loadedRegions.get(regionPos));
}
@NotNull
@Override
public Optional<CustomCropsRegion> getRegion(RegionPos regionPos) {
return Optional.ofNullable(getLoadedRegion(regionPos).orElse(adaptor.loadRegion(this, regionPos, false)));
}
@NotNull
@Override
public CustomCropsRegion getOrCreateRegion(RegionPos regionPos) {
return Objects.requireNonNull(getLoadedRegion(regionPos).orElse(adaptor.loadRegion(this, regionPos, true)));
}
private boolean shouldUnloadRegion(RegionPos regionPos) {
for (int chunkX = regionPos.x() * 32; chunkX < regionPos.x() * 32 + 32; chunkX++) {
for (int chunkZ = regionPos.z() * 32; chunkZ < regionPos.z() * 32 + 32; chunkZ++) {
// if a chunk is unloaded, then it should not be in the loaded chunks map
ChunkPos pos = ChunkPos.of(chunkX, chunkZ);
if (isChunkLoaded(pos) || this.lazyChunks.containsKey(pos)) {
return false;
}
}
}
return true;
}
public boolean unloadRegion(CustomCropsRegion region) {
Optional<CustomCropsRegion> previousRegion = getLoadedRegion(region.regionPos());
if (previousRegion.isPresent()) {
if (previousRegion.get() != region) {
BukkitCustomCropsPlugin.getInstance().getPluginLogger().severe("Failed to remove the region. The provided region instance is inconsistent with the one in the cache. " + region.regionPos());
return false;
}
} else {
return false;
}
RegionPos regionPos = region.regionPos();
for (int chunkX = regionPos.x() * 32; chunkX < regionPos.x() * 32 + 32; chunkX++) {
for (int chunkZ = regionPos.z() * 32; chunkZ < regionPos.z() * 32 + 32; chunkZ++) {
ChunkPos pos = ChunkPos.of(chunkX, chunkZ);
if (!unloadLazyChunk(pos)) {
unloadChunk(pos, false);
}
}
}
this.loadedRegions.remove(region.regionPos());
this.adaptor.saveRegion(this, region);
return true;
}
}

View File

@@ -15,16 +15,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.api.mechanic.item.fertilizer;
package net.momirealms.customcrops.api.core.world;
import net.momirealms.customcrops.api.mechanic.item.Fertilizer;
import com.flowpowered.nbt.Tag;
import net.momirealms.customcrops.api.core.SynchronizedCompoundMap;
public interface SoilRetain extends Fertilizer {
public interface DataBlock {
Tag<?> set(String key, Tag<?> tag);
Tag<?> get(String key);
Tag<?> remove(String key);
/**
* Get the chance of taking effect
* Get the data map
*
* @return chance
* @return data map
*/
double getChance();
SynchronizedCompoundMap compoundMap();
}

View File

@@ -0,0 +1,53 @@
/*
* 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.customcrops.api.core.world;
import org.jetbrains.annotations.NotNull;
public class DelayedTickTask implements Comparable<DelayedTickTask> {
private static int taskID;
private final int time;
private final BlockPos blockPos;
private final int id;
public DelayedTickTask(int time, BlockPos blockPos) {
this.time = time;
this.blockPos = blockPos;
this.id = taskID++;
}
public BlockPos blockPos() {
return blockPos;
}
public int getTime() {
return time;
}
@Override
public int compareTo(@NotNull DelayedTickTask o) {
if (this.time > o.time) {
return 1;
} else if (this.time < o.time) {
return -1;
} else {
return Integer.compare(this.id, o.id);
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) <2024> <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.customcrops.api.core.world;
import org.bukkit.Location;
import org.bukkit.World;
public record Pos3(int x, int y, int z) {
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Pos3 other = (Pos3) obj;
if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x)) {
return false;
}
if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y)) {
return false;
}
if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.x));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.y));
hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.z));
return hash;
}
public static Pos3 from(Location location) {
return new Pos3(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
public Location toLocation(World world) {
return new Location(world, x, y, z);
}
public Pos3 add(int x, int y, int z) {
return new Pos3(this.x + x, this.y + y, this.z + z);
}
public ChunkPos toChunkPos() {
return ChunkPos.fromPos3(this);
}
public int chunkX() {
return (int) Math.floor((double) this.x() / 16.0);
}
public int chunkZ() {
return (int) Math.floor((double) this.z() / 16.0);
}
@Override
public String toString() {
return "Pos3{" +
"x=" + x +
", y=" + y +
", z=" + z +
'}';
}
}

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