Merge pull request #3 from AtlasMediaGroup/Displayable-GUI-API

- Implement GUI API
- Documentation for a majority of the API in Patchwork
- Implement Particle API
- Implement interpolation utils for color interpolation (blocks, particles, and text applicable)
- Finish Command API
- Remove Banning API
- Created utility classes for various features of the Kyori Adventure API
- Removed TFM
This commit is contained in:
Paldiu 2023-06-09 21:22:35 -05:00 committed by GitHub
commit 9ef703ae7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 6639 additions and 1527 deletions

View File

@ -6,18 +6,20 @@ import org.bukkit.plugin.java.JavaPlugin;
public class Corvo extends JavaPlugin
{
@Override
public void onEnable() {
CommonsBase.getInstance()
.getRegistrations()
.getModuleRegistry()
.addModule(this);
}
@Override
public void onDisable() {
public void onDisable()
{
CommonsBase.getInstance()
.getRegistrations()
.getModuleRegistry()
.removeModule(this);
}
@Override
public void onEnable()
{
CommonsBase.getInstance()
.getRegistrations()
.getModuleRegistry()
.addModule(this);
}
}

View File

@ -0,0 +1,43 @@
package me.totalfreedom.corvo.listener;
import io.papermc.paper.event.block.BlockBreakBlockEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerInteractEvent;
public class PlayerInteractionListener implements Listener
{
@EventHandler
public void playerBreakBlock(final BlockBreakEvent event)
{
}
@EventHandler
public void blockBreakBlock(final BlockBreakBlockEvent event)
{
}
@EventHandler
public void playerOpenContainer(final InventoryOpenEvent event)
{
}
@EventHandler
public void playerCloseContainer(final InventoryCloseEvent event)
{
}
@EventHandler
public void playerInteraction(final PlayerInteractEvent event)
{
}
}

View File

@ -5,6 +5,7 @@ import me.totalfreedom.datura.punishment.Cager;
import me.totalfreedom.datura.punishment.Halter;
import me.totalfreedom.datura.punishment.Locker;
import me.totalfreedom.datura.sql.MySQL;
import me.totalfreedom.service.SubscriptionProvider;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
@ -24,14 +25,36 @@ public class Datura extends JavaPlugin
.getModuleRegistry()
.addModule(this);
CommonsBase.getInstance().getRegistrations().getServiceRegistry().register(this, locker);
CommonsBase.getInstance().getRegistrations().getServiceRegistry().register(this, cager);
CommonsBase.getInstance()
.getRegistrations()
.getServiceTaskRegistry()
.registerService(SubscriptionProvider.syncService(this, locker));
CommonsBase.getInstance()
.getRegistrations()
.getServiceTaskRegistry()
.registerService(SubscriptionProvider.syncService(this, cager));
Bukkit.getPluginManager().registerEvents(halter, this);
Bukkit.getPluginManager()
.registerEvents(halter, this);
}
public MySQL getSQL()
{
return sql;
}
public Halter getHalter()
{
return halter;
}
public Locker getLocker()
{
return locker;
}
public Cager getCager()
{
return cager;
}
}

View File

@ -1,66 +0,0 @@
package me.totalfreedom.datura.banning;
import me.totalfreedom.security.ban.BanID;
import java.time.Instant;
import java.time.temporal.ChronoField;
public final class BanUID implements BanID
{
private final char prefix;
private final int numericalTag;
private BanUID(final boolean permanent)
{
if (permanent)
{
prefix = 'P';
} else
{
prefix = 'T';
}
final Instant instant = Instant.now();
final String stringBuilder = String.valueOf(instant.get(ChronoField.DAY_OF_YEAR)) + // The first three numbers between 001 -> 365
instant.get(ChronoField.HOUR_OF_DAY) + // next two numbers between 00 -> 23
instant.get(ChronoField.MINUTE_OF_HOUR) + // next two numbers between 00 -> 59
instant.get(ChronoField.MILLI_OF_SECOND); // last three numbers between 000 -> 999
numericalTag = Integer.parseInt(stringBuilder);
}
public static BanUID createTempID()
{
return new BanUID(false);
}
public static BanUID createPermID()
{
return new BanUID(true);
}
@Override
public String getID()
{
return getIDPrefix() + "-" + getNumericalTag();
}
@Override
public char getIDPrefix()
{
return prefix;
}
@Override
public int getNumericalTag()
{
return numericalTag;
}
@Override
public boolean isPermanent()
{
return getIDPrefix() == 'P';
}
}

View File

@ -1,82 +0,0 @@
package me.totalfreedom.datura.banning;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.UUID;
public final class SimpleBan implements Ban
{
private final BanID id;
private final UUID offenderID;
private final String reason;
private final String issuer;
private final Instant creationTime;
private final Instant expiry;
public SimpleBan(
final UUID offenderID,
final String reason,
final String issuer,
final Instant creationTime,
final Instant expiry)
{
if (expiry == null)
{
this.id = BanUID.createPermID();
} else
{
this.id = BanUID.createTempID();
}
this.offenderID = offenderID;
this.reason = reason;
this.issuer = issuer;
this.creationTime = creationTime;
this.expiry = expiry;
}
@Override
public BanID getBanID()
{
return id;
}
@Override
public UUID getOffenderID()
{
return offenderID;
}
@Override
public String getReason()
{
return reason;
}
@Override
public String getBanIssuer()
{
return issuer;
}
@Override
public Instant getCreationTime()
{
return creationTime;
}
@Override
public @Nullable Instant getExpiry()
{
return expiry;
}
@Override
public boolean isExpired()
{
return Instant.now().compareTo(expiry) >= 0;
}
}

View File

@ -0,0 +1,89 @@
/*
* 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 me.totalfreedom.datura.cmd;
import me.totalfreedom.command.Commander;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Info;
import me.totalfreedom.command.annotation.Permissive;
import me.totalfreedom.command.annotation.Subcommand;
import me.totalfreedom.datura.Datura;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@Info(name = "cage", description = "Cage a player.",
usage = "/cage <player> <on|off> [material]")
@Permissive(perm = "datura.cage")
public class CageCommand extends Commander
{
protected CageCommand(final @NotNull JavaPlugin plugin)
{
super(plugin);
}
@Completion(args = {"%player%"}, index = 0)
@Completion(args = {"on", "off"}, index = 1)
@Subcommand(permission = "datura.cage", args = {Player.class, String.class})
public void cagePlayer(final CommandSender sender, final Player player, final String string)
{
switch (string.toLowerCase())
{
case "on" ->
{
((Datura) getPlugin()).getCager()
.cagePlayer(player.getUniqueId());
sender.sendPlainMessage("Caged " + player.getName() + ".");
}
case "off" ->
{
((Datura) getPlugin()).getCager()
.uncagePlayer(player.getUniqueId());
sender.sendPlainMessage("Liberated " + player.getName() + ".");
}
}
}
@Completion(args = {"[material]"}, index = 2)
@Subcommand(permission = "datura.cage.custom", args = {Player.class, String.class, Material.class})
public void cagePlayer(final CommandSender sender, final Player player, final String string,
final Material material)
{
switch (string.toLowerCase())
{
case "on" ->
{
((Datura) getPlugin()).getCager()
.cagePlayer(player.getUniqueId(), material);
sender.sendPlainMessage("Caged " + player.getName() + ".");
}
case "off" ->
{
((Datura) getPlugin()).getCager()
.uncagePlayer(player.getUniqueId());
sender.sendPlainMessage("Liberated " + player.getName() + ".");
}
}
}
}

View File

@ -1,26 +0,0 @@
package me.totalfreedom.datura.cmd;
import me.totalfreedom.command.CommandBase;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Info;
import me.totalfreedom.command.annotation.Permissive;
import me.totalfreedom.command.annotation.Subcommand;
import me.totalfreedom.datura.Datura;
import org.bukkit.entity.Player;
@Completion(args = {"%player%"}, index = 0)
@Info(name = "kick", description = "Kick a player from the server.", usage = "/kick <player>")
@Permissive(perm = "datura.kick")
public class KickCommand extends CommandBase
{
protected KickCommand(final Datura plugin)
{
super(plugin);
}
@Subcommand(permission = "datura.kick", args = {Player.class})
public void kick(final Player player)
{
player.kickPlayer("You have been kicked from the server.");
}
}

View File

@ -0,0 +1,41 @@
package me.totalfreedom.datura.cmd;
import me.totalfreedom.command.Commander;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Info;
import me.totalfreedom.command.annotation.Permissive;
import me.totalfreedom.command.annotation.Subcommand;
import me.totalfreedom.datura.Datura;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@Info(name = "locker", description = "Lock a player, preventing them from interacting with their game client.",
usage = "/locker <player> <on|off>", aliases = {"lock", "lockup"})
@Permissive(perm = "datura.locker")
public final class LockerCommand extends Commander
{
public LockerCommand(final @NotNull Datura plugin)
{
super(plugin);
}
@Completion(args = {"%player%"}, index = 0)
@Completion(args = {"on", "off"}, index = 1)
@Subcommand(permission = "datura.locker", args = {Player.class, String.class})
public void lockPlayer(final CommandSender sender, final Player player, final String string)
{
if (string.equalsIgnoreCase("on"))
{
((Datura) getPlugin()).getLocker()
.lock(player);
sender.sendPlainMessage("Locked " + player.getName() + ".");
} else if (string.equalsIgnoreCase("off"))
{
((Datura) getPlugin()).getLocker()
.unlock(player);
sender.sendPlainMessage("Unlocked " + player.getName() + ".");
}
}
}

View File

@ -1,14 +1,10 @@
package me.totalfreedom.datura.perms;
import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.perm.NodeType;
import me.totalfreedom.security.Node;
import me.totalfreedom.security.NodeType;
public class DefaultNodes
{
private DefaultNodes() {
throw new AssertionError();
}
public static final Node OP = new PermissionNodeBuilder()
.key("freedom.master_key")
.value(true)
@ -16,7 +12,6 @@ public class DefaultNodes
.negated(false)
.wildcard(true)
.build();
public static final Node NON_OP = new PermissionNodeBuilder()
.key("freedom.default")
.value(true)
@ -24,7 +19,6 @@ public class DefaultNodes
.negated(false)
.wildcard(false)
.build();
public static final Node ALL = new PermissionNodeBuilder()
.key("*")
.value(true)
@ -32,7 +26,6 @@ public class DefaultNodes
.negated(false)
.wildcard(true)
.build();
public static final Node NONE = new PermissionNodeBuilder()
.key("freedom.none")
.value(true)
@ -40,4 +33,9 @@ public class DefaultNodes
.negated(false)
.wildcard(false)
.build();
private DefaultNodes()
{
throw new AssertionError();
}
}

View File

@ -1,8 +1,8 @@
package me.totalfreedom.datura.perms;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.security.perm.Group;
import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.Group;
import me.totalfreedom.security.Node;
import net.kyori.adventure.text.Component;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
@ -44,6 +44,12 @@ public class FreedomGroup implements Group
this.attachment = new PermissionAttachment(CommonsBase.getInstance(), this);
}
@Override
public UUID getUniqueId()
{
return UUID.nameUUIDFromBytes(getName().toString()
.getBytes());
}
@Override
public Component getName()
@ -81,12 +87,6 @@ public class FreedomGroup implements Group
return isHidden;
}
@Override
public UUID getUniqueId()
{
return UUID.nameUUIDFromBytes(getName().toString().getBytes());
}
@Override
public Set<Node> permissions()
{
@ -109,7 +109,8 @@ public class FreedomGroup implements Group
public boolean isPermissionSet(@NotNull final String name)
{
final Node node = permissions().stream()
.filter(n -> n.key().equalsIgnoreCase(name))
.filter(n -> n.key()
.equalsIgnoreCase(name))
.findFirst()
.orElse(null);
@ -121,7 +122,8 @@ public class FreedomGroup implements Group
{
final Node node = permissions()
.stream()
.filter(n -> n.bukkit().equals(perm))
.filter(n -> n.bukkit()
.equals(perm))
.findFirst()
.orElse(null);
@ -132,7 +134,8 @@ public class FreedomGroup implements Group
public boolean hasPermission(@NotNull final String name)
{
final Node node = permissions().stream()
.filter(n -> n.key().equalsIgnoreCase(name))
.filter(n -> n.key()
.equalsIgnoreCase(name))
.findFirst()
.orElse(null);
@ -144,7 +147,8 @@ public class FreedomGroup implements Group
{
final Node node = permissions()
.stream()
.filter(n -> n.bukkit().equals(perm))
.filter(n -> n.bukkit()
.equals(perm))
.findFirst()
.orElse(null);
@ -152,19 +156,19 @@ public class FreedomGroup implements Group
}
/**
* Adds a permission to the relative PermissionAttachment for this group.
* This method is not thread-safe and should not be called asynchronously.
* Adds a permission to the relative PermissionAttachment for this group. This method is not thread-safe and should
* not be called asynchronously.
* <p>
* This method is only here for compatibility with the Bukkit API.
*
* @param plugin The plugin responsible for this attachment. May not be null
* or disabled.
* @param plugin The plugin responsible for this attachment. May not be null or disabled.
* @param name Name of the permission to attach
* @param value Value of the permission
* @return This group's PermissionAttachment.
*/
@Override
public @NotNull PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name, final boolean value)
public @NotNull PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name,
final boolean value)
{
attachment.setPermission(name, value);
return attachment;
@ -177,7 +181,8 @@ public class FreedomGroup implements Group
}
@Override
public @Nullable PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name, final boolean value, final int ticks)
public @Nullable PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name,
final boolean value, final int ticks)
{
attachment.setPermission(name, value);
return attachment;

View File

@ -3,7 +3,7 @@ package me.totalfreedom.datura.perms;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.Datura;
import me.totalfreedom.datura.user.SimpleUserData;
import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.Node;
import me.totalfreedom.user.User;
import me.totalfreedom.user.UserData;
import net.kyori.adventure.text.Component;
@ -23,9 +23,9 @@ import java.util.Set;
import java.util.UUID;
/**
* The superinterface User extends PermissionHolder,
* which is an extension of {@link org.bukkit.permissions.Permissible}.
* This means that our permission data can be interchanged with other permission plugins.
* The superinterface User extends PermissionHolder, which is an extension of
* {@link org.bukkit.permissions.Permissible}. This means that our permission data can be interchanged with other
* permission plugins.
*/
public class FreedomUser implements User
{
@ -45,7 +45,7 @@ public class FreedomUser implements User
final Datura datura = CommonsBase.getInstance()
.getRegistrations()
.getModuleRegistry()
.getModule(Datura.class)
.getProvider(Datura.class)
.getModule();
UserData data = SimpleUserData.fromSQL(datura.getSQL(), uuid.toString());
@ -64,10 +64,23 @@ public class FreedomUser implements User
}
@Override
public UserData getUserData() {
public UserData getUserData()
{
return userData;
}
@Override
public Component getDisplayName()
{
return displayName;
}
@Override
public boolean isOnline()
{
return Bukkit.getPlayer(uuid) != null;
}
@Override
public UUID getUniqueId()
{
@ -96,18 +109,6 @@ public class FreedomUser implements User
return permissions.remove(node);
}
@Override
public Component getDisplayName()
{
return displayName;
}
@Override
public boolean isOnline()
{
return Bukkit.getPlayer(uuid) != null;
}
@Override
public boolean isPermissionSet(@NotNull final String name)
{
@ -137,7 +138,8 @@ public class FreedomUser implements User
}
@Override
public @NotNull PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name, final boolean value)
public @NotNull PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name,
final boolean value)
{
final Player player = Bukkit.getPlayer(uuid);
if (player != null)
@ -161,7 +163,8 @@ public class FreedomUser implements User
}
@Override
public @Nullable PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name, final boolean value, final int ticks)
public @Nullable PermissionAttachment addAttachment(@NotNull final Plugin plugin, @NotNull final String name,
final boolean value, final int ticks)
{
final Player player = Bukkit.getPlayer(uuid);
if (player != null)

View File

@ -1,7 +1,7 @@
package me.totalfreedom.datura.perms;
import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.perm.NodeType;
import me.totalfreedom.security.Node;
import me.totalfreedom.security.NodeType;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
@ -9,21 +9,23 @@ record PermissionNode(String key,
boolean value,
long expiry,
NodeType type,
boolean wildcard,
boolean negated) implements Node
boolean wildcard) implements Node
{
@Override
public Permission bukkit()
{
return new Permission(key(),
value() ? PermissionDefault.TRUE : PermissionDefault.FALSE);
value()
? PermissionDefault.TRUE
: PermissionDefault.FALSE);
}
@Override
public boolean compare(final Node node)
{
return node.key().equalsIgnoreCase(key())
return node.key()
.equalsIgnoreCase(key())
&& node.value() == value()
&& node.type() == type();
}
@ -31,7 +33,7 @@ record PermissionNode(String key,
@Override
public boolean isExpired()
{
if (isPermanent())
if (!isTemporary())
{
return false;
}
@ -39,15 +41,9 @@ record PermissionNode(String key,
return System.currentTimeMillis() > expiry();
}
@Override
public boolean isPermanent()
{
return expiry() == -1;
}
@Override
public boolean isTemporary()
{
return !isPermanent();
return expiry() > -1;
}
}

View File

@ -1,8 +1,8 @@
package me.totalfreedom.datura.perms;
import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.perm.NodeBuilder;
import me.totalfreedom.security.perm.NodeType;
import me.totalfreedom.security.Node;
import me.totalfreedom.security.NodeBuilder;
import me.totalfreedom.security.NodeType;
public class PermissionNodeBuilder implements NodeBuilder
{

View File

@ -2,7 +2,7 @@ package me.totalfreedom.datura.punishment;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.service.Service;
import me.totalfreedom.utils.Shaper;
import me.totalfreedom.utils.ShapeUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
@ -14,8 +14,8 @@ import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -28,10 +28,11 @@ public class Cager extends Service
public Cager()
{
super("cage_service");
super("cager-service");
this.cagedPlayers = new HashSet<>();
this.cageLocations = new HashMap<>();
Bukkit.getPluginManager().registerEvents(new CageListener(), CommonsBase.getInstance());
Bukkit.getPluginManager()
.registerEvents(new CageListener(), CommonsBase.getInstance());
}
/**
@ -49,6 +50,38 @@ public class Cager extends Service
cageLocations.put(uuid, createCage(player.getLocation(), Material.GLASS));
}
/**
* This method generates a cube centered around the passed location, made of the provided material. This method
* returns the passed location object. We use the {@link ShapeUtils} class to generate the cube, which allows us to
* define custom shapes using {@link DoubleUnaryOperator}s.
*
* @param location The location to center the cube around.
* @param material The material to use for the cube.
* @return The center location of the cube (the passed location).
* @see ShapeUtils
* @see DoubleUnaryOperator
*/
public Location createCage(final Location location, final Material material)
{
final ShapeUtils shapeUtils = new ShapeUtils(location.getWorld(), 0.0, 4.0);
final List<Location> cubed = new LinkedList<>();
cubed.addAll(shapeUtils.generate(5, t -> t, t -> 4.0, t -> t));
cubed.addAll(shapeUtils.generate(5, t -> t, t -> 0.0, t -> t));
cubed.addAll(shapeUtils.generate(5, t -> 0.0, t -> t, t -> t));
cubed.addAll(shapeUtils.generate(5, t -> 4.0, t -> t, t -> t));
cubed.addAll(shapeUtils.generate(5, t -> t, t -> t, t -> 0.0));
cubed.addAll(shapeUtils.generate(5, t -> t, t -> t, t -> 4.0));
for (final Location l : cubed)
{
location.getWorld()
.getBlockAt(l)
.setType(material);
}
return location.clone(); // Return the passed location as that is the center of the cube.
}
public void cagePlayer(final UUID uuid, final Material material)
{
final Player player = Bukkit.getPlayer(uuid);
@ -74,8 +107,7 @@ public class Cager extends Service
}
/**
* This method will check to make sure each caged player remains within their cage.
* We use
* This method will check to make sure each caged player remains within their cage. We use
* <p>
* <code>{@link Location#distanceSquared(Location)} * {@link Math#pow(double, double)}</code>
* <p>
@ -92,12 +124,14 @@ public class Cager extends Service
final Location cageLocation = getCageLocation(player);
final boolean inside;
if (!player.getWorld().equals(cageLocation.getWorld()))
if (!player.getWorld()
.equals(cageLocation.getWorld()))
{
inside = false;
} else
{
inside = player.getLocation().distanceSquared(cageLocation) > (Math.pow(2.5, 2.0));
inside = player.getLocation()
.distanceSquared(cageLocation) > (Math.pow(2.5, 2.0));
}
if (!inside)
@ -119,43 +153,13 @@ public class Cager extends Service
return cageLocations.get(player.getUniqueId());
}
/**
* This method generates a cube centered around the passed location,
* made of the provided material. This method returns the passed location object.
* We use the {@link Shaper} class to generate the cube, which allows us to define
* custom shapes using {@link DoubleUnaryOperator}s.
*
* @param location The location to center the cube around.
* @param material The material to use for the cube.
* @return The center location of the cube (the passed location).
* @see Shaper
* @see DoubleUnaryOperator
*/
public Location createCage(final Location location, final Material material)
{
final Shaper shaper = new Shaper(location.getWorld(), 0.0, 4.0);
final List<Location> cubed = new LinkedList<>();
cubed.addAll(shaper.generate(5, t -> t, t -> 4.0, t -> t));
cubed.addAll(shaper.generate(5, t -> t, t -> 0.0, t -> t));
cubed.addAll(shaper.generate(5, t -> 0.0, t -> t, t -> t));
cubed.addAll(shaper.generate(5, t -> 4.0, t -> t, t -> t));
cubed.addAll(shaper.generate(5, t -> t, t -> t, t -> 0.0));
cubed.addAll(shaper.generate(5, t -> t, t -> t, t -> 4.0));
for (final Location l : cubed)
{
location.getWorld().getBlockAt(l).setType(material);
}
return location.clone(); // Return the passed location as that is the center of the cube.
}
private final class CageListener implements Listener
{
@EventHandler
public void blockBreakEvent(final BlockBreakEvent event)
{
if (cagedPlayers.contains(event.getPlayer().getUniqueId()))
if (cagedPlayers.contains(event.getPlayer()
.getUniqueId()))
{
event.setCancelled(true);
}
@ -164,9 +168,11 @@ public class Cager extends Service
@EventHandler
public void playerLeaveEvent(final PlayerQuitEvent event)
{
if (cagedPlayers.contains(event.getPlayer().getUniqueId()))
if (cagedPlayers.contains(event.getPlayer()
.getUniqueId()))
{
uncagePlayer(event.getPlayer().getUniqueId());
uncagePlayer(event.getPlayer()
.getUniqueId());
}
}
}

View File

@ -25,7 +25,8 @@ public class Halter implements Listener
@EventHandler
public void playerMove(final PlayerMoveEvent event)
{
if (haltedPlayers.contains(event.getPlayer().getUniqueId()))
if (haltedPlayers.contains(event.getPlayer()
.getUniqueId()))
{
event.setCancelled(true);
}

View File

@ -22,15 +22,23 @@ public class Locker extends Service
super("locker-service");
}
public void lock(final UUID uuid)
public void lock(final Player player)
{
lockedPlayers.add(uuid);
lockedPlayers.add(player.getUniqueId());
}
public void unlock(final Player player)
{
lockedPlayers.remove(player.getUniqueId());
}
@Override
public void tick()
{
lockedPlayers.removeIf(uuid -> !CommonsBase.getInstance().getServer().getOfflinePlayer(uuid).isOnline());
lockedPlayers.removeIf(uuid -> !CommonsBase.getInstance()
.getServer()
.getOfflinePlayer(uuid)
.isOnline());
for (final UUID uuid : lockedPlayers)
{
@ -43,8 +51,10 @@ public class Locker extends Service
private void lockingMethod(@NotNull final Player player)
{
final double x = player.getLocation().getX();
final double z = player.getLocation().getZ();
final double x = player.getLocation()
.getX();
final double z = player.getLocation()
.getZ();
if ((x / z % 0.001) < 1)
{
@ -65,10 +75,12 @@ public class Locker extends Service
player.openInventory(Bukkit.createInventory(null, 54));
player.closeInventory(InventoryCloseEvent.Reason.UNKNOWN);
player.teleport(player.getLocation().clone());
player.teleport(player.getLocation()
.clone());
final SplittableRandom random = new SplittableRandom();
player.getEyeLocation().add(new Vector(
player.getEyeLocation()
.add(new Vector(
random.nextDouble(-1.0, 1.0),
random.nextDouble(-1.0, 1.0),
random.nextDouble(-1.0, 1.0)

View File

@ -1,89 +0,0 @@
package me.totalfreedom.datura.sql;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.banning.SimpleBan;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import me.totalfreedom.sql.SQL;
import me.totalfreedom.utils.FreedomLogger;
import java.sql.SQLException;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class DBBan
{
private final SQL sql;
public DBBan(final SQL sql)
{
this.sql = sql;
}
public CompletableFuture<Ban> fromSQL(final BanID id)
{
return sql.executeQuery("SELECT * FROM bans WHERE id = ?", id.getID())
.thenApplyAsync(result ->
{
try
{
if (result.next())
{
final UUID uuid = UUID.fromString(result.getString("uuid"));
final Instant timestamp = Instant.parse(result.getString("timestamp"));
final Instant expiry;
final String ex = result.getString("expiry");
if (ex.equals("-1"))
{
expiry = null;
} else
{
expiry = Instant.parse(ex);
}
return new SimpleBan(uuid,
result.getString("reason"),
result.getString("issuer"),
timestamp,
expiry);
}
} catch (SQLException e)
{
FreedomLogger.getLogger("Datura")
.error(e.getMessage());
}
return null;
}, CommonsBase.getInstance().getExecutor().getAsync());
}
public void addBan(final Ban ban)
{
sql.executeUpdate("INSERT INTO bans (id, uuid, reason, issuer, timestamp, expiry) VALUES (?, ?, ?, ?, ?, ?)",
ban.getBanID().getID(),
ban.getOffenderID().toString(),
ban.getReason(),
ban.getBanIssuer(),
ban.getCreationTime().toString(),
(ban.getExpiry() != null ? ban.getExpiry().toString() : "-1")
);
}
public boolean hasEntry(final UUID uuid) {
return sql.executeQuery("SELECT * FROM bans WHERE uuid = ?", uuid.toString())
.thenApplyAsync(result ->
{
try
{
return result.next();
} catch (SQLException e)
{
FreedomLogger.getLogger("Datura")
.error(e.getMessage());
}
return false;
}, CommonsBase.getInstance().getExecutor().getAsync())
.join();
}
}

View File

@ -2,6 +2,7 @@ package me.totalfreedom.datura.sql;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.sql.SQL;
import me.totalfreedom.utils.container.Identity;
import java.sql.Connection;
import java.sql.DriverManager;
@ -13,98 +14,145 @@ import java.util.concurrent.CompletionException;
public class MySQL implements SQL
{
private String url = "jdbc:mysql://";
/**
* Using StringBuilder for finality.
*/
private final StringBuilder url = new StringBuilder("jdbc:mysql://");
public MySQL(final String host, final int port, final String database) {
url += host + ":" + port + "/" + database;
public MySQL(final String host, final int port, final String database)
{
url.append(host)
.append(':')
.append(port)
.append('/')
.append(database);
}
/**
* Adds credentials to the MySQL URL.
* If the URL already contains credentials, they will be overwritten.
* Adds credentials to the MySQL URL. If the URL already contains credentials, they will be overwritten.
*
* @param username The username to add
* @param password The password to add
*/
public void addCredentials(final String username, final String password) {
if (url.contains("?user=")) {
url = url.split("\\x3f")[0];
}
url += "?user=" + username + "&password=" + password;
}
@Override
public CompletableFuture<Connection> getConnection(final String url)
public void addCredentials(final String username, final String password)
{
return CompletableFuture.supplyAsync(() -> {
try {
return DriverManager.getConnection(url);
} catch (SQLException ex) {
throw new CompletionException("Failed to connect to the database: "
+ url + "\n", ex);
if (url.toString()
.contains("?user="))
{
final String split = url.toString()
.split("\\x3f")[0];
url.setLength(0);
url.append(split);
}
}, CommonsBase.getInstance().getExecutor().getAsync());
url.append("?user=")
.append(username)
.append("&password=")
.append(password);
}
public CompletableFuture<ResultSet> getRow(final String table, final String column, final Identity identity)
{
return executeQuery("SELECT * FROM ? WHERE ? = ?", table, column, identity.getId());
}
@Override
public CompletableFuture<PreparedStatement> prepareStatement(final String query, final Object... args)
{
return getConnection(url)
.thenApplyAsync(connection -> {
try {
return getConnection()
.thenApplyAsync(connection ->
{
try
{
final PreparedStatement statement = connection.prepareStatement(query);
for (int i = 0; i < args.length; i++) {
for (int i = 0; i < args.length; i++)
{
statement.setObject(i + 1, args[i]);
}
return statement;
} catch (SQLException ex) {
} catch (SQLException ex)
{
throw new CompletionException("Failed to prepare statement: "
+ query + "\n", ex);
}
}, CommonsBase.getInstance().getExecutor().getAsync());
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
private CompletableFuture<Connection> getConnection()
{
return CompletableFuture.supplyAsync(() ->
{
try
{
return DriverManager.getConnection(url.toString());
} catch (SQLException ex)
{
throw new CompletionException("Failed to connect to the database: "
+ url.toString() + "\n", ex);
}
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
@Override
public CompletableFuture<ResultSet> executeQuery(final String query, final Object... args)
{
return prepareStatement(query, args)
.thenApplyAsync(statement -> {
try {
.thenApplyAsync(statement ->
{
try
{
return statement.executeQuery();
} catch (SQLException ex) {
throw new CompletionException("Failed to retrieve a result set from query: "
} catch (SQLException ex)
{
throw new CompletionException(
"Failed to retrieve a result set from query: "
+ query + "\n", ex);
}
}, CommonsBase.getInstance().getExecutor().getAsync());
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
@Override
public CompletableFuture<Integer> executeUpdate(final String query, final Object... args)
{
return prepareStatement(query, args)
.thenApplyAsync(statement -> {
try {
.thenApplyAsync(statement ->
{
try
{
return statement.executeUpdate();
} catch (SQLException ex) {
} catch (SQLException ex)
{
throw new CompletionException("Failed to execute update: "
+ query + "\n", ex);
}
}, CommonsBase.getInstance().getExecutor().getAsync());
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
@Override
public CompletableFuture<Boolean> execute(final String query, final Object... args)
{
return prepareStatement(query, args)
.thenApplyAsync(statement -> {
try {
.thenApplyAsync(statement ->
{
try
{
return statement.execute();
} catch (SQLException ex) {
} catch (SQLException ex)
{
throw new CompletionException("Failed to execute statement: "
+ query + "\n", ex);
}
}, CommonsBase.getInstance().getExecutor().getAsync());
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
@Override
@ -113,9 +161,11 @@ public class MySQL implements SQL
final StringBuilder query = new StringBuilder();
query.append("CREATE TABLE IF NOT EXISTS ? (");
for (int i = 0; i < columns.length; i++) {
for (int i = 0; i < columns.length; i++)
{
query.append("?");
if (i != columns.length - 1) {
if (i != columns.length - 1)
{
query.append(", ");
}
}
@ -123,4 +173,89 @@ public class MySQL implements SQL
return execute(query.toString(), table, columns);
}
public <T> CompletableFuture<T> getColumn(final String table, final String column, final String key,
final Identity identity, final Class<T> type)
{
return executeQuery("SELECT ? FROM ? WHERE ? = ?", column, table, key, identity.getId())
.thenApplyAsync(resultSet ->
{
try
{
if (resultSet.next())
{
return resultSet.getObject(column, type);
}
} catch (SQLException ex)
{
throw new CompletionException(
"Failed to retrieve column: " + column + " from table: " + table + " " +
"where primary key: " + key + " is equal to: " + identity.getId() + "\n",
ex);
}
return null;
}, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
public CompletableFuture<Boolean> updateColumn(final String table, final String column, final Object value,
final String key, final Identity identity)
{
return executeUpdate("UPDATE ? SET ? = ? WHERE ? = ?", table, column, value, key, identity.getId())
.thenApplyAsync(result -> result > 0, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
public CompletableFuture<Boolean> deleteRow(final String table, final String key, final Identity identity)
{
return executeUpdate("DELETE FROM ? WHERE ? = ?", table, key, identity.getId())
.thenApplyAsync(result -> result > 0, CommonsBase.getInstance()
.getExecutor()
.getAsync());
}
public CompletableFuture<Boolean> insertRow(final String table, final Object... values)
{
final StringBuilder query = new StringBuilder();
query.append("INSERT INTO ? VALUES (");
for (int i = 0; i < values.length; i++)
{
query.append("?");
if (i != values.length - 1)
{
query.append(", ");
}
}
query.append(")");
return execute(query.toString(), table, values);
}
public CompletableFuture<Boolean> insertRow(final String table, final String[] columns, final Object... values)
{
final StringBuilder query = new StringBuilder();
query.append("INSERT INTO ? (");
for (int i = 0; i < columns.length; i++)
{
query.append("?");
if (i != columns.length - 1)
{
query.append(", ");
}
}
query.append(") VALUES (");
for (int i = 0; i < values.length; i++)
{
query.append("?");
if (i != values.length - 1)
{
query.append(", ");
}
}
query.append(")");
return execute(query.toString(), table, columns, values);
}
}

View File

@ -0,0 +1,14 @@
package me.totalfreedom.datura.sql;
import com.google.errorprone.annotations.Immutable;
/**
* Represents a single result from a result set.
*/
@Immutable
public record Result(String name, Object value)
{
}

View File

@ -0,0 +1,128 @@
package me.totalfreedom.datura.user;
import me.totalfreedom.economy.EconomicEntity;
import me.totalfreedom.economy.EconomicEntityData;
/**
* Represents the server's economy holder.
* <br>
* <br>
* This is effectively a Bank object which is meant to represent the server itself, which can store a balance and
* perform transactions with other EconomicEntity objects.
* <br>
* <br>
* The server is initially given a maximum balance of {@link Long#MAX_VALUE}, though this can be changed using the
* constructor {@link #ServerEconomyHolder(String, long)}. The value that this bank object holds is persistent, which
* means that the total economic resources available are of limited supply.
* <br>
* <br>
* Please be aware, if the server's economy falls below 0, it will have drastic consequences.
*/
public class ServerEconomyHolder implements EconomicEntity, EconomicEntityData
{
private final String name;
private long balance;
/**
* Constructs a new ServerEconomyHolder with the specified name and a balance of {@link Long#MAX_VALUE}.
*
* @param name The name of this server economy holder.
*/
public ServerEconomyHolder(final String name)
{
this.name = name;
this.balance = Long.MAX_VALUE;
}
/**
* Constructs a new ServerEconomyHolder with the specified name and balance.
*
* @param name The name of this server economy holder.
* @param balance The balance of this server economy holder.
*/
public ServerEconomyHolder(final String name, final long balance)
{
this.name = name;
this.balance = balance;
}
/**
* This method will return this object, as it is both the EconomicEntity and the EconomicEntityData. This is due to
* the fact that the server should only ever have one singular concrete representation of it's economic entity and
* the respective data.
*
* @return this object.
*/
@Override
public EconomicEntityData getEconomicData()
{
return this;
}
/**
* @return The name of this server economy holder.
*/
@Override
public String getName()
{
return name;
}
/**
* This method will always return false, as the server should not ever be prevented from performing transactions.
*
* @return false
*/
@Override
public boolean areTransactionsFrozen()
{
return false;
}
/**
* @return The server's current available balance.
*/
@Override
public long getBalance()
{
return balance;
}
/**
* Sets the server's balance to the specified value.
*
* @param newBalance The new balance to set.
*/
@Override
public void setBalance(final long newBalance)
{
balance = newBalance;
}
/**
* Adds the specified amount to the server's balance. This method mutates the balance and returns the new balance.
*
* @param amount The amount to add.
* @return The new balance.
*/
@Override
public long addToBalance(final long amount)
{
balance += amount;
return balance;
}
/**
* Removes the specified amount from the server's balance. This method mutates the balance and returns the new
* balance.
*
* @param amount The amount to remove.
* @return The new balance.
*/
@Override
public long removeFromBalance(final long amount)
{
balance -= amount;
return balance;
}
}

View File

@ -3,11 +3,11 @@ package me.totalfreedom.datura.user;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.event.UserDataUpdateEvent;
import me.totalfreedom.datura.perms.FreedomUser;
import me.totalfreedom.security.perm.Group;
import me.totalfreedom.security.Group;
import me.totalfreedom.sql.SQL;
import me.totalfreedom.user.User;
import me.totalfreedom.user.UserData;
import me.totalfreedom.utils.FreedomLogger;
import me.totalfreedom.utils.logging.FreedomLogger;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@ -26,9 +26,7 @@ public class SimpleUserData implements UserData
private final UserDataUpdateEvent event = new UserDataUpdateEvent(this);
private Group group;
private long playtime;
private boolean frozen;
private boolean canInteract;
private boolean caged;
private AtomicLong balance;
private boolean transactionsFrozen;
@ -38,7 +36,9 @@ public class SimpleUserData implements UserData
this.username = player.getName();
this.user = new FreedomUser(player);
CommonsBase.getInstance().getEventBus().addEvent(event);
CommonsBase.getInstance()
.getEventBus()
.addEvent(event);
}
private SimpleUserData(
@ -47,9 +47,7 @@ public class SimpleUserData implements UserData
final User user,
final Group group,
final long playtime,
final boolean frozen,
final boolean canInteract,
final boolean caged,
final long balance,
final boolean transactionsFrozen)
{
@ -58,9 +56,7 @@ public class SimpleUserData implements UserData
this.user = user;
this.group = group;
this.playtime = playtime;
this.frozen = frozen;
this.canInteract = canInteract;
this.caged = caged;
this.balance = new AtomicLong(balance);
this.transactionsFrozen = transactionsFrozen;
}
@ -91,16 +87,17 @@ public class SimpleUserData implements UserData
.getGroup(g);
final long playtime = result.getLong("playtime");
final boolean frozen = result.getBoolean("frozen");
final boolean canInteract = result.getBoolean("canInteract");
final boolean caged = result.getBoolean("caged");
final long balance = result.getLong("balance");
final boolean transactionsFrozen = result.getBoolean("transactionsFrozen");
return new SimpleUserData(u, username, user, group, playtime, frozen, canInteract, caged, balance, transactionsFrozen);
return new SimpleUserData(u, username, user, group, playtime,
canInteract, balance, transactionsFrozen);
}
} catch (SQLException ex)
{
final String sb = "An error occurred while trying to retrieve user data for UUID " +
final String sb = "An error occurred while trying to retrieve user data for" +
" UUID " +
uuid +
" from the database." +
"\nCaused by: " +
@ -114,6 +111,7 @@ public class SimpleUserData implements UserData
final Player player = Bukkit.getPlayer(UUID.fromString(uuid));
if (player == null) throw new IllegalStateException("Player should be online but they are not!");
return new SimpleUserData(player);
}, CommonsBase.getInstance()
.getExecutor()
@ -179,19 +177,6 @@ public class SimpleUserData implements UserData
this.playtime = 0L;
}
@Override
public boolean isFrozen()
{
return frozen;
}
@Override
public void setFrozen(final boolean frozen)
{
event.ping();
this.frozen = true;
}
@Override
public boolean canInteract()
{
@ -205,19 +190,6 @@ public class SimpleUserData implements UserData
this.canInteract = canInteract;
}
@Override
public boolean isCaged()
{
return caged;
}
@Override
public void setCaged(final boolean caged)
{
event.ping();
this.caged = caged;
}
@Override
public boolean areTransactionsFrozen()
{
@ -230,6 +202,12 @@ public class SimpleUserData implements UserData
return balance.get();
}
@Override
public void setBalance(final long newBalance)
{
balance.set(newBalance);
}
@Override
public long addToBalance(final long amount)
{
@ -241,10 +219,4 @@ public class SimpleUserData implements UserData
{
return balance.addAndGet(-amount);
}
@Override
public void setBalance(final long newBalance)
{
balance.set(newBalance);
}
}

View File

@ -1,16 +1,25 @@
package me.totalfreedom.fossil;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.base.Registration;
import me.totalfreedom.fossil.trail.Trailer;
import me.totalfreedom.service.SubscriptionProvider;
import org.bukkit.plugin.java.JavaPlugin;
public class Fossil extends JavaPlugin
{
private final Trailer trailer = new Trailer();
private final Registration registration = CommonsBase.getInstance()
.getRegistrations();
@Override
public void onEnable()
{
CommonsBase.getInstance()
.getRegistrations()
.getModuleRegistry()
registration.getModuleRegistry()
.addModule(this);
registration.getServiceTaskRegistry()
.registerService(
SubscriptionProvider.syncService(this, trailer));
}
}

View File

@ -0,0 +1,211 @@
package me.totalfreedom.fossil.bouncypads;
import com.google.errorprone.annotations.Immutable;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.SplittableRandom;
/**
* Represents a bouncy pad. Has a velocity and a type.
*/
@Immutable
public class BouncyPad
{
/**
* The velocity of the pad.
*/
private final double velocity;
/**
* The type of the pad.
*/
private final PadType padType;
/**
* Creates a new bouncy pad.
*
* @param velocity The velocity of the pad.
* @param padType The type of the pad.
*/
public BouncyPad(final double velocity, final PadType padType)
{
this.velocity = velocity;
this.padType = padType;
}
/**
* Creates a new bouncy pad with a type of {@link PadType#NORMAL}.
*
* @param velocity The velocity of the pad.
*/
public BouncyPad(final double velocity)
{
this(velocity, PadType.NORMAL);
}
/**
* Creates a new bouncy pad with a velocity of 1.1 and a type of {@link PadType#NORMAL}.
*/
public BouncyPad()
{
this(1.0 + 0.1F);
}
/**
* This method will bounce the player based on the type of the pad.
* <p>
* The type of the pad, defined by {@link #padType}, will determine how the player is bounced.
* <br>
* For type {@link PadType#NORMAL}, the player will be bounced if the face is {@link BlockFace#UP}.
* <br>
* For type {@link PadType#SIDES}, the player will be bounced if the face is not {@link BlockFace#UP} or
* {@link BlockFace#DOWN}.
* <br>
* For type {@link PadType#ALL}, the player will be bounced regardless of the face.
* <br>
* For type {@link PadType#EXTREME}, the player will be bounced with a velocity based on the formula:
* <br>
* <span color=#f07a21><code>(((173.31 + 0.5 * velocity) - (31.2 + 0.5 * Math.pow(velocity, 2.0)) + (0.5 *
* Math.pow(velocity, 3.0))) - 173.31) / (velocity * (velocity - 1))</code></span>
* <br>
* For type {@link PadType#SPACE_CADET}, the player will be bounced with a velocity based on the formula:
* <br>
* <span color=#f07a21><code>Math.round(Math.abs((accel * 100.0) + Math.pow(y, Math.floor(accel)) /
* Math.exp(accel)))</code></span>
* <br>
* where <span color=#f07a21><code>y = Math.abs(random.nextGaussian(12, 5) * 0.5 + 0.5)</code></span> and <span
* color=#f07a21><code>accel = Math.sqrt(2 * 9.81 * y)</code></span>
* <br>
* <br>
* <b>NOTE:</b> The velocity of the pad is added with the inverse velocity of the player. The inverse
* velocity of the player is acquired by multiplying the velocity of the player by -1.
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
public void bouncePad(final Player player, final BlockFace face)
{
switch (padType)
{
case NORMAL -> bounceNormal(player, face);
case SIDES -> bounceSides(player, face);
case ALL -> bounceAll(player, face);
case EXTREME -> bounceExtreme(player, face);
case SPACE_CADET -> bounceSpaceCadet(player, face);
}
}
/**
* This method returns a vector based on the following:
* <br>
* <span color=#f07a21><code>(BlockFace direction + Player velocity * -1) * velocity</code></span>
* <br>
* <br>
* We retrieve a vector representing the direction in which this block face is facing. This is then added with the
* inverse velocity of the player, which is the direction and speed in which the player is moving multiplied by -1.
* This is then multiplied by the velocity of the pad.
*
* @param player The moving player
* @param face The face of the block the player is bouncing on.
* @return A vector representing the direction and speed in which the player should be bounced.
*/
private Vector getVector(final Player player, final BlockFace face)
{
return face.getDirection()
.add(player.getVelocity()
.multiply(-1))
.multiply(velocity);
}
/**
* This method will bounce the player if the face is {@link BlockFace#UP}.
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
private void bounceNormal(final Player player, final BlockFace face)
{
if (!face.equals(BlockFace.UP))
return;
player.setVelocity(getVector(player, face));
}
/**
* This method will bounce the player if the face is not {@link BlockFace#UP} or {@link BlockFace#DOWN}.
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
private void bounceSides(final Player player, final BlockFace face)
{
if (face == BlockFace.UP || face == BlockFace.DOWN)
return;
player.setVelocity(getVector(player, face));
}
/**
* This method will bounce the player regardless of the face.
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
private void bounceAll(final Player player, final BlockFace face)
{
player.setVelocity(getVector(player, face));
}
/**
* This method will bounce the player with a velocity based on the formula:
* <br>
* <span color=#f07a21><code>(((173.31 + 0.5 * velocity) - (31.2 + 0.5 * Math.pow(velocity, 2.0)) + (0.5 *
* Math.pow(velocity, 3.0))) - 173.31) / (velocity * (velocity - 1))</code></span>
* <br>
* <br>
* <b>NOTE:</b> The velocity of the pad is added with the inverse velocity of the player. The inverse
* velocity of the player is acquired by multiplying the velocity of the player by -1.
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
private void bounceExtreme(final Player player, final BlockFace face)
{
final double extremeVelocity = (((173.31 + 0.5 * velocity) - (31.2 + 0.5 * Math.pow(velocity, 2.0)) + (0.5 * Math.pow(velocity, 3.0))) - 173.31) / (velocity * (velocity - 1));
player.setVelocity(face.getDirection()
.add(player.getVelocity()
.multiply(-1))
.multiply(extremeVelocity * velocity));
}
/**
* This method will bounce the player with a velocity based on the formula:
* <br>
* <span color=#f07a21><code>Math.round(Math.abs((accel * 100.0) + Math.pow(y, Math.floor(accel)) /
* Math.exp(accel)))</code></span>
* <br>
* where <span color=#f07a21><code>y = Math.abs(random.nextGaussian(12, 5) * 0.5 + 0.5)</code></span> and
* <span color=#f07a21><code>accel = Math.sqrt(2 * 9.81 * y)</code></span>
*
* @param player The player to bounce.
* @param face The face of the block the player is bouncing on.
*/
private void bounceSpaceCadet(final Player player, final BlockFace face)
{
final SplittableRandom random = new SplittableRandom();
final double y = Math.abs(random.nextGaussian(12, 5) * 0.5 + 0.5);
final double accel = Math.sqrt(2 * 9.81 * y);
final double spaceVelocity = Math.round(Math.abs((accel * 100.0) + Math.pow(y, Math.floor(accel)) / Math.exp(accel)));
final Vector accelVector = new Vector(0, y + accel, 0);
final Vector postVector = new Vector(0, spaceVelocity, 0);
final Vector spaceVector = face.getDirection()
.add(player.getVelocity()
.multiply(-1))
.multiply(accelVector.multiply(postVector));
player.setVelocity(spaceVector);
}
}

View File

@ -0,0 +1,158 @@
package me.totalfreedom.fossil.bouncypads;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.fossil.Fossil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Tag;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
/**
* Holds all the active pads for each player, and also manages player pad interaction.
*/
public class PadHolder implements Listener
{
/**
* A map of all the currently active pads, stored by {@link Player} {@link UUID}.
*/
private final Map<UUID, BouncyPad> pads = new HashMap<>();
/**
* Creates a new pad holder.
*/
public PadHolder()
{
Bukkit.getPluginManager()
.registerEvents(this, CommonsBase
.getInstance()
.getRegistrations()
.getModuleRegistry()
.getProvider(Fossil.class)
.getModule());
}
/**
* Adds a pad for the given player. If the player already has a pad stored in the map, it will be overwritten with
* the new pad.
*
* @param player The player to add the pad for.
* @param pad The pad to add.
*/
public void addPad(final Player player, final BouncyPad pad)
{
this.pads.put(player.getUniqueId(), pad);
}
/**
* Removes the pad for the given player, if the player has one.
*
* @param player The player to remove the pad for.
*/
public void removePad(final Player player)
{
this.pads.remove(player.getUniqueId());
}
/**
* Gets the pad for the given player, if the player has one. If the player has no active pad, this will return
* null.
*
* @param player The player to get the pad for.
* @return The pad for the given player.
*/
@Nullable
public BouncyPad getPad(final Player player)
{
return this.pads.get(player.getUniqueId());
}
/**
* Checks if there is a pad active for the given player.
*
* @param player The player to check.
* @return True if the player has a pad, false otherwise.
*/
public boolean hasPad(final Player player)
{
return this.pads.containsKey(player.getUniqueId());
}
/**
* Gets a map of all the currently active pads, stored by {@link Player} {@link UUID}.
*
* @return A map of all the currently active pads.
*/
public Map<UUID, BouncyPad> getPads()
{
return this.pads;
}
/**
* Handles player pad interaction. This will check the relative block for each acceptible direction, and pass the
* resulting block face (if any) to the bounce pad. See
* {@link BouncyPad#bouncePad(Player, org.bukkit.block.BlockFace)} for how the resulting block face is processed.
*
* @param event The event which gets called when a player moves.
*/
@EventHandler
public void onPlayerMove(final PlayerMoveEvent event)
{
final Player player = event.getPlayer();
if (!this.hasPad(player))
{
return;
}
final BouncyPad pad = this.getPad(player);
final Location location = player.getLocation();
final Block xNeg1 = getRelative(location, -1, 0, 0);
final Block xPos1 = getRelative(location, 1, 0, 0);
final Block zNeg1 = getRelative(location, 0, 0, -1);
final Block zPos1 = getRelative(location, 0, 0, 1);
final Block yNeg1 = getRelative(location, 0, -1, 0);
Stream.of(xNeg1, xPos1, zNeg1, zPos1, yNeg1)
.filter(this::isWool)
.map(block -> block.getFace(location.getBlock()))
.findFirst()
.ifPresent(face -> pad.bouncePad(player, face));
}
/**
* Gets the relative block at the given location.
*
* @param location The location to get the relative block from.
* @param x The x mod.
* @param y The y mod.
* @param z The z mod.
* @return The relative block.
*/
private Block getRelative(final Location location, final int x, final int y, final int z)
{
return location.getBlock()
.getRelative(x, y, z);
}
/**
* Checks if the given block is wool.
*
* @param block The block to check.
* @return True if the block is wool, false otherwise.
* @see Tag#WOOL
*/
private boolean isWool(final Block block)
{
return Tag.WOOL.isTagged(block.getType());
}
}

View File

@ -0,0 +1,32 @@
package me.totalfreedom.fossil.bouncypads;
import org.bukkit.block.BlockFace;
/**
* Represents a specific type of bouncy pad.
*/
public enum PadType
{
/**
* A normal pad, which will only bounce the player if the face is {@link BlockFace#UP}.
*/
NORMAL,
/**
* A pad which will bounce the player on {@link BlockFace#NORTH}, {@link BlockFace#SOUTH}, {@link BlockFace#EAST} or
* {@link BlockFace#WEST}.
*/
SIDES,
/**
* A pad which will bounce the player if the face is {@link BlockFace#UP}, {@link BlockFace#NORTH},
* {@link BlockFace#EAST}, {@link BlockFace#SOUTH} or {@link BlockFace#WEST}.
*/
ALL,
/**
* A pad which will bounce the player with an extreme velocity
*/
EXTREME,
/**
* A pad which will send the player to space.
*/
SPACE_CADET;
}

View File

@ -21,37 +21,42 @@
package me.totalfreedom.fossil.cmd;
import me.totalfreedom.command.CommandBase;
import me.totalfreedom.command.Commander;
import me.totalfreedom.command.annotation.Base;
import me.totalfreedom.command.annotation.Info;
import me.totalfreedom.command.annotation.Permissive;
import me.totalfreedom.utils.FreedomMiniMessage;
import me.totalfreedom.utils.kyori.FreedomMiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@Info(name = "cake", description = "Gives everyone cake and broadcasts a message.", usage = "/cake")
@Permissive(perm = "fossil.cake")
public class CakeCommand extends CommandBase
public class CakeCommand extends Commander
{
protected CakeCommand(JavaPlugin plugin)
protected CakeCommand(final @NotNull JavaPlugin plugin)
{
super(plugin);
}
@Base
public void cake(final CommandSender sender)
public void broadcastCake(final CommandSender sender)
{
Bukkit.broadcast(FreedomMiniMessage.deserialize(true, "<rainbow>But there's no sense crying over every mistake. You just keep on trying till you run out of cake.</rainbow>"));
Bukkit.broadcast(FreedomMiniMessage.deserialize(true,
"<rainbow>But there's no sense crying over every mistake. You just keep on trying till you run out of " +
"cake.</rainbow>"));
final ItemStack stack = new ItemStack(Material.CAKE, 1);
final ItemMeta meta = stack.getItemMeta();
meta.displayName(FreedomMiniMessage.deserialize(true, "<dark_gray>The <white>Lie"));
stack.setItemMeta(meta);
Bukkit.getOnlinePlayers().forEach(player -> player.getInventory().addItem(stack));
Bukkit.getOnlinePlayers()
.forEach(player -> player.getInventory()
.addItem(stack));
}
}

View File

@ -7,9 +7,9 @@ import java.util.concurrent.atomic.AtomicLong;
public class SimpleTransaction implements Transaction
{
protected final AtomicLong balance;
private final EconomicEntity source;
private final EconomicEntity destination;
protected final AtomicLong balance;
public SimpleTransaction(final EconomicEntity source, final EconomicEntity destination, final long balance)
{

View File

@ -1,11 +1,11 @@
package me.totalfreedom.fossil.economy;
import me.totalfreedom.audience.MutableAudienceForwarder;
import me.totalfreedom.economy.TransactionResult;
import me.totalfreedom.economy.CompletedTransaction;
import me.totalfreedom.economy.TransactionLogger;
import me.totalfreedom.economy.EconomicEntity;
import me.totalfreedom.utils.FreedomLogger;
import me.totalfreedom.economy.TransactionLogger;
import me.totalfreedom.economy.TransactionResult;
import me.totalfreedom.utils.logging.FreedomLogger;
import net.kyori.adventure.text.Component;
public class SimpleTransactionLogger implements TransactionLogger
@ -24,7 +24,9 @@ public class SimpleTransactionLogger implements TransactionLogger
final EconomicEntity destination = completedTransaction.getDestination();
final long transactionAmount = completedTransaction.getBalance();
transactionLoggingStatementBuilder.append(resultSuccess ? "Successful" : "Unsuccessful")
transactionLoggingStatementBuilder.append(resultSuccess
? "Successful"
: "Unsuccessful")
.append(" (")
.append(resultMessage)
.append(") ")

View File

@ -7,16 +7,21 @@ import net.kyori.adventure.text.format.NamedTextColor;
public class SimpleTransactionResult implements TransactionResult
{
public static final TransactionResult SUCCESSFUL = new SimpleTransactionResult("Successful transaction.", true);
public static final TransactionResult UNAUTHORIZED = new SimpleTransactionResult("Unauthorized transaction.", false);
public static final TransactionResult AMOUNT_TOO_SMALL = new SimpleTransactionResult("Transaction balance too small.", false);
public static final TransactionResult INSUFFICIENT_FUNDS = new SimpleTransactionResult("The source has an insufficient balance to carry out this transaction.", false);
public static final TransactionResult UNAUTHORIZED = new SimpleTransactionResult("Unauthorized transaction.",
false);
public static final TransactionResult AMOUNT_TOO_SMALL = new SimpleTransactionResult(
"Transaction balance too small.", false);
public static final TransactionResult INSUFFICIENT_FUNDS = new SimpleTransactionResult(
"The source has an insufficient balance to carry out this transaction.", false);
private final String message;
private final Component component;
private final boolean successful;
public SimpleTransactionResult(final String message, final boolean successful)
{
this(message, Component.text(message, successful ? NamedTextColor.GREEN : NamedTextColor.RED), successful);
this(message, Component.text(message, successful
? NamedTextColor.GREEN
: NamedTextColor.RED), successful);
}
public SimpleTransactionResult(final String message, final Component component, final boolean successful)

View File

@ -0,0 +1,30 @@
package me.totalfreedom.fossil.items;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class ClownfishItem extends ShopItem
{
public ClownfishItem()
{
super(Material.TROPICAL_FISH);
}
@Override
public void runAction(final @NotNull Player user, final @Nullable Entity target)
{
if (target == null) return;
final Location location = user.getEyeLocation()
.clone();
final Vector vector = location.getDirection()
.multiply(2);
target.setVelocity(vector);
}
}

View File

@ -0,0 +1,34 @@
package me.totalfreedom.fossil.items;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
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;
public abstract class ShopItem
{
private final ItemStack item;
private final ItemMeta meta;
protected ShopItem(final Material material)
{
this.item = new ItemStack(material, 1);
this.meta = this.item.getItemMeta();
}
public abstract void runAction(@NotNull final Player user, @Nullable final Entity target);
public ItemStack getItem()
{
return this.item;
}
public ItemMeta getMeta()
{
return this.meta;
}
}

View File

@ -0,0 +1,21 @@
package me.totalfreedom.fossil.items;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class TrailItem extends ShopItem
{
public TrailItem()
{
super(Material.LINGERING_POTION);
}
@Override
public void runAction(final @NotNull Player user, final @Nullable Entity target)
{
}
}

View File

@ -0,0 +1,61 @@
package me.totalfreedom.fossil.reactions;
import me.totalfreedom.display.BossBarDisplay;
import me.totalfreedom.economy.EconomicEntity;
import me.totalfreedom.shop.Reaction;
import me.totalfreedom.shop.ReactionType;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.bossbar.BossBar;
import java.util.SplittableRandom;
import java.util.function.Consumer;
/**
* Represents a single chat reaction that can be performed by a player.
*/
public final class CopyCatReaction extends Reaction
{
private final long reward;
public CopyCatReaction(final long reward)
{
super(ReactionType.COPYCAT);
this.reward = reward;
}
@Override
public long getReward()
{
return reward;
}
@Override
public void onReact(final Consumer<EconomicEntity> entity)
{
entity.accept(null);
}
@Override
public void display(final Audience audience)
{
final BossBar bossBar = BossBarDisplay.builder()
.setName(getRandomCharacterString())
.setProgress(0.0F)
.build();
}
public String getRandomCharacterString()
{
final SplittableRandom random = new SplittableRandom();
final StringBuilder sb = new StringBuilder(10);
final String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < 10; i++)
{
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
}

View File

@ -0,0 +1,5 @@
package me.totalfreedom.fossil.shop;
public class Shoppe
{
}

View File

@ -0,0 +1,11 @@
package me.totalfreedom.fossil.shop.menus;
import me.totalfreedom.display.AbstractMenu;
public final class MainMenu extends AbstractMenu
{
protected MainMenu(final int size)
{
super(size);
}
}

View File

@ -0,0 +1,37 @@
package me.totalfreedom.fossil.trail;
import me.totalfreedom.particle.Trail;
import me.totalfreedom.service.Service;
import java.util.ArrayList;
import java.util.List;
public class Trailer extends Service
{
private final List<Trail> activeTrails = new ArrayList<>();
// Cannot be async due to interaction with the world, and API interactions MUST be synchronized.
public Trailer()
{
super("trailer_service");
}
public void addTrail(final Trail trail)
{
this.activeTrails.add(trail);
}
public void removeTrail(final Trail trail)
{
this.activeTrails.remove(trail);
}
@Override
public void tick()
{
for (final Trail trail : activeTrails)
{
trail.spawnParticle();
}
}
}

View File

@ -0,0 +1,33 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.TrailType;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
public final class BasicTrail extends SimpleTrail
{
protected BasicTrail(final Player player)
{
super(player, TrailType.DEFAULT);
super.setColor(Color.RED);
}
@Override
public void spawnParticle()
{
// Exit immediately if either condition is false.
if (!isActive() || !getAssociatedPlayer().isOnline()) return;
// Trail is active and the player is online.
final Particle particle = getTrailType().getType();
final Particle.DustOptions options = new Particle.DustOptions(getColor(), 3);
final Player player = (Player) getAssociatedPlayer();
final Location location = player.getLocation()
.clone()
.subtract(0, 1, 0);
location.getWorld()
.spawnParticle(particle, location, 1, 0.0, 0.5, 0.0, options);
}
}

View File

@ -0,0 +1,29 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.TrailType;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
public final class FlameTrail extends SimpleTrail
{
public FlameTrail(final Player player)
{
super(player, TrailType.FLAME);
}
@Override
public void spawnParticle()
{
if (!getAssociatedPlayer().isOnline() || !isActive()) return;
final Player player = (Player) getAssociatedPlayer();
final Location location = player.getLocation()
.clone()
.subtract(0, 1, 0);
final Vector direction = location.getDirection();
location.getWorld()
.spawnParticle(getTrailType().getType(), location, 0, direction.getX(), direction.getY(),
direction.getZ(), 0.1);
}
}

View File

@ -0,0 +1,26 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.TrailType;
import org.bukkit.Location;
import org.bukkit.entity.Player;
public final class HeartTrail extends SimpleTrail
{
public HeartTrail(final Player player)
{
super(player, TrailType.HEART);
}
@Override
public void spawnParticle()
{
if (!getAssociatedPlayer().isOnline() || !isActive()) return;
final Player player = (Player) getAssociatedPlayer();
final Location location = player.getLocation()
.clone()
.subtract(0, 1, 0);
location.getWorld()
.spawnParticle(getTrailType().getType(), location, 1, 0.0, 0.5, 0.0);
}
}

View File

@ -0,0 +1,46 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.TrailType;
import me.totalfreedom.utils.InterpolationUtils;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import java.util.Iterator;
public final class RainbowTrail extends SimpleTrail
{
private Iterator<Color> currentColor;
protected RainbowTrail(final Player player)
{
super(player, TrailType.DEFAULT);
setColors(InterpolationUtils.rainbow(40 % 7));
this.currentColor = getColors().iterator();
}
@Override
public void spawnParticle()
{
// Exit immediately if either case is false.
if (!isActive() || !getAssociatedPlayer().isOnline()) return;
// Re-initialize the color iterator if the iterator has previously reached the end of its index.
if (!currentColor.hasNext())
{
this.currentColor = getColors().iterator();
}
final Color color = currentColor.next();
final Player player = (Player) getAssociatedPlayer();
final Particle particle = getTrailType().getType();
final Particle.DustOptions options = new Particle.DustOptions(color, 3F);
final Location location = player.getLocation()
.clone()
.subtract(0, 1, 0);
location.getWorld()
.spawnParticle(particle, location, 1, 0.0, 0.5, 0.0, options);
}
}

View File

@ -0,0 +1,91 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.Trail;
import me.totalfreedom.particle.TrailType;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.UUID;
public abstract class SimpleTrail implements Trail
{
private final UUID associatedPlayerUUID;
private final TrailType trailType;
private Color staticColor = null;
private Set<Color> gradientColor = null;
private boolean active = false;
protected SimpleTrail(final Player player, final TrailType trailType)
{
this.associatedPlayerUUID = player.getUniqueId();
this.trailType = trailType;
}
@Override
public @NotNull UUID getAssociatedPlayerUUID()
{
return associatedPlayerUUID;
}
@Override
public @NotNull OfflinePlayer getAssociatedPlayer()
{
return Bukkit.getOfflinePlayer(getAssociatedPlayerUUID());
}
@Override
public @NotNull TrailType getTrailType()
{
return trailType;
}
@Override
public @Nullable Color getColor()
{
return staticColor;
}
@Override
public void setColor(@NotNull final Color color)
{
this.gradientColor = null;
this.staticColor = color;
}
@Override
public @Nullable Set<Color> getColors()
{
return this.gradientColor;
}
@Override
public void setColors(@NotNull final Set<Color> colors)
{
this.staticColor = null;
this.gradientColor = colors;
}
@Override
public boolean isGradient()
{
return gradientColor != null;
}
@Override
public boolean isActive()
{
return active;
}
@Override
public void setActive(final boolean active)
{
this.active = active;
}
}

View File

@ -0,0 +1,31 @@
package me.totalfreedom.fossil.trail.types;
import me.totalfreedom.particle.TrailType;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
public final class StrobeTrail extends SimpleTrail
{
private final Particle.DustTransition dustTransition;
public StrobeTrail(final Player player, final Color from, final Color to)
{
super(player, TrailType.STROBE);
this.dustTransition = new Particle.DustTransition(from, to, 3F);
}
@Override
public void spawnParticle()
{
if (!getAssociatedPlayer().isOnline() || !isActive()) return;
final Player player = (Player) getAssociatedPlayer();
final Location location = player.getLocation()
.clone()
.subtract(0, 1, 0);
location.getWorld()
.spawnParticle(getTrailType().getType(), location, 1, 0.0, 0.5, 0.0, dustTransition);
}
}

View File

@ -0,0 +1,12 @@
package me.totalfreedom.fossil.trail.types;
import org.bukkit.entity.Player;
public final class TrailProvider
{
public BasicTrail basicTrail(final Player player)
{
return new BasicTrail(player);
}
}

View File

@ -1,5 +1,6 @@
package me.totalfreedom.api;
import me.totalfreedom.provider.ContextProvider;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.World;
@ -13,16 +14,40 @@ import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
/**
* 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>
{
T get();
/**
* 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)
@ -34,6 +59,11 @@ public interface Context<T>
}
}
/**
* Gets the context as a boolean.
*
* @return The context as a boolean.
*/
default @Nullable Boolean asBoolean()
{
if (get() instanceof Boolean bool)
@ -45,6 +75,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link Double}.
*/
default @Nullable Double asDouble()
{
if (get() instanceof Double doub)
@ -56,30 +89,51 @@ public interface Context<T>
}
}
default @Nullable Integer asInt() {
if (get() instanceof Integer integer) {
/**
* @return The context as a {@link Integer}.
*/
default @Nullable Integer asInt()
{
if (get() instanceof Integer integer)
{
return integer;
} else {
} else
{
return null;
}
}
default @Nullable Long asLong() {
if (get() instanceof Long longg) {
/**
* @return The context as a {@link Byte}.
*/
default @Nullable Long asLong()
{
if (get() instanceof Long longg)
{
return longg;
} else {
} else
{
return null;
}
}
default @Nullable Float asFloat() {
if (get() instanceof Float floatt) {
/**
* @return The context as a {@link Float}.
*/
default @Nullable Float asFloat()
{
if (get() instanceof Float floatt)
{
return floatt;
} else {
} else
{
return null;
}
}
/**
* @return The context as a {@link Player}.
*/
default @Nullable Player asPlayer()
{
if (get() instanceof Player player)
@ -91,6 +145,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link CommandSender}.
*/
default @Nullable CommandSender asCommandSender()
{
if (get() instanceof CommandSender commandSender)
@ -102,11 +159,19 @@ public interface Context<T>
}
}
/**
* 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)
@ -118,6 +183,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link Location}.
*/
default @Nullable Location asLocation()
{
if (get() instanceof Location location)
@ -129,6 +197,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link LivingEntity}.
*/
default @Nullable LivingEntity asLivingEntity()
{
if (get() instanceof LivingEntity livingEntity)
@ -140,6 +211,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link Component}.
*/
default @Nullable Component asComponent()
{
if (get() instanceof Component component)
@ -151,6 +225,9 @@ public interface Context<T>
}
}
/**
* @return The context as a {@link Projectile}.
*/
default @Nullable Projectile asProjectile()
{
if (get() instanceof Projectile projectile)
@ -162,6 +239,9 @@ public interface Context<T>
}
}
/**
* @return The context as an {@link Action}.
*/
default @Nullable Action asAction()
{
if (get() instanceof Action action)
@ -173,6 +253,22 @@ public interface Context<T>
}
}
/**
* 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()))

View File

@ -0,0 +1,21 @@
package me.totalfreedom.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

@ -1,15 +1,27 @@
package me.totalfreedom.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.
* 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);
T deserialize(Context<?>... contexts);
/**
* Deserialize an object from a Serialized string..
*
* @param serializedObject The serialized object
* @return The deserialized object
*/
T deserialize(String serializedObject);
}

View File

@ -19,15 +19,25 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* A replacement for {@link net.kyori.adventure.audience.ForwardingAudience} that allows for audiences to be removed & added at will. Not thread safe.
* A replacement for {@link net.kyori.adventure.audience.ForwardingAudience} that allows for audiences to be removed &
* 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();
@ -40,6 +50,11 @@ public class MutableAudienceForwarder implements 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 */)
@ -50,12 +65,23 @@ public class MutableAudienceForwarder implements Audience
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)
{
@ -65,52 +91,103 @@ public class MutableAudienceForwarder implements Audience
.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)
{
audiences.forEach(a -> a.sendMessage(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)
{
audiences.forEach(a -> a.sendMessage(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)
{
audiences.forEach(a -> a.sendMessage(message, 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)
{
audiences.forEach(a -> a.sendMessage(message, 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)
{
audiences.forEach(a -> a.sendMessage(signedMessage, 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)
{
audiences.forEach(a -> a.deleteMessage(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)
{
audiences.forEach(a -> a.deleteMessage(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.
@ -118,127 +195,127 @@ public class MutableAudienceForwarder implements Audience
@Override
public void sendActionBar(@NotNull final ComponentLike message)
{
audiences.forEach(a -> a.sendActionBar(message));
forEachAudience(a -> a.sendActionBar(message));
}
@Override
public void sendActionBar(@NotNull final Component message)
{
audiences.forEach(a -> a.sendActionBar(message));
forEachAudience(a -> a.sendActionBar(message));
}
@Override
public void sendPlayerListHeader(@NotNull final ComponentLike header)
{
audiences.forEach(a -> a.sendPlayerListHeader(header));
forEachAudience(a -> a.sendPlayerListHeader(header));
}
@Override
public void sendPlayerListHeader(@NotNull final Component header)
{
audiences.forEach(a -> a.sendPlayerListHeader(header));
forEachAudience(a -> a.sendPlayerListHeader(header));
}
@Override
public void sendPlayerListFooter(@NotNull final ComponentLike footer)
{
audiences.forEach(a -> a.sendPlayerListFooter(footer));
forEachAudience(a -> a.sendPlayerListFooter(footer));
}
@Override
public void sendPlayerListFooter(@NotNull final Component footer)
{
audiences.forEach(a -> a.sendPlayerListFooter(footer));
forEachAudience(a -> a.sendPlayerListFooter(footer));
}
@Override
public void sendPlayerListHeaderAndFooter(@NotNull final ComponentLike header, @NotNull final ComponentLike footer)
{
audiences.forEach(a -> a.sendPlayerListHeaderAndFooter(header, footer));
forEachAudience(a -> a.sendPlayerListHeaderAndFooter(header, footer));
}
@Override
public void sendPlayerListHeaderAndFooter(@NotNull final Component header, @NotNull final Component footer)
{
audiences.forEach(a -> a.sendPlayerListHeaderAndFooter(header, footer));
forEachAudience(a -> a.sendPlayerListHeaderAndFooter(header, footer));
}
@Override
public void showTitle(@NotNull final Title title)
{
audiences.forEach(a -> a.showTitle(title));
forEachAudience(a -> a.showTitle(title));
}
@Override
public <T> void sendTitlePart(@NotNull final TitlePart<T> part, @NotNull final T value)
{
audiences.forEach(a -> a.sendTitlePart(part, value));
forEachAudience(a -> a.sendTitlePart(part, value));
}
@Override
public void clearTitle()
{
audiences.forEach(Audience::clearTitle);
forEachAudience(Audience::clearTitle);
}
@Override
public void resetTitle()
{
audiences.forEach(Audience::resetTitle);
forEachAudience(Audience::resetTitle);
}
@Override
public void showBossBar(@NotNull final BossBar bar)
{
audiences.forEach(a -> a.showBossBar(bar));
forEachAudience(a -> a.showBossBar(bar));
}
@Override
public void hideBossBar(@NotNull final BossBar bar)
{
audiences.forEach(a -> a.hideBossBar(bar));
forEachAudience(a -> a.hideBossBar(bar));
}
@Override
public void playSound(@NotNull final Sound sound)
{
audiences.forEach(a -> a.playSound(sound));
forEachAudience(a -> a.playSound(sound));
}
@Override
public void playSound(@NotNull final Sound sound, final double x, final double y, final double z)
{
audiences.forEach(a -> a.playSound(sound, x, y, z));
forEachAudience(a -> a.playSound(sound, x, y, z));
}
@Override
public void playSound(@NotNull final Sound sound, final Sound.@NotNull Emitter emitter)
{
audiences.forEach(a -> a.playSound(sound, emitter));
forEachAudience(a -> a.playSound(sound, emitter));
}
@Override
public void stopSound(@NotNull final Sound sound)
{
audiences.forEach(a -> a.stopSound(sound));
forEachAudience(a -> a.stopSound(sound));
}
@Override
public void stopSound(@NotNull final SoundStop stop)
{
audiences.forEach(a -> a.stopSound(stop));
forEachAudience(a -> a.stopSound(stop));
}
@Override
public void openBook(final Book.@NotNull Builder book)
{
audiences.forEach(a -> a.openBook(book));
forEachAudience(a -> a.openBook(book));
}
@Override
public void openBook(@NotNull final Book book)
{
audiences.forEach(a -> a.openBook(book));
forEachAudience(a -> a.openBook(book));
}
}

View File

@ -2,46 +2,89 @@ package me.totalfreedom.base;
import me.totalfreedom.event.EventBus;
import me.totalfreedom.service.FreedomExecutor;
import me.totalfreedom.service.SubscriptionProvider;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
/**
* The base class for Patchwork.
*/
public class CommonsBase 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();
/**
* 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 CommonsBase getInstance()
{
return JavaPlugin.getPlugin(CommonsBase.class);
}
@Override
public void onEnable()
public void onDisable()
{
getRegistrations().getServiceRegistry().register(this, eventBus);
getExecutor().getSync()
.execute(() -> getRegistrations()
.getServiceRegistry()
.startAll());
Bukkit.getScheduler()
.runTaskLater(this, () -> getRegistrations()
.getServiceTaskRegistry()
.stopAllServices(), 1L);
getRegistrations().getServiceTaskRegistry()
.unregisterService(EventBus.class);
}
@Override
public void onDisable()
public void onEnable()
{
getRegistrations().getServiceRegistry().stopAll();
getRegistrations().getServiceRegistry().unregister(EventBus.class, eventBus);
}
public Registration getRegistrations()
{
return registration;
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;

View File

@ -1,64 +1,102 @@
package me.totalfreedom.base;
import me.totalfreedom.data.GroupRegistry;
import me.totalfreedom.data.BanRegistry;
import me.totalfreedom.data.ConfigRegistry;
import me.totalfreedom.data.ModuleRegistry;
import me.totalfreedom.data.ServiceRegistry;
import me.totalfreedom.data.UserRegistry;
import me.totalfreedom.data.EventRegistry;
import me.totalfreedom.data.GroupRegistry;
import me.totalfreedom.data.ModuleRegistry;
import me.totalfreedom.data.ServiceTaskRegistry;
import me.totalfreedom.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 CommonsBase#getRegistrations()}
*/
public class Registration
{
/**
* The {@link EventRegistry}
*/
private final EventRegistry eventRegistry;
/**
* The {@link UserRegistry}
*/
private final UserRegistry userRegistry;
private final ServiceRegistry serviceRegistry;
/**
* The {@link ServiceTaskRegistry}
*/
private final ServiceTaskRegistry serviceTaskRegistry;
/**
* The {@link ModuleRegistry}
*/
private final ModuleRegistry moduleRegistry;
/**
* The {@link GroupRegistry}
*/
private final GroupRegistry groupRegistry;
private final BanRegistry banRegistry;
/**
* The {@link ConfigRegistry}
*/
private final ConfigRegistry configRegistry;
public Registration()
/**
* Constructs a new Registration object and initializes all registries.
*/
Registration()
{
this.eventRegistry = new EventRegistry();
this.userRegistry = new UserRegistry();
this.serviceRegistry = new ServiceRegistry();
this.serviceTaskRegistry = new ServiceTaskRegistry();
this.moduleRegistry = new ModuleRegistry();
this.groupRegistry = new GroupRegistry();
this.banRegistry = new BanRegistry();
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;
}
public ServiceRegistry getServiceRegistry()
/**
* @return The {@link ServiceTaskRegistry}
*/
public ServiceTaskRegistry getServiceTaskRegistry()
{
return serviceRegistry;
return serviceTaskRegistry;
}
/**
* @return The {@link GroupRegistry}
*/
public GroupRegistry getGroupRegistry()
{
return groupRegistry;
}
public BanRegistry getBanRegistry()
{
return banRegistry;
}
/**
* @return The {@link ConfigRegistry}
*/
public ConfigRegistry getConfigRegistry()
{
return configRegistry;

View File

@ -0,0 +1,202 @@
package me.totalfreedom.command;
import me.totalfreedom.api.Context;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Subcommand;
import me.totalfreedom.provider.ContextProvider;
import me.totalfreedom.utils.logging.FreedomLogger;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* 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.
* <span color=#ff0000>
* <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.
* </span>
*/
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 ConsoleCommandSender && 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
{
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 == String.class)
continue;
final Context<?> context = () -> provider.fromString(arg, argType);
objects[i] = context.get();
}
try
{
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

@ -1,171 +0,0 @@
package me.totalfreedom.command;
import me.totalfreedom.api.Context;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Subcommand;
import me.totalfreedom.provider.ContextProvider;
import me.totalfreedom.utils.FreedomLogger;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
public class BukkitDelegator extends Command implements PluginIdentifiableCommand
{
private final JavaPlugin plugin;
private final CommandBase command;
private final boolean noConsole;
BukkitDelegator(final JavaPlugin plugin, final CommandBase 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 (commandLabel.isEmpty() || !commandLabel.equalsIgnoreCase(getName()))
return false;
if (sender instanceof ConsoleCommandSender && 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)
{
final Class<?>[] argTypes = node.args();
if (argTypes.length != args.length)
continue;
Object[] objects = new Object[0];
for (int i = 0; i < argTypes.length; i++)
{
final Class<?> argType = argTypes[i];
final String arg = args[i];
if (argType == String.class)
continue;
final Context<?> context = () -> provider.fromString(arg);
if (!argType.isInstance(context.get()))
{
throw new IllegalStateException(); // TODO: Change this.
}
objects = Arrays.copyOf(objects, objects.length + 1);
objects[objects.length - 1] = context.get();
}
try
{
command.getSubcommands().get(node).invoke(command, objects);
} catch (Exception ex)
{
FreedomLogger.getLogger("Patchwork")
.error(ex);
}
}
return false;
}
if (command.getBaseMethodPair() != null)
{
try
{
command.getBaseMethodPair().value().invoke(command, sender);
} catch (Exception ex)
{
FreedomLogger.getLogger("Patchwork")
.error(ex);
}
}
return true;
}
@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

@ -1,89 +0,0 @@
package me.totalfreedom.command;
import me.totalfreedom.command.annotation.*;
import me.totalfreedom.utils.Pair;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Stream;
public abstract class CommandBase
{
private final JavaPlugin plugin;
private final Info info;
private final Permissive perms;
private final Map<Subcommand, Method> subcommands;
private final Set<Completion> completions;
private final Pair<Base, Method> baseMethodPair;
protected CommandBase(final 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.baseMethodPair = new Pair<>(method.getDeclaredAnnotation(Base.class), method);
} else
{
this.baseMethodPair = null;
}
registerAnnotations();
}
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);
}
public Pair<Base, Method> getBaseMethodPair()
{
return baseMethodPair;
}
Info getInfo()
{
return this.info;
}
Permissive getPerms()
{
return this.perms;
}
public JavaPlugin getPlugin()
{
return this.plugin;
}
Map<Subcommand, Method> getSubcommands()
{
return this.subcommands;
}
Set<Completion> getCompletions()
{
return this.completions;
}
}

View File

@ -1,25 +1,44 @@
package me.totalfreedom.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;
}
// TODO: Figure out how to use CommandExecutor and TabCompleter.
// We need to find a way to resolve PluginCommands so we can
// set the executor and tab completer as necessary.
// OR we need to find an alternative way to process tab completions.
public <T extends CommandBase> void registerCommand(final T command)
/**
* 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 BukkitDelegator delegate = new BukkitDelegator(plugin, command);
final BukkitDelegate delegate = new BukkitDelegate(command);
Bukkit.getCommandMap().register(plugin.getName(), delegate);
Bukkit.getCommandMap()
.register(plugin.getName(), delegate);
}
}

View File

@ -0,0 +1,186 @@
package me.totalfreedom.command;
import me.totalfreedom.command.annotation.Base;
import me.totalfreedom.command.annotation.Completion;
import me.totalfreedom.command.annotation.Info;
import me.totalfreedom.command.annotation.Permissive;
import me.totalfreedom.command.annotation.Subcommand;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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;
/**
* 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

@ -4,8 +4,8 @@ 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}
* 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

@ -1,15 +1,31 @@
package me.totalfreedom.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;
@Target(ElementType.TYPE)
/**
* 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.METHOD)
@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 me.totalfreedom.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 <span color=#ff0000><b>NOT</b></span> intended for implementation and should
* <span color=#ff0000><b>NOT</b></span> be used.</u>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Completions
{
/**
* @return The {@link Completion} annotations.
*/
Completion[] value();
}

View File

@ -3,14 +3,39 @@ package me.totalfreedom.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

@ -3,12 +3,31 @@ package me.totalfreedom.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

@ -1,12 +1,40 @@
package me.totalfreedom.command.annotation;
import me.totalfreedom.command.CommandHandler;
import me.totalfreedom.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
* (current supported arguments can be found in the {@link ContextProvider}), 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}.
* <br>
* 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.
* <br>
* 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.
*/
@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

@ -1,40 +1,132 @@
package me.totalfreedom.config;
import org.bukkit.configuration.file.YamlConfiguration;
import me.totalfreedom.api.Context;
import me.totalfreedom.provider.ContextProvider;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Represents a configuration file of any type.
*/
public interface Configuration
{
YamlConfiguration asYaml();
/**
* 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);
<T> T get(String path, Class<T> type);
/**
* 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);
<T> T getOrDefault(String path, Class<T> type, T fallback);
/**
* 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 me.totalfreedom.config;
public final class YamlWrapper
{
}

View File

@ -1,34 +0,0 @@
package me.totalfreedom.data;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class BanRegistry
{
private final List<Ban> bansList = new ArrayList<>();
public boolean addBan(final Ban ban) {
return bansList.add(ban);
}
public boolean removeBan(final Ban ban) {
return bansList.remove(ban);
}
@Nullable
public Ban getBan(final BanID banID)
{
for (final Ban ban : bansList)
{
if (ban.getBanID().matches(banID))
{
return ban;
}
}
return null;
}
}

View File

@ -5,20 +5,43 @@ import me.totalfreedom.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

@ -6,25 +6,51 @@ import me.totalfreedom.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)

View File

@ -1,40 +1,76 @@
package me.totalfreedom.data;
import me.totalfreedom.security.perm.Group;
import me.totalfreedom.security.Group;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.ArrayList;
import java.util.List;
/**
* 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<>();
}
public boolean registerGroup(final Group group) {
/**
* 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);
}
public boolean unregisterGroup(final Group 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);
}
public Group getGroup(final String name) {
/**
* 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) {
for (final Group group : groups)
{
final String n = s.serialize(group.getName());
if (n.equalsIgnoreCase(name)) {
if (n.equalsIgnoreCase(name))
{
return group;
}
}
return null;
}
public List<Group> getGroups() {
/**
* @return The list of groups.
*/
public List<Group> getGroups()
{
return groups;
}
}

View File

@ -6,15 +6,29 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
/**
* 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))
@ -24,18 +38,30 @@ public class ModuleRegistry
this.plugins.add(plugin);
}
public void removeModule(final JavaPlugin plugin) {
/**
* Removes a module from the registry.
*
* @param plugin The module to remove.
*/
public void removeModule(final JavaPlugin plugin)
{
this.plugins.remove(plugin);
}
@SuppressWarnings("unchecked")
public <T extends JavaPlugin> ModuleProvider<T> getModule(final Class<T> clazz)
/**
* 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 () -> (T) plugin;
return () -> clazz.cast(plugin);
}
}

View File

@ -1,70 +0,0 @@
package me.totalfreedom.data;
import me.totalfreedom.service.Service;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicePriority;
import java.util.ArrayList;
import java.util.List;
public class ServiceRegistry
{
private final List<Service> services;
public ServiceRegistry()
{
this.services = new ArrayList<>();
}
public void startAll()
{
for (final Service service : this.services)
{
service.start();
}
}
public void stopAll()
{
for (final Service service : this.services)
{
service.stop();
}
}
@SuppressWarnings("unchecked")
// Suppressing is fine here; we know that the service is of type T extends Service,
// and calling getClass() on this object would effectively be Class<T>, though we may lose
// the identity of the code signature in the process.
// In this case, that is fine.
public <T extends Service> void register(final Plugin plugin, final T service)
{
this.services.add(service);
if (!service.getClass().isInstance(service))
{
throw new UnknownError("""
A critical issue has been encountered:
The service %s is not an instance of itself.
This is a critical issue and should be reported immediately.
""".formatted(service.getClass().getName()));
}
Bukkit.getServicesManager().register(
(Class<T>) service.getClass(),
service,
plugin,
ServicePriority.Normal);
}
public <T extends Service> RegisteredServiceProvider<T> getService(final Class<T> clazz)
{
return Bukkit.getServicesManager().getRegistration(clazz);
}
public void unregister(final Class<? extends Service> clazz, final Service service)
{
this.services.remove(service);
Bukkit.getServicesManager().unregister(clazz, service);
}
}

View File

@ -0,0 +1,309 @@
package me.totalfreedom.data;
import me.totalfreedom.service.Service;
import me.totalfreedom.service.ServiceSubscription;
import me.totalfreedom.service.SubscriptionProvider;
import me.totalfreedom.service.Task;
import me.totalfreedom.service.TaskSubscription;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* 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 service 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

@ -2,34 +2,99 @@ package me.totalfreedom.data;
import me.totalfreedom.user.User;
import me.totalfreedom.user.UserData;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
/**
* 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,197 @@
package me.totalfreedom.display;
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;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 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,381 @@
package me.totalfreedom.display;
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;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 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,39 @@
package me.totalfreedom.display;
import me.totalfreedom.service.Task;
import org.bukkit.Bukkit;
import java.time.Duration;
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 me.totalfreedom.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,498 @@
package me.totalfreedom.display;
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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
/**
* 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,97 @@
package me.totalfreedom.display;
import me.totalfreedom.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
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,250 @@
package me.totalfreedom.display;
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;
import java.time.Duration;
/**
* 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

@ -6,7 +6,8 @@ package me.totalfreedom.economy;
public interface EconomicEntity
{
/**
* Gets the {@link EconomicEntityData} (which contains various common metadata about this {@link EconomicEntity}) associated with this class
* Gets the {@link EconomicEntityData} (which contains various common metadata about this {@link EconomicEntity})
* associated with this class
*
* @return the {@link EconomicEntityData}
*/

View File

@ -15,6 +15,13 @@ public interface EconomicEntityData
*/
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
*
@ -30,11 +37,4 @@ public interface EconomicEntityData
* @return the new balance
*/
long removeFromBalance(final long amount);
/**
* Sets the balance of the associated instance
*
* @param newBalance the new balance
*/
void setBalance(final long newBalance);
}

View File

@ -3,7 +3,8 @@ package me.totalfreedom.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
* 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
{

View File

@ -27,7 +27,8 @@ public class EventBus extends Service
public <T extends FEvent> T getEvent(final Class<T> eventClass)
{
final FEvent e = eventSet.stream()
.filter(event -> event.getEventClass().equals(eventClass))
.filter(event -> event.getEventClass()
.equals(eventClass))
.findFirst()
.orElse(null);
@ -37,7 +38,8 @@ public class EventBus extends Service
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))
.filter(event -> event.getEventClass()
.equals(eventClass))
.findFirst()
.map(eventClass::cast)
.orElse(null);

View File

@ -7,24 +7,32 @@ class SubscriptionBox<T extends FEvent>
{
private final List<EventSubscription<T>> subscriptions;
public SubscriptionBox() {
public SubscriptionBox()
{
this.subscriptions = new ArrayList<>();
}
public void addSubscription(final EventSubscription<T> subscription) {
public void addSubscription(final EventSubscription<T> subscription)
{
subscriptions.add(subscription);
}
public void removeSubscription(final EventSubscription<?> subscription) {
public void removeSubscription(final EventSubscription<?> subscription)
{
subscriptions.remove(subscription);
}
public void tick() {
subscriptions.forEach(s -> {
if (!s.event().shouldCall()) return;
public void tick()
{
subscriptions.forEach(s ->
{
if (!s.event()
.shouldCall()) return;
s.callback().call(s.event());
s.event().reset();
s.callback()
.call(s.event());
s.event()
.reset();
});
}
}

View File

@ -0,0 +1,57 @@
package me.totalfreedom.logging;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.UUID;
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,63 @@
package me.totalfreedom.logging;
import org.bukkit.Location;
import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
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,33 @@
package me.totalfreedom.logging;
import com.google.errorprone.annotations.Immutable;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.UUID;
@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,138 @@
package me.totalfreedom.logging;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.inventory.ItemStack;
import java.time.Instant;
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 me.totalfreedom.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

@ -1,11 +1,124 @@
package me.totalfreedom.particle;
import me.totalfreedom.api.Interpolator;
import me.totalfreedom.utils.InterpolationUtils;
import org.bukkit.Color;
import org.bukkit.OfflinePlayer;
import org.bukkit.Particle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.UUID;
/**
* 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();
@Nullable Color getColor();
/**
* 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

@ -4,10 +4,30 @@ 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),

View File

@ -3,6 +3,7 @@ package me.totalfreedom.provider;
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;
@ -14,13 +15,14 @@ import java.util.stream.Stream;
public class ContextProvider
{
public Object fromString(final String string)
public <T> T fromString(final String string, final Class<T> clazz)
{
return Stream.of(toBoolean(string),
toDouble(string),
toInt(string),
toLong(string),
toFloat(string),
toMaterial(string),
toPlayer(string),
toWorld(string),
toLocation(string),
@ -28,26 +30,26 @@ public class ContextProvider
toComponent(string))
.filter(Objects::nonNull)
.findFirst()
.orElse(string);
.map(clazz::cast)
.orElse(null);
}
private @Nullable Boolean toBoolean(final String string)
{
try
{
return Boolean.parseBoolean(string);
} catch (Exception e)
{
// 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)
{
try
{
return Double.parseDouble(string);
} catch (Exception e)
} catch (NumberFormatException ignored)
{
return null;
}
@ -58,7 +60,7 @@ public class ContextProvider
try
{
return Integer.parseInt(string);
} catch (Exception e)
} catch (NumberFormatException ignored)
{
return null;
}
@ -69,7 +71,7 @@ public class ContextProvider
try
{
return Long.parseLong(string);
} catch (Exception e)
} catch (NumberFormatException ignored)
{
return null;
}
@ -80,31 +82,27 @@ public class ContextProvider
try
{
return Float.parseFloat(string);
} catch (Exception e)
} catch (NumberFormatException ignored)
{
return null;
}
}
private @Nullable Material toMaterial(final String string)
{
return Material.matchMaterial(string);
}
private @Nullable Player toPlayer(final String string)
{
return Bukkit.getPlayer(string);
}
private @Nullable CommandSender toCommandSender(final String string)
{
if (toPlayer(string) == null) return null;
return toPlayer(string);
}
private @Nullable World toWorld(final String string)
{
return Bukkit.getWorld(string);
}
// If we decide to, we can "modify" this to use spaces
// and adjust our inputs accordingly.
/**
* When using this method, the input string must be formatted as
* <br>
@ -127,6 +125,13 @@ public class ContextProvider
return new Location(toWorld(split[0]), x, y, z);
}
private @Nullable CommandSender toCommandSender(final String string)
{
if (toPlayer(string) == null) return null;
return toPlayer(string);
}
private @NotNull Component toComponent(final String string)
{
return Component.text(string);

View File

@ -0,0 +1,41 @@
package me.totalfreedom.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

@ -1,4 +1,4 @@
package me.totalfreedom.security.perm;
package me.totalfreedom.security;
import org.bukkit.permissions.Permission;
@ -21,11 +21,7 @@ public interface Node
boolean isExpired();
boolean isPermanent();
boolean isTemporary();
boolean wildcard();
boolean negated();
}

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security.perm;
package me.totalfreedom.security;
public interface NodeBuilder
{

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security.perm;
package me.totalfreedom.security;
public enum NodeType
{

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security.perm;
package me.totalfreedom.security;
import org.bukkit.permissions.Permissible;

View File

@ -1,78 +0,0 @@
package me.totalfreedom.security.ban;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.UUID;
/**
* Represents a physical ban entry. This is used to store information about a ban,
* such as the player who was banned, the reason for why they were banned, the individual who issued the ban,
* when the ban expires, and when the ban was created.
* <br>
* Ban information is stored in the Database with the {@link BanID} as the PRIMARY KEY.
*/
public interface Ban
{
/**
* Gets the ID of this ban. This is an object which represents a string prefixed with either a T or a P,
* and suffixed with a 6-10 digit numerical code. This is used to identify the ban in the database, and for
* easier ban referencing.
*
* @return The ID of this ban.
*/
BanID getBanID();
/**
* Gets the UUID of the player who was banned. This is formatted as a UUID
* which allows us to retrieve the particular player instance, if applicable, and also
* have a true identifier to check against user logins.
*
* @return The UUID of the player who was banned.
*/
UUID getOffenderID();
/**
* Gets the reason that the player was banned for. Typically, the default reason is "You are banned!".
* We've forced implementations to require a reason, as it's important to know why a player was banned.
*
* @return The reason that the player was banned for.
*/
String getReason();
/**
* Gets the username of the individual who issued the ban. This is not a reliable way to store data, but
* in our case, we should not need to interact with the ban issuer from the code. This is simply for
* reference purposes.
*
* @return The username of the individual who issued the ban.
*/
String getBanIssuer();
/**
* Gets the {@link Instant} which this ban was created.
*
* @return The ban's creation time.
*/
Instant getCreationTime();
/**
* Gets the {@link Instant} which this ban is due to expire, if applicable.
* This method is annotated as {@link Nullable}, as permanent bans do not have an expiry date.
*
* @return The ban's expiry time, or null if the ban is permanent.
*/
@Nullable
Instant getExpiry();
/**
* Checks if the ban has expired. This will return false if:
* <ul>
* <li>The {@link Instant} returned by {@link #getExpiry()} is null.</li>
* <li>The {@link Instant} returned by {@link #getExpiry()} is after the current time.</li>
* </ul>
*
* @return True if the ban has expired, false otherwise.
*/
boolean isExpired();
}

View File

@ -1,52 +0,0 @@
package me.totalfreedom.security.ban;
/**
* Represents an ID for a ban. These are formatted either as:
* <p>
* P-00129381
* <br>
* T-00128381
* <br>
* </p>
* Where P marks a ban as permanent, and T marks a ban as temporary.
*/
public interface BanID
{
/**
* This method returns the full Ban ID.
*
* @return The actual ID.
*/
String getID();
/**
* This method returns the ban type denominator character for the Ban ID.
* This would either be T or P, where T = temporary and P = permanent.
*
* @return The ban type denominator character for the Ban ID.
*/
char getIDPrefix();
/**
* Gets the numerical tag of this ban ID.
* This would be the numerical section of the full Ban ID.
*
* @return The numerical tag of this ban ID.
*/
int getNumericalTag();
/**
* Checks the prefix of the Ban ID to see whether if it is permanent.
*
* @return true if the Ban ID is prefixed with a P, false otherwise.
*/
boolean isPermanent();
default boolean matches(BanID other) {
if (other == null) {
return false;
}
return (getIDPrefix() == other.getIDPrefix())
&& (getNumericalTag() == other.getNumericalTag());
}
}

View File

@ -1,18 +0,0 @@
package me.totalfreedom.security.perm;
import net.kyori.adventure.text.Component;
public interface Group extends PermissionHolder
{
Component getName();
Component getPrefix();
Component getAbbreviation();
int getWeight();
boolean isDefault();
boolean isHidden();
}

View File

@ -2,58 +2,141 @@ package me.totalfreedom.service;
import me.totalfreedom.base.CommonsBase;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/**
* 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(CommonsBase.getInstance(), r);
asyncExecutor = r -> Bukkit.getScheduler().runTaskAsynchronously(CommonsBase.getInstance(), r);
syncExecutor = r -> Bukkit.getScheduler()
.runTask(CommonsBase.getInstance(), r);
asyncExecutor = r -> Bukkit.getScheduler()
.runTaskAsynchronously(CommonsBase.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;
}
public Executor scheduled(final long delay, final long period)
{
return r -> Bukkit.getScheduler()
.runTaskTimer(
CommonsBase.getInstance(),
r,
delay,
period);
}
public Executor scheduledAsync(final long delay, final long period)
{
return r -> Bukkit.getScheduler()
.runTaskTimerAsynchronously(
CommonsBase.getInstance(),
r,
delay,
period);
}
public void runSync(@NotNull final Task task)
{
getSync().execute(task);
}
public void runAsync(@NotNull final Task task)
{
getAsync().execute(task);
}
}

View File

@ -1,47 +1,47 @@
package me.totalfreedom.service;
import me.totalfreedom.base.CommonsBase;
/**
* 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;
private boolean isActive = false;
/**
* Creates a new service with the given name.
*
* @param name The name of the service.
*/
protected Service(final String name)
{
this.name = name;
}
public void start()
{
isActive = true;
CommonsBase.getInstance()
.getExecutor()
.getSync()
.execute(() ->
{
while (isActive)
{
tick();
}
});
}
public void stop()
{
isActive = false;
}
/**
* 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;
}
public boolean isActive()
{
return isActive;
}
}

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