-
-
Notifications
You must be signed in to change notification settings - Fork 146
Expand file tree
/
Copy pathUtil.java
More file actions
1610 lines (1486 loc) · 67.6 KB
/
Copy pathUtil.java
File metadata and controls
1610 lines (1486 loc) · 67.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package world.bentobox.bentobox.util;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import com.google.common.base.Enums;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.World.Environment;
import org.bukkit.attribute.Attribute;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Allay;
import org.bukkit.entity.Animals;
import org.bukkit.entity.Bat;
import org.bukkit.entity.EnderDragon;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Flying;
import org.bukkit.entity.IronGolem;
import org.bukkit.entity.Monster;
import org.bukkit.entity.Player;
import org.bukkit.entity.PufferFish;
import org.bukkit.entity.Shulker;
import org.bukkit.entity.Slime;
import org.bukkit.entity.Snowman;
import org.bukkit.entity.Tameable;
import org.bukkit.entity.WaterMob;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.KeybindComponent;
import net.kyori.adventure.text.ScoreComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.nms.AbstractMetaData;
import world.bentobox.bentobox.nms.GetMetaData;
import world.bentobox.bentobox.nms.PasteHandler;
import world.bentobox.bentobox.nms.PasteHandlerImpl;
import world.bentobox.bentobox.nms.WorldRegenerator;
import world.bentobox.bentobox.nms.WorldRegeneratorImpl;
/**
* A set of utility methods
*
* @author tastybento
* @author Poslovitch
*/
public class Util {
/**
* The section sign character used for legacy color codes, replacing ChatColor.COLOR_CHAR.
*/
private static final String COLOR_CHAR = "\u00A7";
/**
* The Sulfur Cube entity type (Minecraft 26.2), resolved at runtime so the code still
* compiles against earlier API versions where the constant does not exist. {@code null}
* when absent, in which case the {@code ==} comparisons against it are simply false.
*/
private static final EntityType SULFUR_CUBE = Enums.getIfPresent(EntityType.class, "SULFUR_CUBE")
.orNull();
/**
* Use standard color code definition: {@code &<hex>}.
*/
private static final Pattern HEX_PATTERN = Pattern.compile("&#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})");
/**
* Pattern to detect legacy color codes (ampersand or section sign followed by a color/format character).
*/
private static final Pattern LEGACY_CODE_PATTERN = Pattern.compile("[&\u00A7][0-9a-fk-orA-FK-OR]");
/**
* Pattern to detect legacy hex color codes like {@code &#RRGGBB} or {@code §x§R§R...}.
*/
private static final Pattern LEGACY_HEX_CODE_PATTERN = Pattern.compile("&#[0-9a-fA-F]{3,6}|\u00A7x(\u00A7[0-9a-fA-F]){6}");
/**
* Pattern to match the BungeeCord/Spigot {@code &x&R&R&G&G&B&B} hex format
* (after {@code §} has been normalised to {@code &}).
* Produced by {@link LegacyComponentSerializer} with {@code useUnusualXRepeatedCharacterHexFormat()}.
*/
private static final Pattern BUNGEE_HEX_PATTERN = Pattern.compile("&x(&[0-9a-fA-F]){6}");
/**
* MiniMessage instance for parsing MiniMessage-formatted strings.
*/
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
/**
* Serializer for converting Components to plain text (no formatting).
*/
private static final PlainTextComponentSerializer PLAIN_SERIALIZER = PlainTextComponentSerializer.plainText();
/**
* Map of legacy color code characters to their MiniMessage tag equivalents.
*/
private static final java.util.Map<Character, String> LEGACY_TO_MM_MAP = java.util.Map.ofEntries(
java.util.Map.entry('0', "black"),
java.util.Map.entry('1', "dark_blue"),
java.util.Map.entry('2', "dark_green"),
java.util.Map.entry('3', "dark_aqua"),
java.util.Map.entry('4', "dark_red"),
java.util.Map.entry('5', "dark_purple"),
java.util.Map.entry('6', "gold"),
java.util.Map.entry('7', "gray"),
java.util.Map.entry('8', "dark_gray"),
java.util.Map.entry('9', "blue"),
java.util.Map.entry('a', "green"),
java.util.Map.entry('b', "aqua"),
java.util.Map.entry('c', "red"),
java.util.Map.entry('d', "light_purple"),
java.util.Map.entry('e', "yellow"),
java.util.Map.entry('f', "white"),
java.util.Map.entry('k', "obfuscated"),
java.util.Map.entry('l', "bold"),
java.util.Map.entry('m', "strikethrough"),
java.util.Map.entry('n', "underlined"),
java.util.Map.entry('o', "italic"),
java.util.Map.entry('r', "reset")
);
/**
* Pattern to match inline command bracket syntax used in sendRawMessage.
*/
private static final Pattern INLINE_CMD_PATTERN = Pattern.compile("\\[(run_command|suggest_command|copy_to_clipboard|open_url|hover): ([^\\]]+)]", Pattern.CASE_INSENSITIVE);
private static final String NETHER = "_nether";
private static final String THE_END = "_the_end";
private static final String SNAPSHOT = "-SNAPSHOT";
private static final String SERVER_VERSION = Bukkit.getMinecraftVersion();
private static String serverVersion = null;
private static BentoBox plugin = BentoBox.getInstance();
private static PasteHandler pasteHandler = null;
private static WorldRegenerator regenerator = null;
private static GetMetaData metaData;
private Util() {}
/**
* Used for testing only
*/
public static void setPlugin(BentoBox p) {
plugin = p;
}
/**
* Returns the server version
* @return server version
*/
public static String getServerVersion() {
if (serverVersion == null) {
String serverPackageName = Bukkit.getServer().getClass().getPackage().getName();
serverVersion = serverPackageName.substring(serverPackageName.lastIndexOf('.') + 1);
}
return serverVersion;
}
/**
* This returns the coordinate of where an island should be on the grid.
*
* @param location - the location location to query
* @return Location of closest island
*/
public static Location getClosestIsland(Location location) {
int dist = plugin.getIWM().getIslandDistance(location.getWorld()) * 2;
long x = Math.round((double) location.getBlockX() / dist) * dist + plugin.getIWM().getIslandXOffset(location.getWorld());
long z = Math.round((double) location.getBlockZ() / dist) * dist + plugin.getIWM().getIslandZOffset(location.getWorld());
int y = plugin.getIWM().getIslandHeight(location.getWorld());
return new Location(location.getWorld(), x, y, z);
}
/**
* Converts a serialized location to a Location. Returns null if string is
* empty
*
* @param s - serialized location in format "world:x:y:z:y:p"
* @return Location
*/
public static Location getLocationString(final String s) {
if (s == null || s.trim().isEmpty()) {
return null;
}
final String[] parts = s.split(":");
if (parts.length == 6) {
final World w = Bukkit.getWorld(parts[0]);
if (w == null) {
return null;
}
// Parse string as double just in case
int x = (int) Double.parseDouble(parts[1]);
int y = (int) Double.parseDouble(parts[2]);
int z = (int) Double.parseDouble(parts[3]);
final float yaw = Float.intBitsToFloat(Integer.parseInt(parts[4]));
final float pitch = Float.intBitsToFloat(Integer.parseInt(parts[5]));
return new Location(w, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
return null;
}
/**
* Converts a location to a simple string representation
* If location is null, returns empty string
* Only stores block ints. Inverse function returns block centers
*
* @param l - the location
* @return String of location in format "world:x:y:z:y:p"
*/
public static String getStringLocation(final Location l) {
if (l == null || l.getWorld() == null) {
return "";
}
return l.getWorld().getName() + ":" + l.getBlockX() + ":" + l.getBlockY() + ":" + l.getBlockZ() + ":" + Float.floatToIntBits(l.getYaw()) + ":" + Float.floatToIntBits(l.getPitch());
}
/**
* Converts a name like IRON_INGOT into Iron Ingot to improve readability
*
* @param ugly
* The string such as IRON_INGOT
* @return A nicer version, such as Iron Ingot
*
* Credits to mikenon on GitHub!
*/
public static String prettifyText(String ugly) {
StringBuilder fin = new StringBuilder();
ugly = ugly.toLowerCase(java.util.Locale.ENGLISH);
if (ugly.contains("_")) {
String[] splt = ugly.split("_");
int i = 0;
for (String s : splt) {
i += 1;
fin.append(Character.toUpperCase(s.charAt(0))).append(s.substring(1));
if (i < splt.length) {
fin.append(" ");
}
}
} else {
fin.append(Character.toUpperCase(ugly.charAt(0))).append(ugly.substring(1));
}
return fin.toString();
}
/**
* Return an immutable list of online players this player can see, i.e. are not invisible
* @param user - the User - if null, all player names on the server are shown
* @return a list of online players this player can see
*/
public static List<String> getOnlinePlayerList(User user) {
if (user == null || !user.isPlayer()) {
// Console and null get to see every player
return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList();
}
// Otherwise prevent invisible players from seeing
return Bukkit.getOnlinePlayers().stream().filter(p -> user.getPlayer().canSee(p)).map(Player::getName).toList();
}
/**
* Returns all of the items that begin with the given start,
* ignoring case. Intended for tabcompletion.
*
* @param list - string list
* @param start - first few chars of a string
* @return List of items that start with the letters
*/
public static List<String> tabLimit(final List<String> list, final String start) {
final List<String> returned = new ArrayList<>();
for (String s : list) {
if (s == null) {
continue;
}
if (s.toLowerCase(java.util.Locale.ENGLISH).startsWith(start.toLowerCase(java.util.Locale.ENGLISH))) {
returned.add(s);
}
}
return returned;
}
public static String xyz(Vector location) {
return location.getBlockX() + "," + location.getBlockY() + "," + location.getBlockZ();
}
/**
* Checks is world = world2 irrespective of the world type. Only strips _nether and _the_end from world name.
* @param world - world
* @param world2 - world
* @return true if the same
*/
public static boolean sameWorld(World world, World world2) {
return stripName(world).equals(stripName(world2));
}
private static String stripName(World world) {
if (world.getName().endsWith(NETHER)) {
return world.getName().substring(0, world.getName().length() - NETHER.length());
}
if (world.getName().endsWith(THE_END)) {
return world.getName().substring(0, world.getName().length() - THE_END.length());
}
return world.getName();
}
/**
* Convert world to an overworld
* @param world - world
* @return over world or null if world is null or a world cannot be found
*/
@Nullable
public static World getWorld(@Nullable World world) {
if (world == null) {
return null;
}
return world.getEnvironment().equals(Environment.NORMAL) ? world : Bukkit.getWorld(world.getName().replace(NETHER, "").replace(THE_END, ""));
}
/**
* Lists files found in the jar in the folderPath with the suffix given
* @param jar - the jar file
* @param folderPath - the path within the jar
* @param suffix - the suffix required
* @return a list of files
*/
public static List<String> listJarFiles(JarFile jar, String folderPath, String suffix) {
List<String> result = new ArrayList<>();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String path = entry.getName();
if (!path.startsWith(folderPath)) {
continue;
}
if (entry.getName().endsWith(suffix)) {
result.add(entry.getName());
}
}
return result;
}
/**
* Converts block face direction to radial degrees. Returns 0 if block face
* is not radial.
*
* @param face - blockface
* @return degrees
*/
public static float blockFaceToFloat(BlockFace face) {
return switch (face) {
case EAST -> 90F;
case EAST_NORTH_EAST -> 67.5F;
case NORTH_EAST -> 45F;
case NORTH_NORTH_EAST -> 22.5F;
case NORTH_NORTH_WEST -> 337.5F;
case NORTH_WEST -> 315F;
case SOUTH -> 180F;
case SOUTH_EAST -> 135F;
case SOUTH_SOUTH_EAST -> 157.5F;
case SOUTH_SOUTH_WEST -> 202.5F;
case SOUTH_WEST -> 225F;
case WEST -> 270F;
case WEST_NORTH_WEST -> 292.5F;
case WEST_SOUTH_WEST -> 247.5F;
default -> 0F;
};
}
/**
* Returns a Date instance corresponding to the input, or null if the input could not be parsed.
* @param gitHubDate the input to parse
* @return the Date instance following a {@code yyyy-MM-dd HH:mm:ss} format, or {@code null}.
* @since 1.3.0
*/
@Nullable
public static Date parseGitHubDate(@NonNull String gitHubDate) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return format.parse(gitHubDate.replace('T', ' ').replace("Z", ""));
} catch (ParseException e) {
return null;
}
}
/**
* Returns whether this entity is naturally hostile towards the player or not.
* @param entity the entity to check.
* @return {@code true} if this entity is hostile, {@code false} otherwise.
* @since 1.4.0
*/
public static boolean isHostileEntity(Entity entity) {
// MagmaCube extends Slime
// Slime extends Mob
// Ghast and Phantom extends Flying
// Flying extends Mob
// Shulker is Golem, but other Golems cannot be added here.
// EnderDragon extends LivingEntity
// Most of hostile mobs extends Monster.
// PufferFish is a unique fix.
// Sulfur Cube (26.2) is slime-like but passive, so it must not be treated as hostile
// even if it implements Slime at runtime.
return (entity instanceof Monster || entity instanceof Flying || entity instanceof Slime ||
entity instanceof Shulker || entity instanceof EnderDragon || entity instanceof PufferFish)
&& (SULFUR_CUBE == null || entity.getType() != SULFUR_CUBE);
}
/**
* Returns whether this entity is naturally passive towards the player or not.
* This means that this entity normally won't hurt the player.
* @param entity the entity to check.
* @return {@code true} if this entity is passive, {@code false} otherwise.
* @since 1.4.0
*/
public static boolean isPassiveEntity(Entity entity) {
if (entity == null || entity.getType() == null) {
return true;
}
// Check built-in class hierarchy for common passive mobs
boolean isPassiveByClass = entity instanceof Animals
|| entity instanceof IronGolem
|| entity instanceof Snowman
|| entity instanceof Bat
|| entity instanceof Allay;
// Check WaterMob hierarchy, excluding PufferFish (hostile)
boolean isPassiveWaterMob = entity instanceof WaterMob && !(entity instanceof PufferFish);
// Check for newer entity types by their enum name (String comparison is safe across versions)
boolean isCopperGolem = entity.getType().name().equals("COPPER_GOLEM");
// And the sniffer
boolean isSniffer = entity.getType().name().equals("SNIFFER");
// And the Sulfur Cube (26.2): slime-like but passive
boolean isSulfurCube = SULFUR_CUBE != null && entity.getType() == SULFUR_CUBE;
return isPassiveByClass || isPassiveWaterMob || isCopperGolem || isSniffer || isSulfurCube;
}
public static boolean isTamableEntity(Entity entity) {
return entity instanceof Tameable tameable && tameable.isTamed();
}
/*
* PaperLib methods for addons to call
*/
/**
* Teleports an Entity to the target location, loading the chunk asynchronously first if needed.
* @param entity The Entity to teleport
* @param location The Location to Teleport to
* @return Future that completes with the result of the teleport
*/
@NonNull
public static CompletableFuture<Boolean> teleportAsync(@Nonnull Entity entity, @Nonnull Location location) {
return teleportAsync(entity, location, TeleportCause.PLUGIN);
}
/**
* Teleports an Entity to the target location, loading the chunk asynchronously first if needed.
* @param entity The Entity to teleport
* @param location The Location to Teleport to
* @param cause The cause for the teleportation
* @return Future that completes with the result of the teleport
*/
@SuppressWarnings("unchecked")
@NonNull
public static CompletableFuture<Boolean> teleportAsync(@Nonnull Entity entity, @Nonnull Location location,
TeleportCause cause) {
try {
// Use reflection to check if the method exists
Method method = Entity.class.getMethod("teleportAsync", Location.class, TeleportCause.class);
if (method != null) {
// Invoke the method using reflection on the entity instance
return (CompletableFuture<Boolean>) method.invoke(entity, location, cause);
}
} catch (NoSuchMethodException e) {
// Method does not exist, fallback to Spigot behavior
} catch (Exception e) {
plugin.logStacktrace(e); // Report other exceptions
}
// Fallback for Spigot servers
entity.teleport(location, cause);
return CompletableFuture.completedFuture(true);
}
/**
* Gets the chunk at the target location, loading it asynchronously if needed.
* @param loc Location to get chunk for
* @return Future that completes with the chunk
*/
@NonNull
public static CompletableFuture<Chunk> getChunkAtAsync(@NonNull Location loc) {
return getChunkAtAsync(Objects.requireNonNull(loc.getWorld()), loc.getBlockX() >> 4, loc.getBlockZ() >> 4,
true);
}
/**
* Gets the chunk at the target location, loading it asynchronously if needed.
* @param loc Location to get chunk for
* @param gen Should the chunk generate or not. Only respected on some MC versions, 1.13 for CB, 1.12 for Paper
* @return Future that completes with the chunk, or null if the chunk did not exists and generation was not requested.
*/
@NonNull
public static CompletableFuture<Chunk> getChunkAtAsync(@NonNull Location loc, boolean gen) {
return getChunkAtAsync(Objects.requireNonNull(loc.getWorld()), loc.getBlockX() >> 4, loc.getBlockZ() >> 4, gen);
}
/**
* Gets the chunk at the target location, loading it asynchronously if needed.
* @param world World to load chunk for
* @param x X coordinate of the chunk to load
* @param z Z coordinate of the chunk to load
* @return Future that completes with the chunk
*/
@NonNull
public static CompletableFuture<Chunk> getChunkAtAsync(@Nonnull World world, int x, int z) {
return getChunkAtAsync(world, x, z, true);
}
/**
* Gets the chunk at the target location, loading it asynchronously if needed.
* @param world World to load chunk for
* @param x X coordinate of the chunk to load
* @param z Z coordinate of the chunk to load
* @param gen Should the chunk generate or not. Only respected on some MC versions, 1.13 for CB, 1.12 for Paper
* @return Future that completes with the chunk, or null if the chunk did not exists and generation was not requested.
*/
@SuppressWarnings("unchecked")
@NonNull
public static CompletableFuture<Chunk> getChunkAtAsync(@Nonnull World world, int x, int z, boolean gen) {
try {
// Use reflection to check if the method exists
Method method = World.class.getMethod("getChunkAtAsync", int.class, int.class, boolean.class);
if (method != null) {
// Invoke the method using reflection
return (CompletableFuture<Chunk>) method.invoke(world, x, z, gen);
}
} catch (NoSuchMethodException e) {
// Method does not exist, fallback to default behavior
} catch (Exception e) {
BentoBox.getInstance().logStacktrace(e);
}
// Fallback
return CompletableFuture.completedFuture(world.getChunkAt(x, z, gen));
}
/**
* Checks if the chunk has been generated or not. Only works on Paper 1.12+ or any 1.13.1+ version
* @param loc Location to check if the chunk is generated
* @return If the chunk is generated or not
*/
public static boolean isChunkGenerated(@NonNull Location loc) {
return isChunkGenerated(Objects.requireNonNull(loc.getWorld()), loc.getBlockX() >> 4, loc.getBlockZ() >> 4);
}
/**
* Checks if the chunk has been generated or not. Only works on Paper 1.12+ or any 1.13.1+ version
* @param world World to check for
* @param x X coordinate of the chunk to check
* @param z Z coordinate of the chunk to checl
* @return If the chunk is generated or not
*/
public static boolean isChunkGenerated(@Nonnull World world, int x, int z) {
return world.isChunkGenerated(x, z);
}
/**
* Checks if the given version is compatible with the required version.
*
* <p>
* A version is considered compatible if:
* <ul>
* <li>The major, minor, and patch components of the given version are greater than or equal to those of the required version.</li>
* <li>If the numeric components are equal, the absence of "-SNAPSHOT" in the given version takes precedence (i.e., release versions are considered more compatible than SNAPSHOT versions).</li>
* </ul>
* </p>
*
* @param version the version to check, in the format "major.minor.patch[-SNAPSHOT]".
* @param requiredVersion the required version, in the format "major.minor.patch[-SNAPSHOT]".
* @return {@code true} if the given version is compatible with the required version; {@code false} otherwise.
*
* <p>
* Examples:
* <ul>
* <li>{@code isVersionCompatible("2.1.0", "2.0.0-SNAPSHOT")} returns {@code true}</li>
* <li>{@code isVersionCompatible("2.0.0", "2.0.0-SNAPSHOT")} returns {@code true}</li>
* <li>{@code isVersionCompatible("2.0.0-SNAPSHOT", "2.0.0")} returns {@code false}</li>
* <li>{@code isVersionCompatible("1.9.9", "2.0.0-SNAPSHOT")} returns {@code false}</li>
* </ul>
* </p>
*/
public static boolean isVersionCompatible(String version, String requiredVersion) {
String[] versionParts = version.replace(SNAPSHOT, "").split("\\.");
String[] requiredVersionParts = requiredVersion.replace(SNAPSHOT, "").split("\\.");
for (int i = 0; i < Math.max(versionParts.length, requiredVersionParts.length); i++) {
int vPart = i < versionParts.length ? Integer.parseInt(versionParts[i]) : 0;
int rPart = i < requiredVersionParts.length ? Integer.parseInt(requiredVersionParts[i]) : 0;
if (vPart > rPart) {
return true;
} else if (vPart < rPart) {
return false;
}
}
// If numeric parts are equal, prioritize SNAPSHOT as lower precedence
boolean isVersionSnapshot = version.contains(SNAPSHOT);
boolean isRequiredSnapshot = requiredVersion.contains(SNAPSHOT);
// If required version is a full release but current version is SNAPSHOT, it's incompatible
return isRequiredSnapshot || !isVersionSnapshot;
}
/**
* Check if the server has access to the Paper API
* @return True for Paper environments
*/
public static boolean isPaper() {
try {
Class.forName("com.destroystokyo.paper.PaperConfig");
return true; // Paper-specific class exists
} catch (ClassNotFoundException e) {
return false; // Not a Paper server
}
}
/**
* This method translates color codes in given string and strips whitespace after them.
* This code parses both: hex and old color codes.
* Multi-line strings are processed line by line to ensure each line retains its own
* color codes, since Adventure's LegacyComponentSerializer may omit repeated color
* codes for consecutive segments of the same color.
* @param textToColor Text which color codes must be parsed.
* @return String text with parsed colors and stripped whitespaces after them.
* @deprecated Use {@link #parseMiniMessageOrLegacy(String)} for Component output,
* or {@link #componentToLegacy(Component)} if a legacy string is needed.
*/
@Deprecated(since = "3.2.0")
@NonNull
public static String translateColorCodes(@NonNull String textToColor) {
// Process each line independently so color codes are not lost at line boundaries.
// Adventure's LegacyComponentSerializer omits repeated §X codes when consecutive
// components share the same color, causing lines 2+ to lose their color when split.
if (textToColor.contains("\n")) {
return Arrays.stream(textToColor.split("\n", -1))
.map(Util::translateColorCodes)
.collect(Collectors.joining("\n"));
}
// Use matcher to find hex patterns in given text.
Matcher matcher = HEX_PATTERN.matcher(textToColor);
// Increase buffer size by 32 like it is in bungee cord api. Use buffer because it is sync.
StringBuilder buffer = new StringBuilder(textToColor.length() + 32);
while (matcher.find()) {
String group = matcher.group(1);
if (group.length() == 6) {
// 6-digit hex: keep as &#RRGGBB for LEGACY_SERIALIZER to handle natively.
matcher.appendReplacement(buffer, "&#" + group);
} else {
// 3-digit hex: expand to 6-digit &#RRGGBB format (e.g., &#fff -> &#ffffff).
matcher.appendReplacement(buffer, "&#"
+ group.charAt(0) + group.charAt(0)
+ group.charAt(1) + group.charAt(1)
+ group.charAt(2) + group.charAt(2));
}
}
// Use Adventure's LegacyComponentSerializer to translate '&' color codes
// then serialize back with section sign, and strip spaces after color codes.
String withHexCodes = matcher.appendTail(buffer).toString();
String translated = SECTION_SERIALIZER.serialize(LEGACY_SERIALIZER.deserialize(withHexCodes));
return Util.stripSpaceAfterColorCodes(translated);
}
/**
* Strips spaces immediately after color codes. Used by {@link User#getTranslation(String, String...)}.
* @param textToStrip - text to strip
* @return text with spaces after color codes removed
* @since 1.9.0
* @deprecated No longer needed with MiniMessage format. Legacy locale files had spaces
* after color codes as a translation tool hack; MiniMessage tags don't need this.
*/
@Deprecated(since = "3.2.0")
@NonNull
public static String stripSpaceAfterColorCodes(String textToStrip) {
if (textToStrip == null) return "";
// The legacy locale hack of writing "&c Hello" (with an intentional space so
// primitive auto-translators wouldn't glue "&c" onto "Hello") only applies when
// the §X code appears at a boundary — start of string, after whitespace, or
// immediately after another §X code. Mid-text codes (e.g. "Page §e1§7 of §e4"
// where §7 is preceded by a digit) must NOT strip the following space, because
// that space is content, not the hack. Also skip §r (reset) in all cases.
// See https://github.com/BentoBoxWorld/AOneBlock/issues/495.
textToStrip = textToStrip.replaceAll("(?<=^|\\s|\u00A7.)(\u00A7[^rR])\\s", "$1");
return textToStrip;
}
/**
* Returns whether the input is an integer or not.
* @param nbr the input.
* @param parse whether the input should be checked to ensure it can be parsed as an Integer without throwing an exception.
* @return {@code true} if the input is an integer, {@code false} otherwise.
* @since 1.10.0
*/
public static boolean isInteger(@NonNull String nbr, boolean parse) {
// Original code from Jonas Klemming on StackOverflow (https://stackoverflow.com/q/237159).
// I slightly refined it to catch more edge cases.
// It is a faster alternative to catch malformed strings than the NumberFormatException.
int length = nbr.length();
if (length == 0) {
return false;
}
int i = 0;
if (nbr.charAt(0) == '-' || nbr.charAt(0) == '+') {
if (length == 1) {
return false;
}
i = 1;
}
boolean trailingDot = false;
for (; i < length; i++) {
char c = nbr.charAt(i);
if (trailingDot && c != '0') {
// We only accept 0's after a trailing dot.
return false;
}
if (c == '.') {
if (i == length - 1) {
// We're at the end of the integer, so it's okay
return true;
} else {
// we will need to make sure there is nothing else but 0's after the dot.
trailingDot = true;
}
} else if (!trailingDot && (c < '0' || c > '9')) {
return false;
}
}
// these tests above should have caught most likely issues
// We now need to make sure parsing the input as an Integer won't cause issues
if (parse) {
try {
Integer.parseInt(nbr); // NOSONAR we don't care about the result of this operation
return true;
} catch (NumberFormatException e) {
return false;
}
}
// Everything's green!
return true;
}
/**
* Get a UUID from a string. The string can be a known player's name or a UUID
* @param nameOrUUID - name or UUID
* @return UUID or null if unknown
* @since 1.13.0
*/
@Nullable
public static UUID getUUID(@NonNull String nameOrUUID) {
UUID targetUUID = plugin.getPlayers().getUUID(nameOrUUID);
if (targetUUID != null) return targetUUID;
// Check if UUID is being used
try {
return UUID.fromString(nameOrUUID);
} catch (Exception e) {
// Do nothing
}
return null;
}
/**
* Run a list of commands for a user
* @param user - user affected by the commands
* @param commands - a list of commands
* @param commandType - the type of command being run - used in the console error message
*/
public static void runCommands(User user, @NonNull List<String> commands, String commandType) {
runCommands(user, user.getName(), commands, commandType);
}
/**
* Run a list of commands for a user
* @param user - user affected by the commands
* @param ownerName - name of the island owner, or the user's name if it is the user's island
* @param commands - a list of commands
* @param commandType - the type of command being run - used in the console error message
* @since 1.22.0
*/
public static void runCommands(User user, String ownerName, @NonNull List<String> commands, String commandType) {
commands.forEach(command -> {
command = command.replace("[player]", user.getName());
command = command.replace("[owner]", ownerName);
if (command.startsWith("[SUDO]")) {
// Execute the command by the player
if (!user.isOnline() || !user.performCommand(command.substring(6))) {
plugin.logError("Could not execute " + commandType + " command for " + user.getName() + ": " + command.substring(6));
}
} else {
// Otherwise execute as the server console
if (!Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command)) {
plugin.logError("Could not execute " + commandType + " command as console: " + command);
}
}
});
}
/**
* Resets the player's heath to maximum
* @param player - player
*/
public static void resetHealth(Player player) {
try {
// Paper
double maxHealth = player.getAttribute(Attribute.MAX_HEALTH).getBaseValue();
player.setHealth(maxHealth);
} catch (Exception e) {
// Spigot
player.setHealth(20D);
}
}
/**
* Set the regenerator the plugin will use
* @param regenerator the regenerator
*/
public static void setRegenerator(WorldRegenerator regenerator) {
Util.regenerator = regenerator;
}
/**
* Get metadata decoder
* @return an accelerated metadata class for this server
*/
public static AbstractMetaData getMetaData() {
if (metaData == null) {
metaData = new GetMetaData();
}
return metaData;
}
/**
* Get the regenerator the plugin will use
* @return an accelerated regenerator class for this server
*/
public static WorldRegenerator getRegenerator() {
if (regenerator == null) {
regenerator = new WorldRegeneratorImpl();
}
return regenerator;
}
/**
* Checks what version the server is running and picks the appropriate NMS handler, or fallback
* @return PasteHandler
*/
public static PasteHandler getPasteHandler() {
if (pasteHandler == null) {
pasteHandler = new PasteHandlerImpl();
}
return pasteHandler;
}
/**
* Broadcast a localized message to all players with the permission {@link Server#BROADCAST_CHANNEL_USERS}
*
* @param localeKey locale key for the message to broadcast
* @param variables any variables for the message
* @return number of message recipients
*/
public static int broadcast(String localeKey, String... variables) {
int count = 0;
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission(Server.BROADCAST_CHANNEL_USERS)) {
User.getInstance(p).sendMessage(localeKey, variables);
count++;
}
}
return count;
}
/**
* This method removes all special characters that are not allowed in filenames (windows).
* It also includes any white-spaces, as for some reason, I do like it more without them.
* Also, all cases are lower cased for easier blueprint mapping.
* @param input Input that need to be sanitized.
* @return A sanitized input without illegal characters in names.
*/
public static String sanitizeInput(String input)
{
return Util.stripColor(
Util.translateColorCodes(input.replaceAll("[\\\\/:*?\"<>|\s]", "_"))).
toLowerCase();
}
/**
* Attempts to find the first matching enum constant from an array of possible string representations.
* This method sequentially checks each string against the enum constants of the specified enum class
* by normalizing the string values to uppercase before comparison, enhancing the likelihood of a match
* if the enum constants are defined in uppercase.
*
* @param enumClass the Class object of the enum type to be checked against
* @param values an array of string values which are potential matches for the enum constants
* @param <T> the type parameter of the enum
* @return the first matching enum constant if a match is found; otherwise, returns null
*/
public static <T extends Enum<T>> T findFirstMatchingEnum(Class<T> enumClass, String... values) {
if (enumClass == null || values == null) {
return null;
}
for (String value : values) {
Optional<T> enumConstant = Arrays.stream(enumClass.getEnumConstants()).filter(e -> e.name().equals(value.toUpperCase())).findFirst();
if (enumConstant.isPresent()) {
return enumConstant.get();
}
}
return null; // Return null if no match is found
}
/**
* This checks the stack trace for @Test to determine if a test is calling the code and skips.
* @return true if it's a test.
*/
public static boolean inTest() {
return Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch(e -> e.getClassName().endsWith("Test"));
}
/**
* Strings old-style §-based color codes (used by ChatColor) from text
* @param input text with color codes
* @return unformatted text
*/
public static String stripColor(String input) {
return input.replaceAll("(?i)§[0-9A-FK-ORX]", ""); // Use regex because it's fast and reliable
}
/**
* Simple utility method to check if the server version is at least the target version.
*/
public static boolean isVersionAtLeast(String targetVersion) {
// Simple string comparison may be sufficient for minor versions,
// but a proper numeric check is safer for major releases.
try {
// Get major, minor, patch versions
String[] currentParts = SERVER_VERSION.split("\\.");
String[] targetParts = targetVersion.split("\\.");
for (int i = 0; i < targetParts.length; i++) {
int current = (i < currentParts.length) ? Integer.parseInt(currentParts[i]) : 0;
int target = Integer.parseInt(targetParts[i]);
if (current > target) return true;
if (current < target) return false;