Migrates the entire package nomenclature to be more direct and straightforward. (#17)

Signed-off-by: Paul Reilly <pawereus@gmail.com>
This commit is contained in:
Paldiu
2023-08-01 22:34:18 -05:00
committed by GitHub
parent e1a6b5e587
commit 21463c50fe
146 changed files with 595 additions and 608 deletions

View File

@ -0,0 +1,281 @@
package fns.patchwork.api;
import fns.patchwork.provider.ContextProvider;
import java.util.function.Function;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.block.Action;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents an object context. This class is a simple generic type wrapper that can be used to ensure data types. This
* class is also used to provide a simple way to map data types.
*
* @param <T> The type of the context.
* @see ContextProvider
*/
@FunctionalInterface
public interface Context<T>
{
/**
* Maps the context to another context.
*
* @param mapper The mapper function.
* @param <S> The type of the mapped context.
* @return The mapped context.
*/
default <S> Context<S> map(@NotNull final Function<T, S> mapper)
{
return () -> mapper.apply(get());
}
/**
* Gets the context.
*
* @return The context.
*/
T get();
/**
* Gets the context as a string.
*
* @return The context as a string.
*/
default @Nullable String asString()
{
if (get() instanceof String string)
{
return string;
} else
{
return null;
}
}
/**
* Gets the context as a boolean.
*
* @return The context as a boolean.
*/
default @Nullable Boolean asBoolean()
{
if (get() instanceof Boolean bool)
{
return bool;
} else
{
return null;
}
}
/**
* @return The context as a {@link Double}.
*/
default @Nullable Double asDouble()
{
if (get() instanceof Double doub)
{
return doub;
} else
{
return null;
}
}
/**
* @return The context as a {@link Integer}.
*/
default @Nullable Integer asInt()
{
if (get() instanceof Integer integer)
{
return integer;
} else
{
return null;
}
}
/**
* @return The context as a {@link Byte}.
*/
default @Nullable Long asLong()
{
if (get() instanceof Long longg)
{
return longg;
} else
{
return null;
}
}
/**
* @return The context as a {@link Float}.
*/
default @Nullable Float asFloat()
{
if (get() instanceof Float floatt)
{
return floatt;
} else
{
return null;
}
}
/**
* @return The context as a {@link Player}.
*/
default @Nullable Player asPlayer()
{
if (get() instanceof Player player)
{
return player;
} else
{
return null;
}
}
/**
* @return The context as a {@link CommandSender}.
*/
default @Nullable CommandSender asCommandSender()
{
if (get() instanceof CommandSender commandSender)
{
return commandSender;
} else
{
return null;
}
}
/**
* This is the same as calling {@link #get()} and then calling {@link Object#toString()} on the result.
*
* @return The context as a {@link String} literal.
*/
default @NotNull String literal()
{
return get().toString();
}
/**
* @return The context as a {@link World}.
*/
default @Nullable World asWorld()
{
if (get() instanceof World world)
{
return world;
} else
{
return null;
}
}
/**
* @return The context as a {@link Location}.
*/
default @Nullable Location asLocation()
{
if (get() instanceof Location location)
{
return location;
} else
{
return null;
}
}
/**
* @return The context as a {@link LivingEntity}.
*/
default @Nullable LivingEntity asLivingEntity()
{
if (get() instanceof LivingEntity livingEntity)
{
return livingEntity;
} else
{
return null;
}
}
/**
* @return The context as a {@link Component}.
*/
default @Nullable Component asComponent()
{
if (get() instanceof Component component)
{
return component;
} else
{
return null;
}
}
/**
* @return The context as a {@link Projectile}.
*/
default @Nullable Projectile asProjectile()
{
if (get() instanceof Projectile projectile)
{
return projectile;
} else
{
return null;
}
}
/**
* @return The context as an {@link Action}.
*/
default @Nullable Action asAction()
{
if (get() instanceof Action action)
{
return action;
} else
{
return null;
}
}
/**
* Gets the context as a custom class. This will cast the object to the class if it is an instance of it.
* <br>
* Typically, Context objects are useful when used to collect unknown data and then cast it to a known type.
* <br>
* In the case where we know what the data should be but the compiler or the runtime does not, the object is wrapped
* in a Context which then exposes multiple methods to get the data as one of the known types.
* <p>
* For example, if we have a Context&lt;Object&gt; and we already know that the wrapped object should be of type X,
* we can use <code>X.class</code> on this method to retrieve the actual object. That is, to say, if there is not
* already a predefined method to get the object as the type we want.
*
* @param clazz
* @param <U>
* @return
*/
default <U> @Nullable U asCustom(Class<U> clazz)
{
if (clazz.isInstance(get()))
{
return clazz.cast(get());
} else
{
return null;
}
}
}

View File

@ -0,0 +1,21 @@
package fns.patchwork.api;
/**
* Interpolates a range of values and returns the results in a {@link Double} array.
* <br>
* This is a functional interface, to allow for lambda expressions, but also for anonymous custom interpolation
* implementations.
*/
@FunctionalInterface
public interface Interpolator
{
/**
* Interpolates a range of values and returns the results in a {@link Double} array.
*
* @param from The starting value.
* @param to The ending value.
* @param max The number of values to interpolate.
* @return The interpolated values.
*/
double[] interpolate(final double from, final double to, final int max);
}

View File

@ -0,0 +1,27 @@
package fns.patchwork.api;
/**
* This interface represents a Serializable object. Objects which require custom serialization and cannot simply
* override or call the default {@link Object#toString()} method should implement this interface.
*
* @param <T> The type of object to serialize
*/
public interface Serializable<T>
{
/**
* Serialize an object to a string. Ideally, this should serialize to an SQL query for easy data transfer.
*
* @param object The object to serialize
* @return The serialized object
*/
String serialize(T object);
/**
* Deserialize an object from a Serialized string..
*
* @param serializedObject The serialized object
* @return The deserialized object
*/
T deserialize(String serializedObject);
}

View File

@ -0,0 +1,320 @@
package fns.patchwork.audience;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.chat.SignedMessage;
import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.TitlePart;
import org.jetbrains.annotations.NotNull;
/**
* A replacement for {@link net.kyori.adventure.audience.ForwardingAudience} that allows for audiences to be removed and
* added at will. Not thread safe.
* <p>
* This is intended for use in toggleable logging systems, for example, potion spy.
*/
// TODO: Work on thread-safety (or thread-safe alternative)
public class MutableAudienceForwarder implements Audience
{
/**
* The audiences that this forwards to.
*/
private final Set<Audience> audiences = new HashSet<>();
/**
* Creates a new {@link MutableAudienceForwarder} with the given audiences.
*
* @param audiences The audiences to forward to.
* @return The new {@link MutableAudienceForwarder}.
*/
public static MutableAudienceForwarder from(final Audience... audiences)
{
final MutableAudienceForwarder audienceForwarder = new MutableAudienceForwarder();
for (final Audience audience : audiences)
{
audienceForwarder.addAudience(audience);
}
return audienceForwarder;
}
/**
* Adds an audience to this forwarder.
*
* @param audience The audience to add.
*/
public void addAudience(final Audience audience)
{
if (audiences.contains(audience) || audience == this /* Protect against honest self-referential calls */)
{
return;
}
audiences.add(audience);
}
/**
* Removes an audience from this forwarder.
*
* @param audience The audience to remove.
* @return Whether the audience was removed.
*/
public boolean removeAudience(final Audience audience)
{
return audiences.remove(audience);
}
/**
* Filters the audiences in the stream by the given predicate.
*
* @param filter a filter that determines if an audience should be included
* @return The first Audience found that matches the filter.
*/
@Override
public @NotNull Audience filterAudience(@NotNull final Predicate<? super Audience> filter)
{
return audiences.stream()
.filter(filter)
.findFirst()
.orElseThrow();
}
/**
* Applies a consumer to each audience in the stream.
*
* @param action the action to apply.
*/
@Override
public void forEachAudience(@NotNull final Consumer<? super Audience> action)
{
audiences.forEach(action);
}
/**
* Sends a {@link ComponentLike} to every audience within the stream.
*
* @param message The message to send.
* @see Audience#sendMessage(ComponentLike)
* @see #forEachAudience(Consumer)
*/
@Override
public void sendMessage(@NotNull final ComponentLike message)
{
forEachAudience(a -> a.sendMessage(message));
}
/**
* Sends a {@link Component} to every audience within the stream.
*
* @param message The message to send
* @see Audience#sendMessage(Component)
* @see #forEachAudience(Consumer)
*/
@Override
public void sendMessage(@NotNull final Component message)
{
forEachAudience(a -> a.sendMessage(message));
}
/**
* Sends a {@link SignedMessage} to every audience within the stream.
*
* @param message the component content of the message
* @param boundChatType the bound chat type of the message
* @see Audience#sendMessage(Component, ChatType.Bound)
* @see #forEachAudience(Consumer)
*/
@Override
public void sendMessage(@NotNull final Component message, final ChatType.@NotNull Bound boundChatType)
{
forEachAudience(a -> a.sendMessage(message, boundChatType));
}
/**
* Sends a {@link SignedMessage} to every audience within the stream.
*
* @param message the component content of the message
* @param boundChatType the bound chat type of the message
* @see Audience#sendMessage(ComponentLike, ChatType.Bound)
* @see #forEachAudience(Consumer)
*/
@Override
public void sendMessage(@NotNull final ComponentLike message, final ChatType.@NotNull Bound boundChatType)
{
forEachAudience(a -> a.sendMessage(message, boundChatType));
}
/**
* Sends a {@link SignedMessage} to every audience within the stream.
*
* @param signedMessage the signed message data to send
* @param boundChatType the bound chat type of the message
*/
@Override
public void sendMessage(@NotNull final SignedMessage signedMessage, final ChatType.@NotNull Bound boundChatType)
{
forEachAudience(a -> a.sendMessage(signedMessage, boundChatType));
}
/**
* Deletes a signed message from the audiences chat.
*
* @param signedMessage the message to delete
*/
@Override
public void deleteMessage(@NotNull final SignedMessage signedMessage)
{
forEachAudience(a -> a.deleteMessage(signedMessage));
}
/**
* Deletes a signed message from the audiences chat using the provided chat signature.
*
* @param signature the signature associated with the message to delete.
*/
@Override
public void deleteMessage(final SignedMessage.@NotNull Signature signature)
{
forEachAudience(a -> a.deleteMessage(signature));
}
// The methods below here will (probably) never be used, however it's good to keep them for completeness' sake.
@Override
public void sendActionBar(@NotNull final ComponentLike message)
{
forEachAudience(a -> a.sendActionBar(message));
}
@Override
public void sendActionBar(@NotNull final Component message)
{
forEachAudience(a -> a.sendActionBar(message));
}
@Override
public void sendPlayerListHeader(@NotNull final ComponentLike header)
{
forEachAudience(a -> a.sendPlayerListHeader(header));
}
@Override
public void sendPlayerListHeader(@NotNull final Component header)
{
forEachAudience(a -> a.sendPlayerListHeader(header));
}
@Override
public void sendPlayerListFooter(@NotNull final ComponentLike footer)
{
forEachAudience(a -> a.sendPlayerListFooter(footer));
}
@Override
public void sendPlayerListFooter(@NotNull final Component footer)
{
forEachAudience(a -> a.sendPlayerListFooter(footer));
}
@Override
public void sendPlayerListHeaderAndFooter(@NotNull final ComponentLike header, @NotNull final ComponentLike footer)
{
forEachAudience(a -> a.sendPlayerListHeaderAndFooter(header, footer));
}
@Override
public void sendPlayerListHeaderAndFooter(@NotNull final Component header, @NotNull final Component footer)
{
forEachAudience(a -> a.sendPlayerListHeaderAndFooter(header, footer));
}
@Override
public void showTitle(@NotNull final Title title)
{
forEachAudience(a -> a.showTitle(title));
}
@Override
public <T> void sendTitlePart(@NotNull final TitlePart<T> part, @NotNull final T value)
{
forEachAudience(a -> a.sendTitlePart(part, value));
}
@Override
public void clearTitle()
{
forEachAudience(Audience::clearTitle);
}
@Override
public void resetTitle()
{
forEachAudience(Audience::resetTitle);
}
@Override
public void showBossBar(@NotNull final BossBar bar)
{
forEachAudience(a -> a.showBossBar(bar));
}
@Override
public void hideBossBar(@NotNull final BossBar bar)
{
forEachAudience(a -> a.hideBossBar(bar));
}
@Override
public void playSound(@NotNull final Sound sound)
{
forEachAudience(a -> a.playSound(sound));
}
@Override
public void playSound(@NotNull final Sound sound, final double x, final double y, final double z)
{
forEachAudience(a -> a.playSound(sound, x, y, z));
}
@Override
public void playSound(@NotNull final Sound sound, final Sound.@NotNull Emitter emitter)
{
forEachAudience(a -> a.playSound(sound, emitter));
}
@Override
public void stopSound(@NotNull final Sound sound)
{
forEachAudience(a -> a.stopSound(sound));
}
@Override
public void stopSound(@NotNull final SoundStop stop)
{
forEachAudience(a -> a.stopSound(stop));
}
@Override
public void openBook(final Book.@NotNull Builder book)
{
forEachAudience(a -> a.openBook(book));
}
@Override
public void openBook(@NotNull final Book book)
{
forEachAudience(a -> a.openBook(book));
}
}

View File

@ -0,0 +1,108 @@
package fns.patchwork.base;
import fns.patchwork.display.adminchat.AdminChatDisplay;
import fns.patchwork.event.EventBus;
import fns.patchwork.service.FreedomExecutor;
import fns.patchwork.service.SubscriptionProvider;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
/**
* The base class for Patchwork.
*/
public class Patchwork extends JavaPlugin
{
/**
* The {@link EventBus} for this plugin.
*/
private final EventBus eventBus = new EventBus(this);
/**
* The {@link Registration} object for this plugin.
*/
private final Registration registration = new Registration();
/**
* The {@link FreedomExecutor} for this plugin.
*/
private final FreedomExecutor executor = new FreedomExecutor();
/**
* The {@link AdminChatDisplay} for this plugin.
*/
private final AdminChatDisplay acdisplay = new AdminChatDisplay();
/**
* Provides this plugin instance through a safe static method. This is effectively the same thing as using
* {@link JavaPlugin#getPlugin(Class)}
*
* @return the plugin instance
*/
public static Patchwork getInstance()
{
return JavaPlugin.getPlugin(Patchwork.class);
}
@Override
public void onDisable()
{
Bukkit.getScheduler()
.runTaskLater(this, () -> getRegistrations()
.getServiceTaskRegistry()
.stopAllServices(), 1L);
getRegistrations().getServiceTaskRegistry()
.unregisterService(EventBus.class);
}
@Override
public void onEnable()
{
getRegistrations().getServiceTaskRegistry()
.registerService(SubscriptionProvider.asyncService(this, eventBus));
getExecutor().getSync()
.execute(() -> getRegistrations()
.getServiceTaskRegistry()
.startAllServices());
}
/**
* Gets the {@link FreedomExecutor} for this plugin.
*
* @return the {@link FreedomExecutor}
*/
public FreedomExecutor getExecutor()
{
return executor;
}
/**
* Get's the Registration object for this plugin. This object contains every registry class for the various features
* provided by this plugin.
*
* @return the Registration object
*/
public Registration getRegistrations()
{
return registration;
}
/**
* Gets the {@link EventBus} for this plugin. The EventBus is used to register and listen to custom events provided
* by Freedom Network Suite.
*
* @return the {@link EventBus}
*/
public EventBus getEventBus()
{
return eventBus;
}
/**
* Gets the {@link AdminChatDisplay} for this plugin. The AdminChatDisplay is used to display messages sent in
* adminchat.
*
* @return the {@link AdminChatDisplay}
*/
public AdminChatDisplay getAdminChatDisplay()
{
return acdisplay;
}
}

View File

@ -0,0 +1,104 @@
package fns.patchwork.base;
import fns.patchwork.data.ConfigRegistry;
import fns.patchwork.data.EventRegistry;
import fns.patchwork.data.GroupRegistry;
import fns.patchwork.data.ModuleRegistry;
import fns.patchwork.data.ServiceTaskRegistry;
import fns.patchwork.data.UserRegistry;
/**
* This class is a holder for each registry in the data package.
* <br>
* Registries such as {@link ModuleRegistry} and {@link ServiceTaskRegistry} can be found as final objects in this
* class. These registries should only ever be accessed through the single Registration object in CommonsBase using
* {@link Patchwork#getRegistrations()}
*/
public class Registration
{
/**
* The {@link EventRegistry}
*/
private final EventRegistry eventRegistry;
/**
* The {@link UserRegistry}
*/
private final UserRegistry userRegistry;
/**
* The {@link ServiceTaskRegistry}
*/
private final ServiceTaskRegistry serviceTaskRegistry;
/**
* The {@link ModuleRegistry}
*/
private final ModuleRegistry moduleRegistry;
/**
* The {@link GroupRegistry}
*/
private final GroupRegistry groupRegistry;
/**
* The {@link ConfigRegistry}
*/
private final ConfigRegistry configRegistry;
/**
* Constructs a new Registration object and initializes all registries.
*/
Registration()
{
this.eventRegistry = new EventRegistry();
this.userRegistry = new UserRegistry();
this.serviceTaskRegistry = new ServiceTaskRegistry();
this.moduleRegistry = new ModuleRegistry();
this.groupRegistry = new GroupRegistry();
this.configRegistry = new ConfigRegistry();
}
/**
* @return The {@link ModuleRegistry}
*/
public ModuleRegistry getModuleRegistry()
{
return moduleRegistry;
}
/**
* @return The {@link EventRegistry}
*/
public EventRegistry getEventRegistry()
{
return eventRegistry;
}
/**
* @return The {@link UserRegistry}
*/
public UserRegistry getUserRegistry()
{
return userRegistry;
}
/**
* @return The {@link ServiceTaskRegistry}
*/
public ServiceTaskRegistry getServiceTaskRegistry()
{
return serviceTaskRegistry;
}
/**
* @return The {@link GroupRegistry}
*/
public GroupRegistry getGroupRegistry()
{
return groupRegistry;
}
/**
* @return The {@link ConfigRegistry}
*/
public ConfigRegistry getConfigRegistry()
{
return configRegistry;
}
}

View File

@ -0,0 +1,30 @@
package fns.patchwork.base;
import fns.patchwork.user.User;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class Shortcuts
{
private Shortcuts()
{
throw new AssertionError();
}
public static <T extends JavaPlugin> T provideModule(final Class<T> pluginClass)
{
return Patchwork.getInstance()
.getRegistrations()
.getModuleRegistry()
.getProvider(pluginClass)
.getModule();
}
public static User getUser(final Player player)
{
return Patchwork.getInstance()
.getRegistrations()
.getUserRegistry()
.getUser(player);
}
}

View File

@ -0,0 +1,242 @@
package fns.patchwork.command;
import fns.patchwork.command.annotation.Completion;
import fns.patchwork.command.annotation.Subcommand;
import fns.patchwork.provider.ContextProvider;
import fns.patchwork.utils.logging.FreedomLogger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
/**
* This class is acts as a delegate between our custom command implementation and the Bukkit API.
* <br>
* This class is not meant to be used directly, and is only public to allow for the Bukkit API to access it. As a
* result, this file will remain undocumented.
* <br>
* <br>
* This class is not thread-safe.
* <br>
* This class is not meant to be extended.
* <br>
* This class is not meant to be instantiated.
* <br>
* This class is not meant to be used outside Patchwork.
*/
public final class BukkitDelegate extends Command implements PluginIdentifiableCommand
{
private final JavaPlugin plugin;
private final Commander command;
private final boolean noConsole;
BukkitDelegate(final Commander command)
{
super(command.getInfo()
.name());
this.command = command;
this.plugin = command.getPlugin();
this.setDescription(command.getInfo()
.description());
this.setUsage(command.getInfo()
.usage());
this.setPermission(command.getPerms()
.perm());
this.setAliases(Arrays.asList(command.getInfo()
.aliases()));
this.permissionMessage(Component.text(command.getPerms()
.noPerms()));
this.noConsole = command.getPerms()
.onlyPlayers();
}
@Override
public boolean execute(@NotNull final CommandSender sender,
@NotNull final String commandLabel,
@NotNull final String[] args)
{
if (!(sender instanceof Player) && noConsole)
{
sender.sendMessage(Component.text("This command can only be run by players."));
return true;
}
if (getPermission() != null && !sender.hasPermission(getPermission()))
{
Component permissionMessage = permissionMessage();
if (permissionMessage == null)
permissionMessage = Component.text("You do not have permission to use this command.");
sender.sendMessage(permissionMessage);
return true;
}
if (args.length > 0)
{
final ContextProvider provider = new ContextProvider();
final Set<Subcommand> nodes = command.getSubcommands()
.keySet();
for (final Subcommand node : nodes)
{
processSubCommands(args, sender, provider, node);
}
return true;
}
if (command.getBaseMethod() != null)
{
try
{
if (noConsole)
{
command.getBaseMethod()
.invoke(command, (Player) sender);
}
else
{
command.getBaseMethod()
.invoke(command, sender);
}
}
catch (Exception ex)
{
FreedomLogger.getLogger("Patchwork")
.error(ex);
}
return true;
}
return false;
}
private void processSubCommands(final @NotNull String @NotNull [] args,
final CommandSender sender, final ContextProvider provider,
final Subcommand node)
{
final Class<?>[] argTypes = node.args();
if (argTypes.length > args.length)
return;
final Object[] objects = new Object[argTypes.length + 1];
for (int i = 0; i < argTypes.length; i++)
{
final Class<?> argType = argTypes[i];
final String arg = args[i];
if (argType.equals(String.class))
{
if (i == argTypes.length - 1)
{
final String[] reasonArgs = Arrays.copyOfRange(args, i, args.length - 1);
final String reason = String.join(" ", reasonArgs);
objects[i] = reason;
}
else
{
continue;
}
}
if (argType.equals(Location.class))
{
final String[] locationArgs = Arrays.copyOfRange(args, i, i + 3);
final String location = String.join(" ", locationArgs);
objects[i] = location;
}
final Object obj = provider.fromString(arg, argType);
if (obj == null)
{
FreedomLogger.getLogger("Datura")
.error("Failed to parse argument " + arg + " for type " + argType.getName());
return;
}
objects[i] = obj;
}
try
{
if (noConsole)
{
command.getSubcommands()
.get(node)
.invoke(command, (Player) sender, objects);
}
else
{
command.getSubcommands()
.get(node)
.invoke(command, sender, objects);
}
}
catch (Exception ex)
{
FreedomLogger.getLogger("Patchwork")
.error(ex);
}
}
@Override
public List<String> tabComplete(final CommandSender sender, final String alias, final String[] args)
{
final Set<Completion> completions = command.getCompletions();
final List<String> results = new ArrayList<>();
for (final Completion completion : completions)
{
if (completion.index() != args.length)
{
continue;
}
for (final String p : completion.args())
{
switch (p)
{
case "%player%" -> results.addAll(Bukkit.getOnlinePlayers()
.stream()
.map(Player::getName)
.toList());
case "%world%" -> results.addAll(Bukkit.getWorlds()
.stream()
.map(World::getName)
.toList());
case "%number%" -> results.addAll(List.of(
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9"));
case "%location%" -> results.add("world x y z");
default -> results.add(p);
}
}
}
return results.stream()
.filter(s -> s.startsWith(args[args.length - 1]))
.toList();
}
@Override
public @NotNull Plugin getPlugin()
{
return this.plugin;
}
}

View File

@ -0,0 +1,44 @@
package fns.patchwork.command;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandMap;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Handles the registration of commands. The plugin which initializes this class should be the plugin that is
* registering the commands.
*/
public class CommandHandler
{
/**
* The plugin that this command handler is registered to.
* <br>
* This should be the plugin instance which is trying to register the commands.
*/
private final JavaPlugin plugin;
/**
* Creates a new command handler.
*
* @param plugin The plugin that this command handler is registered to.
*/
public CommandHandler(final JavaPlugin plugin)
{
this.plugin = plugin;
}
/**
* Registers a command. This method will automatically delegate the command information to the Bukkit API and
* register with the {@link CommandMap}.
*
* @param command The command to register.
* @param <T> The type of the command.
*/
public <T extends Commander> void registerCommand(final T command)
{
final BukkitDelegate delegate = new BukkitDelegate(command);
Bukkit.getCommandMap()
.register(plugin.getName(), delegate);
}
}

View File

@ -0,0 +1,185 @@
package fns.patchwork.command;
import fns.patchwork.command.annotation.Base;
import fns.patchwork.command.annotation.Completion;
import fns.patchwork.command.annotation.Info;
import fns.patchwork.command.annotation.Permissive;
import fns.patchwork.command.annotation.Subcommand;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* This is the base command class which should be extended when creating a new command. Commands must be annotated with
* the {@link Info} and {@link Permissive} annotations in order to be properly registered with the
* {@link CommandHandler}.
* <p>
* One single method can be annotated with the {@link Base} annotation to specify that method should be called when the
* command is executed without any arguments.
* <br>
* You are allowed to have as many methods as you want which are annotated with the {@link Subcommand} annotation. These
* methods will be called when the command is executed with the specified subcommand.
* <br>
* You are also allowed to use multiple {@link Completion} annotations per method to define multiple tab completions for
* a single subcommand. This would be useful in the case where you would like to include specific completion cases, but
* also support basic String completion cases.
* <br>
* When creating {@link Completion} annotations, you only need to register arguments a single time per class. For more
* information, see {@link Subcommand}.
*/
public abstract class Commander
{
/**
* The plugin which owns this command.
*/
private final JavaPlugin plugin;
/**
* The {@link Info} annotation for this command.
*/
private final Info info;
/**
* The {@link Permissive} annotation for this command.
*/
private final Permissive perms;
/**
* A map of all subcommands and their related methods for this command.
*/
private final Map<Subcommand, Method> subcommands;
/**
* A set of all {@link Completion} annotations for this command.
*/
private final Set<Completion> completions;
/**
* The method which should be called when the command is executed without any arguments.
*/
private final Method baseMethod;
/**
* Initializes this command object. The provided {@link JavaPlugin} should be the plugin which contains the
* command.
* <p>
* This constructor will automatically register all subcommands and completions for this command. It will also
* automatically infer all required information from the provided {@link Info} and {@link Permissive} annotations.
*
* @param plugin The plugin which contains this command.
*/
protected Commander(final @NotNull JavaPlugin plugin)
{
this.info = this.getClass()
.getDeclaredAnnotation(Info.class);
this.perms = this.getClass()
.getDeclaredAnnotation(Permissive.class);
this.plugin = plugin;
this.subcommands = new HashMap<>();
this.completions = new HashSet<>();
if (this.getClass()
.isAnnotationPresent(Base.class))
{
final Method method = Stream.of(this.getClass()
.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Base.class))
.findFirst()
.orElseThrow(() -> new RuntimeException(
"Base annotation present but no method found."));
this.baseMethod = method;
} else
{
this.baseMethod = null;
}
registerAnnotations();
}
/**
* Registers all subcommands and completions for this command.
*/
private void registerAnnotations()
{
Stream.of(this.getClass()
.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(Subcommand.class))
.forEach(method -> this.subcommands.put(
method.getDeclaredAnnotation(Subcommand.class),
method));
List.of(this.getClass()
.getDeclaredAnnotationsByType(Completion.class))
.stream()
.forEach(completions::add);
}
/**
* Gets the method which should be called when the command is executed without any arguments.
* <br>
* This method will return null if the command does not have a base method.
*
* @return The base method for this command, or null if the command does not have a base method.
*/
@Nullable
public Method getBaseMethod()
{
return baseMethod;
}
/**
* Gets the {@link Info} annotation for this command.
* <br>
* This method will never return null as this annotation is required for the command to be registered.
*
* @return The {@link Info} annotation for this command.
*/
@NotNull
Info getInfo()
{
return this.info;
}
/**
* Gets the {@link Permissive} annotation for this command.
* <br>
* This method will never return null as this annotation is required for the command to be registered.
*
* @return The {@link Permissive} annotation for this command.
*/
@NotNull
Permissive getPerms()
{
return this.perms;
}
/**
* @return The plugin which owns this command.
*/
@NotNull
public JavaPlugin getPlugin()
{
return this.plugin;
}
/**
* @return A map of all subcommands and their related methods for this command.
*/
@NotNull
Map<Subcommand, Method> getSubcommands()
{
return this.subcommands;
}
/**
* @return A set of all {@link Completion} annotations for this command.
*/
@Nullable
Set<Completion> getCompletions()
{
return this.completions;
}
}

View File

@ -0,0 +1,13 @@
package fns.patchwork.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This annotation is used to mark a method as the command's default method. This is the method that will be run to
* execute the command when a user inputs /{command}
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Base
{
}

View File

@ -0,0 +1,31 @@
package fns.patchwork.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Represents a tab completion for a command.
* <p>
* This will register at class level, and does not retain method information. As a result, you only need to register the
* arguments a single time, and it will always be used in tab completions.
*/
@Target(ElementType.TYPE)
@Repeatable(Completions.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface Completion
{
/**
* An array of possible arguments for this particular index, represented by {@link #index()}.
*
* @return An array of possible arguments for tab completion.
*/
String[] args();
/**
* @return The index in which these arguments should be shown.
*/
int index();
}

View File

@ -0,0 +1,22 @@
package fns.patchwork.command.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A marker interface which represents a holder for multiple {@link Completion} annotations.
* <br>
* <u>This interface is <b>NOT</b> intended for implementation and should
* <b>NOT</b> be used.</u>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Completions
{
/**
* @return The {@link Completion} annotations.
*/
Completion[] value();
}

View File

@ -0,0 +1,41 @@
package fns.patchwork.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This interface holds the information for each command. This annotation defines the command's name, description,
* usage, and aliases. Commands <b><u>must</u></b> have this annotation present to be registered with the handler.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Info
{
/**
* Technically, this is the only required value you must supply yourself. However, it is HIGHLY recommended you
* supply the other optional values as well, for better customization of your command.
*
* @return The command's name.
*/
String name();
/**
* By default, this is set to <u>"This is the default command description."</u>
*
* @return The command's description.
*/
String description() default "This is the default command description.";
/**
* By default, this is set to <u>"/&lt;command&gt;"</u>
*
* @return The command's usage.
*/
String usage() default "/<command>";
/**
* By default, this returns an empty array.
*
* @return The command's aliases.
*/
String[] aliases() default {};
}

View File

@ -0,0 +1,33 @@
package fns.patchwork.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This annotation holds the permission information for each command. This annotation defines the command's permission,
* whether it is only for players, and the message to send if the sender does not have permission to use the command.
* <p>
* Classes <u><b>MUST</b></u> have this annotation present to be registered with the handler.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Permissive
{
/**
* @return The command's permission.
*/
String perm();
/**
* By default, this is set to <u>false</u>.
*
* @return True if the command is only for players, false otherwise.
*/
boolean onlyPlayers() default false;
/**
* By default, this is set to <u>"You do not have permission to use this command."</u>
*
* @return The message to send if the sender does not have permission to use the command.
*/
String noPerms() default "You do not have permission to use this command.";
}

View File

@ -0,0 +1,46 @@
package fns.patchwork.command.annotation;
import fns.patchwork.command.CommandHandler;
import fns.patchwork.provider.ContextProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation should be used to mark methods as subcommand methods. Subcommand methods can have custom arguments
* <i>(current supported arguments can be found in the {@link ContextProvider})</i>, and can also have a custom
* permission. These subcommands can also be annotated with {@link Completions} to provide tab completions for the
* subcommand. The subcommand method must be public, and must be in a class that is registered with the
* {@link CommandHandler}.
* <p>
* Tab completions with the {@link Completions} annotation are only supported for subcommands. When registering
* completions, you only need to define the completion arguments a single time. If there are other methods which
* function as optional additional arguments for the subcommand, the previously registered arguments will still be
* present when the user does their tab completion.
* <p>
* For example, if you have a subcommand method with the arguments {@code (Player, String)}, and another method which
* has the arguments {@code (Player, String, String)}, the tab completions for the second method will still have the
* {@code Player} and {@code String} arguments registered from the first method. You will only need to provide a
* {@link Completion} for the additional 3rd argument.
* <p>
* Additionally, if the final argument is a String object, the BukkitDelegate will automatically append any additional
* arguments to the end of the String. For example, if you have a subcommand method with the arguments
* {@code (Player, String)}, and the user executes the command with the arguments {@code /command playerName arg2 arg3},
* the {@code String} argument will be {@code "arg2 arg3"}. This allows for us to use a String at the end of our
* subcommand arguments to allow for the user to input a reason.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subcommand
{
/**
* @return The permission to use when executing this subcommand.
*/
String permission();
/**
* @return The arguments, as classes, to use when registering this subcommand.
*/
Class<?>[] args() default {};
}

View File

@ -0,0 +1,131 @@
package fns.patchwork.config;
import fns.patchwork.api.Context;
import fns.patchwork.provider.ContextProvider;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Represents a configuration file of any type.
*/
public interface Configuration
{
/**
* Saves the configuration to the file.
*
* @throws IOException If the operation cannot be completed.
*/
void save() throws IOException;
/**
* Loads the configuration from the file.
*
* @throws IOException If the operation cannot be completed.
*/
void load() throws IOException;
/**
* @return The name of the file.
*/
String getFileName();
/**
* @return The actual Configuration {@link File}.
*/
File getConfigurationFile();
/**
* Gets a String object from the associated path.
*
* @param path The path to get the String from.
* @return The String object.
*/
String getString(String path);
/**
* Gets a Boolean object from the associated path.
*
* @param path The path to get the Boolean from.
* @return The Boolean object.
*/
Boolean getBoolean(String path);
/**
* Gets a List object from the associated path. This method will use {@link Context}s and the
* {@link ContextProvider} to get the object types in the list. If the objects cannot be inferred, the method will
* return a list of generic {@link Object}s.
*
* @param path The path to get the List from.
* @param <T> The type of the objects in the list.
* @return The List object.
*/
<T> List<T> getList(String path);
/**
* Gets a List object from the associated path. The List that is returned will be the String values which are stored
* within the configuration file at the given path.
*
* @param path The path to get the List from.
* @return The List object.
*/
List<String> getStringList(String path);
/**
* Gets an Integer from the associated path.
*
* @param path The path to get the Integer from.
* @return The Integer object.
*/
Integer getInt(String path);
/**
* Gets a Long from the associated path.
*
* @param path The path to get the Long from.
* @return The Long object.
*/
Long getLong(String path);
/**
* Gets a Double from the associated path.
*
* @param path The path to get the Double from.
* @return The Double object.
*/
Double getDouble(String path);
/**
* Sets the value at the given path to the given value.
*
* @param path The path to set the value at.
* @param value The value to set.
* @param <T> The type of the value.
*/
<T> void set(String path, T value);
/**
* Gets the value at the given path as the given type.
* <p>
* This method will use {@link Context}s and the {@link ContextProvider} to get the object type. If the object type
* cannot be inferred, the method will return a generic {@link Object}.
*
* @param path The path to get the value from.
* @param <T> The type of the value.
* @return The value at the given path.
*/
<T> T get(String path);
/**
* Gets the value at the given path as the given type.
* <p>
* This method will use {@link Context}s and the {@link ContextProvider} to get the object type. If the object type
* cannot be inferred, the method will return the given fallback value.
*
* @param path The path to get the value from.
* @param fallback The fallback value to return if the value at the given path is null.
* @param <T> The type of the value.
* @return The value at the given path.
*/
<T> T getOrDefault(String path, T fallback);
}

View File

@ -0,0 +1,6 @@
package fns.patchwork.config;
public final class YamlWrapper
{
}

View File

@ -0,0 +1,48 @@
package fns.patchwork.data;
import fns.patchwork.config.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* A registry for all the configurations.
*/
public class ConfigRegistry
{
/**
* A map of all the configurations.
*/
private final Map<String, Configuration> configurationList = new HashMap<>();
/**
* Registers a configuration.
*
* @param name The name of the configuration.
* @param configuration The configuration.
*/
public void register(final String name, final Configuration configuration)
{
configurationList.put(name, configuration);
}
/**
* Unregisters a configuration.
*
* @param name The name of the configuration.
*/
public void unregister(final String name)
{
configurationList.remove(name);
}
/**
* Gets a configuration.
*
* @param name The name of the configuration.
* @return The configuration.
*/
public Configuration getConfiguration(final String name)
{
return configurationList.get(name);
}
}

View File

@ -0,0 +1,64 @@
package fns.patchwork.data;
import fns.patchwork.event.FEvent;
import fns.patchwork.provider.EventProvider;
import java.util.ArrayList;
import java.util.List;
/**
* A registry for {@link FEvent}s.
*/
public class EventRegistry
{
/**
* The list of events.
*/
private final List<FEvent> events;
/**
* Creates a new event registry.
*/
public EventRegistry()
{
this.events = new ArrayList<>();
}
/**
* Registers an event.
*
* @param event The event to register.
*/
public void register(final FEvent event)
{
this.events.add(event);
}
/**
* Unregisters an event.
*
* @param event The event to unregister.
*/
public void unregister(final FEvent event)
{
this.events.remove(event);
}
/**
* Gets an {@link EventProvider} for the specified event class which contains the actual {@link FEvent} instance.
*
* @param clazz The event class.
* @param <T> The event type.
* @return The event provider.
*/
public <T extends FEvent> EventProvider<T> getEvent(final Class<T> clazz)
{
for (final FEvent event : this.events)
{
if (clazz.isInstance(event))
{
return () -> clazz.cast(event);
}
}
return null;
}
}

View File

@ -0,0 +1,75 @@
package fns.patchwork.data;
import fns.patchwork.security.Group;
import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
/**
* A registry for {@link Group}s.
*/
public class GroupRegistry
{
/**
* The list of groups.
*/
private final List<Group> groups;
/**
* Creates a new group registry.
*/
public GroupRegistry()
{
this.groups = new ArrayList<>();
}
/**
* Registers a group.
*
* @param group The group to register.
* @return {@code true} if the group was registered, {@code false} otherwise.
*/
public boolean registerGroup(final Group group)
{
return groups.add(group);
}
/**
* Unregisters a group.
*
* @param group The group to unregister.
* @return {@code true} if the group was unregistered, {@code false} otherwise.
*/
public boolean unregisterGroup(final Group group)
{
return groups.remove(group);
}
/**
* Gets a group by name.
*
* @param name The name of the group.
* @return The group, or {@code null} if no group was found.
*/
public Group getGroup(final String name)
{
final PlainTextComponentSerializer s = PlainTextComponentSerializer.plainText();
for (final Group group : groups)
{
final String n = s.serialize(group.getName());
if (n.equalsIgnoreCase(name))
{
return group;
}
}
return null;
}
/**
* @return The list of groups.
*/
public List<Group> getGroups()
{
return groups;
}
}

View File

@ -0,0 +1,69 @@
package fns.patchwork.data;
import fns.patchwork.provider.ModuleProvider;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.plugin.java.JavaPlugin;
/**
* A registry for modules.
*/
public class ModuleRegistry
{
/**
* The list of modules.
*/
private final List<JavaPlugin> plugins;
/**
* Creates a new module registry.
*/
public ModuleRegistry()
{
this.plugins = new ArrayList<>();
}
/**
* Adds a module to the registry.
*
* @param plugin The module to add.
*/
public void addModule(final JavaPlugin plugin)
{
if (this.plugins.contains(plugin))
{
return;
}
this.plugins.add(plugin);
}
/**
* Removes a module from the registry.
*
* @param plugin The module to remove.
*/
public void removeModule(final JavaPlugin plugin)
{
this.plugins.remove(plugin);
}
/**
* Gets a module from the registry wrapped in a {@link ModuleProvider}.
*
* @param clazz The class of the module.
* @param <T> The type of the module.
* @return The module.
*/
public <T extends JavaPlugin> ModuleProvider<T> getProvider(final Class<T> clazz)
{
for (final JavaPlugin plugin : plugins)
{
if (clazz.isInstance(plugin))
{
return () -> clazz.cast(plugin);
}
}
return () -> null;
}
}

View File

@ -0,0 +1,308 @@
package fns.patchwork.data;
import fns.patchwork.service.Service;
import fns.patchwork.service.ServiceSubscription;
import fns.patchwork.service.SubscriptionProvider;
import fns.patchwork.service.Task;
import fns.patchwork.service.TaskSubscription;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* A registry for all services and tasks registered with Patchwork.
* <br>
* This class is <b>not</b> thread-safe, and should only be accessed from the main server thread.
* <br>
* <br>
* <b>Services</b> are tickable tasks which execute every single game tick. They are registered using
* {@link #registerService(ServiceSubscription)} and can be started using {@link #startService(Class)}.
* <br>
* <br>
* <b>Tasks</b> are runnable tasks which execute at the provided times in the {@link Task} and
* {@link TaskSubscription} classes. These define whether the Task is repeating, delayed, or just a one-time task. Tasks
* are registered using {@link #registerTask(TaskSubscription)} and can be started using {@link #startTask(Class)}.
* <br>
* <br>
* <b>ServiceSubscriptions</b> and <b>TaskSubscriptions</b> can both be easily obtained using the
* {@link SubscriptionProvider} utility class.
*
* @see Service
* @see Task
* @see ServiceSubscription
* @see TaskSubscription
* @see SubscriptionProvider
*/
public class ServiceTaskRegistry
{
/**
* A list of all services registered with the registry.
*/
private final List<ServiceSubscription<?>> services;
/**
* A list of all tasks registered with the registry.
*/
private final List<TaskSubscription<?>> tasks;
/**
* Creates a new instance of the registry and initializes the service and task lists to new empty
* {@link ArrayList}s.
*/
public ServiceTaskRegistry()
{
this.services = new ArrayList<>();
this.tasks = new ArrayList<>();
}
/**
* Starts all services registered with the registry.
* <br>
* This method should be <i>avoided</i>, due to the fact that <b><i>modules may have registered their services after
* this method has already been called.</i></b> In this case, it is preferred to start each service using
* {@link #startService(Class)}.
* <br>
* However, <i><b>Patchwork calls this method when the server is starting up</b></i>, as Patchwork is the central
* resource manager for registered tasks and services. Patchwork will call this method on the first server tick, so
* unless you are registering services <b>AND</b> starting them <b>POST WORLD</b>, you do not need to worry about
* starting your services.
*/
public void startAllServices()
{
for (final ServiceSubscription<?> service : this.services)
{
service.start();
}
}
/**
* Starts all tasks registered with the registry.
* <br>
* This method should be <i>avoided</i>, due to the fact that <b><i>modules may have registered their tasks after
* this method has already been called.</i></b> In this case, it is preferred to start each task using
* {@link #startTask(Class)}.
* <br>
* However, <i><b>Patchwork calls this method when the server is starting up</b></i>, as Patchwork is the central
* resource manager for registered tasks and services. Patchwork will call this method on the first server tick, so
* unless you are registering tasks <b>AND</b> starting them <b>POST WORLD</b>, you do not need to worry about
* starting your tasks.
*/
public void startAllTasks()
{
for (final TaskSubscription<?> task : this.tasks)
{
task.start();
}
}
/**
* Stops all services registered with the registry.
* <br>
* This method should be <i>avoided</i>, due to the fact that <b><i>modules should be handling their own
* registrations</i></b>. It is preferred to use {@link #stopService(Class)} for each service you would like to
* stop.
* <br>
* However, <b><i>Patchwork calls this method when the server is shutting down</i></b>, or when Patchwork is being
* disabled, as Patchwork is the central resource manager for registered tasks and services. Unless you are
* <b>modifying service states while the server is running</b>, you do not need to worry about disabling or
* unregistering your services.
*/
public void stopAllServices()
{
for (final ServiceSubscription<?> service : this.services)
{
service.stop();
}
}
/**
* Stops all tasks registered with the registry.
* <br>
* This method should be <i>avoided</i>, due to the fact that <b><i>modules should be handling their own
* registrations</i></b>. It is preferred to use {@link #stopTask(Class)} for each task you would like to stop.
* <br>
* However, <b><i>Patchwork calls this method when the server is shutting down</i></b>, or when Patchwork is being
* disabled, as Patchwork is the central resource manager for registered tasks and services. Unless you are
* <b>modifying task states while the server is running</b>, you do not need to worry about disabling or
* unregistering your tasks.
*/
public void stopAllTasks()
{
for (final TaskSubscription<?> task : this.tasks)
{
task.stop();
}
}
/**
* Registers a service with the registry.
* <br>
* <i>Services must be registered using <b>ServiceSubscriptions</b></i>, which can be easily obtained through the
* {@link SubscriptionProvider} utility class.
*
* @param service The service you are trying to register.
* @param <T> A generic type for type inference of the service being registered.
*/
public <T extends Service> void registerService(final ServiceSubscription<T> service)
{
this.services.add(service);
}
/**
* Registers a task with the registry.
* <br>
* <i>Tasks must be registered using <b>TaskSubscriptions</b></i>, which can be easily obtained through the
* {@link SubscriptionProvider} utility class.
*
* @param task The task you are trying to register.
* @param <T> A generic type for type inference of the task being registered.
*/
public <T extends Task> void registerTask(final TaskSubscription<T> task)
{
this.tasks.add(task);
}
/**
* Starts a service using the specified {@link Service} class.
* <br>
* <i>The service should already be registered with the registry as a <b>ServiceSubscription</b></i>.
*
* @param clazz The class of the service you are trying to start.
* @see ServiceSubscription
* @see #registerService(ServiceSubscription)
*/
public void startService(final Class<? extends Service> clazz)
{
this.getService(clazz)
.start();
}
/**
* Gets a {@link ServiceSubscription} from the registry using the specified class.
* <br>
* <b>The class should be the <u>service class you are trying to locate</u>, not the class for the subscription
* itself</b>.
* <br>
* <i>The service should have been registered previously as a <b>ServiceSubscription</b></i>.
*
* @param clazz The class of the service you are trying to locate.
* @param <T> A generic type for type inference of the service requested.
* @return The {@link ServiceSubscription} for the specified class, or null if it could not be found.
* @see #registerService(ServiceSubscription)
* @see ServiceSubscription
*/
@Nullable
public <T extends Service> ServiceSubscription<T> getService(final Class<T> clazz)
{
for (final ServiceSubscription<?> service : this.services)
{
if (service.getService()
.getClass()
.equals(clazz))
{
return (ServiceSubscription<T>) service;
}
}
return null;
}
/**
* Stops a service using the specified {@link Service} class.
* <br>
* <i>The service should already be registered with the registry as a <b>ServiceSubscription</b></i>.
*
* @param clazz The class of the service you are trying to stop.
* @see #registerService(ServiceSubscription)
* @see ServiceSubscription
*/
public void stopService(final Class<? extends Service> clazz)
{
this.getService(clazz)
.stop();
}
/**
* Starts a task using the specified {@link Task} class.
* <br>
* <i>The task should already be registered with the registry as a <b>TaskSubscription</b></i>.
*
* @param clazz The class of the task you are trying to start.
* @see #registerTask(TaskSubscription)
* @see TaskSubscription
*/
public void startTask(final Class<? extends Task> clazz)
{
this.getTask(clazz)
.start();
}
/**
* Gets a {@link TaskSubscription} from the registry using the specified class.
* <br>
* <b>The class should be the <u>task class you are trying to locate</u>, not the class for the subscription
* itself</b>.
* <br>
* <i>The task should have been registered previously as a <b>TaskSubscription</b></i>.
*
* @param clazz The class of the task you are trying to locate.
* @param <T> A generic type for type inference of the task requested.
* @return The {@link TaskSubscription} for the specified class, or null if it could not be found.
* @see #registerTask(TaskSubscription)
* @see TaskSubscription
*/
public <T extends Task> TaskSubscription<T> getTask(final Class<T> clazz)
{
for (final TaskSubscription<?> task : this.tasks)
{
if (task.getTask()
.getClass()
.equals(clazz))
{
return (TaskSubscription<T>) task;
}
}
return null;
}
/**
* Stops a task using the specified {@link Task} class.
* <br>
* <i>The task should already be registered with the registry as a <b>TaskSubscription</b></i>.
*
* @param clazz The class of the task you are trying to stop.
* @see #registerTask(TaskSubscription)
* @see TaskSubscription
*/
public void stopTask(final Class<? extends Task> clazz)
{
this.getTask(clazz)
.stop();
}
/**
* Unregisters a service from the registry.
* <br>
* <i>The service should have been registered previously as a <b>ServiceSubscription</b></i>.
*
* @param clazz The service you are trying to unregister.
* @see #registerService(ServiceSubscription)
* @see ServiceSubscription
*/
public void unregisterService(final Class<? extends Service> clazz)
{
this.services.remove(getService(clazz));
}
/**
* Unregisters a task from the registry.
* <br>
* <i>The task should have been registered previously as a <b>TaskSubscription</b></i>.
*
* @param clazz The task you are trying to unregister.
* @see #registerTask(TaskSubscription)
* @see TaskSubscription
*/
public void unregisterTask(final Class<? extends Task> clazz)
{
this.tasks.remove(getTask(clazz));
}
}

View File

@ -0,0 +1,101 @@
package fns.patchwork.data;
import fns.patchwork.user.User;
import fns.patchwork.user.UserData;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.entity.Player;
/**
* A registry for {@link UserData} objects.
*/
public class UserRegistry
{
/**
* A map of {@link User} objects to {@link UserData} objects.
*/
private final Map<User, UserData> userDataMap;
/**
* Creates a new {@link UserRegistry}.
*/
public UserRegistry()
{
this.userDataMap = new HashMap<>();
}
/**
* Gets the {@link UserData} object for the given {@link User}.
*
* @param user The {@link User} to get the {@link UserData} for.
* @return The {@link UserData} object for the given {@link User}.
*/
public UserData getUserData(final User user)
{
return userDataMap.get(user);
}
/**
* Gets the {@link UserData} object for the given {@link Player}.
*/
public UserData fromPlayer(final Player player)
{
return userDataMap.entrySet()
.stream()
.filter(entry -> entry.getKey()
.getUniqueId()
.equals(player.getUniqueId()))
.findFirst()
.map(Map.Entry::getValue)
.orElse(null);
}
/**
* Gets the {@link User} object for the given {@link Player}.
*
* @param player The {@link Player} to get the {@link User} for.
* @return The {@link User} object for the given {@link Player}.
*/
public User getUser(final Player player)
{
return userDataMap.entrySet()
.stream()
.filter(entry -> entry.getKey()
.getUniqueId()
.equals(player.getUniqueId()))
.findFirst()
.map(Map.Entry::getKey)
.orElse(null);
}
/**
* Registers the given {@link User} and {@link UserData} objects.
*
* @param user The {@link User} to register.
* @param userData The {@link UserData} to register.
*/
public void registerUserData(final User user, final UserData userData)
{
userDataMap.put(user, userData);
}
/**
* Unregisters the given {@link User} and {@link UserData} objects.
*
* @param user The {@link User} to unregister.
*/
public void unregisterUserData(final User user)
{
userDataMap.remove(user);
}
/**
* Gets the map of {@link User} objects to {@link UserData} objects.
*
* @return The map of {@link User} objects to {@link UserData} objects.
*/
public Map<User, UserData> getUserDataMap()
{
return userDataMap;
}
}

View File

@ -0,0 +1,196 @@
package fns.patchwork.display;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a menu that can be opened by a player.
*/
public abstract class AbstractMenu
{
/**
* A map of all menus by their UUID.
*/
private static final Map<UUID, AbstractMenu> invByUUID = new HashMap<>();
/**
* A map of all open menus by the player's UUID.
*/
private static final Map<UUID, UUID> openInvs = new HashMap<>();
private final Map<Integer, ClickAction> actionMap;
/**
* The displayable that represents this menu.
*/
private final Displayable displayable;
/**
* The UUID of the displayable that represents this menu.
*/
private final UUID displayableUUID;
/**
* Creates a new menu with the specified size.
*
* @param size The size of the menu.
*/
protected AbstractMenu(final int size)
{
actionMap = new HashMap<>();
this.displayable = new Displayable(size);
this.displayableUUID = UUID.randomUUID();
invByUUID.put(getDisplayableUUID(), this);
}
/**
* @return A map of all menus by their UUID.
*/
public static Map<UUID, AbstractMenu> getInvByUUID()
{
return invByUUID;
}
/**
* @return A map of all open menus by the player's UUID.
*/
public static Map<UUID, UUID> getOpenInvs()
{
return openInvs;
}
/**
* @return The displayable UUID of this menu.
*/
public UUID getDisplayableUUID()
{
return displayableUUID;
}
/**
* Sets the item at the specified slot.
*
* @param slot The slot to set the item at.
* @param stack The item to set.
*/
public void setItem(final int slot, final @NotNull ItemStack stack)
{
setItem(slot, stack, null);
}
/**
* Sets the item at the specified slot.
*
* @param slot The slot to set the item at.
* @param stack The item to set.
* @param action The action to perform when the item is clicked.
*/
public void setItem(final int slot, final @NotNull ItemStack stack, final @Nullable ClickAction action)
{
getDisplayable().setItem(slot, stack);
if (action != null)
{
actionMap.put(slot, action);
}
}
/**
* @return The displayable that represents this menu.
*/
public Displayable getDisplayable()
{
return displayable;
}
/**
* Opens this menu for the specified player.
*
* @param player The player to open the menu for.
*/
public void open(final @NotNull Player player)
{
player.openInventory(getDisplayable());
openInvs.put(player.getUniqueId(), getDisplayableUUID());
}
/**
* Deletes this menu.
*/
public void delete()
{
Bukkit.getOnlinePlayers()
.forEach(player ->
{
if (openInvs.get(player.getUniqueId())
.equals(getDisplayableUUID()))
{
close(player);
}
});
invByUUID.remove(getDisplayableUUID());
}
/**
* Closes this menu for the specified player.
*
* @param player The player to close the menu for.
*/
public void close(final @NotNull Player player)
{
player.closeInventory();
openInvs.remove(player.getUniqueId());
}
/**
* @return A map of all actions by their slot.
*/
public Map<Integer, ClickAction> getActions()
{
return actionMap;
}
/**
* Creates a new item with the specified material and name.
*
* @param material The material of the item.
* @param name The name of the item.
* @return The created item.
*/
public ItemStack newItem(final @NotNull Material material, final @NotNull Component name)
{
return this.newItem(material, name, new Component[0]);
}
/**
* Creates a new item with the specified material, name, and lore.
*
* @param material The material of the item.
* @param name The name of the item.
* @param lore The lore of the item.
* @return The created item.
*/
public ItemStack newItem(final @NotNull Material material, final @NotNull Component name,
final @NotNull Component... lore)
{
final ItemStack item = new ItemStack(material, 1);
final ItemMeta meta = item.getItemMeta();
if (meta == null)
{
return item;
}
meta.displayName(name);
final List<Component> metaLore = Arrays.asList(lore);
meta.lore(metaLore);
item.setItemMeta(meta);
return item;
}
}

View File

@ -0,0 +1,380 @@
package fns.patchwork.display;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.Range;
/**
* This class is a wrapper for {@link BossBar} objects. It provides some handy methods for changing the boss bar's
* properties, displaying the bar to {@link Audience}s, and a {@link BossBarBuilder} to easily create new boss bars.
*/
public class BossBarDisplay
{
private BossBar bossBar;
/**
* Creates a new {@link BossBarDisplay} object.
*
* @param bossBar The {@link BossBar} to wrap.
*/
public BossBarDisplay(final BossBar bossBar)
{
this.bossBar = bossBar;
}
/**
* @return A new {@link BossBarBuilder} object.
*/
public static BossBarBuilder builder()
{
return new BossBarBuilder();
}
/**
* Changes the boss bar's color.
*
* @param color The new color.
*/
public void changeColor(final BossBar.Color color)
{
this.bossBar.color(color);
}
/**
* Changes the boss bar's color.
*
* @param overlay The new overlay.
*/
public void changeOverlay(final BossBar.Overlay overlay)
{
this.bossBar.overlay(overlay);
}
/**
* Changes the boss bar's name using a {@link Component}.
*
* @param name The new name.
*/
public void changeName(final Component name)
{
this.bossBar.name(name);
}
/**
* Changes the boss bar's name with a String and a {@link TextColor}.
*
* @param name The new name.
* @param color The name color.
*/
public void changeName(final String name, final TextColor color)
{
this.bossBar.name(Component.text(name, color));
}
/**
* Changes the boss bar's name with a String.
*
* @param name The new name.
*/
public void changeName(final String name)
{
this.bossBar.name(Component.text(name));
}
/**
* Shows this Boss Bar to the specified {@link Audience}.
*
* @param audience The {@link Audience} to show the Boss Bar to.
*/
public void showTo(final Audience audience)
{
audience.showBossBar(getBossBar());
}
/**
* @return The {@link BossBar} object that this class wraps.
*/
public BossBar getBossBar()
{
return this.bossBar;
}
/**
* Sets the {@link BossBar} object that this class wraps.
*
* @param bossBar The new {@link BossBar} object.
*/
public void setBossBar(final BossBar bossBar)
{
this.bossBar = bossBar;
}
/**
* Hides this Boss Bar from the specified {@link Audience}.
*
* @param audience The {@link Audience} to hide the Boss Bar from.
*/
public void hideFrom(final Audience audience)
{
audience.hideBossBar(getBossBar());
}
/**
* Increments the Bar's progress by the specified amount. This must be a range from 0 to 100.
*
* @param progress The new progress.
*/
public void incrementProgress(final @Range(from = 0, to = 100) float progress)
{
final float currentProgress = this.bossBar.progress();
final float newProgress = currentProgress + (progress / 100.0F);
if (newProgress > 1) this.bossBar.progress(1.0F);
else this.bossBar.progress(newProgress);
}
/**
* Decrements the Bar's progress by the specified amount. This must be a range from 0 to 100.
*
* @param progress The new progress.
*/
public void decrementProgress(final @Range(from = 0, to = 100) float progress)
{
final float currentProgress = this.bossBar.progress();
final float newProgress = currentProgress - (progress / 100.0F);
if (newProgress < 0) this.bossBar.progress(0.0F);
else this.bossBar.progress(newProgress);
}
/**
* Sets the Bar's progress to the maximum amount (full bar).
*/
public void maximumProgress()
{
this.bossBar.progress(1.0F);
}
/**
* Sets the Bar's progress to half of the maximum amount (half bar).
*/
public void halfProgress()
{
this.bossBar.progress(0.5F);
}
/**
* Sets the Bar's progress to the minimum amount (empty bar).
*/
public void minimumProgress()
{
this.bossBar.progress(0.0F);
}
/**
* Shows this Boss Bar to the specified {@link ForwardingAudience}.
*
* @param forwardingAudience The {@link ForwardingAudience} to show the Boss Bar to.
*/
public void showForwarded(final ForwardingAudience forwardingAudience)
{
forwardingAudience.showBossBar(getBossBar());
}
/**
* Hides this Boss Bar from the specified {@link ForwardingAudience}.
*
* @param forwardingAudience The {@link ForwardingAudience} to hide the Boss Bar from.
*/
public void hideForwarded(final ForwardingAudience forwardingAudience)
{
forwardingAudience.hideBossBar(getBossBar());
}
/**
* A Builder class for {@link BossBar} objects.
*/
public static final class BossBarBuilder
{
/**
* The flags that this Boss Bar will have.
*/
private final Set<BossBar.Flag> flags = new HashSet<>();
/**
* The Boss Bar's name.
*/
private Component name;
/**
* The Boss Bar's color.
*/
private BossBar.Color color;
/**
* The Boss Bar's overlay.
*/
private BossBar.Overlay overlay;
/**
* The Boss Bar's progress.
*/
@Range(from = 0, to = 1)
private float progress;
/**
* Initializes this Builder object.
*/
public BossBarBuilder()
{
this.name = Component.empty();
this.color = BossBar.Color.GREEN;
this.overlay = BossBar.Overlay.PROGRESS;
this.progress = 0.0F;
}
/**
* Sets the name of the boss bar.
*
* @param name The name of the boss bar.
* @return The builder.
*/
public BossBarBuilder setName(final Component name)
{
this.name = name;
return this;
}
/**
* Sets the name of the boss bar using a String and a {@link TextColor}.
*
* @param name The name of the boss bar.
* @param color The color of the boss bar.
* @return The builder.
*/
public BossBarBuilder setName(final String name, final TextColor color)
{
this.name = Component.text(name, color);
return this;
}
/**
* Sets the name of the boss bar using a String.
*
* @param name The name of the boss bar.
* @return The builder.
*/
public BossBarBuilder setName(final String name)
{
this.name = Component.text(name);
return this;
}
/**
* Adds a flag to the boss bar.
*
* @param flag The flag to add.
* @return The builder.
*/
public BossBarBuilder addFlag(final BossBar.Flag flag)
{
this.flags.add(flag);
return this;
}
/**
* Adds multiple flags to the boss bar.
*
* @param flags The flags to add.
* @return The builder.
*/
public BossBarBuilder addFlags(final BossBar.Flag... flags)
{
this.flags.addAll(List.of(flags));
return this;
}
/**
* Removes a flag from the boss bar.
*
* @param flag The flag to remove.
* @return The builder.
*/
public BossBarBuilder removeFlag(final BossBar.Flag flag)
{
this.flags.remove(flag);
return this;
}
/**
* Removes multiple flags from the boss bar.
*
* @param flags The flags to remove.
* @return The builder.
*/
public BossBarBuilder removeFlags(final BossBar.Flag... flags)
{
this.flags.removeAll(List.of(flags));
return this;
}
/**
* Clears all flags from the boss bar.
*
* @return The builder.
*/
public BossBarBuilder clearFlags()
{
this.flags.clear();
return this;
}
/**
* Sets the color of the boss bar.
*
* @param color The color of the boss bar.
* @return The builder.
*/
public BossBarBuilder setColor(final BossBar.Color color)
{
this.color = color;
return this;
}
/**
* Sets the overlay of the boss bar.
*
* @param overlay The overlay of the boss bar.
* @return The builder.
*/
public BossBarBuilder setOverlay(final BossBar.Overlay overlay)
{
this.overlay = overlay;
return this;
}
/**
* Sets the progress of the boss bar. This must satisfy {@code 0 <= progress <= 100}.
*
* @param progress The progress of the boss bar.
* @return The builder.
*/
public BossBarBuilder setProgress(final @Range(from = 0, to = 100) float progress)
{
this.progress = progress / 100.0F;
return this;
}
/**
* Builds the boss bar.
*
* @return The {@link BossBar}.
*/
public BossBar build()
{
return BossBar.bossBar(this.name, this.progress, this.color, this.overlay, this.flags);
}
}
}

View File

@ -0,0 +1,38 @@
package fns.patchwork.display;
import fns.patchwork.service.Task;
import java.time.Duration;
import org.bukkit.Bukkit;
public class BossBarTimer extends Task
{
private final BossBarDisplay bossBarDisplay;
private final Duration duration;
private double seconds = 0;
public BossBarTimer(final BossBarDisplay bossBarDisplay, final Duration duration)
{
super("BossBarTimer", -1L, 20L);
this.bossBarDisplay = bossBarDisplay;
this.duration = duration;
bossBarDisplay.minimumProgress();
bossBarDisplay.showTo(Bukkit.getServer());
}
@Override
public void run()
{
if (this.isCancelled()) return;
if (seconds >= duration.getSeconds())
{
bossBarDisplay.hideFrom(Bukkit.getServer());
this.cancel();
return;
}
final float percentage = (float) (seconds / duration.getSeconds()) * 100L;
bossBarDisplay.incrementProgress(percentage);
seconds++;
}
}

View File

@ -0,0 +1,18 @@
package fns.patchwork.display;
import org.bukkit.entity.Player;
/**
* Represents an action to be performed when a player clicks on an inventory slot in the respective
* {@link AbstractMenu}.
*/
@FunctionalInterface
public interface ClickAction
{
/**
* Called when a player clicks on an inventory slot in the respective {@link AbstractMenu}.
*
* @param player The player who clicked.
*/
void onClick(final Player player);
}

View File

@ -0,0 +1,497 @@
package fns.patchwork.display;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A class that represents an inventory that can be displayed to players. This class also represents the inventory
* holder which contains the inventory.
*/
public final class Displayable implements Inventory, InventoryHolder
{
/**
* The size of the inventory. This is always a multiple of 9.
*/
private final int size;
/**
* The contents of the inventory.
*/
private ItemStack[] contents;
/**
* Creates a new Displayable inventory with the given size. You are free to supply any size you want, but it will
* always be rounded up to the next multiple of 9. The maximum size allowed is 54. Any number higher than that will
* be rounded down to 54.
*
* @param size The size of the inventory.
*/
protected Displayable(final int size)
{
if (size < 1 || size > 54)
{
throw new IllegalArgumentException("Invalid size for Displayable inventory");
}
// If the size is not a multiple of nine, find the difference to the next highest multiple of 9 and make up
// the difference.
this.size = (size % 9 == 0)
? size
: size + (9 - size % 9);
this.contents = new ItemStack[size];
}
@Override
public int getSize()
{
return size;
}
@Override
public int getMaxStackSize()
{
return 64;
}
/**
* @param size The new maximum stack size for items in this inventory.
* @deprecated This method is not supported by Displayable inventories.
*/
@Override
@Deprecated(since = "1.19.4")
public void setMaxStackSize(final int size)
{
// No implementation required
}
@Override
public @Nullable ItemStack getItem(final int index)
{
if (index < 0 || index >= size)
{
return null;
}
return contents[index];
}
@Override
public void setItem(final int index, final @Nullable ItemStack item)
{
if (index < 0 || index >= size)
{
return;
}
contents[index] = item;
}
@Override
public @NotNull HashMap<Integer, ItemStack> addItem(final @NotNull ItemStack... items)
throws IllegalArgumentException
{
final HashMap<Integer, ItemStack> remainingItems = new HashMap<>();
for (final ItemStack item : items)
{
int remainingAmount = item.getAmount();
for (int i = 0; i < size; i++)
{
final ItemStack current = contents[i];
if (current == null)
{
final int maxStackSize = item.getMaxStackSize();
final int amountToAdd = Math.min(remainingAmount, maxStackSize);
final ItemStack clone = item.clone();
clone.setAmount(amountToAdd);
contents[i] = clone;
remainingAmount -= amountToAdd;
if (remainingAmount == 0)
{
break;
}
}
}
if (remainingAmount > 0)
{
remainingItems.put(remainingItems.size(), new ItemStack(item.getType(), remainingAmount));
}
}
return remainingItems;
}
@Override
public @NotNull HashMap<Integer, ItemStack> removeItem(final @NotNull ItemStack... items)
throws IllegalArgumentException
{
final HashMap<Integer, ItemStack> removedItems = new HashMap<>();
for (final ItemStack item : items)
{
int remainingAmount = item.getAmount();
for (int i = 0; i < size; i++)
{
final ItemStack current = contents[i];
if (current != null && current.isSimilar(item))
{
final int amountToRemove = Math.min(remainingAmount, current.getAmount());
current.setAmount(current.getAmount() - amountToRemove);
remainingAmount -= amountToRemove;
if (current.getAmount() <= 0)
{
contents[i] = null;
}
if (remainingAmount == 0)
{
break;
}
}
}
if (remainingAmount < item.getAmount())
{
removedItems.put(removedItems.size(),
new ItemStack(item.getType(), item.getAmount() - remainingAmount));
}
}
return removedItems;
}
@Override
public @NotNull HashMap<Integer, ItemStack> removeItemAnySlot(final @NotNull ItemStack... items)
throws IllegalArgumentException
{
return removeItem(items);
}
@Override
public @Nullable ItemStack @NotNull [] getContents()
{
return contents.clone();
}
@Override
public void setContents(final @Nullable ItemStack @NotNull [] items) throws IllegalArgumentException
{
if (items == null)
{
throw new IllegalArgumentException("Items cannot be null");
}
if (items.length != size)
{
throw new IllegalArgumentException("Invalid size of items array");
}
System.arraycopy(items, 0, contents, 0, size);
}
@Override
public @Nullable ItemStack @NotNull [] getStorageContents()
{
return contents;
}
@Override
public void setStorageContents(final @Nullable ItemStack @NotNull [] items) throws IllegalArgumentException
{
this.contents = items;
}
@Override
public boolean contains(final @NotNull Material material) throws IllegalArgumentException
{
for (final ItemStack item : contents)
{
if (item != null && item.getType() == material)
{
return true;
}
}
return false;
}
@Override
public boolean contains(final @Nullable ItemStack item)
{
if (item == null)
{
return false;
}
for (final ItemStack content : contents)
{
if (content != null && content.isSimilar(item))
{
return true;
}
}
return false;
}
@Override
public boolean contains(final @NotNull Material material, final int amount) throws IllegalArgumentException
{
int totalAmount = 0;
for (final ItemStack item : contents)
{
if (item != null && item.getType() == material)
{
totalAmount += item.getAmount();
if (totalAmount >= amount)
{
return true;
}
}
}
return false;
}
@Override
public boolean contains(final @Nullable ItemStack item, final int amount)
{
if (item == null)
{
return false;
}
int totalAmount = 0;
for (final ItemStack content : contents)
{
if (content != null && content.isSimilar(item))
{
totalAmount += content.getAmount();
if (totalAmount == amount)
{
return true;
}
}
}
return false;
}
@Override
public boolean containsAtLeast(final @Nullable ItemStack item, final int amount)
{
if (item == null)
{
return false;
}
int totalAmount = 0;
for (final ItemStack content : contents)
{
if (content != null && content.isSimilar(item))
{
totalAmount += content.getAmount();
if (totalAmount >= amount)
{
return true;
}
}
}
return false;
}
@Override
public @NotNull HashMap<Integer, ? extends ItemStack> all(final @NotNull Material material)
throws IllegalArgumentException
{
final HashMap<Integer, ItemStack> matchingItems = new HashMap<>();
for (int i = 0; i < size; i++)
{
final ItemStack item = contents[i];
if (item != null && item.getType() == material)
{
matchingItems.put(i, item);
}
}
return matchingItems;
}
@Override
public @NotNull HashMap<Integer, ? extends ItemStack> all(final @Nullable ItemStack item)
{
final HashMap<Integer, ItemStack> matchingItems = new HashMap<>();
if (item != null)
{
for (int i = 0; i < size; i++)
{
final ItemStack content = contents[i];
if (content != null && content.isSimilar(item))
{
matchingItems.put(i, content);
}
}
}
return matchingItems;
}
@Override
public int first(final @NotNull Material material) throws IllegalArgumentException
{
for (int i = 0; i < size; i++)
{
final ItemStack item = contents[i];
if (item != null && item.getType() == material)
{
return i;
}
}
return -1;
}
@Override
public int first(final @NotNull ItemStack item)
{
for (int i = 0; i < size; i++)
{
final ItemStack content = contents[i];
if (content != null && content.isSimilar(item))
{
return i;
}
}
return -1;
}
@Override
public int firstEmpty()
{
for (int i = 0; i < size; i++)
{
if (contents[i] == null)
{
return i;
}
}
return -1;
}
@Override
public boolean isEmpty()
{
for (final ItemStack content : contents)
{
if (content != null)
{
return false;
}
}
return true;
}
@Override
public void remove(final @NotNull Material material) throws IllegalArgumentException
{
for (int i = 0; i < size; i++)
{
final ItemStack item = contents[i];
if (item != null && item.getType() == material)
{
contents[i] = null;
}
}
}
@Override
public void remove(final @NotNull ItemStack item)
{
for (int i = 0; i < size; i++)
{
final ItemStack content = contents[i];
if (content != null && content.isSimilar(item))
{
contents[i] = null;
}
}
}
@Override
public void clear(final int index)
{
if (index >= 0 && index < size)
{
contents[index] = null;
}
}
@Override
public void clear()
{
for (int i = 0; i < size; i++)
{
contents[i] = null;
}
}
@Override
public int close()
{
return 0;
}
@Override
public @NotNull List<HumanEntity> getViewers()
{
return new ArrayList<>();
}
@Override
public @NotNull InventoryType getType()
{
return InventoryType.CHEST;
}
@Override
public @Nullable InventoryHolder getHolder()
{
return this;
}
@Override
public @Nullable InventoryHolder getHolder(final boolean useSnapshot)
{
return this;
}
@Override
public @NotNull ListIterator<ItemStack> iterator()
{
return iterator(0);
}
@Override
public @NotNull ListIterator<ItemStack> iterator(final int index)
{
return List.of(contents)
.listIterator(index);
}
@Override
public @Nullable Location getLocation()
{
return null;
}
@Override
public @NotNull Inventory getInventory()
{
return this;
}
}

View File

@ -0,0 +1,98 @@
package fns.patchwork.display;
import fns.patchwork.utils.kyori.FreedomAdventure;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
/**
* A view of a {@link Displayable} inventory.
* <p>
* This class can be used to display two separate {@link Displayable} objects to the Player.
*/
public class DisplayableView extends InventoryView
{
/**
* The upper inventory involved in this transaction.
*/
private final Displayable top;
/**
* The lower inventory involved in this transaction.
*/
private final Displayable bottom;
/**
* The player viewing the inventories involved in this transaction.
*/
private final Player player;
/**
* The type of inventory this transaction is for.
*/
private final InventoryType type;
/**
* The title of the main inventory involved in this transaction. The main inventory should always be the top
* inventory.
*/
private String title;
/**
* Creates a new DisplayableView.
*
* @param player The player viewing the inventories involved in this transaction.
* @param top The upper inventory involved in this transaction.
* @param bottom The lower inventory involved in this transaction.
*/
public DisplayableView(final Player player, final Displayable top, final Displayable bottom)
{
this.player = player;
this.top = top;
this.bottom = bottom;
this.type = InventoryType.CHEST;
this.title = FreedomAdventure.toPlainText(type.defaultTitle());
}
@Override
public @NotNull Inventory getTopInventory()
{
return top;
}
@Override
public @NotNull Inventory getBottomInventory()
{
return bottom;
}
@Override
public @NotNull HumanEntity getPlayer()
{
return player;
}
@Override
public @NotNull InventoryType getType()
{
return type;
}
@Override
@Deprecated(forRemoval = true, since = "1.16")
public @NotNull String getTitle()
{
return title;
}
@Override
public void setTitle(final @NotNull String title)
{
this.title = title;
}
@Override
public @NotNull String getOriginalTitle()
{
return FreedomAdventure.toPlainText(type.defaultTitle());
}
}

View File

@ -0,0 +1,249 @@
package fns.patchwork.display;
import java.time.Duration;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
/**
* A wrapper class for {@link Title}s that allows for easy display to an {@link Audience}.
*/
public class TitleDisplay
{
/**
* The {@link Title} to display.
*/
private Title title;
/**
* Creates a new {@link TitleDisplay} with the given {@link Title}.
*
* @param title The {@link Title} to display.
*/
public TitleDisplay(final Title title)
{
this.title = title;
}
/**
* @return A new {@link TitleBuilder} which can be used to create new {@link Title}s.
*/
public static TitleBuilder builder()
{
return new TitleBuilder();
}
/**
* Displays the {@link Title} to the given {@link Audience}.
*
* @param audience The {@link Audience} to display the {@link Title} to.
*/
public void displayTo(final Audience audience)
{
audience.clearTitle();
audience.showTitle(getTitle());
}
/**
* @return The {@link Title} to display.
*/
public Title getTitle()
{
return this.title;
}
/**
* Sets the {@link Title} to display.
*
* @param title The {@link Title} to display.
*/
public void setTitle(final Title title)
{
this.title = title;
}
/**
* Displays the {@link Title} to the given {@link ForwardingAudience}.
*
* @param forwardingAudience The {@link ForwardingAudience} to display the {@link Title} to.
*/
public void displayForwarded(final ForwardingAudience forwardingAudience)
{
forwardingAudience.clearTitle();
forwardingAudience.showTitle(getTitle());
}
/**
* A builder class for {@link Title}s.
*/
public static final class TitleBuilder
{
/**
* The main title of the {@link Title}.
*/
private Component mainTitle;
/**
* The subtitle of the {@link Title}.
*/
private Component subTitle;
/**
* How long the Title should fade in for.
*/
private Duration fadeIn;
/**
* How long the Title should fade out for.
*/
private Duration fadeOut;
/**
* How long the Title should be displayed for.
*/
private Duration displayDuration;
/**
* Creates a new {@link TitleBuilder} with default values. The default values are:
* <ul>
* <li>Empty main title</li>
* <li>Empty subtitle</li>
* <li>Default fade in time</li>
* <li>Default fade out time</li>
* <li>Default display duration</li>
* </ul>
*
* @see Title#DEFAULT_TIMES
*/
public TitleBuilder()
{
this.mainTitle = Component.empty();
this.subTitle = Component.empty();
this.fadeIn = Title.DEFAULT_TIMES.fadeIn();
this.fadeOut = Title.DEFAULT_TIMES.fadeOut();
this.displayDuration = Title.DEFAULT_TIMES.stay();
}
/**
* Sets the main title of the {@link Title}.
*
* @param title The main title of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setMainTitle(final String title)
{
this.mainTitle = Component.text(title);
return this;
}
/**
* Sets the main title of the {@link Title}.
*
* @param title The main title of the {@link Title}.
* @param titleColor The color of the main title.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setMainTitle(final String title, final TextColor titleColor)
{
this.mainTitle = Component.text(title, titleColor);
return this;
}
/**
* Sets the main title of the {@link Title}.
*
* @param mainTitle The main title of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setMainTitle(final Component mainTitle)
{
this.mainTitle = mainTitle;
return this;
}
/**
* Sets the subtitle of the {@link Title}.
*
* @param title The subtitle of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setSubTitle(final String title)
{
this.subTitle = Component.text(title);
return this;
}
/**
* Sets the subtitle of the {@link Title}.
*
* @param title The subtitle of the {@link Title}.
* @param titleColor The color of the subtitle.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setSubTitle(final String title, final TextColor titleColor)
{
this.subTitle = Component.text(title, titleColor);
return this;
}
/**
* Sets the subtitle of the {@link Title}.
*
* @param subTitle The subtitle of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setSubTitle(final Component subTitle)
{
this.subTitle = subTitle;
return this;
}
/**
* Sets the fade in time of the {@link Title}.
*
* @param duration The fade in time of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setFadeIn(final Duration duration)
{
this.fadeIn = duration;
return this;
}
/**
* Sets the fade out time of the {@link Title}.
*
* @param duration The fade out time of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setFadeOut(final Duration duration)
{
this.fadeOut = duration;
return this;
}
/**
* Sets the display duration of the {@link Title}.
*
* @param duration The display duration of the {@link Title}.
* @return The {@link TitleBuilder} instance.
*/
public TitleBuilder setDisplayDuration(final Duration duration)
{
this.displayDuration = duration;
return this;
}
/**
* Builds the {@link Title} with the given parameters.
*
* @return The built {@link Title}.
*/
public Title build()
{
return Title.title(
this.mainTitle,
this.subTitle,
Title.Times.times(this.fadeIn, this.displayDuration, this.fadeOut)
);
}
}
}

View File

@ -0,0 +1,119 @@
package fns.patchwork.display.adminchat;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
public class ACFormatBuilder
{
private char openTag = '[';
private char closeTag = ']';
private TextColor prefixColor = NamedTextColor.DARK_RED;
private TextColor bracketColor = NamedTextColor.WHITE;
private TextColor nameColor = NamedTextColor.AQUA;
private TextColor rankColor = NamedTextColor.GOLD;
private String prefix = "Admin";
private String chatSplitter = ">>";
private ACFormatBuilder()
{
}
public static ACFormatBuilder format()
{
return new ACFormatBuilder();
}
public ACFormatBuilder openBracket(final char openTag)
{
this.openTag = openTag;
return this;
}
public ACFormatBuilder closeBracket(final char closeTag)
{
this.closeTag = closeTag;
return this;
}
public ACFormatBuilder prefixColor(final TextColor prefixColor)
{
this.prefixColor = prefixColor;
return this;
}
public ACFormatBuilder bracketColor(final TextColor bracketColor)
{
this.bracketColor = bracketColor;
return this;
}
public ACFormatBuilder prefix(final String prefix)
{
this.prefix = prefix;
return this;
}
public ACFormatBuilder chatSplitter(final String chatSplitter)
{
this.chatSplitter = chatSplitter;
return this;
}
public ACFormatBuilder nameColor(final TextColor nameColor)
{
this.nameColor = nameColor;
return this;
}
public ACFormatBuilder rankColor(final TextColor rankColor)
{
this.rankColor = rankColor;
return this;
}
String openBracket()
{
return String.valueOf(openTag);
}
String closeBracket()
{
return String.valueOf(closeTag);
}
TextColor prefixColor()
{
return prefixColor;
}
TextColor bracketColor()
{
return bracketColor;
}
TextColor nameColor()
{
return nameColor;
}
TextColor rankColor()
{
return rankColor;
}
String prefix()
{
return prefix;
}
String chatSplitter()
{
return chatSplitter;
}
public AdminChatFormat build()
{
return new AdminChatFormat(this);
}
}

View File

@ -0,0 +1,141 @@
package fns.patchwork.display.adminchat;
import fns.patchwork.base.Patchwork;
import fns.patchwork.base.Shortcuts;
import fns.patchwork.security.Groups;
import fns.patchwork.user.UserData;
import io.papermc.paper.event.player.AsyncChatEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class AdminChatDisplay
{
protected static final String ACPERM = "patchwork.adminchat";
private final Map<UUID, AdminChatFormat> adminChatFormat = new HashMap<>();
private final Set<UUID> toggledChat = new HashSet<>();
public AdminChatDisplay()
{
new ACListener(this);
}
public void addPlayer(final Player player, final AdminChatFormat format)
{
adminChatFormat.put(player.getUniqueId(), format);
}
public void removePlayer(final Player player)
{
adminChatFormat.remove(player.getUniqueId());
}
public boolean hasPlayer(final Player player)
{
return adminChatFormat.containsKey(player.getUniqueId());
}
public void updateFormat(final Player player, final AdminChatFormat newFormat)
{
adminChatFormat.put(player.getUniqueId(), newFormat);
}
public AdminChatFormat getFormat(final Player player)
{
return adminChatFormat.get(player.getUniqueId());
}
public Set<UUID> getPlayers()
{
return adminChatFormat.keySet();
}
public Map<UUID, AdminChatFormat> getAdminChatFormat()
{
return adminChatFormat;
}
public boolean isToggled(final Player player)
{
return toggledChat.contains(player.getUniqueId());
}
public void toggleChat(final Player player)
{
if (toggledChat.contains(player.getUniqueId()))
{
toggledChat.remove(player.getUniqueId());
} else
{
toggledChat.add(player.getUniqueId());
}
}
public void adminChatMessage(final CommandSender sender, final Component message)
{
Bukkit.getOnlinePlayers()
.forEach(player ->
{
if (player.hasPermission(ACPERM))
{
final Component formatted = Component.empty();
formatted.append(getFormat(player).format(sender.getName(), Groups.fromSender(sender)))
.append(Component.space())
.append(message);
player.sendMessage(formatted);
}
});
}
public static final class ACListener implements Listener
{
private final AdminChatDisplay display;
public ACListener(final AdminChatDisplay display)
{
this.display = display;
Bukkit.getPluginManager()
.registerEvents(this, Shortcuts.provideModule(Patchwork.class));
}
@EventHandler
public void playerChat(final AsyncChatEvent event)
{
if (display.isToggled(event.getPlayer()))
{
event.setCancelled(true);
display.adminChatMessage(event.getPlayer(), event.message());
}
}
@EventHandler
public void playerJoin(final PlayerJoinEvent event)
{
final Player player = event.getPlayer();
if (player.hasPermission(ACPERM))
{
final UserData data = Patchwork.getInstance()
.getRegistrations()
.getUserRegistry()
.fromPlayer(player);
if (data.hasCustomACFormat())
{
display.addPlayer(player, data.getCustomACFormat());
} else
{
display.addPlayer(player, AdminChatFormat.DEFAULT);
}
}
}
}
}

View File

@ -0,0 +1,107 @@
package fns.patchwork.display.adminchat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public final class AdminChatFormat
{
public static final AdminChatFormat DEFAULT = ACFormatBuilder.format()
.build();
private final Component prefix;
private final Component userName;
private final Component rank;
private final Component chatSplitter;
private final Component fullFormat;
AdminChatFormat(final ACFormatBuilder builder)
{
this.prefix = Component.text(builder.openBracket(), builder.bracketColor())
.append(Component.text(builder.prefix(), builder.prefixColor()))
.append(Component.text(builder.closeBracket(), builder.bracketColor()));
this.userName = Component.text("%name%", builder.nameColor());
this.rank = Component.text(builder.openBracket(), builder.bracketColor())
.append(Component.text("%rank%", builder.rankColor()))
.append(Component.text(builder.closeBracket(), builder.bracketColor()));
// Nice formatting :(
if (builder.chatSplitter()
.equals(":"))
{
this.chatSplitter = Component.text(":", NamedTextColor.WHITE);
} else
{
this.chatSplitter = Component.space()
.append(Component.text(builder.chatSplitter(), NamedTextColor.WHITE));
}
// Formatting because []: is cleaner than [] :, but anything else such as [] >> or [] -> looks better with the space between.
this.fullFormat = prefix.appendSpace()
.append(userName)
.appendSpace()
.append(rank)
.append(chatSplitter)
.appendSpace();
}
public static AdminChatFormat deserialize(final String serialized)
{
final Component dez = LegacyComponentSerializer.legacyAmpersand()
.deserialize(serialized);
final Component prefix = dez.children()
.get(0);
final Component userName = dez.children()
.get(1);
final Component rank = dez.children()
.get(2);
final Component chatSplitter = dez.children()
.get(3);
return ACFormatBuilder.format()
.prefix(((TextComponent) prefix).content())
.prefixColor(prefix.color())
.nameColor(userName.color())
.rankColor(rank.color())
.chatSplitter(((TextComponent) chatSplitter).content())
.build();
}
public Component getPrefix()
{
return prefix;
}
public Component getUserName()
{
return userName;
}
public Component getRank()
{
return rank;
}
public Component getFullFormat()
{
return fullFormat;
}
public Component format(final String name, final String rank)
{
return fullFormat.replaceText(b ->
{
b.matchLiteral("%name%")
.replacement(name);
b.matchLiteral("%rank%")
.replacement(rank);
});
}
public String serialize()
{
return LegacyComponentSerializer.legacyAmpersand()
.serialize(fullFormat);
}
}

View File

@ -0,0 +1,14 @@
package fns.patchwork.economy;
/**
* Represents an immutable transaction that has been fully handled by a {@link Transactor} instance
*/
public interface CompletedTransaction extends Transaction
{
/**
* Gets the final result of the {@link Transaction}
*
* @return the result
*/
TransactionResult getResult();
}

View File

@ -0,0 +1,22 @@
package fns.patchwork.economy;
/**
* An entity that is able to transfer sums of currency between other {@link EconomicEntity}
*/
public interface EconomicEntity
{
/**
* Gets the {@link EconomicEntityData} (which contains various common metadata about this {@link EconomicEntity})
* associated with this class
*
* @return the {@link EconomicEntityData}
*/
EconomicEntityData getEconomicData();
/**
* The name of the economic entity
*
* @return the economic entity's name
*/
String getName();
}

View File

@ -0,0 +1,40 @@
package fns.patchwork.economy;
/**
* Metadata associated with a {@link EconomicEntity}
*/
public interface EconomicEntityData
{
/***
* @return the transaction freeze state
*/
boolean areTransactionsFrozen();
/***
* @return the balance
*/
long getBalance();
/**
* Sets the balance of the associated instance
*
* @param newBalance the new balance
*/
void setBalance(final long newBalance);
/**
* Adds the provided amount to the associated instance's balance
*
* @param amount the amount to add
* @return the new balance
*/
long addToBalance(final long amount);
/**
* Subtracts the provided amount from the associated instance's balance
*
* @param amount the amount to subtract
* @return the new balance
*/
long removeFromBalance(final long amount);
}

View File

@ -0,0 +1,33 @@
package fns.patchwork.economy;
/**
* A transaction that can be changed.
* <p>
* IMPORTANT NOTE: Please ensure that all modifications of {@link MutableTransaction} happen BEFORE it is passed to a
* {@link Transactor} implementation
*/
public interface MutableTransaction extends Transaction
{
/**
* Adds a number to the balance transferred in this transaction
*
* @param amount the amount to add
* @return the new amount
*/
long addToBalance(final long amount);
/**
* Subtracts a number from the balance transferred in this transaction
*
* @param amount the amount to remove
* @return the new amount
*/
long removeFromBalance(final long amount);
/**
* Sets the balance transferred in this transaction
*
* @param newBalance the new balance of the transaction
*/
void setBalance(final long newBalance);
}

View File

@ -0,0 +1,23 @@
package fns.patchwork.economy;
/**
* A class that denotes the transfer of currency between two EconomicEntity instances.
*/
public interface Transaction
{
/**
* @return the initiating entity
*/
EconomicEntity getSource();
/**
* @return the destination entity
*/
EconomicEntity getDestination();
/**
* @return the balance transferred in this Transaction
*/
long getBalance();
}

View File

@ -0,0 +1,14 @@
package fns.patchwork.economy;
/**
* A class that intercepts transactions after they are completed and logs them to a data point
*/
public interface TransactionLogger
{
/**
* Logs a completed transaction
*
* @param completedTransaction the completed transaction to log
*/
void logTransaction(CompletedTransaction completedTransaction);
}

View File

@ -0,0 +1,15 @@
package fns.patchwork.economy;
import net.kyori.adventure.text.Component;
/**
* A class that represents the result of a transaction
*/
public interface TransactionResult
{
String getMessage();
boolean isSuccessful();
Component getComponent();
}

View File

@ -0,0 +1,15 @@
package fns.patchwork.economy;
/**
* A class that implements the completion of transactions.
*/
public interface Transactor
{
/**
* Processes a transaction
*
* @param transaction the transaction to process
* @return the completed transaction
*/
CompletedTransaction handleTransaction(MutableTransaction transaction);
}

View File

@ -0,0 +1,7 @@
package fns.patchwork.event;
@FunctionalInterface
public interface Callback<T extends FEvent>
{
void call(T event);
}

View File

@ -0,0 +1,69 @@
package fns.patchwork.event;
import fns.patchwork.api.Context;
import fns.patchwork.base.Patchwork;
import fns.patchwork.service.Service;
import java.util.HashSet;
import java.util.Set;
public class EventBus extends Service
{
private final Patchwork plugin;
private final Set<FEvent> eventSet = new HashSet<>();
private final SubscriptionBox<?> runningSubscriptions = new SubscriptionBox<>();
public EventBus(final Patchwork plugin)
{
super("event_bus");
this.plugin = plugin;
}
public void addEvent(final FEvent event)
{
eventSet.add(event);
}
public <T extends FEvent> T getEvent(final Class<T> eventClass)
{
final FEvent e = eventSet.stream()
.filter(event -> event.getEventClass()
.equals(eventClass))
.findFirst()
.orElse(null);
return eventClass.cast(e);
}
public <T extends FEvent> EventSubscription<T> subscribe(final Class<T> eventClass, final Callback<T> callback)
{
final Context<T> eventContext = () -> eventSet.stream()
.filter(event -> event.getEventClass()
.equals(eventClass))
.findFirst()
.map(eventClass::cast)
.orElse(null);
if (eventContext.get() == null)
{
throw new IllegalArgumentException("Event class " + eventClass.getName() + " is not registered.");
}
return new EventSubscription<>(eventContext.get(), callback);
}
public void unsubscribe(final EventSubscription<?> subscription)
{
runningSubscriptions.removeSubscription(subscription);
}
public Patchwork getCommonsBase()
{
return plugin;
}
@Override
public void tick()
{
runningSubscriptions.tick();
}
}

View File

@ -0,0 +1,15 @@
package fns.patchwork.event;
public record EventSubscription<T extends FEvent>(T event, Callback<T> callback)
{
public boolean cancel()
{
return event().cancel();
}
public boolean isCancelled()
{
return event().isCancelled();
}
}

View File

@ -0,0 +1,40 @@
package fns.patchwork.event;
public abstract class FEvent
{
private boolean isCancelled;
private boolean triggered;
protected FEvent()
{
this.isCancelled = false;
}
public void ping()
{
this.triggered = true;
}
public void reset()
{
this.triggered = false;
}
boolean shouldCall()
{
return triggered;
}
public boolean cancel()
{
this.isCancelled = true;
return isCancelled();
}
public boolean isCancelled()
{
return isCancelled;
}
public abstract Class<? extends FEvent> getEventClass();
}

View File

@ -0,0 +1,38 @@
package fns.patchwork.event;
import java.util.ArrayList;
import java.util.List;
class SubscriptionBox<T extends FEvent>
{
private final List<EventSubscription<T>> subscriptions;
public SubscriptionBox()
{
this.subscriptions = new ArrayList<>();
}
public void addSubscription(final EventSubscription<T> subscription)
{
subscriptions.add(subscription);
}
public void removeSubscription(final EventSubscription<?> subscription)
{
subscriptions.remove(subscription);
}
public void tick()
{
subscriptions.forEach(s ->
{
if (!s.event()
.shouldCall()) return;
s.callback()
.call(s.event());
s.event()
.reset();
});
}
}

View File

@ -0,0 +1,56 @@
package fns.patchwork.logging;
import java.time.Instant;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public final class BlockInteraction implements Interaction<BlockData>
{
private final Location location;
private final UUID whoClicked;
private final Instant when;
private final BlockData originalState;
private final BlockData newState;
public BlockInteraction(final Player player, final BlockData originalState, final BlockData newState)
{
this.location = player.getLocation();
this.whoClicked = player.getUniqueId();
this.when = Instant.now();
this.originalState = originalState;
this.newState = newState;
}
@Override
public @NotNull UUID getWhoClicked()
{
return whoClicked;
}
@Override
public @NotNull BlockData getOriginalState()
{
return originalState;
}
@Override
public @NotNull BlockData getNewState()
{
return newState;
}
@Override
public @NotNull Instant getWhen()
{
return when;
}
@Override
public @NotNull Location getLocation()
{
return location;
}
}

View File

@ -0,0 +1,62 @@
package fns.patchwork.logging;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public final class ContainerInteraction implements Interaction<List<ItemStack>>
{
private final UUID whoClicked;
private final List<ItemStack> originalState;
private final List<ItemStack> newState;
private final Instant when;
private final Location location;
public ContainerInteraction(final Player player, final Container originalState, final Container newState)
{
this.whoClicked = player.getUniqueId();
this.originalState = Collections.unmodifiableList(Arrays.asList(originalState.getInventory()
.getContents()));
this.newState = Collections.unmodifiableList(Arrays.asList(newState.getInventory()
.getContents()));
this.location = originalState.getLocation();
this.when = Instant.now();
}
@Override
public @NotNull UUID getWhoClicked()
{
return whoClicked;
}
@Override
public @NotNull List<ItemStack> getOriginalState()
{
return originalState;
}
@Override
public @NotNull List<ItemStack> getNewState()
{
return newState;
}
@Override
public @NotNull Instant getWhen()
{
return when;
}
@Override
public @NotNull Location getLocation()
{
return location;
}
}

View File

@ -0,0 +1,32 @@
package fns.patchwork.logging;
import com.google.errorprone.annotations.Immutable;
import java.time.Instant;
import java.util.UUID;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
@Immutable
public interface Interaction<T>
{
@NotNull
static String format(@NotNull final Interaction<?> interaction)
{
return new InteractionFormatter().formatInteraction(interaction);
}
@NotNull
UUID getWhoClicked();
@NotNull
T getOriginalState();
@NotNull
T getNewState();
@NotNull
Instant getWhen();
@NotNull
Location getLocation();
}

View File

@ -0,0 +1,137 @@
package fns.patchwork.logging;
import java.time.Instant;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.inventory.ItemStack;
public final class InteractionFormatter
{
public String formatInteraction(final Interaction<?> interaction)
{
final String location = formatLocation(interaction.getLocation());
final String world = formatWorld(interaction.getLocation()
.getWorld());
final String player = interaction.getWhoClicked()
.toString();
final String block = formatBlock(interaction.getLocation()
.getBlock());
final String when = formatTime(interaction.getWhen());
if (interaction instanceof ContainerInteraction container)
{
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Container ")
.append(block)
.append(" at location ")
.append(location)
.append(" in world ")
.append(world)
.append(" was opened by ")
.append(player)
.append(" at ")
.append(when)
.append("\nHere is a list of items changed:\n");
container.getOriginalState()
.stream()
.filter(item ->
{
final ItemStack newItem = container.getNewState()
.stream()
.filter(item2 -> item2.isSimilar(item))
.findFirst()
.orElse(null);
return newItem == null || newItem.getAmount() != item.getAmount();
})
.forEach(item ->
{
final ItemStack newItem = container.getNewState()
.stream()
.filter(item2 -> item2.isSimilar(item))
.findFirst()
.orElse(null);
stringBuilder.append("Item ")
.append(formatItemStack(item))
.append(" was changed to ")
.append(formatItemStack(newItem))
.append("\n");
});
stringBuilder.append(".");
return stringBuilder.toString();
} else if (interaction instanceof BlockInteraction blockData)
{
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Block ")
.append(block)
.append(" at location ")
.append(location)
.append(" in world ")
.append(world)
.append(" was changed by ")
.append(player)
.append(" at ")
.append(when)
.append("\nBlock was changed from ")
.append(blockData.getOriginalState()
.getAsString())
.append(" to ")
.append(blockData.getNewState()
.getAsString())
.append(".");
return stringBuilder.toString();
} else
{
throw new IllegalArgumentException("Unknown interaction type: " + interaction.getClass()
.getName());
}
}
// Format: <x>,<y>,<z>
public String formatLocation(final Location location)
{
return String.format("%s,%s,%s", location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
// Format: <world>
public String formatWorld(final World world)
{
return world.getName();
}
// Format: <material>
public String formatBlock(final Block block)
{
return block.getType()
.toString()
.toLowerCase();
}
public String formatTime(final Instant instant)
{
final String trimmed = instant.toString()
.replaceAll("[TZ]", " ");
final int dotIndex = trimmed.indexOf('.');
return (dotIndex != -1)
? trimmed.substring(0, dotIndex)
: trimmed;
}
// Format: <item>,<amount>
public String formatItemStack(final ItemStack stack)
{
if (stack == null)
{
return String.format("%s,%s", "empty", "0");
}
return String.format("%s,%s", stack.getType()
.toString()
.toLowerCase(), stack.getAmount());
}
}

View File

@ -0,0 +1,50 @@
package fns.patchwork.particle;
import org.bukkit.Particle;
import org.bukkit.World;
/**
* A utility class for the 24 different note colors available in Minecraft. Each note is represented as a double value
* between 0 and 1. Furthermore, each note is a multiple of 1/24 within that range of 0 to 1.
* <p>
* For example, the note G is represented as 1/24, or 0.042. The note C is represented as 6/24, or 0.25.
* <p>
* When spawning particles, the count must be set to 0 and the extra value set between 0 and 1. The extra value is the
* size of the note particle. To add a color, use one of the provided methods in this class for the xOffset value in
* {@link World#spawnParticle(Particle, double, double, double, int, double, double, double, double)}. The xOffset value
* is the note color, with the yOffset and zOffset values set to 0.
*/
public final class NoteColorUtil
{
public static final double CYAN_NOTE_F_SHARP_LOW = 0;
public static final double CYAN_NOTE_G = 0.042;
public static final double GRAY_NOTE_G_SHARP = 0.083;
public static final double GRAY_NOTE_A = 0.125;
public static final double GRAY_NOTE_A_SHARP = 0.167;
public static final double MAGENTA_NOTE_B = 0.208;
public static final double RED_NOTE_C = 0.25;
public static final double YELLOW_NOTE_C_SHARP = 0.292;
public static final double YELLOW_NOTE_D = 0.333;
public static final double YELLOW_NOTE_D_SHARP_LOW = 0.375;
public static final double GRAY_NOTE_E = 0.417;
public static final double GRAY_NOTE_F = 0.458;
public static final double GRAY_NOTE_F_SHARP = 0.5;
public static final double LIGHT_BLUE_NOTE_G = 0.542;
public static final double BLUE_NOTE_G_SHARP = 0.583;
public static final double PURPLE_NOTE_A = 0.625;
public static final double PURPLE_NOTE_A_SHARP = 0.667;
public static final double PURPLE_NOTE_B = 0.708;
public static final double GRAY_NOTE_C = 0.75;
public static final double GRAY_NOTE_C_SHARP = 0.792;
public static final double GRAY_NOTE_D = 0.833;
public static final double YELLOW_NOTE_D_SHARP_HIGH = 0.875;
public static final double YELLOW_NOTE_E = 0.917;
public static final double YELLOW_NOTE_F = 0.958;
public static final double CYAN_NOTE_F_SHARP_HIGH = 1;
public static final double BLACK_NOTE_NA = 32768;
private NoteColorUtil()
{
throw new AssertionError();
}
}

View File

@ -0,0 +1,123 @@
package fns.patchwork.particle;
import fns.patchwork.api.Interpolator;
import fns.patchwork.utils.InterpolationUtils;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Color;
import org.bukkit.OfflinePlayer;
import org.bukkit.Particle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a Trail instance for a specific player.
*/
public interface Trail
{
/**
* Returns the UUID of the player associated with the trail. This is for usage with our persistant storage container
* so that we can safely send and retrieve the trails without having to directly reference a player object.
* <br>
* TL;DR Memory optimization!
*
* @return The UUID of the player associated with this trail.
*/
@NotNull
UUID getAssociatedPlayerUUID();
/**
* Returns the player associated with this trail. Trails are user specific, and should be persistent across all
* usages. This is also used when displaying the particles, as they will be relative to the player's back, which is
* an inverse offset of the player's eye location. We use OfflinePlayer as we can make a simple check and cast to
* determine if the player is online when spawning trails.
*
* @return The player associated with this Trail.
*/
@NotNull
OfflinePlayer getAssociatedPlayer();
/**
* Gets the Trail Type of this trail. This is used to determine what type of trail this is, and what
* {@link Particle} it should use.
*
* @return The Trail Type of this trail.
* @see TrailType
*/
@NotNull
TrailType getTrailType();
/**
* This method is nullable because if the value of {@link #isGradient()} is true, then {@link #getColors()} should
* be used instead, as that will contain the color data for our trail.
* <br>
* However, this method will also be null if the particle type is not colorable.
*
* @return The color of the trail, or null if the trail is a gradient or non-colorable.
* @see Particle
* @see #getColors()
*/
@Nullable
Color getColor();
/**
* Sets the static color of the trail. If you are trying to use a gradient, use {@link #setColors(Set)} instead.
* <br>
*
* @param color The color to set the trail to.
*/
void setColor(@NotNull Color color);
/**
* This method is nullable because if the value of {@link #isGradient()} is false, then {@link #getColor()} should
* be used instead, as our trail is a single static color.
* <br>
* However, this method will also be null if the particle type is not colorable.
*
* @return The colors of the trail, or null if the trail is not a gradient or non-colorable.
* @see #getColor()
* @see Particle
* @see InterpolationUtils
* @see Interpolator
*/
@Nullable
Set<Color> getColors();
/**
* Sets the colors of the trail. If you are trying to use a static color, use {@link #setColor(Color)} instead.
* <br>
* This should be used for trails that iterate over a set of colors, such as a rainbow trail.
*
* @param colors The colors to set the trail to. It is recommended to use {@link InterpolationUtils} to generate
* interpolated gradients for this.
*/
void setColors(@NotNull Set<Color> colors);
/**
* Validates whether this Trail is a gradient or a static trail.
* <br>
* This is entirely based on whether {@link #getColors()} returns null or not.
*
* @return True if {@link #getColors()} is not null, false otherwise.
*/
boolean isGradient();
/**
* Gets whether the trail is active.
*
* @return True if the trail is active, false if it is not.
*/
boolean isActive();
/**
* Turn the trail on or off.
*
* @param active True if the trail should be active, false if it should not.
*/
void setActive(final boolean active);
/**
* Spawns a particle (if gradient, the next particle) on the supplied location object.
*/
void spawnParticle();
}

View File

@ -0,0 +1,52 @@
package fns.patchwork.particle;
import org.bukkit.Particle;
public enum TrailType
{
/**
* Default trail type. Uses {@link Particle#REDSTONE}. This trail is colorable. Use {@link Particle.DustOptions} to
* set the particle properties.
*/
DEFAULT(Particle.REDSTONE),
/**
* A trail that uses {@link Particle#HEART}. This is not modifiable and will always have the same size shape and
* color.
*/
HEART(Particle.HEART),
/**
* A trail that uses {@link Particle#FLAME}. This is not modifiable and will always have the same size shape and
* color.
*/
FLAME(Particle.FLAME),
/**
* A trail that uses {@link Particle#REDSTONE}. This particle however is rainbow-colored by default and cannot have
* additional options set.
*/
RAINBOW(Particle.REDSTONE),
/**
* A trail that uses {@link Particle#NOTE}. This is colorable, however you are limited to the 24 different note
* colors available in Minecraft.
*/
MUSIC(Particle.NOTE),
SNOW(Particle.SNOWBALL),
SPELL(Particle.SPELL_MOB),
SPELL_AMBIENT(Particle.SPELL_MOB_AMBIENT),
PORTAL(Particle.PORTAL),
ENCHANTMENT(Particle.ENCHANTMENT_TABLE),
STROBE(Particle.DUST_COLOR_TRANSITION),
VIBRATION(Particle.VIBRATION),
SPARK(Particle.ELECTRIC_SPARK);
final Particle type;
TrailType(final Particle type)
{
this.type = type;
}
public Particle getType()
{
return type;
}
}

View File

@ -0,0 +1,205 @@
package fns.patchwork.provider;
import fns.patchwork.command.BukkitDelegate;
import fns.patchwork.command.annotation.Subcommand;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* This class is used to provide context to subcommand methods. This class is used by the BukkitDelegate to parse
* arguments for subcommands. The following types are supported:
* <ul>
* <li>Boolean</li>
* <li>Double</li>
* <li>Integer</li>
* <li>Long</li>
* <li>Float</li>
* <li>Material</li>
* <li>Player</li>
* <li>World</li>
* <li>Location</li>
* <li>CommandSender</li>
* <li>Component</li>
* </ul>
* All of these types can be parsed from a String input. If the String cannot be parsed into any of
* these types, then null will be returned.
*
* @see #fromString(String, Class)
*/
public class ContextProvider
{
public <T> T fromString(final String string, final Class<T> clazz)
{
return Stream.of(toBoolean(string, clazz),
toDouble(string, clazz),
toInt(string, clazz),
toLong(string, clazz),
toFloat(string, clazz),
toMaterial(string, clazz),
toPlayer(string, clazz),
toWorld(string, clazz),
toLocation(string, clazz),
toComponent(string, clazz))
.filter(Objects::nonNull)
.findFirst()
.map(clazz::cast)
.orElse(null);
}
private @Nullable Boolean toBoolean(final String string, final Class<?> clazz)
{
if (clazz != Boolean.class)
return null;
// Previously we used Boolean#parseBoolean, but that will always return a value and does not throw
// an exception. This means that if the string is not "true" or "false", it will return false.
if (string.equalsIgnoreCase("true"))
return true;
if (string.equalsIgnoreCase("false"))
return false;
return null;
}
private @Nullable Double toDouble(final String string, final Class<?> clazz)
{
if (clazz != Double.class)
return null;
try
{
return Double.parseDouble(string);
}
catch (NumberFormatException ignored)
{
return null;
}
}
private @Nullable Integer toInt(final String string, final Class<?> clazz)
{
if (clazz != Integer.class)
return null;
try
{
return Integer.parseInt(string);
}
catch (NumberFormatException ignored)
{
return null;
}
}
private @Nullable Long toLong(final String string, final Class<?> clazz)
{
if (clazz != Long.class)
return null;
try
{
return Long.parseLong(string);
}
catch (NumberFormatException ignored)
{
return null;
}
}
private @Nullable Float toFloat(final String string, final Class<?> clazz)
{
if (clazz != Float.class)
return null;
try
{
return Float.parseFloat(string);
}
catch (NumberFormatException ignored)
{
return null;
}
}
private @Nullable Material toMaterial(final String string, final Class<?> clazz)
{
if (clazz != Material.class)
return null;
return Material.matchMaterial(string);
}
private @Nullable Player toPlayer(final String string, final Class<?> clazz)
{
if (clazz != Player.class)
return null;
return Bukkit.getPlayer(string);
}
private @Nullable World toWorld(final String string, final Class<?> clazz)
{
if (clazz != World.class)
return null;
return Bukkit.getWorld(string);
}
/**
* When using this method, the next four arguments must be world, x, y, z.
* The world must be a valid world name, and x, y, and z must be valid doubles.
* If any of these are invalid, this will return null.
*
* @param string The string to parse
* @return A location object if xyz is valid
* @see BukkitDelegate#processSubCommands(String[], CommandSender, ContextProvider, Subcommand)
*/
private @Nullable Location toLocation(final String string, final Class<?> clazz)
{
if (clazz != Location.class)
return null;
final String[] split = string.split(" ");
if (split.length != 4 || toWorld(split[0], World.class) == null)
return null;
try
{
final double x = Double.parseDouble(split[1]);
final double y = Double.parseDouble(split[2]);
final double z = Double.parseDouble(split[3]);
return new Location(toWorld(split[0], World.class), x, y, z);
}
catch (NumberFormatException ex)
{
return null;
}
}
private @Nullable Component toComponent(final String string, final Class<?> clazz)
{
if (clazz != Component.class)
return null;
return Component.text(string);
}
public @NotNull <T> List<@Nullable T> getList(final List<String> resolvable, final Class<T> clazz)
{
final List<T> resolved = new ArrayList<>();
for (final String entry : resolvable)
{
resolved.add(this.fromString(entry, clazz));
}
return resolved;
}
}

View File

@ -0,0 +1,9 @@
package fns.patchwork.provider;
import fns.patchwork.event.FEvent;
@FunctionalInterface
public interface EventProvider<T extends FEvent>
{
T getEvent();
}

View File

@ -0,0 +1,9 @@
package fns.patchwork.provider;
import org.bukkit.plugin.java.JavaPlugin;
@FunctionalInterface
public interface ModuleProvider<T extends JavaPlugin>
{
T getModule();
}

View File

@ -0,0 +1,8 @@
package fns.patchwork.provider;
import fns.patchwork.service.Service;
public interface ServiceProvider<T extends Service>
{
T getService();
}

View File

@ -0,0 +1,41 @@
package fns.patchwork.security;
import net.kyori.adventure.text.Component;
/**
* Represents a permissible group which holds a set of permissions that can then be applied to a User / Player.
*/
public interface Group extends PermissionHolder
{
/**
* @return The name of the group.
*/
Component getName();
/**
* @return The prefix of the group.
*/
Component getPrefix();
/**
* @return The abbreviation of the group.
*/
Component getAbbreviation();
/**
* @return The weight of the group.
*/
int getWeight();
/**
* If more than one group is marked as default, the first retrieved default group will be used.
*
* @return Whether this is the default group.
*/
boolean isDefault();
/**
* @return Whether the group is hidden.
*/
boolean isHidden();
}

View File

@ -0,0 +1,55 @@
package fns.patchwork.security;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public enum Groups
{
NON_OP("patchwork.group.non-op", "Non-OP"),
OP("patchwork.group.op", "OP"),
SUPER_ADMIN("patchwork.group.super", "Super Admin"),
SENIOR_ADMIN("patchwork.group.senior", "Senior Admin"),
DEVELOPER("patchwork.group.dev", "Developer"),
EXECUTIVE("patchwork.group.exec", "Executive"),
OWNER("patchwork.group.owner", "Owner");
private final String permission;
private final String name;
Groups(final String permission, final String name)
{
this.permission = permission;
this.name = name;
}
public String getName()
{
return this.name;
}
public String getPermission()
{
return this.permission;
}
@Override
public String toString()
{
return this.permission;
}
public static String fromPlayer(final Player player) {
for (final Groups group : values()) {
if (player.hasPermission(group.getPermission())) {
return group.getName();
}
}
return Groups.NON_OP.getName();
}
public static String fromSender(final CommandSender sender) {
if (!(sender instanceof Player)) return "CONSOLE";
return fromPlayer((Player) sender);
}
}

View File

@ -0,0 +1,24 @@
package fns.patchwork.security;
import javax.annotation.concurrent.Immutable;
import org.bukkit.permissions.Permission;
@Immutable
public interface Node
{
String key();
Permission bukkit();
NodeType type();
boolean compare(Node node);
long expiry();
boolean isExpired();
boolean isTemporary();
boolean wildcard();
}

View File

@ -0,0 +1,14 @@
package fns.patchwork.security;
public interface NodeBuilder
{
NodeBuilder key(String key);
NodeBuilder expiry(long expiry);
NodeBuilder type(NodeType type);
NodeBuilder wildcard(boolean wildcard);
Node build();
}

View File

@ -0,0 +1,10 @@
package fns.patchwork.security;
public enum NodeType
{
INHERITANCE,
PREFIX,
SUFFIX,
PERMISSION,
WEIGHT
}

View File

@ -0,0 +1,16 @@
package fns.patchwork.security;
import java.util.Set;
import java.util.UUID;
import org.bukkit.permissions.Permissible;
public interface PermissionHolder extends Permissible
{
UUID getUniqueId();
Set<Node> permissions();
boolean addPermission(Node node);
boolean removePermission(Node node);
}

View File

@ -0,0 +1,141 @@
package fns.patchwork.service;
import fns.patchwork.base.Patchwork;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
/**
* This is a holder class for {@link Executor} objects that are used to delegate runnable tasks to the Bukkit Scheduler.
* This class is here for both convenience purposes, and also for the sake of providing easy access to executors for
* {@link CompletableFuture} invocations.
*/
public class FreedomExecutor
{
/**
* An executor which runs tasks synchronously.
*/
private final Executor syncExecutor;
/**
* An executor which runs tasks asynchronously.
*/
private final Executor asyncExecutor;
/**
* Creates a new {@link FreedomExecutor} instance.
*/
public FreedomExecutor()
{
syncExecutor = r -> Bukkit.getScheduler()
.runTask(Patchwork.getInstance(), r);
asyncExecutor = r -> Bukkit.getScheduler()
.runTaskAsynchronously(Patchwork.getInstance(), r);
}
/**
* Creates a new {@link Executor} that is capable of executing a runnable one singular time, synchronously.
*
* @param plugin The plugin to run the task for.
* @return A new {@link Executor} instance.
*/
public Executor singleExecutor(final JavaPlugin plugin)
{
return r -> Bukkit.getScheduler()
.runTask(plugin, r);
}
/**
* Creates a new {@link Executor} that is capable of executing a runnable one singular time, synchronously. This
* Executor will wait for the supplied delay before executing the runnable.
*
* @param plugin The plugin to run the task for.
* @param delay The delay to wait before executing the runnable.
* @return A new {@link Executor} instance.
*/
public Executor delayedExecutor(final JavaPlugin plugin, final long delay)
{
return r -> Bukkit.getScheduler()
.runTaskLater(plugin, r, delay);
}
/**
* Creates a new {@link Executor} tthat is capable of executing a runnable on a periodic basis, synchronously. This
* executor can also be supplied a delay to indicate it should wait the specified amount of time before executing
* the runnable for the first time.
*
* @param plugin The plugin to run the task for.
* @param initialDelay The delay to wait before executing the runnable for the first time.
* @param period The period to wait between each execution of the runnable.
* @return A new {@link Executor} instance.
*/
public Executor periodicExecutor(final JavaPlugin plugin, final long initialDelay, final long period)
{
return r -> Bukkit.getScheduler()
.runTaskTimer(plugin, r, initialDelay, period);
}
/**
* Creates a new {@link Executor} that is capable of executing a runnable one singular time, asynchronously.
*
* @param plugin The plugin to run the task for.
* @return A new {@link Executor} instance.
*/
public Executor asynchronousSingleExecutor(final JavaPlugin plugin)
{
return r -> Bukkit.getScheduler()
.runTaskAsynchronously(plugin, r);
}
/**
* Creates a new {@link Executor} that is capable of executing a runnable one singular time, asynchronously. This
* Executor will wait for the supplied delay before executing the runnable.
*
* @param plugin The plugin to run the task for.
* @param delay The delay to wait before executing the runnable.
* @return A new {@link Executor} instance.
*/
public Executor asynchronousDelayedExecutor(final JavaPlugin plugin, final long delay)
{
return r -> Bukkit.getScheduler()
.runTaskLaterAsynchronously(plugin, r, delay);
}
/**
* Creates a new {@link Executor} tthat is capable of executing a runnable on a periodic basis, asynchronously. This
* executor can also be supplied a delay to indicate it should wait the specified amount of time before executing
* the runnable for the first time.
*
* @param plugin The plugin to run the task for.
* @param delay The delay to wait before executing the runnable for the first time.
* @param period The period to wait between each execution of the runnable.
* @return A new {@link Executor} instance.
*/
public Executor asynchronousPeriodicExecutor(final JavaPlugin plugin, final long delay, final long period)
{
return r -> Bukkit.getScheduler()
.runTaskTimerAsynchronously(plugin, r, delay, period);
}
/**
* Gets the synchronous executor instance. This is a convenience for {@link CompletableFuture} invocations, when
* defining a custom executor for the {@link CompletableFuture}.
*
* @return The synchronous executor instance.
*/
public Executor getSync()
{
return syncExecutor;
}
/**
* Gets the asynchronous executor instance. This is a convenience for {@link CompletableFuture} invocations, when
* defining a custom executor for the {@link CompletableFuture}.
*
* @return The asynchronous executor instance.
*/
public Executor getAsync()
{
return asyncExecutor;
}
}

View File

@ -0,0 +1,47 @@
package fns.patchwork.service;
/**
* Represents a ticking service. Services may be asynchronous or synchronous, however there are some restrictions:
* <ul>
* <li>Sync services may not have a complexity greater than 5.</li>
* <li>Async services may not interact with the Bukkit API in any form.</li>
* </ul>
*/
public abstract class Service
{
/**
* The name of the service.
*/
private final String name;
/**
* Creates a new service with the given name.
*
* @param name The name of the service.
*/
protected Service(final String name)
{
this.name = name;
}
/**
* This method is called every single tick, or at least, every tick interval described by the holding
* {@link ServiceSubscription}. Since this runs every single tick, the body of this method should not have a
* complexity higher than 5. This includes Cyclomatic, Cognitive, and NPath complexities. If the service is
* asynchronous, there is a bit more flexibility with the complexity rating, extending to no more than 10. However,
* it's generally good practice to keep the complexity of ticking services as low as possible to avoid extensive
* resource consumption.
*
* @see ServiceSubscription
* @see SubscriptionProvider
*/
public abstract void tick();
/**
* @return The name of the service.
*/
public String getName()
{
return name;
}
}

View File

@ -0,0 +1,188 @@
package fns.patchwork.service;
import java.util.concurrent.Executor;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
/**
* Represents a subscription to a {@link Service}.
* <p>
* Subscriptions contain some information about the service itself and it's presence on the scheduler. For example,
* {@link #getServiceId()} will return the ID of the task which was returned by the scheduler. Subscriptions also manage
* the state of the service, using {@link #isActive()} to determine if the service is currently running.
* <br>
* <br>
* The subscription itself provides type inference to safely store the actual service instance. This is useful for when
* we need to access the service itself, without calling to the service directly.
*
* @param <T> The type of service this subscription is for.
*/
public final class ServiceSubscription<T extends Service>
{
/**
* The service this subscription is for.
*/
private final T service;
/**
* Whether this is an asynchronous service.
*/
private final boolean async;
/**
* The executor used to schedule the service.
*/
private final Executor executor;
/**
* The ID of the service from the associated {@link BukkitTask} which was returned by the Scheduler.
*/
private final int serviceId;
/**
* Whether the service is currently running.
*/
private boolean isActive = false;
/**
* Creates a new subscription for the given service. By default, this method will mark this service as a synchronous
* service. This will also initialize the default interval to a single tick.
* <br>
* If you are trying to create an asynchronous service, use
* {@link #ServiceSubscription(JavaPlugin, Service, boolean)} instead.
* <br>
* If you would like to define a custom interval, use either {@link #ServiceSubscription(JavaPlugin, Service, long)}
* or {@link #ServiceSubscription(JavaPlugin, Service, long, boolean)} (for asynchronous services).
*
* @param plugin The plugin which owns the service.
* @param service The service to subscribe to.
*/
ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service)
{
this(plugin, service, 1L, false);
}
/**
* Creates a new subscription for the given service. This will initialize the default interval to a single tick.
* <br>
* If you would like to define a custom interval, use either {@link #ServiceSubscription(JavaPlugin, Service, long)}
* or {@link #ServiceSubscription(JavaPlugin, Service, long, boolean)} (for asynchronous services).
*
* @param plugin The plugin which owns the service.
* @param service The service to subscribe to.
* @param async Whether the service should be scheduled asynchronously.
*/
ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final boolean async)
{
this(plugin, service, 1L, async);
}
/**
* Creates a new subscription for the given service. By default, this will mark the service as synchronous. When
* defining a custom interval, the interval should be less than 20L (the number of ticks in a second). For anything
* that requires an interval greater than 1 second, use a {@link Task} instead.
* <br>
* If you are trying to create an asynchronous service, use
* {@link #ServiceSubscription(JavaPlugin, Service, long, boolean)} instead.
*
* @param plugin The plugin which owns the service.
* @param service The service to subscribe to.
* @param interval The interval at which the service should be scheduled.
*/
ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service, final long interval)
{
this(plugin, service, interval, false);
}
/**
* Creates a new subscription for the given service. When defining a custom interval, the interval should be less
* than 20L (the number of ticks in a second). For anything that requires an interval greater than 1 second, use a
* {@link Task} instead.
*
* @param plugin The plugin which owns the service.
* @param service The service to subscribe to.
* @param interval The interval at which the service should be scheduled.
* @param async Whether the service should be scheduled asynchronously.
*/
ServiceSubscription(@NotNull final JavaPlugin plugin, @NotNull final T service,
final long interval, final boolean async)
{
this.service = service;
this.async = async;
final int[] tempId = new int[1];
if (async)
{
this.executor = r ->
{
final BukkitTask task = Bukkit.getScheduler()
.runTaskTimerAsynchronously(plugin, r, 0, interval);
tempId[0] = task.getTaskId();
};
} else
{
this.executor = r ->
{
final BukkitTask task = Bukkit.getScheduler()
.runTaskTimer(plugin, r, 0, interval);
tempId[0] = task.getTaskId();
};
}
this.serviceId = tempId[0];
}
/**
* Starts the service.
*/
public void start()
{
this.isActive = true;
this.executor.execute(service::tick);
}
/**
* Stops the service.
*/
public void stop()
{
this.isActive = false;
Bukkit.getScheduler()
.cancelTask(this.getServiceId());
}
/**
* Returns the ID of the service from the associated {@link BukkitTask} which was returned by the Scheduler.
*
* @return The ID of the service.
*/
public int getServiceId()
{
return serviceId;
}
/**
* @return The service this subscription is for.
*/
@NotNull
public T getService()
{
return service;
}
/**
* @return Whether this is an asynchronous service.
*/
public boolean isAsync()
{
return async;
}
/**
* @return Whether the service is currently running.
*/
public boolean isActive()
{
return isActive;
}
}

View File

@ -0,0 +1,125 @@
package fns.patchwork.service;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
/**
* Provides static methods for creating {@link ServiceSubscription} and {@link TaskSubscription} objects.
*/
public final class SubscriptionProvider
{
/**
* Prevents instantiation of this class.
*/
private SubscriptionProvider()
{
throw new AssertionError();
}
/**
* Creates a new {@link ServiceSubscription} object that will run the given {@link Service} object on the main
* thread a single time.
*
* @param plugin The plugin that owns the service.
* @param service The service to run.
* @param <S> Type inference to maintain the service type.
* @return The new {@link ServiceSubscription} object.
*/
@NotNull
@Contract(value = "_, _ -> new", pure = false)
public static final <S extends Service> ServiceSubscription<S> syncService(@NotNull final JavaPlugin plugin,
@NotNull final S service)
{
return new ServiceSubscription<>(plugin, service);
}
/**
* Creates a new {@link ServiceSubscription} object that will run the given {@link Service} object on the main
* thread at the given interval.
*
* @param plugin The plugin that owns the service.
* @param interval The interval to run the service at.
* @param service The service to run.
* @param <S> Type inference to maintain the service type.
* @return The new {@link ServiceSubscription} object.
*/
@NotNull
@Contract(value = "_,_,_ -> new", pure = false)
public static final <S extends Service> ServiceSubscription<S> syncService(@NotNull final JavaPlugin plugin,
final long interval,
@NotNull final S service)
{
return new ServiceSubscription<>(plugin, service, interval);
}
/**
* Creates a new {@link ServiceSubscription} object that will run the given {@link Service} object on the default
* tick interval, which is a single tick. This method will create an asynchronous service.
*
* @param plugin The plugin that owns the service.
* @param service The service to run.
* @param <S> Type inference to maintain the service type.
* @return The new {@link ServiceSubscription} object.
*/
@NotNull
@Contract(value = "_, _ -> new", pure = false)
public static final <S extends Service> ServiceSubscription<S> asyncService(@NotNull final JavaPlugin plugin,
@NotNull final S service)
{
return new ServiceSubscription<>(plugin, service, true);
}
/**
* Creates a new {@link ServiceSubscription} object that will run the given {@link Service} object on the given
* interval. This method will create an asynchronous service.
*
* @param plugin The plugin that owns the service.
* @param interval The interval to run the service at.
* @param service The service to run.
* @param <S> Type inference to maintain the service type.
* @return The new {@link ServiceSubscription} object.
*/
@NotNull
@Contract(value = "_,_,_ -> new", pure = false)
public static final <S extends Service> ServiceSubscription<S> asyncService(@NotNull final JavaPlugin plugin,
final long interval,
@NotNull final S service)
{
return new ServiceSubscription<>(plugin, service, interval, true);
}
/**
* Creates a new {@link TaskSubscription} object that will run the given {@link Task} object synchronously on the
* main thread.
*
* @param plugin The plugin that owns the task.
* @param task The task to run.
* @param <T> Type inference to maintain the task type.
* @return The new {@link TaskSubscription} object.
*/
@NotNull
@Contract(value = "_, _ -> new", pure = false)
public static final <T extends Task> TaskSubscription<T> runSyncTask(@NotNull final JavaPlugin plugin,
@NotNull final T task)
{
return new TaskSubscription<>(plugin, task, false);
}
/**
* Creates a new {@link TaskSubscription} object that will run the given {@link Task} object asynchronously on the
* main thread.
*
* @param plugin The plugin that owns the task.
* @param task The task to run.
* @param <T> Type inference to maintain the task type.
* @return The new {@link TaskSubscription} object.
*/
@NotNull
@Contract(value = "_, _ -> new", pure = false)
public static final <T extends Task> TaskSubscription<T> runAsyncTask(@NotNull final JavaPlugin plugin,
@NotNull final T task)
{
return new TaskSubscription<>(plugin, task, true);
}
}

View File

@ -0,0 +1,153 @@
package fns.patchwork.service;
import fns.patchwork.utils.DurationTools;
import java.time.Duration;
import org.bukkit.scheduler.BukkitRunnable;
/**
* Represents a task that can be run asynchronously or synchronously.
*/
public abstract class Task extends BukkitRunnable
{
/**
* The name of the task.
*/
private final String name;
/**
* The delay of the task.
*/
private final long delay;
/**
* The interval of the task.
*/
private final long interval;
/**
* Creates a new task with the given name. This will initialize a task with no initail delay and no interval.
*
* @param name The name of the task.
*/
protected Task(final String name)
{
this(name, -1L, -1L);
}
/**
* Creates a new task with the given name, delay, and interval.
* <br>
* It's important to note that the delay and interval are in ticks. One tick is equal to 1/20th of a second, which
* means there are 20 ticks are in one second. If your interval is intended to be anything less than 20 ticks, you
* should use a {@link Service} instead.
*
* @param name The name of the task.
* @param delay The delay of the task.
* @param interval The interval of the task.
*/
protected Task(final String name, final long delay, final long interval)
{
this.name = name;
this.delay = delay;
this.interval = interval;
}
/**
* Creates a new task with the given name and delay. This will intialize a single execute task with an initial delay
* before execution.
*
* @param name The name of the task.
* @param delay How long the task should wait before executing.
*/
protected Task(final String name, final long delay)
{
this(name, delay, -1L);
}
/**
* Creates a new task with the given name and delay. This is the same as longs, except that here, we naturally
* support durations which are automatically converted to ticks for you. This means that using
* {@link Duration#ofSeconds(long)} will work as expected.
*
* @param name The name of the task.
* @param delay How long the task should wait before executing.
*/
protected Task(final String name, final Duration delay)
{
this(name, DurationTools.getTicks(delay), -1L);
}
/**
* Creates a new task with the given name, delay, and interval. This is the same as longs, except that here, we
* naturally support durations which are automatically converted to ticks for you. This means that using
* {@link Duration#ofSeconds(long)} will work as expected.
*
* @param name The name of the task.
* @param delay How long the task should wait before executing.
* @param interval How long the task should wait between executions.
*/
protected Task(final String name, final Duration delay, final Duration interval)
{
this(name, DurationTools.getTicks(delay), DurationTools.getTicks(interval));
}
/**
* Creates a new task with the given name, delay, and interval. This method is a convenience method to use a
* {@link Duration} for the interval, while also being able to specify the delay as -1L so the task does not have an
* initial delay before execution.
*
* @param name The name of the task.
* @param delay The delay of the task.
* @param interval The interval of the task.
*/
protected Task(final String name, final long delay, final Duration interval)
{
this(name, delay, DurationTools.getTicks(interval));
}
/**
* @return True if the task is running, false otherwise.
*/
public boolean isRunning()
{
return !isCancelled();
}
/**
* @return The name of the task.
*/
public String getName()
{
return name;
}
/**
* @return True if the task is repeating, false otherwise.
*/
public boolean isRepeating()
{
return this.interval > 0L;
}
/**
* @return True if the task is delayed, false otherwise.
*/
public boolean isDelayed()
{
return this.delay > 0L;
}
/**
* @return The interval between each task execution.
*/
public long getInterval()
{
return interval;
}
/**
* @return The initial delay before the first execution of this task.
*/
public long getDelay()
{
return delay;
}
}

View File

@ -0,0 +1,217 @@
package fns.patchwork.service;
import fns.patchwork.utils.container.Pair;
import java.util.concurrent.Executor;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
/**
* Represents a subscription to a task. Task subscriptions offer a nice wrapper for managing tasks, which are inevitably
* just bukkit runnables with a bit more lenience in terms of instantiation modification and execution. It also offers a
* more intuitive way to manage our tasks; rather than having to keep track of task ids for each {@link BukkitTask}
* object that gets returned by the {@link BukkitScheduler}.
*
* @param <T> The type of task.
*/
public final class TaskSubscription<T extends Task>
{
/**
* The task that is being subscribed to.
*/
private final T task;
/**
* The task id of the task.
*/
private final int taskId;
/**
* True if the task is async, false otherwise.
*/
private final boolean async;
/**
* The executor that will execute the task.
*/
private final Executor executor;
/**
* True if the task is active, false otherwise. By default, this is set to false, and will be marked as true when
* the task is started.
*/
private boolean isActive = false;
/**
* Creates a new task subscription.
*
* @param plugin The plugin which owns the task.
* @param task The task that is being subscribed to.
* @param async True if the task is async, false otherwise.
*/
TaskSubscription(final JavaPlugin plugin, final T task, final boolean async)
{
this.task = task;
this.async = async;
final long delay = (task.isDelayed()
? task.getDelay()
: 0);
final long period = (task.isRepeating()
? task.getInterval()
: 0);
final Pair<Integer, Executor> integerExecutorPair = async
? getAsync(plugin, delay, period)
: getSync(plugin, delay, period);
this.executor = integerExecutorPair.value();
this.taskId = integerExecutorPair.key();
}
/**
* Gets the executor and task id for an async task, wrapped in a {@link Pair}&lt;{@link Integer},
* {@link Executor}&gt;.
* <br>
* This will return a Pair where {@link Pair#value()} is an asynchronous executor.
*
* @param plugin The plugin which owns the task.
* @param delay The delay of the task.
* @param period The period of the task.
* @return The executor and task id for an asynchronous task.
*/
private Pair<Integer, Executor> getAsync(final JavaPlugin plugin, final long delay, final long period)
{
final Executor executor1;
final int[] tempId = new int[1];
if (period != 0)
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTaskTimerAsynchronously(plugin, r, delay, period);
tempId[0] = task1.getTaskId();
};
} else if (delay != 0)
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTaskLaterAsynchronously(plugin, r, delay);
tempId[0] = task1.getTaskId();
};
} else
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTaskAsynchronously(plugin, r);
tempId[0] = task1.getTaskId();
};
}
return new Pair<>(tempId[0], executor1);
}
/**
* Gets the executor and task id for a sync task, wrapped in a {@link Pair}&lt;{@link Integer},
* {@link Executor}&gt;.
* <br>
* This will return a Pair where {@link Pair#value()} is a synchronous executor.
*
* @param plugin The plugin which owns the task.
* @param delay The delay of the task.
* @param period The period of the task.
* @return The executor and task id for a synchronous task.
*/
private Pair<Integer, Executor> getSync(final JavaPlugin plugin, final long delay, final long period)
{
final Executor executor1;
final int[] tempId = new int[1];
if (period != 0)
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTaskTimer(plugin, r, delay, period);
tempId[0] = task1.getTaskId();
};
} else if (delay != 0)
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTaskLater(plugin, r, delay);
tempId[0] = task1.getTaskId();
};
} else
{
executor1 = r ->
{
final BukkitTask task1 = Bukkit.getScheduler()
.runTask(plugin, r);
tempId[0] = task1.getTaskId();
};
}
return new Pair<>(tempId[0], executor1);
}
/**
* Starts the task.
*/
public void start()
{
this.isActive = true;
executor.execute(task);
}
/**
* Stops the task.
*/
public void stop()
{
this.isActive = false;
Bukkit.getScheduler()
.cancelTask(this.getTaskId());
}
/**
* @return The task id of the task.
*/
public int getTaskId()
{
return taskId;
}
/**
* @return The task that is being subscribed to.
*/
public T getTask()
{
return task;
}
/**
* @return True if the task is async, false otherwise.
*/
public boolean isAsync()
{
return async;
}
/**
* @return The executor that will execute the task.
*/
public Executor getExecutor()
{
return executor;
}
/**
* @return True if the task is active, false otherwise.
*/
public boolean isActive()
{
return isActive;
}
}

View File

@ -0,0 +1,21 @@
package fns.patchwork.shop;
import fns.patchwork.economy.EconomicEntity;
import java.time.Duration;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
public interface Reactable
{
Component getReactionMessage();
Duration getReactionDuration();
ReactionType getReactionType();
long getReward();
void display(final Audience audience);
void onReact(final EconomicEntity entity);
}

View File

@ -0,0 +1,60 @@
package fns.patchwork.shop;
import java.time.Duration;
import net.kyori.adventure.text.Component;
/**
* Represents a chat reaction that can be performed by a player.
*/
public abstract class Reaction implements Reactable
{
private final Duration reactionDuration;
private final ReactionType reactionType;
private final long reward;
private Component reactionMessage = Component.empty();
protected Reaction(final ReactionType type)
{
this(50L, type);
}
protected Reaction(final long reward, final ReactionType type)
{
this(30L, reward, type);
}
protected Reaction(final long seconds, final long reward, final ReactionType reactionType)
{
this(Duration.ofSeconds(seconds), reward, reactionType);
}
protected Reaction(final Duration duration, final long reward, final ReactionType reactionType)
{
this.reward = reward;
this.reactionDuration = duration;
this.reactionType = reactionType;
}
@Override
public Component getReactionMessage()
{
return reactionMessage;
}
public void setReactionMessage(final Component message)
{
this.reactionMessage = message;
}
@Override
public Duration getReactionDuration()
{
return reactionDuration;
}
@Override
public ReactionType getReactionType()
{
return reactionType;
}
}

View File

@ -0,0 +1,56 @@
package fns.patchwork.shop;
import fns.patchwork.base.Patchwork;
import fns.patchwork.display.BossBarDisplay;
import fns.patchwork.display.BossBarTimer;
import fns.patchwork.economy.EconomicEntity;
import fns.patchwork.service.Task;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.bossbar.BossBar;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class ReactionTask extends Task implements Listener
{
private final Reaction reaction;
private final BossBarDisplay bossBarDisplay;
public ReactionTask(final String name, final Reaction reaction)
{
super(name, -1L, -1);
this.reaction = reaction;
final BossBar bossBar = BossBarDisplay.builder()
.setName(reaction.getReactionMessage())
.setColor(BossBar.Color.GREEN)
.setProgress(0.0F)
.build();
this.bossBarDisplay = new BossBarDisplay(bossBar);
}
@Override
public void run()
{
if (isCancelled())
{
}
final BossBarTimer timer = new BossBarTimer(bossBarDisplay, reaction.getReactionDuration());
timer.runTaskTimer(Patchwork.getInstance(), 0L, timer.getInterval());
}
@EventHandler
public void onPlayerChat(final AsyncChatEvent event)
{
if (event.message()
.equals(reaction.getReactionMessage()))
{
final EconomicEntity entity = Patchwork.getInstance()
.getRegistrations()
.getUserRegistry()
.getUser(event.getPlayer());
reaction.onReact(entity);
}
}
}

View File

@ -0,0 +1,6 @@
package fns.patchwork.shop;
public enum ReactionType
{
COPYCAT, UNSCRAMBLE, MATH;
}

View File

@ -0,0 +1,18 @@
package fns.patchwork.sql;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.CompletableFuture;
public interface SQL
{
CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args);
CompletableFuture<ResultSet> executeQuery(final String query, final Object... args);
CompletableFuture<Integer> executeUpdate(final String query, final Object... args);
CompletableFuture<Boolean> execute(final String query, final Object... args);
CompletableFuture<Boolean> createTable(final String table, final String... columns);
}

View File

@ -0,0 +1,53 @@
package fns.patchwork.sql;
import java.io.File;
import java.util.Properties;
public interface SQLProperties
{
Properties getProperties(File propertiesFile);
default Properties getDefaultProperties()
{
final Properties properties = new Properties();
properties.setProperty("driver", "sqlite");
properties.setProperty("host", "localhost");
properties.setProperty("port", "3306");
properties.setProperty("database", "database.db");
properties.setProperty("username", "root");
properties.setProperty("password", "password");
return properties;
}
default String toURLPlain()
{
return String.format("jdbc:%s://%s:%s/%s",
this.getDriver(),
this.getHost(),
this.getPort(),
this.getDatabase());
}
String getDriver();
String getHost();
String getPort();
String getDatabase();
default String toURLWithLogin()
{
return String.format("jdbc:%s://%s:%s/%s?user=%s&password=%s",
this.getDriver(),
this.getHost(),
this.getPort(),
this.getDatabase(),
this.getUsername(),
this.getPassword());
}
String getUsername();
String getPassword();
}

View File

@ -0,0 +1,28 @@
package fns.patchwork.user;
import fns.patchwork.economy.EconomicEntity;
import fns.patchwork.economy.EconomicEntityData;
import fns.patchwork.security.PermissionHolder;
import net.kyori.adventure.text.Component;
public interface User extends PermissionHolder, EconomicEntity
{
@Override
default EconomicEntityData getEconomicData()
{
return getUserData();
}
// Implement a few EconomicEntity methods in the User interface
@Override
default String getName()
{
return getUserData().getUsername();
}
UserData getUserData();
Component getDisplayName();
boolean isOnline();
}

View File

@ -0,0 +1,39 @@
package fns.patchwork.user;
import fns.patchwork.display.adminchat.AdminChatFormat;
import fns.patchwork.economy.EconomicEntityData;
import fns.patchwork.security.Group;
import java.util.UUID;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface UserData extends EconomicEntityData
{
@NotNull UUID getUniqueId();
String getUsername();
User getUser();
@Nullable Group getGroup();
void setGroup(@Nullable Group group);
long getPlaytime();
void setPlaytime(long playtime);
void addPlaytime(long playtime);
void resetPlaytime();
boolean canInteract();
void setInteractionState(boolean canInteract);
boolean hasCustomACFormat();
void setCustomACFormat(final String customACFormat);
AdminChatFormat getCustomACFormat();
}

View File

@ -0,0 +1,28 @@
package fns.patchwork.utils;
import java.time.Duration;
public final class DurationTools
{
// One tick is 1/20th of a second which is about 50ms.
public static final Duration TICK = Duration.ofMillis(50L);
// One second is 20 ticks.
public static final Duration SECOND = TICK.multipliedBy(20L);
// One minute is 60 seconds.
public static final Duration MINUTE = SECOND.multipliedBy(60L);
private DurationTools()
{
throw new AssertionError();
}
public static final long getTicks(final Duration duration)
{
return duration.toMillis() / 50L;
}
public static final Duration getTickedSeconds(final long seconds)
{
return SECOND.multipliedBy(seconds);
}
}

View File

@ -0,0 +1,135 @@
package fns.patchwork.utils;
import fns.patchwork.api.Interpolator;
import java.util.LinkedHashSet;
import java.util.Set;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Color;
public final class InterpolationUtils
{
private InterpolationUtils()
{
throw new AssertionError();
}
public static Set<Color> rainbow(final int length)
{
final LinkedHashSet<Color> base = new LinkedHashSet<>();
final Set<Color> redToOrange = hsvGradient(length, Color.RED, Color.ORANGE, InterpolationUtils::linear);
final Set<Color> orangeToYellow = hsvGradient(length, Color.ORANGE, Color.YELLOW, InterpolationUtils::linear);
final Set<Color> yellowToGreen = hsvGradient(length, Color.YELLOW, Color.GREEN, InterpolationUtils::linear);
final Set<Color> greenToBlue = hsvGradient(length, Color.GREEN, Color.BLUE, InterpolationUtils::linear);
final Set<Color> blueToPurple = hsvGradient(length, Color.BLUE, Color.PURPLE, InterpolationUtils::linear);
final Set<Color> purpleToRed = hsvGradient(length, Color.PURPLE, Color.RED, InterpolationUtils::linear);
base.addAll(redToOrange);
base.addAll(orangeToYellow);
base.addAll(yellowToGreen);
base.addAll(greenToBlue);
base.addAll(blueToPurple);
base.addAll(purpleToRed);
return base;
}
private static Set<Color> hsvGradient(final int length, final Color from, final Color to,
final Interpolator interpolator)
{
// returns a float-array where hsv[0] = hue, hsv[1] = saturation, hsv[2] = value/brightness
final float[] hsvFrom = java.awt.Color.RGBtoHSB(from.getRed(), from.getGreen(), from.getBlue(), null);
final float[] hsvTo = java.awt.Color.RGBtoHSB(to.getRed(), to.getGreen(), to.getBlue(), null);
final double[] h = interpolator.interpolate(hsvFrom[0], hsvTo[0], length);
final double[] s = interpolator.interpolate(hsvFrom[1], hsvTo[1], length);
final double[] v = interpolator.interpolate(hsvFrom[2], hsvTo[2], length);
final LinkedHashSet<Color> gradient = new LinkedHashSet<>();
for (int i = 0; i < length; i++)
{
final int rgb = java.awt.Color.HSBtoRGB((float) h[i], (float) s[i], (float) v[i]);
final Color color = Color.fromRGB(rgb);
gradient.add(color);
}
return gradient;
}
private static double[] linear(final double from, final double to, final int max)
{
final double[] res = new double[max];
for (int i = 0; i < max; i++)
{
res[i] = from + i * ((to - from) / (max - 1));
}
return res;
}
public static Set<TextColor> rainbowComponent(final int length)
{
final LinkedHashSet<TextColor> base = new LinkedHashSet<>();
final Set<TextColor> redToOrange = componentRGBGradient(length, NamedTextColor.RED,
NamedTextColor.GOLD, InterpolationUtils::linear);
final Set<TextColor> orangeToYellow = componentRGBGradient(length, NamedTextColor.GOLD,
NamedTextColor.YELLOW, InterpolationUtils::linear);
final Set<TextColor> yellowToGreen = componentRGBGradient(length, NamedTextColor.YELLOW,
NamedTextColor.GREEN, InterpolationUtils::linear);
final Set<TextColor> greenToBlue = componentRGBGradient(length, NamedTextColor.GREEN,
NamedTextColor.BLUE, InterpolationUtils::linear);
final Set<TextColor> blueToPurple = componentRGBGradient(length, NamedTextColor.BLUE,
NamedTextColor.LIGHT_PURPLE,
InterpolationUtils::linear);
final Set<TextColor> purpleToRed = componentRGBGradient(length, TextColor.color(75, 0, 130),
TextColor.color(255, 0, 0), InterpolationUtils::linear);
base.addAll(redToOrange);
base.addAll(orangeToYellow);
base.addAll(yellowToGreen);
base.addAll(greenToBlue);
base.addAll(blueToPurple);
base.addAll(purpleToRed);
return base;
}
private static Set<TextColor> componentRGBGradient(final int length, final TextColor from, final TextColor to,
final Interpolator interpolator)
{
final double[] r = interpolator.interpolate(from.red(), to.red(), length);
final double[] g = interpolator.interpolate(from.green(), to.green(), length);
final double[] b = interpolator.interpolate(from.blue(), to.blue(), length);
final LinkedHashSet<TextColor> gradient = new LinkedHashSet<>();
for (int i = 0; i < length; i++)
{
final TextColor color = TextColor.color((int) r[i], (int) g[i], (int) b[i]);
gradient.add(color);
}
return gradient;
}
public static Set<Color> standardGradient(final int length, final Color from, final Color to)
{
return rgbGradient(length, from, to, InterpolationUtils::linear);
}
private static Set<Color> rgbGradient(final int length, final Color from, final Color to,
final Interpolator interpolator)
{
final double[] r = interpolator.interpolate(from.getRed(), to.getRed(), length);
final double[] g = interpolator.interpolate(from.getGreen(), to.getGreen(), length);
final double[] b = interpolator.interpolate(from.getBlue(), to.getBlue(), length);
final LinkedHashSet<Color> gradient = new LinkedHashSet<>();
for (int i = 0; i < length; i++)
{
final Color color = Color.fromRGB((int) r[i], (int) g[i], (int) b[i]);
gradient.add(color);
}
return gradient;
}
public static Set<TextColor> standardComponentGradient(final int length, final TextColor from, final TextColor to)
{
return componentRGBGradient(length, from, to, InterpolationUtils::linear);
}
}

View File

@ -0,0 +1,40 @@
package fns.patchwork.utils;
import java.util.LinkedList;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
import org.bukkit.Location;
import org.bukkit.World;
public class ShapeUtils
{
private final double start;
private final double end;
private final World world;
public ShapeUtils(final World world, final double start, final double end)
{
this.start = start;
this.end = end;
this.world = world;
}
public List<Location> generate(final int count, final DoubleUnaryOperator x, final DoubleUnaryOperator y,
final DoubleUnaryOperator z)
{
final double step = (start - end) / (count - 1);
final LinkedList<Location> lset = new LinkedList<>();
for (int i = 0; i < count; i++)
{
final double t = start + i * step;
final double xp = x.applyAsDouble(t);
final double yp = y.applyAsDouble(t);
final double zp = z.applyAsDouble(t);
lset.add(new Location(world, xp, yp, zp));
}
return lset;
}
}

View File

@ -0,0 +1,100 @@
package fns.patchwork.utils;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
public final class Tagged<T>
{
public static final Tagged<EntityType> NON_WIPEABLE = new Tagged<>(EntityType.ITEM_FRAME, EntityType.BLOCK_DISPLAY,
EntityType.ITEM_DISPLAY, EntityType.LEASH_HITCH,
EntityType.CHEST_BOAT, EntityType.BOAT,
EntityType.TEXT_DISPLAY,
EntityType.GLOW_ITEM_FRAME,
EntityType.ARMOR_STAND, EntityType.PAINTING,
EntityType.PLAYER, EntityType.DROPPED_ITEM,
EntityType.MINECART, EntityType.MINECART_CHEST,
EntityType.MINECART_COMMAND,
EntityType.MINECART_FURNACE,
EntityType.MINECART_HOPPER,
EntityType.FISHING_HOOK, EntityType.DONKEY,
EntityType.HORSE, EntityType.IRON_GOLEM,
EntityType.RABBIT, EntityType.SNOWMAN,
EntityType.VILLAGER, EntityType.WOLF);
public static final Tagged<EntityType> MINECARTS = new Tagged<>(EntityType.MINECART, EntityType.MINECART_CHEST,
EntityType.MINECART_COMMAND,
EntityType.MINECART_FURNACE,
EntityType.MINECART_HOPPER,
EntityType.MINECART_MOB_SPAWNER,
EntityType.MINECART_TNT);
public static final Tagged<EntityType> BOATS = new Tagged<>(EntityType.BOAT, EntityType.CHEST_BOAT);
public static final Tagged<EntityType> HOSTILE = new Tagged<>(EntityType.BLAZE, EntityType.CAVE_SPIDER,
EntityType.CREEPER, EntityType.DROWNED,
EntityType.ELDER_GUARDIAN, EntityType.ENDER_CRYSTAL,
EntityType.ENDER_DRAGON, EntityType.ENDERMAN,
EntityType.ENDERMITE, EntityType.EVOKER,
EntityType.EVOKER_FANGS, EntityType.GHAST,
EntityType.GIANT, EntityType.GUARDIAN,
EntityType.HOGLIN, EntityType.HUSK,
EntityType.ILLUSIONER, EntityType.MAGMA_CUBE,
EntityType.PHANTOM, EntityType.PIGLIN,
EntityType.PIGLIN_BRUTE, EntityType.PILLAGER,
EntityType.RAVAGER, EntityType.SHULKER,
EntityType.SILVERFISH, EntityType.SKELETON,
EntityType.SLIME, EntityType.SPIDER, EntityType.STRAY,
EntityType.VEX, EntityType.VINDICATOR,
EntityType.WARDEN, EntityType.WITCH,
EntityType.WITHER, EntityType.WITHER_SKELETON,
EntityType.ZOGLIN, EntityType.ZOMBIE,
EntityType.ZOMBIE_VILLAGER,
EntityType.ZOMBIFIED_PIGLIN);
public static final Tagged<EntityType> PASSIVE = new Tagged<>(EntityType.BAT, EntityType.BEE, EntityType.CAT,
EntityType.CHICKEN, EntityType.COD, EntityType.COW,
EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX,
EntityType.GOAT, EntityType.HORSE,
EntityType.IRON_GOLEM, EntityType.LLAMA,
EntityType.MULE, EntityType.MUSHROOM_COW,
EntityType.OCELOT, EntityType.PANDA,
EntityType.PARROT, EntityType.PIG,
EntityType.POLAR_BEAR, EntityType.PUFFERFISH,
EntityType.RABBIT, EntityType.SALMON,
EntityType.SHEEP, EntityType.SKELETON_HORSE,
EntityType.SNOWMAN, EntityType.SQUID,
EntityType.STRIDER, EntityType.TRADER_LLAMA,
EntityType.TROPICAL_FISH, EntityType.TURTLE,
EntityType.VILLAGER, EntityType.WANDERING_TRADER,
EntityType.WOLF);
public static final Tagged<EntityType> PLAYER_RELATED = new Tagged<>(EntityType.PLAYER, EntityType.ARMOR_STAND,
EntityType.DROPPED_ITEM, EntityType.PAINTING,
EntityType.ITEM_FRAME,
EntityType.GLOW_ITEM_FRAME,
EntityType.LEASH_HITCH,
EntityType.FISHING_HOOK,
EntityType.TEXT_DISPLAY,
EntityType.BLOCK_DISPLAY,
EntityType.ITEM_DISPLAY);
private final List<T> taggable;
private Tagged(final T... taggable)
{
this.taggable = new ArrayList<>();
this.taggable.addAll(List.of(taggable));
}
public boolean isTagged(final @NotNull T object)
{
for (final T t : this.taggable)
{
if (object.equals(t))
return true;
}
return false;
}
}

View File

@ -0,0 +1,26 @@
package fns.patchwork.utils.container;
import java.util.UUID;
public class Identity
{
private final String key;
private final UUID id;
public Identity(final String key)
{
this.key = key;
this.id = UUID.nameUUIDFromBytes(key.getBytes());
}
public String getKey()
{
return key;
}
public UUID getId()
{
return id;
}
}

View File

@ -0,0 +1,5 @@
package fns.patchwork.utils.container;
public record Pair<K, V>(K key, V value)
{
}

View File

@ -0,0 +1,5 @@
package fns.patchwork.utils.container;
public record Trio<A, B, C>(A primary, B secondary, C tertiary)
{
}

View File

@ -0,0 +1,5 @@
package fns.patchwork.utils.container;
public record UnaryPair<T>(T first, T second)
{
}

View File

@ -0,0 +1,5 @@
package fns.patchwork.utils.container;
public record UnaryTrio<T>(T primary, T secondary, T tertiary)
{
}

View File

@ -0,0 +1,49 @@
package fns.patchwork.utils.kyori;
import java.util.function.Supplier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
/**
* This class contains the only reference to plain text component serializer, and allows access to it via wrapper
* functions.
*/
public class FreedomAdventure
{
private static final PlainTextComponentSerializer PLAIN_TEXT_COMPONENT_SERIALIZER =
PlainTextComponentSerializer.plainText();
private FreedomAdventure()
{
throw new UnsupportedOperationException("Instantiation of a static utility class is not supported.");
}
public static String toPlainText(final Supplier<Component> supplier)
{
return toPlainText(supplier.get());
}
public static String toPlainText(final Component component)
{
return PLAIN_TEXT_COMPONENT_SERIALIZER.serialize(component);
}
public static Supplier<String> supplyPlainText(final Supplier<Component> supplier)
{
return new StringRepresentationSupplier(supplier.get());
}
public static Supplier<String> supplyPlainText(final Component component)
{
return new StringRepresentationSupplier(component);
}
private record StringRepresentationSupplier(Component component) implements Supplier<String>
{
@Override
public String get()
{
return PLAIN_TEXT_COMPONENT_SERIALIZER.serialize(component);
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 TotalFreedom
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the “Software”), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package fns.patchwork.utils.kyori;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
/**
* This class contains a wrapper for a MiniMessage serializer.
*/
public class FreedomMiniMessage
{
private static final MiniMessage unsafe = MiniMessage.miniMessage();
private static final MiniMessage safe = MiniMessage.builder()
.tags(TagResolver.resolver(
StandardTags.color(),
StandardTags.rainbow(),
StandardTags.gradient(),
StandardTags.newline(),
StandardTags.decorations(TextDecoration.ITALIC),
StandardTags.decorations(TextDecoration.BOLD),
StandardTags.decorations(TextDecoration.STRIKETHROUGH),
StandardTags.decorations(TextDecoration.UNDERLINED)
))
.build();
private FreedomMiniMessage()
{
throw new UnsupportedOperationException("Instantiation of a static utility class is not supported.");
}
/**
* Deserializes an input string using an instance of MiniMessage that is either safe (resolves only a specific set
* of tags) or unsafe (resolves all tags).
*
* @param safe Whether to use a safe instance of MiniMessage
* @param input An input string formatted with MiniMessage's input
* @param placeholders Custom placeholders to use when processing the input
* @return A processed Component
*/
public static Component deserialize(final boolean safe, final String input, final TagResolver... placeholders)
{
return (safe
? FreedomMiniMessage.safe
: unsafe).deserialize(input, placeholders);
}
/**
* Serializes an input component using an instance of MiniMessage that is either safe (resolves only a specific set
* of tags) or unsafe (resolves all tags).
*
* @param safe Whether to use a safe instance of MiniMessage
* @param input An already processed component
* @return A processed Component
*/
public static String serialize(final boolean safe, final Component input)
{
return (safe
? FreedomMiniMessage.safe
: unsafe).serialize(input);
}
}

View File

@ -0,0 +1,83 @@
package fns.patchwork.utils.kyori;
import fns.patchwork.base.Patchwork;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
/**
* This class contains different methods to provide {@link ChatType.Bound} instances for sending messages to players in
* game. This is now a requirement for all message requests to players due to the new chat signature system.
* <br>
* Even though Scissors has this feature disabled, upstream (Paper) and Kyori Adventure have made the appropriate API
* changes to accomodate chat signatures.
* <br>
* As a result, we need to conform to those specifications even if we do not use this feature.
*/
public final class KyoriConstants
{
private static final ChatType type = ChatType.CHAT;
/**
* A singleton {@link ChatType.Bound} for the Patchwork plugin.
*/
public static final ChatType.Bound PATCHWORK = fromPlugin(Patchwork.class);
private KyoriConstants()
{
}
/**
* Represents a Chat Bound for a plugin.
* <br>
* This is a convenience method so you are not required to dependency inject your plugin instance any time that you
* need a Bound.
*
* @param pluginClass The plugin class to get the plugin instance from.
* @return A ChatType.Bound instance for the plugin.
* @see #fromPlugin(JavaPlugin)
*/
public static ChatType.Bound fromPlugin(final Class<? extends JavaPlugin> pluginClass)
{
final JavaPlugin plugin = JavaPlugin.getPlugin(pluginClass);
return fromPlugin(plugin);
}
public static ChatType.Bound fromPlugin(final JavaPlugin plugin)
{
final String name = plugin.getName();
final Component component = Component.text(name, NamedTextColor.GOLD);
return type.bind(component);
}
/**
* Represents a Chat Bound for a player. Chat bounds are required for sending messages to players.
* <br>
* The chat bound is a representation of a validated chat signature, which is tied directly to the user's account
* name. In our case, this is the player's name.
*
* @param player The player to get the bound for.
* @return A ChatType.Bound instance for the player.
*/
public static ChatType.Bound fromPlayer(final Player player)
{
return type.bind(player.name());
}
/**
* Represents a Chat Bound for the console.
* <br>
* The chat bound is a representation of a validated chat signature, which is tied directly to the user's account
* name. In our case, this is the player's name.
*
* @param sender The console to get the bound for.
* @return A ChatType.Bound instance for the console.
*/
public static ChatType.Bound fromConsole(final ConsoleCommandSender sender)
{
return type.bind(sender.name());
}
}

View File

@ -0,0 +1,268 @@
package fns.patchwork.utils.logging;
import fns.patchwork.utils.kyori.FreedomAdventure;
import java.util.function.Supplier;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.chat.SignedMessage;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FreedomLogger implements Audience
{
private final Logger logger;
private boolean debug = false;
private FreedomLogger(final String moduleName)
{
this.logger = LoggerFactory.getLogger("FreedomNetworkSuite::" + moduleName);
}
public static FreedomLogger getLogger(final String moduleName)
{
return new FreedomLogger(moduleName);
}
public void setDebugMode(final boolean debug)
{
this.debug = debug;
}
/**
* This method allows you to log a message to the console, while also returning a Component that could be used to
* message a player.
*
* @param message The message to send.
* @return A component representation of the message.
*/
public Component info(final Supplier<String> message)
{
logger.info(message.get());
return Component.text(message.get());
}
/**
* This method allows you to log a component to the console, while also returning a String representation of the
* component
*
* @param component The component to send.
* @return A string representation of the message.
*/
public String infoComponent(final Supplier<Component> component)
{
return this.infoComponent(component.get());
}
/**
* This method allows you to log a component to the console.
*
* @param component The component to send.
* @return A plain text representation of the message
*/
public String infoComponent(final Component component)
{
final String plainText = FreedomAdventure.toPlainText(component);
logger.info(plainText);
return plainText;
}
/**
* This method allows you to log a message to the console.
*
* @param message The message to send.
*/
public void info(final String message)
{
logger.info(message);
}
/**
* This method allows you to log a warning to the console.
*
* @param message The message to send.
*/
public void warn(final String message)
{
logger.warn(message);
}
/**
* This method allows you to log a warning to the console.
*
* @param component The component to send.
*/
public void warnComponent(final Component component)
{
final String plainText = FreedomAdventure.toPlainText(component);
logger.warn(plainText);
}
/**
* This method logs an error message to the console. It is highly recommended to deconstruct the stack trace and
* pass it in a more readable format to this method.
*
* @param message The message to send.
*/
public void error(final String message)
{
logger.error(message);
}
/**
* This method allows you to log an exception directly to the console.
*
* @param th The exception to log.
*/
public void error(final Throwable th)
{
logger.error("An error occurred:\n", th);
}
/**
* This method allows you to log an error message to the console, while also returning a Component that could be
* used to message a player. It is highly recommended that you deconstruct and limit the stack trace before passing
* it to this method, if you are intending to use it for player communication.
*
* @param message The message to send.
* @return A component representation of the message.
*/
public Component error(final Supplier<String> message)
{
logger.error(message.get());
return Component.text(message.get());
}
/**
* This method allows you to log an error component to the console, while also returning a String representation of
* the error component.
*
* @param component The component to send.
* @return A String representation of the component.
*/
public String errorComponent(final Supplier<Component> component)
{
return this.errorComponent(component.get());
}
/**
* This method logs an error component to the console.
*
* @param component The message to send.
*/
public String errorComponent(final Component component)
{
final String plainText = FreedomAdventure.toPlainText(component);
logger.error(plainText);
return plainText;
}
/**
* This method allows you to log a debug message to the console, while also returning a Component that could be used
* to message a player. This method will only log if debug mode is enabled. If debug mode is not enabled, this
* method will return an empty component.
*
* @param message The message to send.
* @return A component representation of the message.
*/
public Component debug(final Supplier<String> message)
{
if (debug)
{
logger.debug(message.get());
return Component.text(message.get());
}
return Component.empty();
}
/**
* This method allows you to log a debug component to the console, while also returning a String representation of
* the debug component.
*
* @param component The component to send.
* @return A String representation of the message.
*/
public String debugComponent(final Supplier<Component> component)
{
if (debug)
{
return this.debugComponent(component.get());
}
return "";
}
/**
* This method allows you to log a debug component to the console. This method will only log if debug mode is
* enabled.
*
* @param component The component to send.
*/
public String debugComponent(final Component component)
{
final String plainText = FreedomAdventure.toPlainText(component);
this.debug(plainText);
return plainText;
}
/**
* This method allows you to log a debug message to the console. This method will only log if debug mode is
* enabled.
*
* @param message The message to send.
*/
public void debug(final String message)
{
if (debug)
logger.debug(message);
}
@Override
public void sendMessage(@NotNull final ComponentLike message)
{
final Component component = ComponentLike.unbox(message);
if (component == null)
{
this.info("**null component-like**");
return;
}
this.infoComponent(component);
}
@Override
public void sendMessage(@NotNull final Component message)
{
this.infoComponent(message);
}
@Override
public void sendMessage(@NotNull final Component message, final ChatType.@NotNull Bound boundChatType)
{
this.infoComponent(message);
}
@Override
public void sendMessage(@NotNull final ComponentLike message, final ChatType.@NotNull Bound boundChatType)
{
this.sendMessage(message);
}
@Override
public void sendMessage(@NotNull final SignedMessage signedMessage, final ChatType.@NotNull Bound boundChatType)
{
this.info(
signedMessage.message()); // TODO: We might want to investigate whether this logs the ENTIRE message,
// including unsigned & signed content, or only the signed part. This method was written in the assumption
// that it provided all content.
}
}