diff --git a/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/AbstractPlayerControls.java b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/AbstractPlayerControls.java new file mode 100644 index 000000000..ebcba0dc7 --- /dev/null +++ b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/AbstractPlayerControls.java @@ -0,0 +1,139 @@ +package ru.progrm_jarvis.minecraft.commons.control; + +import lombok.*; +import lombok.experimental.FieldDefaults; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * Abstract implementation of {@link PlayerControls} providing its common mechanisms. + */ +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) +public abstract class AbstractPlayerControls

+ implements PlayerControls { + + /** + * Plugin whose player controls those are. + */ + @NonNull P plugin; + + /** + * Whether this control session is a global player container or not + */ + @Getter boolean global; + + /** + * Map of player's currently managed by this player controls and their current active sessions + */ + @NonNull Map<@NonNull Player, @NonNull S> sessions; + + /** + * Reference to the currently set event handler of this player controls + */ + @NonNull AtomicReference> eventHandler = new AtomicReference<>(); + + @Override + public P getBukkitPlugin() { + return plugin; + } + + // a more optimal solution without allocating unneeded Optional objects + @Override + public boolean containsPlayer(@NonNull final Player player) { + return sessions.containsKey(player); + } + + // a more optimal solution without allocating unneeded Optional objects + @Override + public void removePlayer(@NonNull final Player player) { + val session = sessions.get(player); + if (session != null) session.end(); + } + + @Override + public Collection getPlayers() { + return sessions.keySet(); + } + + @Override + @NonNull public S startSession(@NonNull final Player player) { + val session = createSession(player); + sessions.put(player, session); + + return session; + } + + @Override + public @NonNull Optional getSession(@NonNull final Player player) { + return Optional.ofNullable(sessions.get(player)); + } + + /** + * Creates the controls session for the player specified. + * + * @param player player for whom to initialize the controls session + * @return created controls session + * + * @implSpec implementations are not required to add the created session to {@link #sessions} + * as this is done by this method's caller + * + * @see Session#startSession(Player) this method's default caller + */ + @NonNull protected abstract S createSession(Player player); + + /** + * Finalizer called in {@link S#end()} in order to cleanup everything needed when the player end his session. + * + * @param session session to release + * @implSpec should not call to {@link S#end()} as this will (most definitely) lead to infinite recursion + * + * @implSpec implementations are not required to remove the session from {@link #sessions} + * as this is done by this method's caller + * + * @see Session#end() this method's default caller + */ + protected void releaseControls(@NonNull final S session) {} + + @Override + public void subscribe(@NonNull final Consumer eventHandler) { + this.eventHandler.set(eventHandler); + } + + @Nullable + @Override + public Consumer unsubscribe() { + return eventHandler.getAndSet(null); + } + + /** + * Default session object to be used with {@link AbstractPlayerControls}. + */ + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor(access = AccessLevel.PROTECTED) + @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) + public abstract class Session implements PlayerControls.Session { + + /** + * Player whose session this one is + */ + @Getter Player player; + + @Override + public void end() { + //noinspection unchecked + releaseControls((S) this); + sessions.remove(player); + } + } +} diff --git a/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/PlayerControls.java b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/PlayerControls.java new file mode 100644 index 000000000..6be373342 --- /dev/null +++ b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/PlayerControls.java @@ -0,0 +1,87 @@ +package ru.progrm_jarvis.minecraft.commons.control; + +import lombok.NonNull; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import ru.progrm_jarvis.minecraft.commons.player.collection.PlayerContainer; +import ru.progrm_jarvis.minecraft.commons.plugin.BukkitPluginContainer; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * An object which manages the player + * + * @param

type of plugin owning this player controls + * @param type of session created for player whose controls are managed + * @param tye of event to be called by the controls manager + */ +public interface PlayerControls

+ extends BukkitPluginContainer

, PlayerContainer { + + /** + * Starts the new controls session for the player. + * + * @param player player for whom to start the controls session + * @return created controls session for the player (or the one he currently has in this player controls) + * + * @apiNote the session should be ended using {@link S#end()} + */ + @NonNull S startSession(@NonNull Player player); + + @Override + default void addPlayer(@NonNull final Player player) { + startSession(player); + } + + /** + * Gets the current controls session of the specified player. + * + * @param player player for whom to get the controls sessions + * @return optional of the player's current controls session or empty if it doesn't have one + */ + @NonNull Optional getSession(@NonNull Player player); + + @Override + default void removePlayer(@NonNull final Player player) { + getSession(player).ifPresent(Session::end); + } + + @Override + default boolean containsPlayer(@NonNull final Player player) { + return getSession(player).isPresent(); + } + + /** + * Subscribes the event handler on this player controls' events. + * + * @param eventHandler event handler to be used whenever an event is fired + */ + void subscribe(@NonNull Consumer eventHandler); + + /** + * Unsubscribes the current event handler from this player controls' events. + * + * @return event handler which was unsubscribed or {@code null} if there was no subscribed event handler + */ + @Nullable Consumer unsubscribe(); + + /** + * Session of player controls, responsible for handling the player's controls + */ + interface Session { + + /** + * Gets the player manages by this controls session. + * + * @return MANAGED PLAYER + */ + Player getPlayer(); + + /** + * Ends this player controls session. + */ + void end(); + } +} diff --git a/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/common/ScrollPlayerControls.java b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/common/ScrollPlayerControls.java new file mode 100644 index 000000000..09f738bf9 --- /dev/null +++ b/commons/src/main/java/ru/progrm_jarvis/minecraft/commons/control/common/ScrollPlayerControls.java @@ -0,0 +1,113 @@ +package ru.progrm_jarvis.minecraft.commons.control.common; + +import com.comphenix.packetwrapper.WrapperPlayClientHeldItemSlot; +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.Value; +import lombok.experimental.FieldDefaults; +import lombok.val; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import ru.progrm_jarvis.minecraft.commons.control.AbstractPlayerControls; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Optional; + +/** + * Player Controls using the player's hotbar scrolling + */ +public class ScrollPlayerControls

+ extends AbstractPlayerControls { + + public ScrollPlayerControls(@NonNull final P plugin, final boolean global, + @NonNull final Map<@NonNull Player, @NonNull ScrollPlayerControls.Session> sessions) { + super(plugin, global, sessions); + } + + @Override + @NonNull protected Session createSession(final Player player) { + return new Session(player); + } + + /** + * Handles the slot-change packet-event sending the event if needed. + * + * @param player player for whom to handle slot change + * @param slot slot now selected by the player + * + * @implNote initializes the event object only if needed (event handler is not null) + */ + protected void handleSlotChanged(@NonNull final Player player, final int slot) { + val eventHandler = this.eventHandler.get(); + if (eventHandler != null) eventHandler.accept(new Event(player, (byte) (slot - 4))); + } + + /** + * Session of this scroll player controls + */ + public class Session extends AbstractPlayerControls.Session { + + public Session(final Player player) { + super(player); + } + } + + /** + * An event to be passed to event handler whenever a player scrolls. + */ + @Value + @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) + public static class Event { + + /** + * Player whose event this one is + */ + Player player; + + /** + * Positive value for scroll to right, negative value for scroll to left, {@code 0} for no update. + */ + byte delta; + } + + /** + * Function used to fill the player's hotbar with items. + */ + @FunctionalInterface + public interface HotbarFiller { + + /** + * Gets the item to be set in player's inventory at the specified slot. + * + * @param player player for whom to update the item in inventory + * @param slot value between {@code 0} and {@code 8} (inclusive), an index of hotbar slot (from left to right) + * @return non-empty optional containing a non-null item to be set in player's hotbar at the specified slot + * or empty optional if the slot should not be updated for the player + */ + Optional<@NonNull ItemStack> getItem(@Nonnull Player player, int slot); + } + + /** + * Packet handler to be used to intercept incoming selected-slot-change packets. + */ + protected class SlotChangePacketHandler extends PacketAdapter { + + public SlotChangePacketHandler() { + super(ScrollPlayerControls.this.plugin, PacketType.Play.Client.HELD_ITEM_SLOT); + } + + @Override + public void onPacketReceiving(final PacketEvent event) { + val player = event.getPlayer(); + if (sessions.containsKey(player)) { + handleSlotChanged(player, new WrapperPlayClientHeldItemSlot(event.getPacket()).getSlot()); + event.setCancelled(true); + } + } + } +}