callable) {
661 | super(chartId);
662 | this.callable = callable;
663 | }
664 |
665 | @Override
666 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
667 | int value = callable.call();
668 | if (value == 0) {
669 | // Null = skip the chart
670 | return null;
671 | }
672 | return new JsonObjectBuilder().appendField("value", value).build();
673 | }
674 | }
675 |
676 | /**
677 | * An extremely simple JSON builder.
678 | *
679 | * While this class is neither feature-rich nor the most performant one, it's sufficient enough
680 | * for its use-case.
681 | */
682 | public static class JsonObjectBuilder {
683 |
684 | private StringBuilder builder = new StringBuilder();
685 |
686 | private boolean hasAtLeastOneField = false;
687 |
688 | public JsonObjectBuilder() {
689 | builder.append("{");
690 | }
691 |
692 | /**
693 | * Appends a null field to the JSON.
694 | *
695 | * @param key The key of the field.
696 | * @return A reference to this object.
697 | */
698 | public JsonObjectBuilder appendNull(String key) {
699 | appendFieldUnescaped(key, "null");
700 | return this;
701 | }
702 |
703 | /**
704 | * Appends a string field to the JSON.
705 | *
706 | * @param key The key of the field.
707 | * @param value The value of the field.
708 | * @return A reference to this object.
709 | */
710 | public JsonObjectBuilder appendField(String key, String value) {
711 | if (value == null) {
712 | throw new IllegalArgumentException("JSON value must not be null");
713 | }
714 | appendFieldUnescaped(key, "\"" + escape(value) + "\"");
715 | return this;
716 | }
717 |
718 | /**
719 | * Appends an integer field to the JSON.
720 | *
721 | * @param key The key of the field.
722 | * @param value The value of the field.
723 | * @return A reference to this object.
724 | */
725 | public JsonObjectBuilder appendField(String key, int value) {
726 | appendFieldUnescaped(key, String.valueOf(value));
727 | return this;
728 | }
729 |
730 | /**
731 | * Appends an object to the JSON.
732 | *
733 | * @param key The key of the field.
734 | * @param object The object.
735 | * @return A reference to this object.
736 | */
737 | public JsonObjectBuilder appendField(String key, JsonObject object) {
738 | if (object == null) {
739 | throw new IllegalArgumentException("JSON object must not be null");
740 | }
741 | appendFieldUnescaped(key, object.toString());
742 | return this;
743 | }
744 |
745 | /**
746 | * Appends a string array to the JSON.
747 | *
748 | * @param key The key of the field.
749 | * @param values The string array.
750 | * @return A reference to this object.
751 | */
752 | public JsonObjectBuilder appendField(String key, String[] values) {
753 | if (values == null) {
754 | throw new IllegalArgumentException("JSON values must not be null");
755 | }
756 | String escapedValues =
757 | Arrays.stream(values)
758 | .map(value -> "\"" + escape(value) + "\"")
759 | .collect(Collectors.joining(","));
760 | appendFieldUnescaped(key, "[" + escapedValues + "]");
761 | return this;
762 | }
763 |
764 | /**
765 | * Appends an integer array to the JSON.
766 | *
767 | * @param key The key of the field.
768 | * @param values The integer array.
769 | * @return A reference to this object.
770 | */
771 | public JsonObjectBuilder appendField(String key, int[] values) {
772 | if (values == null) {
773 | throw new IllegalArgumentException("JSON values must not be null");
774 | }
775 | String escapedValues =
776 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
777 | appendFieldUnescaped(key, "[" + escapedValues + "]");
778 | return this;
779 | }
780 |
781 | /**
782 | * Appends an object array to the JSON.
783 | *
784 | * @param key The key of the field.
785 | * @param values The integer array.
786 | * @return A reference to this object.
787 | */
788 | public JsonObjectBuilder appendField(String key, JsonObject[] values) {
789 | if (values == null) {
790 | throw new IllegalArgumentException("JSON values must not be null");
791 | }
792 | String escapedValues =
793 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
794 | appendFieldUnescaped(key, "[" + escapedValues + "]");
795 | return this;
796 | }
797 |
798 | /**
799 | * Appends a field to the object.
800 | *
801 | * @param key The key of the field.
802 | * @param escapedValue The escaped value of the field.
803 | */
804 | private void appendFieldUnescaped(String key, String escapedValue) {
805 | if (builder == null) {
806 | throw new IllegalStateException("JSON has already been built");
807 | }
808 | if (key == null) {
809 | throw new IllegalArgumentException("JSON key must not be null");
810 | }
811 | if (hasAtLeastOneField) {
812 | builder.append(",");
813 | }
814 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
815 | hasAtLeastOneField = true;
816 | }
817 |
818 | /**
819 | * Builds the JSON string and invalidates this builder.
820 | *
821 | * @return The built JSON string.
822 | */
823 | public JsonObject build() {
824 | if (builder == null) {
825 | throw new IllegalStateException("JSON has already been built");
826 | }
827 | JsonObject object = new JsonObject(builder.append("}").toString());
828 | builder = null;
829 | return object;
830 | }
831 |
832 | /**
833 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
834 | *
835 | *
This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
836 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
837 | *
838 | * @param value The value to escape.
839 | * @return The escaped value.
840 | */
841 | private static String escape(String value) {
842 | final StringBuilder builder = new StringBuilder();
843 | for (int i = 0; i < value.length(); i++) {
844 | char c = value.charAt(i);
845 | if (c == '"') {
846 | builder.append("\\\"");
847 | } else if (c == '\\') {
848 | builder.append("\\\\");
849 | } else if (c <= '\u000F') {
850 | builder.append("\\u000").append(Integer.toHexString(c));
851 | } else if (c <= '\u001F') {
852 | builder.append("\\u00").append(Integer.toHexString(c));
853 | } else {
854 | builder.append(c);
855 | }
856 | }
857 | return builder.toString();
858 | }
859 |
860 | /**
861 | * A super simple representation of a JSON object.
862 | *
863 | *
This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
864 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
865 | * JsonObject)}.
866 | */
867 | public static class JsonObject {
868 |
869 | private final String value;
870 |
871 | private JsonObject(String value) {
872 | this.value = value;
873 | }
874 |
875 | @Override
876 | public String toString() {
877 | return value;
878 | }
879 | }
880 | }
881 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/BlockifyBreakEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import codes.kooper.blockify.models.View;
5 | import codes.kooper.blockify.types.BlockifyPosition;
6 | import lombok.Getter;
7 | import org.bukkit.block.data.BlockData;
8 | import org.bukkit.entity.Player;
9 | import org.bukkit.event.Cancellable;
10 | import org.bukkit.event.Event;
11 | import org.bukkit.event.HandlerList;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | @Getter
15 | public class BlockifyBreakEvent extends Event implements Cancellable {
16 | private static final HandlerList HANDLERS = new HandlerList();
17 | private boolean cancelled = false;
18 | private final Player player;
19 | private final BlockifyPosition position;
20 | private final BlockData blockData;
21 | private final View view;
22 | private final Stage stage;
23 |
24 | /**
25 | * Event that is called when a player breaks a block in a Blockify stage.
26 | *
27 | * @param player The player that broke the block.
28 | * @param position The position of the block that was broken.
29 | * @param blockData The block data of the block that was broken.
30 | * @param view The view that the player is in.
31 | * @param stage The stage that the player is in.
32 | */
33 | public BlockifyBreakEvent(Player player, BlockifyPosition position, BlockData blockData, View view, Stage stage) {
34 | this.player = player;
35 | this.position = position;
36 | this.blockData = blockData;
37 | this.view = view;
38 | this.stage = stage;
39 | }
40 |
41 | @Override
42 | public @NotNull HandlerList getHandlers() {
43 | return HANDLERS;
44 | }
45 |
46 | public static HandlerList getHandlerList() {
47 | return HANDLERS;
48 | }
49 |
50 | @Override
51 | public boolean isCancelled() {
52 | return cancelled;
53 | }
54 |
55 | @Override
56 | public void setCancelled(boolean cancel) {
57 | this.cancelled = cancel;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/BlockifyInteractEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import codes.kooper.blockify.models.View;
5 | import codes.kooper.blockify.types.BlockifyPosition;
6 | import lombok.Getter;
7 | import org.bukkit.block.data.BlockData;
8 | import org.bukkit.entity.Player;
9 | import org.bukkit.event.Cancellable;
10 | import org.bukkit.event.Event;
11 | import org.bukkit.event.HandlerList;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | @Getter
15 | public class BlockifyInteractEvent extends Event implements Cancellable {
16 | private static final HandlerList HANDLERS = new HandlerList();
17 | private boolean cancelled = false;
18 | private final Player player;
19 | private final BlockifyPosition position;
20 | private final BlockData blockData;
21 | private final View view;
22 | private final Stage stage;
23 |
24 | /**
25 | * Event that is called when a player interacts with a block in a stage.
26 | *
27 | * @param player The player that interacted with the block.
28 | * @param position The position of the block that was interacted with.
29 | * @param blockData The block data of the block that was interacted with.
30 | * @param view The view that the player is currently in.
31 | * @param stage The stage that the player is currently in.
32 | */
33 | public BlockifyInteractEvent(Player player, BlockifyPosition position, BlockData blockData, View view, Stage stage) {
34 | this.player = player;
35 | this.position = position;
36 | this.blockData = blockData;
37 | this.view = view;
38 | this.stage = stage;
39 | }
40 |
41 | @Override
42 | public boolean isCancelled() {
43 | return cancelled;
44 | }
45 |
46 | @Override
47 | public void setCancelled(boolean cancel) {
48 | cancelled = cancel;
49 | }
50 |
51 | @Override
52 | public @NotNull HandlerList getHandlers() {
53 | return HANDLERS;
54 | }
55 |
56 | public static HandlerList getHandlerList() {
57 | return HANDLERS;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/BlockifyPlaceEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import codes.kooper.blockify.models.View;
5 | import codes.kooper.blockify.types.BlockifyPosition;
6 | import lombok.Getter;
7 | import org.bukkit.entity.Player;
8 | import org.bukkit.event.Event;
9 | import org.bukkit.event.HandlerList;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | @Getter
13 | public class BlockifyPlaceEvent extends Event {
14 | private static final HandlerList HANDLERS = new HandlerList();
15 | private final Player player;
16 | private final BlockifyPosition position;
17 | private final View view;
18 | private final Stage stage;
19 |
20 | /**
21 | * Event that is called when a player places a block in the Blockify plugin.
22 | *
23 | * @param player The player that placed the block.
24 | * @param position The position of the block that was placed.
25 | * @param view The view that the player is currently in.
26 | * @param stage The stage that the player is currently in.
27 | */
28 | public BlockifyPlaceEvent(Player player, BlockifyPosition position, View view, Stage stage) {
29 | this.player = player;
30 | this.position = position;
31 | this.view = view;
32 | this.stage = stage;
33 | }
34 |
35 | @Override
36 | public @NotNull HandlerList getHandlers() {
37 | return HANDLERS;
38 | }
39 |
40 | public static HandlerList getHandlerList() {
41 | return HANDLERS;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/CreateStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.event.Event;
6 | import org.bukkit.event.HandlerList;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | @Getter
10 | public class CreateStageEvent extends Event {
11 | private static final HandlerList HANDLERS = new HandlerList();
12 | private final Stage stage;
13 |
14 | /**
15 | * Event that is called when a stage is created.
16 | *
17 | * @param stage The stage that was created
18 | */
19 | public CreateStageEvent(Stage stage) {
20 | this.stage = stage;
21 | }
22 |
23 | @Override
24 | public @NotNull HandlerList getHandlers() {
25 | return HANDLERS;
26 | }
27 |
28 | public static HandlerList getHandlerList() {
29 | return HANDLERS;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/DeleteStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.event.Event;
6 | import org.bukkit.event.HandlerList;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | @Getter
10 | public class DeleteStageEvent extends Event {
11 |
12 | private static final HandlerList HANDLERS = new HandlerList();
13 | private final Stage stage;
14 |
15 | /**
16 | * Event that is called when a stage is deleted.
17 | *
18 | * @param stage The stage that was deleted.
19 | */
20 | public DeleteStageEvent(Stage stage) {
21 | this.stage = stage;
22 | }
23 |
24 | @Override
25 | public @NotNull HandlerList getHandlers() {
26 | return HANDLERS;
27 | }
28 |
29 | public static HandlerList getHandlerList() {
30 | return HANDLERS;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/OnBlockChangeSendEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import codes.kooper.blockify.types.BlockifyChunk;
5 | import lombok.Getter;
6 | import org.bukkit.block.data.BlockData;
7 | import org.bukkit.event.Event;
8 | import org.bukkit.event.HandlerList;
9 | import org.jetbrains.annotations.NotNull;
10 |
11 | import java.util.Map;
12 |
13 | @Getter
14 | public class OnBlockChangeSendEvent extends Event {
15 |
16 | private static final HandlerList HANDLERS = new HandlerList();
17 | private final Stage stage;
18 | private final Map> blocks;
19 |
20 | /**
21 | * Event that is called when block(s) are being changed.
22 | * @param stage The stage that the block change is happening in.
23 | * @param blocks The blocks that are being changed.
24 | */
25 | public OnBlockChangeSendEvent(Stage stage, Map> blocks) {
26 | this.stage = stage;
27 | this.blocks = blocks;
28 | }
29 |
30 |
31 | @Override
32 | public @NotNull HandlerList getHandlers() {
33 | return HANDLERS;
34 | }
35 |
36 | public static HandlerList getHandlerList() {
37 | return HANDLERS;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/PlayerEnterStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.entity.Player;
6 | import org.bukkit.event.Cancellable;
7 | import org.bukkit.event.Event;
8 | import org.bukkit.event.HandlerList;
9 | import org.jetbrains.annotations.NotNull;
10 |
11 | @Getter
12 | public class PlayerEnterStageEvent extends Event implements Cancellable {
13 | private boolean cancelled = false;
14 | private static final HandlerList HANDLERS = new HandlerList();
15 | private final Stage stage;
16 | private final Player player;
17 |
18 | /**
19 | * Event that is called when a player enters a stage.
20 | *
21 | * @param stage The stage the player entered.
22 | * @param player The player that entered the stage.
23 | */
24 | public PlayerEnterStageEvent(Stage stage, Player player) {
25 | this.stage = stage;
26 | this.player = player;
27 | }
28 |
29 | @Override
30 | public @NotNull HandlerList getHandlers() {
31 | return HANDLERS;
32 | }
33 |
34 | public static HandlerList getHandlerList() {
35 | return HANDLERS;
36 | }
37 |
38 | @Override
39 | public boolean isCancelled() {
40 | return cancelled;
41 | }
42 |
43 | @Override
44 | public void setCancelled(boolean cancel) {
45 | this.cancelled = cancel;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/PlayerExitStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.entity.Player;
6 | import org.bukkit.event.Cancellable;
7 | import org.bukkit.event.Event;
8 | import org.bukkit.event.HandlerList;
9 | import org.jetbrains.annotations.NotNull;
10 |
11 | @Getter
12 | public class PlayerExitStageEvent extends Event implements Cancellable {
13 | private boolean cancelled = false;
14 | private static final HandlerList HANDLERS = new HandlerList();
15 | private final Stage stage;
16 | private final Player player;
17 |
18 | /**
19 | * Event that is called when a player exits a stage.
20 | *
21 | * @param stage The stage the player exited.
22 | * @param player The player that exited the stage.
23 | */
24 | public PlayerExitStageEvent(Stage stage, Player player) {
25 | this.stage = stage;
26 | this.player = player;
27 | }
28 |
29 | @Override
30 | public @NotNull HandlerList getHandlers() {
31 | return HANDLERS;
32 | }
33 |
34 | public static HandlerList getHandlerList() {
35 | return HANDLERS;
36 | }
37 |
38 | @Override
39 | public boolean isCancelled() {
40 | return cancelled;
41 | }
42 |
43 | @Override
44 | public void setCancelled(boolean cancel) {
45 | this.cancelled = cancel;
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/PlayerJoinStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.entity.Player;
6 | import org.bukkit.event.Event;
7 | import org.bukkit.event.HandlerList;
8 | import org.jetbrains.annotations.NotNull;
9 |
10 | @Getter
11 | public class PlayerJoinStageEvent extends Event {
12 |
13 | private static final HandlerList HANDLERS = new HandlerList();
14 | private final Stage stage;
15 | private final Player player;
16 |
17 | /**
18 | * Event that is called when a player joins a stage.
19 | *
20 | * @param stage The stage the player joined.
21 | * @param player The player that joined the stage.
22 | */
23 | public PlayerJoinStageEvent(Stage stage, Player player) {
24 | this.stage = stage;
25 | this.player = player;
26 | }
27 |
28 | @Override
29 | public @NotNull HandlerList getHandlers() {
30 | return HANDLERS;
31 | }
32 |
33 | public static HandlerList getHandlerList() {
34 | return HANDLERS;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/events/PlayerLeaveStageEvent.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.events;
2 |
3 | import codes.kooper.blockify.models.Stage;
4 | import lombok.Getter;
5 | import org.bukkit.entity.Player;
6 | import org.bukkit.event.Event;
7 | import org.bukkit.event.HandlerList;
8 | import org.jetbrains.annotations.NotNull;
9 |
10 | @Getter
11 | public class PlayerLeaveStageEvent extends Event {
12 |
13 | private static final HandlerList HANDLERS = new HandlerList();
14 | private final Stage stage;
15 | private final Player player;
16 |
17 | /**
18 | * Event that is called when a player leaves a stage.
19 | *
20 | * @param stage The stage the player is leaving.
21 | * @param player The player that is leaving the stage.
22 | */
23 | public PlayerLeaveStageEvent(Stage stage, Player player) {
24 | this.stage = stage;
25 | this.player = player;
26 | }
27 |
28 | @Override
29 | public @NotNull HandlerList getHandlers() {
30 | return HANDLERS;
31 | }
32 |
33 | public static HandlerList getHandlerList() {
34 | return HANDLERS;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/listeners/StageBoundListener.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.listeners;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.events.PlayerEnterStageEvent;
5 | import codes.kooper.blockify.events.PlayerExitStageEvent;
6 | import codes.kooper.blockify.events.PlayerJoinStageEvent;
7 | import codes.kooper.blockify.events.PlayerLeaveStageEvent;
8 | import codes.kooper.blockify.models.Stage;
9 | import org.bukkit.entity.Player;
10 | import org.bukkit.event.EventHandler;
11 | import org.bukkit.event.Listener;
12 | import org.bukkit.event.player.PlayerJoinEvent;
13 | import org.bukkit.event.player.PlayerMoveEvent;
14 | import org.bukkit.event.player.PlayerQuitEvent;
15 |
16 | import java.util.List;
17 |
18 | public class StageBoundListener implements Listener {
19 |
20 | /**
21 | * This method is an event handler for PlayerMoveEvent.
22 | * It is triggered when a player moves in the game.
23 | *
24 | * The method retrieves the stages associated with the player and checks if the player's new location
25 | * is within any of these stages. If the player has entered a new stage, a PlayerEnterStageEvent is called.
26 | * If the player has exited a stage, a PlayerExitStageEvent is called.
27 | *
28 | * @param event The PlayerMoveEvent object containing information about the move event.
29 | */
30 | @EventHandler
31 | public void onPlayerMove(PlayerMoveEvent event) {
32 | Player player = event.getPlayer();
33 | List stages = Blockify.getInstance().getStageManager().getStages(player);
34 | for (Stage stage : stages) {
35 | if (stage.isLocationWithin(event.getTo())) {
36 | PlayerEnterStageEvent e = new PlayerEnterStageEvent(stage, player);
37 | e.callEvent();
38 | if (e.isCancelled()) {
39 | event.setCancelled(true);
40 | }
41 | } else if (stage.isLocationWithin(event.getFrom())) {
42 | PlayerExitStageEvent e = new PlayerExitStageEvent(stage, player);
43 | e.callEvent();
44 | if (e.isCancelled()) {
45 | event.setCancelled(true);
46 | }
47 | }
48 | }
49 | }
50 |
51 | /**
52 | * This method is an event handler for PlayerJoinEvent.
53 | * It is triggered when a player joins the game.
54 | *
55 | * The method retrieves the stages associated with the player and checks if the player's location
56 | * is within any of these stages. If the player is within a stage, a PlayerEnterStageEvent is called.
57 | *
58 | * @param event The PlayerJoinEvent object containing information about the join event.
59 | */
60 | @EventHandler
61 | public void onPlayerJoin(PlayerJoinEvent event) {
62 | Player player = event.getPlayer();
63 | List stages = Blockify.getInstance().getStageManager().getStages(player);
64 | for (Stage stage : stages) {
65 | if (stage.isLocationWithin(player.getLocation())) {
66 | new PlayerEnterStageEvent(stage, player).callEvent();
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * This method is an event handler for PlayerQuitEvent.
73 | * It is triggered when a player quits the game.
74 | *
75 | * The method retrieves the stages associated with the player and calls a PlayerExitStageEvent for each stage.
76 | *
77 | * @param event The PlayerQuitEvent object containing information about the quit event.
78 | */
79 | @EventHandler
80 | public void onPlayerQuit(PlayerQuitEvent event) {
81 | Player player = event.getPlayer();
82 | List stages = Blockify.getInstance().getStageManager().getStages(player);
83 | for (Stage stage : stages) {
84 | new PlayerExitStageEvent(stage, player).callEvent();
85 | }
86 | }
87 |
88 | /**
89 | * This method is an event handler for PlayerJoinStageEvent.
90 | * It is triggered when a player joins a stage.
91 | *
92 | * The method checks if the player's location is within the stage and calls a PlayerEnterStageEvent if it is.
93 | *
94 | * @param event The PlayerJoinStageEvent object containing information about the join stage event.
95 | */
96 | @EventHandler
97 | public void onPlayerStageJoin(PlayerJoinStageEvent event) {
98 | if (event.getStage().isLocationWithin(event.getPlayer().getLocation())) {
99 | new PlayerEnterStageEvent(event.getStage(), event.getPlayer()).callEvent();
100 | }
101 | }
102 |
103 | /**
104 | * This method is an event handler for PlayerLeaveStageEvent.
105 | * It is triggered when a player leaves a stage.
106 | *
107 | * The method checks if the player's location is within the stage and calls a PlayerExitStageEvent if it is.
108 | *
109 | * @param event The PlayerLeaveStageEvent object containing information about the leave stage event.
110 | */
111 | @EventHandler
112 | public void onPlayerStageLeave(PlayerLeaveStageEvent event) {
113 | if (event.getStage().isLocationWithin(event.getPlayer().getLocation())) {
114 | new PlayerExitStageEvent(event.getStage(), event.getPlayer()).callEvent();
115 | }
116 | }
117 |
118 | /**
119 | * This method is an event handler for PlayerEnterStageEvent.
120 | * It is triggered when a player enters a stage.
121 | *
122 | * The method checks if the stage has players hidden and hides all players in the stage from the player.
123 | *
124 | * @param event The PlayerEnterStageEvent object containing information about the enter stage event.
125 | */
126 | @EventHandler
127 | public void onPlayerEnterStage(PlayerEnterStageEvent event) {
128 | if (!event.getStage().getAudience().isArePlayersHidden()) return;
129 | for (Player player : event.getStage().getAudience().getOnlinePlayers()) {
130 | if (player == null) continue;
131 | player.hidePlayer(Blockify.getInstance(), event.getPlayer());
132 | if (!event.getStage().isLocationWithin(player.getLocation())) continue;
133 | event.getPlayer().hidePlayer(Blockify.getInstance(), player);
134 | }
135 | }
136 |
137 | /**
138 | * This method is an event handler for PlayerExitStageEvent.
139 | * It is triggered when a player exits a stage.
140 | *
141 | * The method checks if the stage has players hidden and shows all players in the stage to the player.
142 | *
143 | * @param event The PlayerExitStageEvent object containing information about the exit stage event.
144 | */
145 | @EventHandler
146 | public void onPlayerExitStage(PlayerExitStageEvent event) {
147 | if (!event.getStage().getAudience().isArePlayersHidden()) return;
148 | for (Player player : event.getStage().getAudience().getOnlinePlayers()) {
149 | if (player == null) continue;
150 | player.showPlayer(Blockify.getInstance(), event.getPlayer());
151 | }
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/managers/BlockChangeManager.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.managers;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.events.OnBlockChangeSendEvent;
5 | import codes.kooper.blockify.models.Audience;
6 | import codes.kooper.blockify.models.Stage;
7 | import codes.kooper.blockify.models.View;
8 | import codes.kooper.blockify.types.BlockifyChunk;
9 | import codes.kooper.blockify.types.BlockifyPosition;
10 | import codes.kooper.blockify.utils.PositionKeyUtil;
11 | import com.github.retrooper.packetevents.PacketEvents;
12 | import com.github.retrooper.packetevents.protocol.player.User;
13 | import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk;
14 | import com.github.retrooper.packetevents.protocol.world.chunk.Column;
15 | import com.github.retrooper.packetevents.protocol.world.chunk.LightData;
16 | import com.github.retrooper.packetevents.protocol.world.chunk.impl.v_1_18.Chunk_v1_18;
17 | import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
18 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData;
19 | import io.github.retrooper.packetevents.util.SpigotConversionUtil;
20 | import io.papermc.paper.math.Position;
21 | import lombok.Getter;
22 | import org.bukkit.Bukkit;
23 | import org.bukkit.Chunk;
24 | import org.bukkit.ChunkSnapshot;
25 | import org.bukkit.Location;
26 | import org.bukkit.block.data.BlockData;
27 | import org.bukkit.entity.Player;
28 | import org.bukkit.scheduler.BukkitTask;
29 |
30 | import java.util.*;
31 | import java.util.concurrent.*;
32 | import java.util.concurrent.atomic.AtomicInteger;
33 |
34 | @Getter
35 | public class BlockChangeManager {
36 | private final ConcurrentHashMap blockChangeTasks;
37 | private final ConcurrentHashMap blockDataToId;
38 | private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
39 |
40 | public BlockChangeManager() {
41 | this.blockChangeTasks = new ConcurrentHashMap<>();
42 | this.blockDataToId = new ConcurrentHashMap<>();
43 | }
44 |
45 | /**
46 | * Sends views to the player.
47 | * Call Asynchronously
48 | *
49 | * @param stage the stage
50 | * @param player the player
51 | */
52 | public void sendViews(Stage stage, Player player) {
53 | for (View view : stage.getViews()) {
54 | sendView(player, view);
55 | }
56 | }
57 |
58 | /**
59 | * Sends a view to the player.
60 | * Call Asynchronously
61 | *
62 | * @param player the player
63 | * @param view the view
64 | */
65 | public void sendView(Player player, View view) {
66 | Audience audience = Audience.fromPlayers(new HashSet<>(Collections.singletonList(player)));
67 | sendBlockChanges(view.getStage(), audience, view.getBlocks().keySet());
68 | }
69 |
70 | /**
71 | * Hides a view from the player.
72 | * Call Asynchronously
73 | *
74 | * @param player the player
75 | * @param view the view
76 | */
77 | public void hideView(Player player, View view) {
78 | Audience audience = Audience.fromPlayers(new HashSet<>(Collections.singletonList(player)));
79 | sendBlockChanges(view.getStage(), audience, view.getBlocks().keySet(), true);
80 | }
81 |
82 | /**
83 | * Hides views from the player.
84 | * Call Asynchronously
85 | *
86 | * @param stage the stage
87 | * @param player the player
88 | */
89 | public void hideViews(Stage stage, Player player) {
90 | for (View view : stage.getViews()) {
91 | hideView(player, view);
92 | }
93 | }
94 |
95 | /**
96 | * Sends a block change to the audience.
97 | *
98 | * @param stage the stage
99 | * @param audience the audience
100 | * @param position the position
101 | */
102 | public void sendBlockChange(Stage stage, Audience audience, BlockifyPosition position) {
103 | BlockifyChunk chunk = new BlockifyChunk(position.getX() >> 4, position.getZ() >> 4);
104 | sendBlockChanges(stage, audience, Collections.singleton(chunk));
105 | }
106 |
107 | /**
108 | * Sends block changes to the audience.
109 | * Call Asynchronously
110 | *
111 | * @param stage the stage
112 | * @param audience the audience
113 | * @param chunks the chunks to send
114 | */
115 | public void sendBlockChanges(Stage stage, Audience audience, Collection chunks) {
116 | sendBlockChanges(stage, audience, chunks, false);
117 | }
118 |
119 | /**
120 | * Sends block changes to the audience.
121 | * Call Asynchronously
122 | *
123 | * @param stage the stage
124 | * @param audience the audience
125 | * @param chunks the chunks to send
126 | * @param unload whether to unload the chunks
127 | */
128 | public void sendBlockChanges(Stage stage, Audience audience, Collection chunks, boolean unload) {
129 | Map> blockChanges = getBlockChanges(stage, chunks);
130 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new OnBlockChangeSendEvent(stage, blockChanges).callEvent());
131 |
132 | // If there is only one block change, send it to the player directly
133 | int blockCount = 0;
134 | for (Map.Entry> entry : blockChanges.entrySet()) {
135 | blockCount += entry.getValue().size();
136 | }
137 | if (blockCount == 1) {
138 | for (Player onlinePlayer : audience.getOnlinePlayers()) {
139 | if (onlinePlayer.getWorld() != stage.getWorld()) continue;
140 | for (Map.Entry> entry : blockChanges.entrySet()) {
141 | Long position = entry.getValue().keySet().iterator().next();
142 | BlockData blockData = entry.getValue().values().iterator().next();
143 | onlinePlayer.sendBlockChange(PositionKeyUtil.toBlockifyPosition(position).toLocation(onlinePlayer.getWorld()), blockData);
144 | }
145 | }
146 | return;
147 | }
148 |
149 | // Less than 3,000 blocks then use the player.sendBlockChanges method
150 | if (blockCount < 3000) {
151 | Map multiBlockChange = new HashMap<>();
152 | for (BlockifyChunk chunk : chunks) {
153 | if (!stage.getWorld().isChunkLoaded(chunk.x(), chunk.z()) || !blockChanges.containsKey(chunk)) continue;
154 | for (Map.Entry entry : blockChanges.get(chunk).entrySet()) {
155 | multiBlockChange.put(PositionKeyUtil.toBlockifyPosition(entry.getKey()).toPosition(), entry.getValue());
156 | }
157 | }
158 | for (Player player : audience.getOnlinePlayers()) {
159 | player.sendMultiBlockChange(multiBlockChange);
160 | }
161 | return;
162 | }
163 |
164 | // Send multiple block changes to the players
165 | for (Player onlinePlayer : audience.getOnlinePlayers()) {
166 | Location playerLocation = onlinePlayer.getLocation();
167 | if (playerLocation.getWorld() != stage.getWorld()) continue;
168 |
169 | // The chunk index is used to keep track of the current chunk being sent
170 | AtomicInteger chunkIndex = new AtomicInteger(0);
171 | // Create an array of chunks to send from the block changes map
172 | List chunksToSend = new ArrayList<>(chunks.stream().toList());
173 | chunksToSend.sort((chunk1, chunk2) -> {
174 | // Get distance from chunks to player
175 | int x = playerLocation.getBlockX() / 16;
176 | int z = playerLocation.getBlockZ() / 16;
177 | int chunkX1 = chunk1.x();
178 | int chunkZ1 = chunk1.z();
179 | int chunkX2 = chunk2.x();
180 | int chunkZ2 = chunk2.z();
181 |
182 | // Calculate squared distances (more efficient than using square root)
183 | int distanceSquared1 = (chunkX1 - x) * (chunkX1 - x) + (chunkZ1 - z) * (chunkZ1 - z);
184 | int distanceSquared2 = (chunkX2 - x) * (chunkX2 - x) + (chunkZ2 - z) * (chunkZ2 - z);
185 |
186 | // Compare distances and return accordingly
187 | return Integer.compare(distanceSquared1, distanceSquared2);
188 | });
189 |
190 | // Create a task to send a chunk to the player every tick
191 | blockChangeTasks.put(onlinePlayer, Bukkit.getScheduler().runTaskTimer(Blockify.getInstance(), () -> {
192 | // Check if player is online, if not, cancel the task
193 | if (!onlinePlayer.isOnline()) {
194 | blockChangeTasks.computeIfPresent(onlinePlayer, (key, task) -> {
195 | task.cancel();
196 | return null;
197 | });
198 | return;
199 | }
200 |
201 | // Loop through chunks per tick
202 | for (int i = 0; i < stage.getChunksPerTick(); i++) {
203 | // If the chunk index is greater than the chunks to send length
204 | if (chunkIndex.get() >= chunksToSend.size()) {
205 | // Safely cancel the task and remove it from the map
206 | blockChangeTasks.computeIfPresent(onlinePlayer, (key, task) -> {
207 | task.cancel();
208 | return null; // Remove the task
209 | });
210 | return;
211 | }
212 |
213 | // Get the chunk from the chunks to send array
214 | BlockifyChunk chunk = chunksToSend.get(chunkIndex.get());
215 | chunkIndex.getAndIncrement();
216 |
217 | // Check if the chunk is loaded; if not, return
218 | if (!stage.getWorld().isChunkLoaded(chunk.x(), chunk.z())) continue;
219 |
220 | // Send the chunk packet to the player
221 | Bukkit.getScheduler().runTaskAsynchronously(Blockify.getInstance(), () -> sendChunkPacket(stage, onlinePlayer, chunk, unload));
222 | }
223 | }, 0L, 1L));
224 | }
225 | }
226 |
227 | /**
228 | * Sends a chunk packet to the player.
229 | * This method submits the task to the thread pool for asynchronous execution.
230 | *
231 | * @param stage the stage
232 | * @param player the player
233 | * @param chunk the chunk
234 | * @param unload whether to unload the chunk
235 | */
236 | public void sendChunkPacket(Stage stage, Player player, BlockifyChunk chunk, boolean unload) {
237 | executorService.submit(() -> processAndSendChunk(stage, player, chunk, unload));
238 | }
239 |
240 | /**
241 | * Processes the chunk and sends the packet to the player.
242 | *
243 | * @param stage the stage
244 | * @param player the player
245 | * @param chunk the chunk
246 | * @param unload whether to unload the chunk
247 | */
248 | private void processAndSendChunk(Stage stage, Player player, BlockifyChunk chunk, boolean unload) {
249 | try {
250 | User packetUser = PacketEvents.getAPI().getPlayerManager().getUser(player);
251 | int ySections = packetUser.getTotalWorldHeight() >> 4;
252 | Map blockData = null;
253 |
254 | if (!unload) {
255 | Map> blockChanges = getBlockChanges(stage, Collections.singleton(chunk));
256 | blockData = blockChanges.get(chunk);
257 | }
258 |
259 | Map blockDataToState = new HashMap<>();
260 | List chunks = new ArrayList<>(ySections);
261 | Chunk bukkitChunk = player.getWorld().getChunkAt(chunk.x(), chunk.z());
262 | ChunkSnapshot chunkSnapshot = bukkitChunk.getChunkSnapshot();
263 | int maxHeight = player.getWorld().getMaxHeight();
264 | int minHeight = player.getWorld().getMinHeight();
265 |
266 | // Pre-fetch default block data for the entire chunk to reduce calls to getBlockData()
267 | BlockData[][][][] defaultBlockData = new BlockData[ySections][16][16][16];
268 | for (int section = 0; section < ySections; section++) {
269 | int baseY = (section << 4) + minHeight;
270 | for (int x = 0; x < 16; x++) {
271 | for (int y = 0; y < 16; y++) {
272 | int worldY = baseY + y;
273 | if (worldY >= minHeight && worldY < maxHeight) {
274 | for (int z = 0; z < 16; z++) {
275 | defaultBlockData[section][x][y][z] = chunkSnapshot.getBlockData(x, worldY, z);
276 | }
277 | }
278 | }
279 | }
280 | }
281 |
282 | // Predefined full light array and bitsets to avoid recreating them for each chunk
283 | byte[] fullLightSection = new byte[2048];
284 | Arrays.fill(fullLightSection, (byte) 0xFF);
285 | byte[][] fullLightArray = new byte[ySections][];
286 | BitSet fullBitSet = new BitSet(ySections);
287 | for (int i = 0; i < ySections; i++) {
288 | fullLightArray[i] = fullLightSection;
289 | fullBitSet.set(i);
290 | }
291 | BitSet emptyBitSet = new BitSet(ySections);
292 |
293 | // Process each chunk section
294 | for (int section = 0; section < ySections; section++) {
295 | Chunk_v1_18 baseChunk = new Chunk_v1_18();
296 |
297 | // Use primitive long keys for block positions to reduce object creation
298 | long baseY = (section << 4) + minHeight;
299 | for (int x = 0; x < 16; x++) {
300 | for (int y = 0; y < 16; y++) {
301 | long worldY = baseY + y;
302 | if (worldY >= minHeight && worldY < maxHeight) {
303 | for (int z = 0; z < 16; z++) {
304 | long positionKey = (((x + ((long) chunk.x() << 4)) & 0x3FFFFFF) << 38)
305 | | ((worldY & 0xFFF) << 26)
306 | | ((z + ((long) chunk.z() << 4)) & 0x3FFFFFF);
307 | BlockData data = null;
308 |
309 | if (!unload && blockData != null) {
310 | data = blockData.get(positionKey);
311 | }
312 |
313 | if (data == null) {
314 | data = defaultBlockData[section][x][y][z];
315 | }
316 |
317 | WrappedBlockState state = blockDataToState.computeIfAbsent(data, SpigotConversionUtil::fromBukkitBlockData);
318 | baseChunk.set(x, y, z, state);
319 | }
320 | }
321 | }
322 | }
323 |
324 | // Set biome data for the chunk section
325 | int biomeId = baseChunk.getBiomeData().palette.stateToId(1);
326 | int storageSize = baseChunk.getBiomeData().storage.getData().length;
327 | for (int index = 0; index < storageSize; index++) {
328 | baseChunk.getBiomeData().storage.set(index, biomeId);
329 | }
330 |
331 | chunks.add(baseChunk);
332 | }
333 |
334 | // Reuse pre-created light data
335 | LightData lightData = new LightData();
336 | lightData.setBlockLightArray(fullLightArray);
337 | lightData.setSkyLightArray(fullLightArray);
338 | lightData.setBlockLightCount(ySections);
339 | lightData.setSkyLightCount(ySections);
340 | lightData.setBlockLightMask(fullBitSet);
341 | lightData.setSkyLightMask(fullBitSet);
342 | lightData.setEmptyBlockLightMask(emptyBitSet);
343 | lightData.setEmptySkyLightMask(emptyBitSet);
344 |
345 | Column column = new Column(chunk.x(), chunk.z(), true, chunks.toArray(new BaseChunk[0]), null);
346 | WrapperPlayServerChunkData chunkData = new WrapperPlayServerChunkData(column, lightData);
347 | packetUser.sendPacketSilently(chunkData);
348 | } catch (Exception e) {
349 | // Handle exceptions appropriately, possibly logging them
350 | e.printStackTrace();
351 | }
352 | }
353 |
354 | private Map> getBlockChanges(Stage stage, Collection chunks) {
355 | Map> blockChanges = new HashMap<>();
356 | Map> highestZIndexes = new HashMap<>();
357 |
358 | for (View view : stage.getViews()) {
359 | int zIndex = view.getZIndex();
360 | for (Map.Entry> entry : view.getBlocks().entrySet()) {
361 | BlockifyChunk chunk = entry.getKey();
362 | if (!chunks.contains(chunk)) continue;
363 |
364 | highestZIndexes.computeIfAbsent(chunk, k -> new HashMap<>());
365 | Map chunkZIndexes = highestZIndexes.get(chunk);
366 | Map chunkBlockChanges = blockChanges.computeIfAbsent(chunk, k -> new HashMap<>());
367 |
368 | for (Map.Entry positionEntry : entry.getValue().entrySet()) {
369 | BlockifyPosition positionKey = positionEntry.getKey();
370 | BlockData blockData = positionEntry.getValue();
371 |
372 | chunkZIndexes.compute(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), (key, currentMaxZIndex) -> {
373 | if (currentMaxZIndex == null || zIndex > currentMaxZIndex) {
374 | chunkBlockChanges.put(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), blockData);
375 | return zIndex;
376 | } else if (zIndex == currentMaxZIndex) {
377 | chunkBlockChanges.put(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), blockData);
378 | }
379 | return currentMaxZIndex;
380 | });
381 | }
382 | }
383 | }
384 | return blockChanges;
385 | }
386 |
387 | /**
388 | * Shutdown the executor service gracefully.
389 | */
390 | public void shutdown() {
391 | executorService.shutdown();
392 | try {
393 | if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
394 | executorService.shutdownNow();
395 | if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
396 | System.err.println("Executor service did not terminate");
397 | }
398 | } catch (InterruptedException e) {
399 | executorService.shutdownNow();
400 | Thread.currentThread().interrupt();
401 | }
402 | }
403 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/managers/StageManager.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.managers;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.events.CreateStageEvent;
5 | import codes.kooper.blockify.events.DeleteStageEvent;
6 | import codes.kooper.blockify.models.Stage;
7 | import lombok.Getter;
8 | import org.bukkit.Bukkit;
9 | import org.bukkit.entity.Player;
10 |
11 | import java.util.ArrayList;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | @Getter
17 | public class StageManager {
18 | private final Map stages;
19 |
20 | public StageManager() {
21 | this.stages = new HashMap<>();
22 | }
23 |
24 | /**
25 | * Create a new stage
26 | * @param stage Stage to create
27 | */
28 | public void createStage(Stage stage) {
29 | if (stages.containsKey(stage.getName())) {
30 | Blockify.getInstance().getLogger().warning("Stage with name " + stage.getName() + " already exists!");
31 | return;
32 | }
33 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new CreateStageEvent(stage).callEvent());
34 | stages.put(stage.getName(), stage);
35 | }
36 |
37 | /**
38 | * Get a stage by name
39 | * @param name Name of the stage
40 | * @return Stage
41 | */
42 | public Stage getStage(String name) {
43 | return stages.get(name);
44 | }
45 |
46 | /**
47 | * Delete a stage by name
48 | * @param name Name of the stage
49 | */
50 | public void deleteStage(String name) {
51 | new DeleteStageEvent(stages.get(name)).callEvent();
52 | stages.remove(name);
53 | }
54 |
55 | /**
56 | * Check if a stage exists
57 | * @param name Name of the stage
58 | * @return boolean
59 | */
60 | public boolean hasStage(String name) {
61 | return stages.containsKey(name);
62 | }
63 |
64 | /**
65 | * Get all stages
66 | * @return List of stages
67 | */
68 | public List getStages(Player player) {
69 | List stages = new ArrayList<>();
70 | for (Stage stage : this.stages.values()) {
71 | if (stage.getAudience().getPlayers().contains(player.getUniqueId())) {
72 | stages.add(stage);
73 | }
74 | }
75 | return stages;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/models/Audience.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.models;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import org.bukkit.entity.Player;
7 |
8 | import java.util.*;
9 | import java.util.stream.Collectors;
10 |
11 | @Setter
12 | @Getter
13 | public class Audience {
14 | private boolean arePlayersHidden;
15 | private final Set players;
16 | private final Map miningSpeeds;
17 |
18 | /**
19 | * @param players The set of players
20 | */
21 | public static Audience fromPlayers(Set players) {
22 | return new Audience(players.stream().map(Player::getUniqueId).collect(Collectors.toSet()), false);
23 | }
24 |
25 | /**
26 | * @param players The set of players
27 | */
28 | public static Audience fromUUIDs(Set players) {
29 | return new Audience(players, false);
30 | }
31 |
32 | /**
33 | * @param players The set of players
34 | * @param arePlayersHidden Whether the players are hidden
35 | */
36 | public static Audience fromPlayers(Set players, boolean arePlayersHidden) {
37 | return new Audience(players.stream().map(Player::getUniqueId).collect(Collectors.toSet()), arePlayersHidden);
38 | }
39 |
40 | /**
41 | * @param players The set of players
42 | * @param arePlayersHidden Whether the players are hidden
43 | */
44 | public static Audience fromUUIDs(Set players, boolean arePlayersHidden) {
45 | return new Audience(players, arePlayersHidden);
46 | }
47 |
48 | /**
49 | * @param players The set of players
50 | * @param arePlayersHidden Whether the players are hidden
51 | */
52 | private Audience(Set players, boolean arePlayersHidden) {
53 | this.players = players;
54 | this.arePlayersHidden = arePlayersHidden;
55 | this.miningSpeeds = new HashMap<>();
56 | }
57 |
58 | /**
59 | * @param player The player to add
60 | * @return The set of uuids of players
61 | */
62 | public Set addPlayer(Player player) {
63 | return addPlayer(player.getUniqueId());
64 | }
65 |
66 | /**
67 | * @param player The uuid of a player to add
68 | * @return The set of uuids of players
69 | */
70 | public Set addPlayer(UUID player) {
71 | players.add(player);
72 | return players;
73 | }
74 |
75 | /**
76 | * @param player The player to remove
77 | * @return The set of uuids of players
78 | */
79 | public Set removePlayer(Player player) {
80 | return removePlayer(player.getUniqueId());
81 | }
82 |
83 | /**
84 | * @param player The uuid of a player to remove
85 | * @return The set of uuids of players
86 | */
87 | public Set removePlayer(UUID player) {
88 | players.remove(player);
89 | return players;
90 | }
91 |
92 | /**
93 | * @return A set of online players in the audience
94 | */
95 | public Set getOnlinePlayers() {
96 | List onlinePlayers = new ArrayList<>();
97 | for (UUID player : players) {
98 | Player p = Blockify.getInstance().getServer().getPlayer(player);
99 | if (p != null) {
100 | onlinePlayers.add(p);
101 | }
102 | }
103 | return new HashSet<>(onlinePlayers);
104 | }
105 |
106 |
107 | /**
108 | * Sets the mining speed for a player
109 | * @param player The player
110 | * @param speed The speed
111 | */
112 | public void setMiningSpeed(Player player, float speed) {
113 | setMiningSpeed(player.getUniqueId(), speed);
114 | }
115 |
116 | /**
117 | * Sets the mining speed for a player
118 | * @param player The player's UUID
119 | * @param speed The speed
120 | */
121 | public void setMiningSpeed(UUID player, float speed) {
122 | if (speed < 0 || speed == 1) {
123 | Blockify.getInstance().getLogger().warning("Invalid mining speed for player " + player + ": " + speed);
124 | return;
125 | }
126 | miningSpeeds.put(player, speed);
127 | }
128 |
129 | /**
130 | * Resets the mining speed for a player
131 | * @param player The player
132 | */
133 | public void resetMiningSpeed(Player player) {
134 | resetMiningSpeed(player.getUniqueId());
135 | }
136 |
137 | /**
138 | * Resets the mining speed for a player
139 | * @param player The player's UUID
140 | */
141 | public void resetMiningSpeed(UUID player) {
142 | miningSpeeds.remove(player);
143 | }
144 |
145 | /**
146 | * Gets the mining speed of a player
147 | * @param player The player
148 | * @return The mining speed
149 | */
150 | public float getMiningSpeed(Player player) {
151 | return getMiningSpeed(player.getUniqueId());
152 | }
153 |
154 | /**
155 | * Gets the mining speed of a player
156 | * @param player The player's UUID
157 | * @return The mining speed
158 | */
159 | public float getMiningSpeed(UUID player) {
160 | return miningSpeeds.getOrDefault(player, 1f);
161 | }
162 |
163 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/models/Pattern.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.models;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 | import org.bukkit.block.data.BlockData;
6 |
7 | import java.util.Map;
8 | import java.util.NavigableMap;
9 | import java.util.TreeMap;
10 | import java.util.function.Predicate;
11 |
12 | @Getter
13 | @Setter
14 | public class Pattern {
15 | private final NavigableMap blockDataMap = new TreeMap<>();
16 | private double totalWeight;
17 |
18 | /**
19 | * Creates a new Pattern with the given BlockData and their respective percentages.
20 | *
21 | * @param blockDataPercentages A map of BlockData and their respective percentages.
22 | * @throws IllegalArgumentException If the percentage values are not in the range [0.0, 1.0],
23 | * if the map is empty, or if the sum of percentages exceeds 1.0.
24 | */
25 | public Pattern(Map blockDataPercentages) {
26 | Predicate inRange = value -> value >= 0.0 && value <= 1.0;
27 |
28 | if (!blockDataPercentages.values().stream().allMatch(inRange)) {
29 | throw new IllegalArgumentException("Percentage values must be in the range [0.0, 1.0]");
30 | }
31 |
32 | if (blockDataPercentages.isEmpty()) {
33 | throw new IllegalArgumentException("Pattern must contain at least one BlockData with a non-zero percentage");
34 | }
35 |
36 | double sum = blockDataPercentages.values().stream().mapToDouble(value -> value).sum();
37 | if (Math.round(sum * 100000) / 100000.0 > 1.0) {
38 | throw new IllegalArgumentException("Sum of percentages must not exceed 1.0");
39 | }
40 |
41 | for (Map.Entry entry : blockDataPercentages.entrySet()) {
42 | totalWeight += entry.getValue();
43 | blockDataMap.put(totalWeight, entry.getKey());
44 | }
45 | }
46 |
47 | /**
48 | * Returns a random BlockData from the Pattern based on the percentages.
49 | *
50 | * @return A random BlockData from the Pattern.
51 | */
52 | public BlockData getRandomBlockData() {
53 | double random = Math.random() * totalWeight;
54 | return blockDataMap.higherEntry(random).getValue();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/models/Stage.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.models;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.types.BlockifyChunk;
5 | import codes.kooper.blockify.types.BlockifyPosition;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import org.bukkit.Location;
9 | import org.bukkit.World;
10 |
11 | import java.util.HashSet;
12 | import java.util.Set;
13 | import java.util.stream.Collectors;
14 |
15 | @Getter
16 | @Setter
17 | public class Stage {
18 | private final String name;
19 | private final World world;
20 | private BlockifyPosition maxPosition, minPosition;
21 | private final Set views;
22 | private int chunksPerTick;
23 | private final Audience audience;
24 |
25 | /**
26 | * Create a new stage.
27 | *
28 | * @param name Name of the stage
29 | * @param world World the stage is in
30 | * @param pos1 First position of the stage
31 | * @param pos2 Second position of the stage
32 | * @param audience Audience to send blocks to
33 | */
34 | public Stage(String name, World world, BlockifyPosition pos1, BlockifyPosition pos2, Audience audience) {
35 | this.name = name;
36 | this.world = world;
37 | this.maxPosition = new BlockifyPosition(Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ()));
38 | this.minPosition = new BlockifyPosition(Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ()));
39 | this.views = new HashSet<>();
40 | this.audience = audience;
41 | this.chunksPerTick = 1;
42 | }
43 |
44 |
45 | /**
46 | * Check if a location is within the stage.
47 | *
48 | * @param location Location to check
49 | * @return True if the location is within the stage
50 | */
51 | public boolean isLocationWithin(Location location) {
52 | return location.getWorld().equals(world) && location.getBlockX() >= minPosition.getX() && location.getBlockX() <= maxPosition.getX() && location.getBlockY() >= minPosition.getY() && location.getBlockY() <= maxPosition.getY() && location.getBlockZ() >= minPosition.getZ() && location.getBlockZ() <= maxPosition.getZ();
53 | }
54 |
55 | /**
56 | * Send blocks to the audience. Should be called asynchronously.
57 | */
58 | public void sendBlocksToAudience() {
59 | Blockify.getInstance().getBlockChangeManager().sendBlockChanges(this, audience, getChunks());
60 | }
61 |
62 | /**
63 | * Refresh blocks to the audience. Should be called after modifying blocks.
64 | * Should be called asynchronously.
65 | *
66 | * @param blocks Blocks to refresh to the audience.
67 | */
68 | public void refreshBlocksToAudience(Set blocks) {
69 | Set chunks = blocks.stream().map(BlockifyPosition::toBlockifyChunk).collect(Collectors.toSet());
70 | Blockify.getInstance().getBlockChangeManager().sendBlockChanges(this, audience, chunks);
71 | }
72 |
73 | /**
74 | * Add a view to the stage.
75 | *
76 | * @param view View to add
77 | */
78 | public void addView(View view) {
79 | if (views.stream().anyMatch(v -> v.getName().equalsIgnoreCase(view.getName()))) {
80 | Blockify.getInstance().getLogger().warning("View with name " + view.getName() + " already exists in stage " + name + "!");
81 | return;
82 | }
83 | views.add(view);
84 | }
85 |
86 | /**
87 | * Remove a view from the stage.
88 | *
89 | * @param view View to remove
90 | */
91 | public void removeView(View view) {
92 | views.remove(view);
93 | }
94 |
95 | /**
96 | * Get a view by name.
97 | *
98 | * @param name Name of the view
99 | * @return View or null if not found
100 | */
101 | public View getView(String name) {
102 | for (View view : views) {
103 | if (view.getName().equalsIgnoreCase(name)) {
104 | return view;
105 | }
106 | }
107 | return null;
108 | }
109 |
110 | /**
111 | * Get all chunks that are being used by this stage.
112 | * If a lot of chunks are present, it is recommended to use this method asynchronously.
113 | *
114 | * @return Set of chunks
115 | */
116 | public Set getChunks() {
117 | Set chunks = new HashSet<>();
118 | for (int x = minPosition.getX() >> 4; x <= maxPosition.getX() >> 4; x++) {
119 | for (int z = minPosition.getZ() >> 4; z <= maxPosition.getZ() >> 4; z++) {
120 | chunks.add(new BlockifyChunk(x, z));
121 | }
122 | }
123 | return chunks;
124 | }
125 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/models/View.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.models;
2 |
3 | import codes.kooper.blockify.types.BlockifyChunk;
4 | import codes.kooper.blockify.types.BlockifyPosition;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 | import org.bukkit.block.data.BlockData;
8 |
9 | import java.util.Set;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | @Getter
13 | @Setter
14 | public class View {
15 | private final ConcurrentHashMap> blocks;
16 | private final Stage stage;
17 | private final String name;
18 | private int zIndex;
19 | private boolean breakable, placeable;
20 | private Pattern pattern;
21 |
22 | /**
23 | * Constructor for the View class.
24 | *
25 | * @param name The name of the view.
26 | * @param stage The stage the view is in.
27 | * @param pattern The pattern of the view.
28 | * @param breakable Whether the view is breakable or not.
29 | */
30 | public View(String name, Stage stage, Pattern pattern, boolean breakable) {
31 | this.name = name;
32 | this.blocks = new ConcurrentHashMap<>();
33 | this.stage = stage;
34 | this.breakable = breakable;
35 | this.pattern = pattern;
36 | this.zIndex = 0;
37 | }
38 |
39 | /**
40 | * Get the highest block at a given x and z coordinate.
41 | *
42 | * @param x The x coordinate.
43 | * @param z The z coordinate.
44 | * @return The highest block at the given x and z coordinate.
45 | */
46 | public BlockifyPosition getHighestBlock(int x, int z) {
47 | for (int y = stage.getMaxPosition().getY(); y >= stage.getMinPosition().getY(); y--) {
48 | BlockifyPosition position = new BlockifyPosition(x, y, z);
49 | if (hasBlock(position) && getBlock(position).getMaterial().isSolid()) {
50 | return position;
51 | }
52 | }
53 | return null;
54 | }
55 |
56 | /**
57 | * Get the lowest block at a given x and z coordinate.
58 | *
59 | * @param x The x coordinate.
60 | * @param z The z coordinate.
61 | * @return The lowest block at the given x and z coordinate.
62 | */
63 | public BlockifyPosition getLowestBlock(int x, int z) {
64 | for (int y = stage.getMinPosition().getY(); y <= stage.getMaxPosition().getY(); y++) {
65 | BlockifyPosition position = new BlockifyPosition(x, y, z);
66 | if (hasBlock(position) && getBlock(position).getMaterial().isSolid()) {
67 | return position;
68 | }
69 | }
70 | return null;
71 | }
72 |
73 | // Returns all blocks in the view
74 | public ConcurrentHashMap> getBlocks() {
75 | return new ConcurrentHashMap<>(blocks);
76 | }
77 |
78 | /**
79 | * Remove a block from the view.
80 | *
81 | * @param position The block to remove.
82 | */
83 | public void removeBlock(BlockifyPosition position) {
84 | if (hasBlock(position)) {
85 | blocks.get(position.toBlockifyChunk()).remove(position);
86 | if (blocks.get(position.toBlockifyChunk()).isEmpty()) {
87 | blocks.remove(position.toBlockifyChunk());
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * Remove a set of blocks from the view.
94 | * Call this method asynchronously if you are removing a large number of blocks.
95 | *
96 | * @param positions The set of blocks to remove.
97 | */
98 | public void removeBlocks(Set positions) {
99 | for (BlockifyPosition position : positions) {
100 | removeBlock(position);
101 | }
102 | }
103 |
104 | /**
105 | * Remove all blocks from the view.
106 | */
107 | public void removeAllBlocks() {
108 | blocks.clear();
109 | }
110 |
111 | /**
112 | * Add a block to the view.
113 | *
114 | * @param position The block to add.
115 | */
116 | public void addBlock(BlockifyPosition position) {
117 | if (!blocks.containsKey(position.toBlockifyChunk())) {
118 | blocks.put(position.toBlockifyChunk(), new ConcurrentHashMap<>());
119 | }
120 | blocks.get(position.toBlockifyChunk()).put(position, pattern.getRandomBlockData());
121 | }
122 |
123 | /**
124 | * Add a set of blocks to the view.
125 | * Call this method asynchronously if you are adding a large number of blocks.
126 | *
127 | * @param positions The set of blocks to add.
128 | */
129 | public void addBlocks(Set positions) {
130 | for (BlockifyPosition position : positions) {
131 | addBlock(position);
132 | }
133 | }
134 |
135 | /**
136 | * Check if a block is in the view.
137 | *
138 | * @param position The position of the block.
139 | * @return Whether the block is in the view.
140 | */
141 | public boolean hasBlock(BlockifyPosition position) {
142 | return blocks.containsKey(position.toBlockifyChunk()) && blocks.get(position.toBlockifyChunk()).containsKey(position);
143 | }
144 |
145 | /**
146 | * Check if a set of blocks are in the view.
147 | * Call this method asynchronously if you are checking a large number of blocks.
148 | *
149 | * @param positions The set of blocks to check.
150 | * @return Whether the set of blocks are in the view.
151 | */
152 | public boolean hasBlocks(Set positions) {
153 | for (BlockifyPosition position : positions) {
154 | if (!hasBlock(position)) {
155 | return false;
156 | }
157 | }
158 | return true;
159 | }
160 |
161 | /**
162 | * Get the block data at a given position.
163 | *
164 | * @param position The position of the block.
165 | * @return The block data at the given position.
166 | */
167 | public BlockData getBlock(BlockifyPosition position) {
168 | return blocks.get(position.toBlockifyChunk()).get(position);
169 | }
170 |
171 |
172 | /**
173 | * Check if a chunk is in the view.
174 | *
175 | * @param x The x coordinate of the chunk.
176 | * @param z The z coordinate of the chunk.
177 | * @return Whether the chunk is in the view.
178 | */
179 | public boolean hasChunk(int x, int z) {
180 | return blocks.containsKey(new BlockifyChunk(x, z));
181 | }
182 |
183 | /**
184 | * Set positions to a given block data.
185 | * Call this method asynchronously if you are setting a large number of blocks.
186 | *
187 | * @param positions The set of positions to set.
188 | * @param blockData The block data.
189 | */
190 | public void setBlocks(Set positions, BlockData blockData) {
191 | for (BlockifyPosition position : positions) {
192 | setBlock(position, blockData);
193 | }
194 | }
195 |
196 | /**
197 | * Set a position to a given block data.
198 | *
199 | * @param position The position of the block.
200 | * @param blockData The block data.
201 | */
202 | public void setBlock(BlockifyPosition position, BlockData blockData) {
203 | if (hasBlock(position)) {
204 | blocks.get(position.toBlockifyChunk()).put(position, blockData);
205 | }
206 | }
207 |
208 | /**
209 | * Reset a block to a random block data from the pattern.
210 | *
211 | * @param position The position of the block.
212 | */
213 | public void resetBlock(BlockifyPosition position) {
214 | if (hasBlock(position)) {
215 | blocks.get(position.toBlockifyChunk()).put(position, pattern.getRandomBlockData());
216 | }
217 | }
218 |
219 | /**
220 | * Reset a set of blocks to random block data from the pattern.
221 | * Call this method asynchronously if you are resetting a large number of blocks.
222 | *
223 | * @param positions The set of blocks to reset.
224 | */
225 | public void resetBlocks(Set positions) {
226 | for (BlockifyPosition position : positions) {
227 | resetBlock(position);
228 | }
229 | }
230 |
231 | /**
232 | * Reset all blocks in the view to random block data from the pattern.
233 | * Call this method asynchronously.
234 | */
235 | public void resetViewBlocks() {
236 | for (BlockifyChunk chunk : blocks.keySet()) {
237 | for (BlockifyPosition position : blocks.get(chunk).keySet()) {
238 | blocks.get(chunk).put(position, pattern.getRandomBlockData());
239 | }
240 | }
241 | }
242 |
243 | /**
244 | * Changes the pattern of the view.
245 | *
246 | * @param pattern The new pattern.
247 | */
248 | public void changePattern(Pattern pattern) {
249 | this.pattern = pattern;
250 | }
251 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/protocol/BlockDigAdapter.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.protocol;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.events.BlockifyBreakEvent;
5 | import codes.kooper.blockify.events.BlockifyInteractEvent;
6 | import codes.kooper.blockify.models.Stage;
7 | import codes.kooper.blockify.models.View;
8 | import codes.kooper.blockify.types.BlockifyPosition;
9 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract;
10 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent;
11 | import com.github.retrooper.packetevents.protocol.packettype.PacketType;
12 | import com.github.retrooper.packetevents.protocol.player.DiggingAction;
13 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
14 | import org.bukkit.Bukkit;
15 | import org.bukkit.GameMode;
16 | import org.bukkit.Material;
17 | import org.bukkit.block.data.BlockData;
18 | import org.bukkit.entity.Player;
19 |
20 | import java.util.List;
21 |
22 | public class BlockDigAdapter extends SimplePacketListenerAbstract {
23 |
24 | @Override
25 | public void onPacketPlayReceive(PacketPlayReceiveEvent event) {
26 | if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
27 | // Packet wrapper
28 | WrapperPlayClientPlayerDigging wrapper = new WrapperPlayClientPlayerDigging(event);
29 | DiggingAction actionType = wrapper.getAction();
30 |
31 | // Extract information from wrapper
32 | Player player = (Player) event.getPlayer();
33 |
34 | // Get stages the player is in. If the player is not in any stages, return.
35 | List stages = Blockify.getInstance().getStageManager().getStages(player);
36 | if (stages == null || stages.isEmpty()) {
37 | return;
38 | }
39 |
40 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ());
41 |
42 | // Find the block in any stage and view using streams
43 | stages.stream()
44 | .filter(stage -> stage.getWorld() == player.getWorld())
45 | .flatMap(stage -> stage.getViews().stream())
46 | .filter(view -> view.hasBlock(position)).min((view1, view2) -> Integer.compare(view2.getZIndex(), view1.getZIndex()))
47 | .ifPresent(view -> {
48 | // Get block data from view
49 | BlockData blockData = view.getBlock(position);
50 |
51 | // Call BlockifyInteractEvent to handle custom interaction
52 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new BlockifyInteractEvent(player, position, blockData, view, view.getStage()).callEvent());
53 |
54 | // Check if block is breakable, if not, send block change packet to cancel the break
55 | if (!view.isBreakable()) {
56 | event.setCancelled(true);
57 | return;
58 | }
59 |
60 | // Block break functionality
61 | if (actionType == DiggingAction.FINISHED_DIGGING || canInstantBreak(player, blockData)) {
62 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> {
63 | // Call BlockifyBreakEvent
64 | BlockifyBreakEvent blockifyBreakEvent = new BlockifyBreakEvent(player, position, blockData, view, view.getStage());
65 | blockifyBreakEvent.callEvent();
66 |
67 | // Set to air
68 | player.sendBlockChange(position.toLocation(player.getWorld()), Material.AIR.createBlockData());
69 | view.setBlock(position, Material.AIR.createBlockData());
70 |
71 | // If block is not cancelled, break the block, otherwise, revert the block
72 | if (blockifyBreakEvent.isCancelled()) {
73 | player.sendBlockChange(position.toLocation(player.getWorld()), blockData);
74 | view.setBlock(position, blockData);
75 | }
76 | });
77 | }
78 | });
79 | }
80 | }
81 |
82 | /**
83 | * Check if player can instantly break block
84 | *
85 | * @param player Player who is digging
86 | * @param blockData BlockData of the block
87 | * @return boolean
88 | */
89 | private boolean canInstantBreak(Player player, BlockData blockData) {
90 | return blockData.getDestroySpeed(player.getInventory().getItemInMainHand(), true) >= blockData.getMaterial().getHardness() * 30 || player.getGameMode() == GameMode.CREATIVE;
91 | }
92 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/protocol/BlockPlaceAdapter.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.protocol;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.events.BlockifyPlaceEvent;
5 | import codes.kooper.blockify.models.Stage;
6 | import codes.kooper.blockify.models.View;
7 | import codes.kooper.blockify.types.BlockifyPosition;
8 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract;
9 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent;
10 | import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent;
11 | import com.github.retrooper.packetevents.protocol.packettype.PacketType;
12 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerBlockPlacement;
13 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange;
14 | import org.bukkit.Bukkit;
15 | import org.bukkit.entity.Player;
16 |
17 | import java.util.List;
18 |
19 | public class BlockPlaceAdapter extends SimplePacketListenerAbstract {
20 |
21 | @Override
22 | public void onPacketPlayReceive(PacketPlayReceiveEvent event) {
23 | if (event.getPacketType() == PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT) {
24 | // Wrapper for the packet
25 | WrapperPlayClientPlayerBlockPlacement wrapper = new WrapperPlayClientPlayerBlockPlacement(event);
26 | Player player = (Player) event.getPlayer();
27 |
28 | // Get the stages the player is in. If the player is not in any stages, return.
29 | List stages = Blockify.getInstance().getStageManager().getStages(player);
30 | if (stages == null || stages.isEmpty()) {
31 | return;
32 | }
33 |
34 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ());
35 |
36 | // Check if the block is in any of the views in the stages
37 | for (Stage stage : stages) {
38 | for (View view : stage.getViews()) {
39 | if (view.hasBlock(position)) {
40 | // Call the event and cancel the placement
41 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new BlockifyPlaceEvent(player, position, view, stage).callEvent());
42 | event.setCancelled(true);
43 | return;
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | @Override
51 | public void onPacketPlaySend(PacketPlaySendEvent event) {
52 | if (event.getPacketType() == PacketType.Play.Server.BLOCK_CHANGE) {
53 | WrapperPlayServerBlockChange wrapper = new WrapperPlayServerBlockChange(event);
54 | Player player = (Player) event.getPlayer();
55 |
56 | // Get the stages the player is in. If the player is not in any stages, return.
57 | List stages = Blockify.getInstance().getStageManager().getStages(player);
58 | if (stages == null || stages.isEmpty()) {
59 | return;
60 | }
61 |
62 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ());
63 | for (Stage stage : stages) {
64 | for (View view : stage.getViews()) {
65 | if (view.hasBlock(position)) {
66 | if (wrapper.getBlockState().getType().getName().equalsIgnoreCase(view.getBlock(position).getMaterial().name())) continue;
67 | event.setCancelled(true);
68 | return;
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/protocol/ChunkLoadAdapter.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.protocol;
2 |
3 | import codes.kooper.blockify.Blockify;
4 | import codes.kooper.blockify.models.Stage;
5 | import codes.kooper.blockify.types.BlockifyChunk;
6 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract;
7 | import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent;
8 | import com.github.retrooper.packetevents.protocol.packettype.PacketType;
9 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData;
10 | import org.bukkit.Bukkit;
11 | import org.bukkit.entity.Player;
12 |
13 | import java.util.List;
14 |
15 | public class ChunkLoadAdapter extends SimplePacketListenerAbstract {
16 |
17 | @Override
18 | public void onPacketPlaySend(PacketPlaySendEvent event) {
19 | if (event.getPacketType() == PacketType.Play.Server.CHUNK_DATA) {
20 | Player player = (Player) event.getPlayer();
21 |
22 | // Wrapper for the chunk data packet
23 | WrapperPlayServerChunkData chunkData = new WrapperPlayServerChunkData(event);
24 | int chunkX = chunkData.getColumn().getX();
25 | int chunkZ = chunkData.getColumn().getZ();
26 |
27 | // Get the stages the player is in. If the player is not in any stages, return.
28 | List stages = Blockify.getInstance().getStageManager().getStages(player);
29 | if (stages == null || stages.isEmpty()) {
30 | return;
31 | }
32 |
33 | // Loop through the stages and views to check if the chunk is in the view.
34 | for (Stage stage : stages) {
35 |
36 | // If the chunk is not in the world, return.
37 | if (!stage.getWorld().equals(player.getWorld())) return;
38 |
39 | if (stage.getChunks().contains(new BlockifyChunk(chunkX, chunkZ))) {
40 | BlockifyChunk blockifyChunk = new BlockifyChunk(chunkX, chunkZ);
41 |
42 | // Cancel the packet to prevent the player from seeing the chunk
43 | event.setCancelled(true);
44 |
45 | // Send the chunk packet to the player
46 | Bukkit.getServer().getScheduler().runTaskAsynchronously(Blockify.getInstance(), () -> Blockify.getInstance().getBlockChangeManager().sendChunkPacket(stage, player, blockifyChunk, false));
47 | }
48 | }
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/types/BlockifyBlockStage.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.types;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 |
6 | @Getter
7 | @Setter
8 | public class BlockifyBlockStage {
9 | private final int entityId;
10 | private byte stage;
11 | private long lastUpdated;
12 | private int task = 0;
13 |
14 | /**
15 | * Stores data for a block stage
16 | * @param stage The stage of the block
17 | * @param lastUpdated The last time the block was updated
18 | */
19 | public BlockifyBlockStage(int entityId, byte stage, long lastUpdated) {
20 | this.entityId = entityId;
21 | this.stage = stage;
22 | this.lastUpdated = lastUpdated;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/types/BlockifyChunk.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.types;
2 |
3 | import org.bukkit.Chunk;
4 |
5 | /**
6 | * Simple class to represent a chunk.
7 | * @param x The x coordinate of the chunk.
8 | * @param z The z coordinate of the chunk.
9 | */
10 | public record BlockifyChunk(int x, int z) {
11 |
12 | /**
13 | * Stringifies the chunk data.
14 | *
15 | * @return The string representation of the chunk.
16 | */
17 | @Override
18 | public String toString() {
19 | return "BlockifyChunk{x=" + x + ", z=" + z + "}";
20 | }
21 |
22 | /**
23 | * Calculate the hash code based on the x and z coordinates.
24 | *
25 | * @return The hash code.
26 | */
27 | @Override
28 | public int hashCode() {
29 | return x * 31 + z;
30 | }
31 |
32 | /**
33 | * Get the chunk key.
34 | *
35 | * @return The chunk key.
36 | */
37 | public long getChunkKey() {
38 | return Chunk.getChunkKey(x, z);
39 | }
40 |
41 | /**
42 | * Check if the object is equal to this chunk.
43 | *
44 | * @param o The object to check.
45 | * @return True if the object is equal to this chunk, false otherwise.
46 | */
47 | @Override
48 | public boolean equals(Object o) {
49 | if (o == this) return true;
50 | if (!(o instanceof BlockifyChunk other)) return false;
51 | return this.x == other.x && this.z == other.z;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/types/BlockifyPosition.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.types;
2 |
3 | import io.papermc.paper.math.BlockPosition;
4 | import io.papermc.paper.math.Position;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 | import org.bukkit.Location;
8 | import org.bukkit.World;
9 | import org.bukkit.block.BlockState;
10 | import org.bukkit.block.data.BlockData;
11 | import org.bukkit.util.Vector;
12 |
13 | import java.util.Set;
14 | import java.util.stream.Collectors;
15 |
16 | @Getter
17 | @Setter
18 | public class BlockifyPosition {
19 | private int x, y, z;
20 |
21 | /**
22 | * Create a new BlockifyPosition
23 | *
24 | * @param x The x coordinate
25 | * @param y The y coordinate
26 | * @param z The z coordinate
27 | */
28 | public BlockifyPosition(int x, int y, int z) {
29 | this.x = x;
30 | this.y = y;
31 | this.z = z;
32 | }
33 |
34 | /**
35 | * Create a new BlockifyPosition
36 | *
37 | * @param location The location to create the BlockifyPosition from
38 | */
39 | public static BlockifyPosition fromLocation(Location location) {
40 | return new BlockifyPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ());
41 | }
42 |
43 | /**
44 | * Create a new BlockifyPosition
45 | *
46 | * @param vector The vector to create the BlockifyPosition from
47 | */
48 | public static BlockifyPosition fromVector(Vector vector) {
49 | return new BlockifyPosition(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ());
50 | }
51 |
52 | /**
53 | * Creates new BlockifyPositions
54 | *
55 | * @param locations The locations to create the BlockifyPosition from
56 | */
57 | public static Set fromLocations(Set locations) {
58 | return locations.stream().map(BlockifyPosition::fromLocation).collect(Collectors.toSet());
59 | }
60 |
61 | /**
62 | * Creates new BlockifyPositions
63 | *
64 | * @param blockPositions The block positions to create the BlockifyPosition from
65 | */
66 | public static Set fromPositions(Set blockPositions) {
67 | return blockPositions.stream().map(BlockifyPosition::fromPosition).collect(Collectors.toSet());
68 | }
69 |
70 | /**
71 | * Create a new BlockifyPosition
72 | *
73 | * @param position The position to create the BlockifyPosition from
74 | */
75 | public static BlockifyPosition fromPosition(Position position) {
76 | return new BlockifyPosition(position.blockX(), position.blockY(), position.blockZ());
77 | }
78 |
79 | /**
80 | * Converts the BlockifyPosition to a BlockPosition
81 | *
82 | * @return The BlockPosition representation of the BlockifyPosition
83 | */
84 | public BlockPosition toBlockPosition() {
85 | return Position.block(x, y, z);
86 | }
87 |
88 | /**
89 | * Converts the BlockifyPosition to a BlockifyChunk
90 | *
91 | * @return The BlockifyChunk at the BlockifyPosition.
92 | */
93 | public BlockifyChunk toBlockifyChunk() {
94 | return new BlockifyChunk(x >> 4, z >> 4);
95 | }
96 |
97 | /**
98 | * Converts the BlockifyPosition to a Location
99 | *
100 | * @param world The world to convert the BlockifyPosition to
101 | * @return The Location representation of the BlockifyPosition
102 | */
103 | public Location toLocation(World world) {
104 | return new Location(world, x, y, z);
105 | }
106 |
107 | /**
108 | * Converts the BlockifyPosition to a Position
109 | *
110 | * @return The Position representation of the BlockifyPosition
111 | */
112 | public Position toPosition() {
113 | return Position.block(x, y, z);
114 | }
115 |
116 | /**
117 | * Converts the BlockifyPosition to a Vector
118 | *
119 | * @return The Vector representation of the BlockifyPosition
120 | */
121 | public Vector toVector() {
122 | return new Vector(x, y, z);
123 | }
124 |
125 | /**
126 | * Get the distance squared between two BlockifyPositions
127 | *
128 | * @param other The other BlockifyPosition
129 | * @return The distance squared between the two BlockifyPositions
130 | */
131 | public double distanceSquared(BlockifyPosition other) {
132 | return Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2) + Math.pow(z - other.z, 2);
133 | }
134 |
135 | /**
136 | * Get the block state at the BlockifyPosition
137 | *
138 | * @param world The world to get the block state from
139 | * @return The block state at the BlockifyPosition
140 | */
141 | public BlockState getBlockState(World world, BlockData blockData) {
142 | BlockState state = toLocation(world).getBlock().getState().copy();
143 | state.setBlockData(blockData);
144 | state.setType(blockData.getMaterial());
145 | return state;
146 | }
147 |
148 | /**
149 | * Get the distance between two BlockifyPositions
150 | *
151 | * @param other The other BlockifyPosition
152 | * @return The distance between the two BlockifyPositions
153 | */
154 | public double distance(BlockifyPosition other) {
155 | return Math.sqrt(distanceSquared(other));
156 | }
157 |
158 | /**
159 | * Get the string representation of the BlockifyPosition
160 | *
161 | * @return The string representation of the BlockifyPosition
162 | */
163 | @Override
164 | public String toString() {
165 | return "BlockifyPosition{x=" + x + ", y=" + y + ", z=" + z + "}";
166 | }
167 |
168 | /**
169 | * Check if the BlockifyPosition is equal to another object
170 | *
171 | * @param o The object to compare to
172 | * @return Whether the BlockifyPosition is equal to the object
173 | */
174 | @Override
175 | public boolean equals(Object o) {
176 | if (o == this) return true;
177 | if (!(o instanceof BlockifyPosition other)) return false;
178 | return this.x == other.x && this.y == other.y && this.z == other.z;
179 | }
180 |
181 | /**
182 | * Get the hash code of the BlockifyPosition
183 | *
184 | * @return The hash code of the BlockifyPosition
185 | */
186 | @Override
187 | public int hashCode() {
188 | int result = x;
189 | result = 31 * result + y;
190 | result = 31 * result + z;
191 | return result;
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/utils/BlockUtils.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.utils;
2 |
3 | import codes.kooper.blockify.types.BlockifyPosition;
4 | import org.bukkit.Location;
5 | import org.bukkit.block.data.Ageable;
6 | import org.bukkit.block.data.BlockData;
7 |
8 | import java.util.ArrayList;
9 | import java.util.HashSet;
10 | import java.util.List;
11 | import java.util.Set;
12 |
13 | public class BlockUtils {
14 |
15 | /**
16 | * Get all the blocks between two positions.
17 | * Call this method asynchronously if you are going to be getting a large amount of blocks.
18 | *
19 | * @param pos1 The first position.
20 | * @param pos2 The second position.
21 | * @return A set of all the blocks between the two positions.
22 | */
23 | public static Set getBlocksBetween(BlockifyPosition pos1, BlockifyPosition pos2) {
24 | Set positions = new HashSet<>();
25 | int minX = Math.min(pos1.getX(), pos2.getX());
26 | int minY = Math.min(pos1.getY(), pos2.getY());
27 | int minZ = Math.min(pos1.getZ(), pos2.getZ());
28 | int maxX = Math.max(pos1.getX(), pos2.getX());
29 | int maxY = Math.max(pos1.getY(), pos2.getY());
30 | int maxZ = Math.max(pos1.getZ(), pos2.getZ());
31 | for (int x = minX; x <= maxX; x++) {
32 | for (int y = minY; y <= maxY; y++) {
33 | for (int z = minZ; z <= maxZ; z++) {
34 | positions.add(new BlockifyPosition(x, y, z));
35 | }
36 | }
37 | }
38 | return positions;
39 | }
40 |
41 | /**
42 | * Get all the locations between two locations.
43 | * Call this method asynchronously if you are going to be getting a large amount of locations.
44 | *
45 | * @param loc1 The first location.
46 | * @param loc2 The second location.
47 | * @return A list of all the locations between the two locations.
48 | */
49 | public static List getLocationsBetween(Location loc1, Location loc2) {
50 | List locations = new ArrayList<>();
51 | int minX = Math.min(loc1.getBlockX(), loc2.getBlockX());
52 | int minY = Math.min(loc1.getBlockY(), loc2.getBlockY());
53 | int minZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ());
54 | int maxX = Math.max(loc1.getBlockX(), loc2.getBlockX());
55 | int maxY = Math.max(loc1.getBlockY(), loc2.getBlockY());
56 | int maxZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ());
57 | for (int x = minX; x <= maxX; x++) {
58 | for (int y = minY; y <= maxY; y++) {
59 | for (int z = minZ; z <= maxZ; z++) {
60 | locations.add(new Location(loc1.getWorld(), x, y, z));
61 | }
62 | }
63 | }
64 | return locations;
65 | }
66 |
67 | /**
68 | * Set the age of a block.
69 | *
70 | * @param blockData The block data.
71 | * @param age The age to set.
72 | * @return The block data with the age set.
73 | */
74 | public static BlockData setAge(BlockData blockData, int age) {
75 | Ageable ageable = (Ageable) blockData;
76 | ageable.setAge(age);
77 | return ageable;
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/src/main/java/codes/kooper/blockify/utils/PositionKeyUtil.java:
--------------------------------------------------------------------------------
1 | package codes.kooper.blockify.utils;
2 |
3 | import codes.kooper.blockify.types.BlockifyPosition;
4 |
5 | public class PositionKeyUtil {
6 | // Bit masks and shifts
7 | private static final long X_MASK = 0x3FFFFFFL;
8 | private static final long Y_MASK = 0xFFFL;
9 | private static final long Z_MASK = 0x3FFFFFFL;
10 | private static final int X_SHIFT = 38;
11 | private static final int Y_SHIFT = 26;
12 |
13 | public static long getPositionKey(int x, int y, int z) {
14 | return ((x & X_MASK) << X_SHIFT)
15 | | ((y & Y_MASK) << Y_SHIFT)
16 | | (z & Z_MASK);
17 | }
18 |
19 | public static int getX(long positionKey) {
20 | return (int) ((positionKey >> X_SHIFT) & X_MASK);
21 | }
22 |
23 | public static int getY(long positionKey) {
24 | return (int) ((positionKey >> Y_SHIFT) & Y_MASK);
25 | }
26 |
27 | public static int getZ(long positionKey) {
28 | return (int) (positionKey & Z_MASK);
29 | }
30 |
31 | public static BlockifyPosition toBlockifyPosition(long positionKey) {
32 | return new BlockifyPosition(getX(positionKey), getY(positionKey), getZ(positionKey));
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/resources/plugin.yml:
--------------------------------------------------------------------------------
1 | name: Blockify
2 | version: '1.1.8-beta'
3 | main: codes.kooper.blockify.Blockify
4 | api-version: '1.20'
5 | depend:
6 | - packetevents
--------------------------------------------------------------------------------