001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.player; 020 021import com.google.common.base.Objects; 022import com.google.common.base.Preconditions; 023import com.google.common.primitives.Ints; 024import com.plotsquared.core.PlotSquared; 025import com.plotsquared.core.collection.ByteArrayUtilities; 026import com.plotsquared.core.command.CommandCaller; 027import com.plotsquared.core.command.RequiredType; 028import com.plotsquared.core.configuration.Settings; 029import com.plotsquared.core.configuration.caption.Caption; 030import com.plotsquared.core.configuration.caption.CaptionMap; 031import com.plotsquared.core.configuration.caption.CaptionUtility; 032import com.plotsquared.core.configuration.caption.LocaleHolder; 033import com.plotsquared.core.configuration.caption.TranslatableCaption; 034import com.plotsquared.core.database.DBFunc; 035import com.plotsquared.core.events.TeleportCause; 036import com.plotsquared.core.location.Location; 037import com.plotsquared.core.permissions.NullPermissionProfile; 038import com.plotsquared.core.permissions.PermissionHandler; 039import com.plotsquared.core.permissions.PermissionProfile; 040import com.plotsquared.core.plot.Plot; 041import com.plotsquared.core.plot.PlotArea; 042import com.plotsquared.core.plot.PlotCluster; 043import com.plotsquared.core.plot.PlotId; 044import com.plotsquared.core.plot.PlotWeather; 045import com.plotsquared.core.plot.flag.implementations.DoneFlag; 046import com.plotsquared.core.plot.world.PlotAreaManager; 047import com.plotsquared.core.plot.world.SinglePlotArea; 048import com.plotsquared.core.plot.world.SinglePlotAreaManager; 049import com.plotsquared.core.synchronization.LockRepository; 050import com.plotsquared.core.util.EventDispatcher; 051import com.plotsquared.core.util.query.PlotQuery; 052import com.plotsquared.core.util.task.RunnableVal; 053import com.plotsquared.core.util.task.TaskManager; 054import com.sk89q.worldedit.extension.platform.Actor; 055import com.sk89q.worldedit.world.gamemode.GameMode; 056import com.sk89q.worldedit.world.item.ItemType; 057import net.kyori.adventure.audience.Audience; 058import net.kyori.adventure.text.Component; 059import net.kyori.adventure.text.minimessage.MiniMessage; 060import net.kyori.adventure.text.minimessage.tag.Tag; 061import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 062import net.kyori.adventure.title.Title; 063import org.apache.logging.log4j.LogManager; 064import org.apache.logging.log4j.Logger; 065import org.checkerframework.checker.nullness.qual.NonNull; 066import org.checkerframework.checker.nullness.qual.Nullable; 067 068import java.nio.ByteBuffer; 069import java.time.Duration; 070import java.time.temporal.ChronoUnit; 071import java.util.ArrayDeque; 072import java.util.Arrays; 073import java.util.Collection; 074import java.util.Collections; 075import java.util.HashMap; 076import java.util.HashSet; 077import java.util.LinkedList; 078import java.util.Locale; 079import java.util.Map; 080import java.util.Queue; 081import java.util.Set; 082import java.util.UUID; 083import java.util.concurrent.CompletableFuture; 084import java.util.concurrent.ConcurrentHashMap; 085import java.util.concurrent.atomic.AtomicInteger; 086 087/** 088 * The abstract class supporting {@code BukkitPlayer} and {@code SpongePlayer}. 089 */ 090public abstract class PlotPlayer<P> implements CommandCaller, OfflinePlotPlayer, LocaleHolder { 091 092 private static final String NON_EXISTENT_CAPTION = "<red>PlotSquared does not recognize the caption: "; 093 094 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotPlayer.class.getSimpleName()); 095 096 // Used to track debug mode 097 private static final Set<PlotPlayer<?>> debugModeEnabled = 098 Collections.synchronizedSet(new HashSet<>()); 099 100 @SuppressWarnings("rawtypes") 101 private static final Map<Class<?>, PlotPlayerConverter> converters = new HashMap<>(); 102 private final LockRepository lockRepository = new LockRepository(); 103 private final PlotAreaManager plotAreaManager; 104 private final EventDispatcher eventDispatcher; 105 private final PermissionHandler permissionHandler; 106 private Map<String, byte[]> metaMap = new HashMap<>(); 107 /** 108 * The metadata map. 109 */ 110 private ConcurrentHashMap<String, Object> meta; 111 private int hash; 112 private Locale locale; 113 // Delayed initialisation 114 private PermissionProfile permissionProfile; 115 116 public PlotPlayer( 117 final @NonNull PlotAreaManager plotAreaManager, final @NonNull EventDispatcher eventDispatcher, 118 final @NonNull PermissionHandler permissionHandler 119 ) { 120 this.plotAreaManager = plotAreaManager; 121 this.eventDispatcher = eventDispatcher; 122 this.permissionHandler = permissionHandler; 123 } 124 125 @SuppressWarnings({"rawtypes", "unchecked"}) 126 public static <T> PlotPlayer<T> from(final @NonNull T object) { 127 // fast path 128 if (converters.containsKey(object.getClass())) { 129 return converters.get(object.getClass()).convert(object); 130 } 131 // slow path, meant to only run once per object#getClass instance 132 Queue<Class<?>> toVisit = new ArrayDeque<>(); 133 toVisit.add(object.getClass()); 134 Class<?> current; 135 while ((current = toVisit.poll()) != null) { 136 PlotPlayerConverter converter = converters.get(current); 137 if (converter != null) { 138 if (current != object.getClass()) { 139 // register shortcut for this sub type to avoid further loops 140 converters.put(object.getClass(), converter); 141 LOGGER.info("Registered {} as with converter for {}", object.getClass(), current); 142 } 143 return converter.convert(object); 144 } 145 // no converter found yet 146 if (current.getSuperclass() != null) { 147 toVisit.add(current.getSuperclass()); // add super class if available 148 } 149 toVisit.addAll(Arrays.asList(current.getInterfaces())); // add interfaces 150 } 151 throw new IllegalArgumentException(String 152 .format( 153 "There is no registered PlotPlayer converter for type %s", 154 object.getClass().getSimpleName() 155 )); 156 } 157 158 public static <T> void registerConverter( 159 final @NonNull Class<T> clazz, 160 final PlotPlayerConverter<T> converter 161 ) { 162 converters.put(clazz, converter); 163 } 164 165 public static Collection<PlotPlayer<?>> getDebugModePlayers() { 166 return Collections.unmodifiableCollection(debugModeEnabled); 167 } 168 169 public static Collection<PlotPlayer<?>> getDebugModePlayersInPlot(final @NonNull Plot plot) { 170 if (debugModeEnabled.isEmpty()) { 171 return Collections.emptyList(); 172 } 173 final Collection<PlotPlayer<?>> players = new LinkedList<>(); 174 for (final PlotPlayer<?> player : debugModeEnabled) { 175 if (player.getCurrentPlot().equals(plot)) { 176 players.add(player); 177 } 178 } 179 return players; 180 } 181 182 protected void setupPermissionProfile() { 183 this.permissionProfile = permissionHandler.getPermissionProfile(this).orElse( 184 NullPermissionProfile.INSTANCE); 185 } 186 187 @Override 188 public final boolean hasPermission( 189 final @Nullable String world, 190 final @NonNull String permission 191 ) { 192 return this.permissionProfile.hasPermission(world, permission); 193 } 194 195 @Override 196 public final boolean hasKeyedPermission( 197 final @Nullable String world, 198 final @NonNull String permission, 199 final @NonNull String key 200 ) { 201 return this.permissionProfile.hasKeyedPermission(world, permission, key); 202 } 203 204 @Override 205 public final boolean hasPermission(@NonNull String permission, boolean notify) { 206 if (!hasPermission(permission)) { 207 if (notify) { 208 sendMessage( 209 TranslatableCaption.of("permission.no_permission_event"), 210 TagResolver.resolver("node", Tag.inserting(Component.text(permission))) 211 ); 212 } 213 return false; 214 } 215 return true; 216 } 217 218 public abstract Actor toActor(); 219 220 public abstract P getPlatformPlayer(); 221 222 /** 223 * Set some session only metadata for this player. 224 * 225 * @param key 226 * @param value 227 */ 228 void setMeta(String key, Object value) { 229 if (value == null) { 230 deleteMeta(key); 231 } else { 232 if (this.meta == null) { 233 this.meta = new ConcurrentHashMap<>(); 234 } 235 this.meta.put(key, value); 236 } 237 } 238 239 /** 240 * Get the session metadata for a key. 241 * 242 * @param key the name of the metadata key 243 * @param <T> the object type to return 244 * @return the value assigned to the key or null if it does not exist 245 */ 246 @SuppressWarnings("unchecked") 247 <T> T getMeta(String key) { 248 if (this.meta != null) { 249 return (T) this.meta.get(key); 250 } 251 return null; 252 } 253 254 <T> T getMeta(String key, T defaultValue) { 255 T meta = getMeta(key); 256 if (meta == null) { 257 return defaultValue; 258 } 259 return meta; 260 } 261 262 public ConcurrentHashMap<String, Object> getMeta() { 263 return meta; 264 } 265 266 /** 267 * Delete the metadata for a key. 268 * - metadata is session only 269 * - deleting other plugin's metadata may cause issues 270 * 271 * @param key 272 */ 273 Object deleteMeta(String key) { 274 return this.meta == null ? null : this.meta.remove(key); 275 } 276 277 278 /** 279 * Returns the name of the player. 280 * 281 * @return the name of the player 282 */ 283 @Override 284 public String toString() { 285 return getName(); 286 } 287 288 /** 289 * Get this player's current plot. 290 * 291 * @return the plot the player is standing on or null if standing on a road or not in a {@link PlotArea} 292 */ 293 @Nullable 294 public Plot getCurrentPlot() { 295 try (final MetaDataAccess<Plot> lastPlotAccess = 296 this.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 297 if (lastPlotAccess.get().orElse(null) == null && !Settings.Enabled_Components.EVENTS) { 298 return this.getLocation().getPlot(); 299 } 300 return lastPlotAccess.get().orElse(null); 301 } 302 } 303 304 /** 305 * Get the total number of allowed plots 306 * 307 * @return number of allowed plots within the scope (globally, or in the player's current world as defined in the settings.yml) 308 */ 309 public int getAllowedPlots() { 310 final int calculatedLimit = hasPermissionRange("plots.plot", Settings.Limit.MAX_PLOTS); 311 return this.eventDispatcher.callPlayerPlotLimit(this, calculatedLimit).limit(); 312 } 313 314 /** 315 * Get the number of plots this player owns. 316 * 317 * @return number of plots within the scope (globally, or in the player's current world as defined in the settings.yml) 318 * @see #getPlotCount(String) 319 * @see #getPlots() 320 */ 321 public int getPlotCount() { 322 if (!Settings.Limit.GLOBAL) { 323 return getPlotCount(getCurrentPlot().getWorldName()); 324 } 325 final AtomicInteger count = new AtomicInteger(0); 326 final UUID uuid = getUUID(); 327 this.plotAreaManager.forEachPlotArea(value -> { 328 if (!Settings.Done.COUNTS_TOWARDS_LIMIT) { 329 for (Plot plot : value.getPlotsAbs(uuid)) { 330 if (!DoneFlag.isDone(plot)) { 331 count.incrementAndGet(); 332 } 333 } 334 } else { 335 count.addAndGet(value.getPlotsAbs(uuid).size()); 336 } 337 }); 338 return count.get(); 339 } 340 341 public int getClusterCount() { 342 if (!Settings.Limit.GLOBAL) { 343 return getClusterCount(getCurrentPlot().getWorldName()); 344 } 345 final AtomicInteger count = new AtomicInteger(0); 346 this.plotAreaManager.forEachPlotArea(value -> { 347 for (PlotCluster cluster : value.getClusters()) { 348 if (cluster.isOwner(getUUID())) { 349 count.incrementAndGet(); 350 } 351 } 352 }); 353 return count.get(); 354 } 355 356 /** 357 * Get the number of plots this player owns in the world. 358 * 359 * @param world the name of the plotworld to check. 360 * @return plot count 361 */ 362 public int getPlotCount(String world) { 363 UUID uuid = getUUID(); 364 int count = 0; 365 for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) { 366 if (!Settings.Done.COUNTS_TOWARDS_LIMIT) { 367 count += 368 area.getPlotsAbs(uuid).stream().filter(plot -> !DoneFlag.isDone(plot)).count(); 369 } else { 370 count += area.getPlotsAbs(uuid).size(); 371 } 372 } 373 return count; 374 } 375 376 public int getClusterCount(String world) { 377 int count = 0; 378 for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) { 379 for (PlotCluster cluster : area.getClusters()) { 380 if (cluster.isOwner(getUUID())) { 381 count++; 382 } 383 } 384 } 385 return count; 386 } 387 388 /** 389 * Get a {@link Set} of plots owned by this player. 390 * 391 * <p> 392 * Take a look at {@link PlotSquared} for more searching functions. 393 * See {@link #getPlotCount()} for the number of plots. 394 * </p> 395 * 396 * @return a {@link Set} of plots owned by the player 397 */ 398 public Set<Plot> getPlots() { 399 return PlotQuery.newQuery().ownedBy(this).asSet(); 400 } 401 402 /** 403 * Return the PlotArea this player is currently in, or null. 404 * 405 * @return Plot area the player is currently in, or {@code null} 406 */ 407 public @Nullable PlotArea getPlotAreaAbs() { 408 return this.plotAreaManager.getPlotArea(getLocation()); 409 } 410 411 public PlotArea getApplicablePlotArea() { 412 Plot plot = getCurrentPlot(); 413 if (plot == null) { 414 return this.plotAreaManager.getApplicablePlotArea(getLocation()); 415 } 416 return plot.getArea(); 417 } 418 419 @Override 420 public @NonNull RequiredType getSuperCaller() { 421 return RequiredType.PLAYER; 422 } 423 424 /** 425 * Get this player's last recorded location or null if they don't any plot relevant location. 426 * 427 * @return The location 428 */ 429 public @NonNull Location getLocation() { 430 Location location = getMeta("location"); 431 if (location != null) { 432 return location; 433 } 434 return getLocationFull(); 435 } 436 437 /////////////// PLAYER META /////////////// 438 439 ////////////// PARTIALLY IMPLEMENTED /////////// 440 441 /** 442 * Get this player's full location (including yaw/pitch) 443 * 444 * @return location 445 */ 446 public abstract Location getLocationFull(); 447 448 //////////////////////////////////////////////// 449 450 /** 451 * Get this player's UUID. 452 * <p>=== !IMPORTANT ===</p> 453 * The UUID is dependent on the mode chosen in the settings.yml and may not be the same as Bukkit has 454 * (especially if using an old version of Bukkit that does not support UUIDs) 455 * 456 * @return UUID 457 */ 458 @Override 459 public @NonNull 460 abstract UUID getUUID(); 461 462 public boolean canTeleport(final @NonNull Location location) { 463 Preconditions.checkNotNull(location, "Specified location cannot be null"); 464 final Location current = getLocationFull(); 465 teleport(location); 466 boolean result = getLocation().equals(location); 467 teleport(current); 468 return result; 469 } 470 471 /** 472 * Teleport this player to a location. 473 * 474 * @param location the target location 475 */ 476 public void teleport(Location location) { 477 teleport(location, TeleportCause.PLUGIN); 478 } 479 480 /** 481 * Teleport this player to a location. 482 * 483 * @param location the target location 484 * @param cause the cause of the teleport 485 */ 486 public abstract void teleport(Location location, TeleportCause cause); 487 488 /** 489 * Kick this player to a location 490 * 491 * @param location the target location 492 */ 493 public void plotkick(Location location) { 494 setMeta("kick", true); 495 teleport(location, TeleportCause.KICK); 496 deleteMeta("kick"); 497 } 498 499 /** 500 * Set this compass target. 501 * 502 * @param location the target location 503 */ 504 public abstract void setCompassTarget(Location location); 505 506 /** 507 * Set player data that will persist restarts. 508 * - Please note that this is not intended to store large values 509 * - For session only data use meta 510 * 511 * @param key metadata key 512 */ 513 public void setAttribute(String key) { 514 setPersistentMeta("attrib_" + key, new byte[]{(byte) 1}); 515 } 516 517 /** 518 * Retrieves the attribute of this player. 519 * 520 * @param key metadata key 521 * @return the attribute will be either {@code true} or {@code false} 522 */ 523 public boolean getAttribute(String key) { 524 if (!hasPersistentMeta("attrib_" + key)) { 525 return false; 526 } 527 return getPersistentMeta("attrib_" + key)[0] == 1; 528 } 529 530 /** 531 * Remove an attribute from a player. 532 * 533 * @param key metadata key 534 */ 535 public void removeAttribute(String key) { 536 removePersistentMeta("attrib_" + key); 537 } 538 539 /** 540 * Sets the local weather for this Player. 541 * 542 * @param weather the weather visible to the player 543 */ 544 public abstract void setWeather(@NonNull PlotWeather weather); 545 546 /** 547 * Get this player's gamemode. 548 * 549 * @return the gamemode of the player. 550 */ 551 public abstract @NonNull GameMode getGameMode(); 552 553 /** 554 * Set this player's gameMode. 555 * 556 * @param gameMode the gamemode to set 557 */ 558 public abstract void setGameMode(@NonNull GameMode gameMode); 559 560 /** 561 * Set this player's local time (ticks). 562 * 563 * @param time the time visible to the player 564 */ 565 public abstract void setTime(long time); 566 567 /** 568 * Determines whether or not the player can fly. 569 * 570 * @return {@code true} if the player is allowed to fly 571 */ 572 public abstract boolean getFlight(); 573 574 /** 575 * Sets whether or not this player can fly. 576 * 577 * @param fly {@code true} if the player can fly, otherwise {@code false} 578 */ 579 public abstract void setFlight(boolean fly); 580 581 /** 582 * Play music at a location for this player. 583 * 584 * @param location where to play the music 585 * @param id the record item id 586 */ 587 public abstract void playMusic(@NonNull Location location, @NonNull ItemType id); 588 589 /** 590 * Check if this player is banned. 591 * 592 * @return {@code true} if the player is banned, {@code false} otherwise. 593 */ 594 public abstract boolean isBanned(); 595 596 /** 597 * Kick this player from the game. 598 * 599 * @param message the reason for the kick 600 */ 601 public abstract void kick(String message); 602 603 public void refreshDebug() { 604 final boolean debug = this.getAttribute("debug"); 605 if (debug && !debugModeEnabled.contains(this)) { 606 debugModeEnabled.add(this); 607 } else if (!debug) { 608 debugModeEnabled.remove(this); 609 } 610 } 611 612 /** 613 * Called when this player quits. 614 */ 615 public void unregister() { 616 Plot plot = getCurrentPlot(); 617 if (plot != null && Settings.Enabled_Components.PERSISTENT_META && plot 618 .getArea() instanceof SinglePlotArea) { 619 PlotId id = plot.getId(); 620 int x = id.getX(); 621 int z = id.getY(); 622 ByteBuffer buffer = ByteBuffer.allocate(14); 623 buffer.putShort((short) x); 624 buffer.putShort((short) z); 625 Location location = getLocation(); 626 buffer.putInt(location.getX()); 627 buffer.putShort((short) location.getY()); 628 buffer.putInt(location.getZ()); 629 setPersistentMeta("quitLocV2", buffer.array()); 630 } else if (hasPersistentMeta("quitLocV2")) { 631 removePersistentMeta("quitLocV2"); 632 } 633 if (plot != null) { 634 this.eventDispatcher.callLeave(this, plot); 635 } 636 if (Settings.Enabled_Components.BAN_DELETER && isBanned()) { 637 for (Plot owned : getPlots()) { 638 owned.getPlotModificationManager().deletePlot(null, null); 639 LOGGER.info("Plot {} was deleted + cleared due to {} getting banned", owned.getId(), getName()); 640 } 641 } 642 if (PlotSquared.platform().expireManager() != null) { 643 PlotSquared.platform().expireManager().storeDate(getUUID(), System.currentTimeMillis()); 644 } 645 PlotSquared.platform().playerManager().removePlayer(this); 646 PlotSquared.platform().unregister(this); 647 648 debugModeEnabled.remove(this); 649 } 650 651 /** 652 * Get the amount of clusters this player owns in the specific world. 653 * 654 * @param world world 655 * @return number of clusters owned 656 */ 657 public int getPlayerClusterCount(String world) { 658 return PlotSquared.get().getClusters(world).stream() 659 .filter(cluster -> getUUID().equals(cluster.owner)).mapToInt(PlotCluster::getArea) 660 .sum(); 661 } 662 663 /** 664 * Get the amount of clusters this player owns. 665 * 666 * @return the number of clusters this player owns 667 */ 668 public int getPlayerClusterCount() { 669 final AtomicInteger count = new AtomicInteger(); 670 this.plotAreaManager.forEachPlotArea(value -> count.addAndGet(value.getClusters().size())); 671 return count.get(); 672 } 673 674 /** 675 * Return a {@code Set} of all plots this player owns in a certain world. 676 * 677 * @param world the world to retrieve plots from 678 * @return a {@code Set} of plots this player owns in the provided world 679 */ 680 public Set<Plot> getPlots(String world) { 681 return PlotQuery.newQuery().inWorld(world).ownedBy(getUUID()).asSet(); 682 } 683 684 public void populatePersistentMetaMap() { 685 if (Settings.Enabled_Components.PERSISTENT_META) { 686 DBFunc.getPersistentMeta(getUUID(), new RunnableVal<>() { 687 @Override 688 public void run(Map<String, byte[]> value) { 689 try { 690 PlotPlayer.this.metaMap = value; 691 if (value.isEmpty()) { 692 return; 693 } 694 695 if (PlotPlayer.this.getAttribute("debug")) { 696 debugModeEnabled.add(PlotPlayer.this); 697 } 698 699 if (!Settings.Teleport.ON_LOGIN) { 700 return; 701 } 702 PlotAreaManager manager = PlotPlayer.this.plotAreaManager; 703 704 if (!(manager instanceof SinglePlotAreaManager)) { 705 return; 706 } 707 PlotArea area = ((SinglePlotAreaManager) manager).getArea(); 708 boolean V2 = false; 709 byte[] arr = PlotPlayer.this.getPersistentMeta("quitLoc"); 710 if (arr == null) { 711 arr = PlotPlayer.this.getPersistentMeta("quitLocV2"); 712 if (arr == null) { 713 return; 714 } 715 V2 = true; 716 removePersistentMeta("quitLocV2"); 717 } else { 718 removePersistentMeta("quitLoc"); 719 } 720 721 if (!getMeta("teleportOnLogin", true)) { 722 return; 723 } 724 ByteBuffer quitWorld = ByteBuffer.wrap(arr); 725 final int plotX = quitWorld.getShort(); 726 final int plotZ = quitWorld.getShort(); 727 PlotId id = PlotId.of(plotX, plotZ); 728 int x = quitWorld.getInt(); 729 int y = V2 ? quitWorld.getShort() : (quitWorld.get() & 0xFF); 730 int z = quitWorld.getInt(); 731 Plot plot = area.getOwnedPlot(id); 732 733 if (plot == null) { 734 return; 735 } 736 737 final Location location = Location.at(plot.getWorldName(), x, y, z); 738 if (plot.isLoaded()) { 739 TaskManager.runTask(() -> { 740 if (getMeta("teleportOnLogin", true)) { 741 teleport(location, TeleportCause.LOGIN); 742 sendMessage( 743 TranslatableCaption.of("teleport.teleported_to_plot")); 744 } 745 }); 746 } else if (!PlotSquared.get().isMainThread(Thread.currentThread())) { 747 if (getMeta("teleportOnLogin", true)) { 748 plot.teleportPlayer( 749 PlotPlayer.this, 750 result -> TaskManager.runTask(() -> { 751 if (getMeta("teleportOnLogin", true)) { 752 if (plot.isLoaded()) { 753 teleport(location, TeleportCause.LOGIN); 754 sendMessage(TranslatableCaption 755 .of("teleport.teleported_to_plot")); 756 } 757 } 758 }) 759 ); 760 } 761 } 762 } catch (Throwable e) { 763 LOGGER.error("Error populating persistent meta for player {}", PlotPlayer.this.getName(), e); 764 } 765 } 766 } 767 ); 768 } 769 } 770 771 byte[] getPersistentMeta(String key) { 772 return this.metaMap.get(key); 773 } 774 775 Object removePersistentMeta(String key) { 776 final Object old = this.metaMap.remove(key); 777 if (Settings.Enabled_Components.PERSISTENT_META) { 778 DBFunc.removePersistentMeta(getUUID(), key); 779 } 780 return old; 781 } 782 783 /** 784 * Access keyed persistent meta data for this player. This returns a meta data 785 * access instance, that MUST be closed. It is meant to be used with try-with-resources, 786 * like such: 787 * <pre>{@code 788 * try (final MetaDataAccess<Integer> access = player.accessPersistentMetaData(PlayerMetaKeys.GRANTS)) { 789 * int grants = access.get(); 790 * access.set(grants + 1); 791 * } 792 * }</pre> 793 * 794 * @param key Meta data key 795 * @param <T> Meta data type 796 * @return Meta data access. MUST be closed after being used 797 */ 798 public @NonNull <T> MetaDataAccess<T> accessPersistentMetaData(final @NonNull MetaDataKey<T> key) { 799 return new PersistentMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey())); 800 } 801 802 /** 803 * Access keyed temporary meta data for this player. This returns a meta data 804 * access instance, that MUST be closed. It is meant to be used with try-with-resources, 805 * like such: 806 * <pre>{@code 807 * try (final MetaDataAccess<Integer> access = player.accessTemporaryMetaData(PlayerMetaKeys.GRANTS)) { 808 * int grants = access.get(); 809 * access.set(grants + 1); 810 * } 811 * }</pre> 812 * 813 * @param key Meta data key 814 * @param <T> Meta data type 815 * @return Meta data access. MUST be closed after being used 816 */ 817 public @NonNull <T> MetaDataAccess<T> accessTemporaryMetaData(final @NonNull MetaDataKey<T> key) { 818 return new TemporaryMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey())); 819 } 820 821 <T> void setPersistentMeta( 822 final @NonNull MetaDataKey<T> key, 823 final @NonNull T value 824 ) { 825 if (key.getType().getRawType().equals(Integer.class)) { 826 this.setPersistentMeta(key.toString(), Ints.toByteArray((int) (Object) value)); 827 } else if (key.getType().getRawType().equals(Boolean.class)) { 828 this.setPersistentMeta(key.toString(), ByteArrayUtilities.booleanToBytes((boolean) (Object) value)); 829 } else { 830 throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType())); 831 } 832 } 833 834 @SuppressWarnings("unchecked") 835 @Nullable <T> T getPersistentMeta(final @NonNull MetaDataKey<T> key) { 836 final byte[] value = this.getPersistentMeta(key.toString()); 837 if (value == null) { 838 return null; 839 } 840 final Object returnValue; 841 if (key.getType().getRawType().equals(Integer.class)) { 842 returnValue = Ints.fromByteArray(value); 843 } else if (key.getType().getRawType().equals(Boolean.class)) { 844 returnValue = ByteArrayUtilities.bytesToBoolean(value); 845 } else { 846 throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType())); 847 } 848 return (T) returnValue; 849 } 850 851 void setPersistentMeta(String key, byte[] value) { 852 boolean delete = hasPersistentMeta(key); 853 this.metaMap.put(key, value); 854 if (Settings.Enabled_Components.PERSISTENT_META) { 855 DBFunc.addPersistentMeta(getUUID(), key, value, delete); 856 } 857 } 858 859 /** 860 * Send a title to the player that fades in, in 10 ticks, stays for 50 ticks and fades 861 * out in 20 ticks 862 * 863 * @param title Title text 864 * @param subtitle Subtitle text 865 * @param replacements Variable replacements 866 */ 867 public void sendTitle( 868 final @NonNull Caption title, final @NonNull Caption subtitle, 869 final @NonNull TagResolver... replacements 870 ) { 871 sendTitle( 872 title, 873 subtitle, 874 Settings.Titles.TITLES_FADE_IN, 875 Settings.Titles.TITLES_STAY, 876 Settings.Titles.TITLES_FADE_OUT, 877 replacements 878 ); 879 } 880 881 /** 882 * Send a title to the player 883 * 884 * @param title Title 885 * @param subtitle Subtitle 886 * @param fadeIn Fade in time (in ticks) 887 * @param stay The title stays for (in ticks) 888 * @param fadeOut Fade out time (in ticks) 889 * @param replacements Variable replacements 890 */ 891 public void sendTitle( 892 final @NonNull Caption title, final @NonNull Caption subtitle, 893 final int fadeIn, final int stay, final int fadeOut, 894 final @NonNull TagResolver... replacements 895 ) { 896 final Component titleComponent = MiniMessage.miniMessage().deserialize(title.getComponent(this), replacements); 897 final Component subtitleComponent = 898 MiniMessage.miniMessage().deserialize(subtitle.getComponent(this), replacements); 899 final Title.Times times = Title.Times.times( 900 Duration.of(Settings.Titles.TITLES_FADE_IN * 50L, ChronoUnit.MILLIS), 901 Duration.of(Settings.Titles.TITLES_STAY * 50L, ChronoUnit.MILLIS), 902 Duration.of(Settings.Titles.TITLES_FADE_OUT * 50L, ChronoUnit.MILLIS) 903 ); 904 getAudience().showTitle(Title 905 .title(titleComponent, subtitleComponent, times)); 906 } 907 908 /** 909 * Method designed to send an ActionBar to a player. 910 * 911 * @param caption Caption 912 * @param replacements Variable replacements 913 */ 914 public void sendActionBar( 915 final @NonNull Caption caption, 916 final @NonNull TagResolver... replacements 917 ) { 918 String message; 919 try { 920 message = caption.getComponent(this); 921 } catch (final CaptionMap.NoSuchCaptionException exception) { 922 // This sends feedback to the player 923 message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey(); 924 // And this also prints it to the console 925 exception.printStackTrace(); 926 } 927 if (message.isEmpty()) { 928 return; 929 } 930 // Replace placeholders, etc 931 message = CaptionUtility.format(this, message) 932 .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&') 933 .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this)); 934 935 936 final Component component = MiniMessage.miniMessage().deserialize(message, replacements); 937 getAudience().sendActionBar(component); 938 } 939 940 @Override 941 public void sendMessage( 942 final @NonNull Caption caption, 943 final @NonNull TagResolver... replacements 944 ) { 945 String message; 946 try { 947 message = caption.getComponent(this); 948 } catch (final CaptionMap.NoSuchCaptionException exception) { 949 // This sends feedback to the player 950 message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey(); 951 // And this also prints it to the console 952 exception.printStackTrace(); 953 } 954 if (message.isEmpty()) { 955 return; 956 } 957 // Replace placeholders, etc 958 message = CaptionUtility.format(this, message) 959 .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&') 960 .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this)); 961 // Parse the message 962 final Component component = MiniMessage.miniMessage().deserialize(message, replacements); 963 if (!Objects.equal(component, this.getMeta("lastMessage")) 964 || System.currentTimeMillis() - this.<Long>getMeta("lastMessageTime") > 5000) { 965 setMeta("lastMessage", component); 966 setMeta("lastMessageTime", System.currentTimeMillis()); 967 getAudience().sendMessage(component); 968 } 969 } 970 971 /** 972 * Sends a message to the command caller, when the future is resolved 973 * 974 * @param caption Caption to send 975 * @param asyncReplacement Async variable replacement 976 * @return A Future to be resolved, after the message was sent 977 * @since 7.1.0 978 */ 979 public final CompletableFuture<Void> sendMessage( 980 @NonNull Caption caption, 981 CompletableFuture<@NonNull TagResolver> asyncReplacement 982 ) { 983 return sendMessage(caption, new CompletableFuture[]{asyncReplacement}); 984 } 985 986 /** 987 * Sends a message to the command caller, when all futures are resolved 988 * 989 * @param caption Caption to send 990 * @param asyncReplacements Async variable replacements 991 * @param replacements Sync variable replacements 992 * @return A Future to be resolved, after the message was sent 993 * @since 7.1.0 994 */ 995 public final CompletableFuture<Void> sendMessage( 996 @NonNull Caption caption, 997 CompletableFuture<@NonNull TagResolver>[] asyncReplacements, 998 @NonNull TagResolver... replacements 999 ) { 1000 return CompletableFuture.allOf(asyncReplacements).whenComplete((unused, throwable) -> { 1001 Set<TagResolver> resolvers = new HashSet<>(Arrays.asList(replacements)); 1002 if (throwable != null) { 1003 sendMessage( 1004 TranslatableCaption.of("errors.error"), 1005 TagResolver.resolver("value", Tag.inserting( 1006 Component.text("Failed to resolve asynchronous caption replacements") 1007 )) 1008 ); 1009 LOGGER.error("Failed to resolve asynchronous tagresolver(s) for " + caption, throwable); 1010 } else { 1011 for (final CompletableFuture<TagResolver> asyncReplacement : asyncReplacements) { 1012 resolvers.add(asyncReplacement.join()); 1013 } 1014 } 1015 sendMessage(caption, resolvers.toArray(TagResolver[]::new)); 1016 }); 1017 } 1018 1019 // Redefine from PermissionHolder as it's required from CommandCaller 1020 @Override 1021 public boolean hasPermission(@NonNull String permission) { 1022 return hasPermission(null, permission); 1023 } 1024 1025 boolean hasPersistentMeta(String key) { 1026 return this.metaMap.containsKey(key); 1027 } 1028 1029 /** 1030 * Check if the player is able to see the other player. 1031 * This does not mean that the other player is in line of sight of the player, 1032 * but rather that the player is permitted to see the other player. 1033 * 1034 * @param other Other player 1035 * @return {@code true} if the player is able to see the other player, {@code false} if not 1036 */ 1037 public abstract boolean canSee(PlotPlayer<?> other); 1038 1039 public abstract void stopSpectating(); 1040 1041 public boolean hasDebugMode() { 1042 return this.getAttribute("debug"); 1043 } 1044 1045 @NonNull 1046 @Override 1047 public Locale getLocale() { 1048 if (this.locale == null) { 1049 this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE); 1050 } 1051 return this.locale; 1052 } 1053 1054 @Override 1055 public void setLocale(final @NonNull Locale locale) { 1056 if (!PlotSquared.get().getCaptionMap(TranslatableCaption.DEFAULT_NAMESPACE).supportsLocale(locale)) { 1057 this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE); 1058 } else { 1059 this.locale = locale; 1060 } 1061 } 1062 1063 @Override 1064 public int hashCode() { 1065 if (this.hash == 0 || this.hash == 485) { 1066 this.hash = 485 + this.getUUID().hashCode(); 1067 } 1068 return this.hash; 1069 } 1070 1071 @Override 1072 public boolean equals(final Object obj) { 1073 if (!(obj instanceof final PlotPlayer<?> other)) { 1074 return false; 1075 } 1076 return this.getUUID().equals(other.getUUID()); 1077 } 1078 1079 /** 1080 * Get the {@link Audience} that represents this plot player 1081 * 1082 * @return Player audience 1083 */ 1084 public @NonNull 1085 abstract Audience getAudience(); 1086 1087 /** 1088 * Get this player's {@link LockRepository} 1089 * 1090 * @return Lock repository instance 1091 */ 1092 public @NonNull LockRepository getLockRepository() { 1093 return this.lockRepository; 1094 } 1095 1096 /** 1097 * Removes any effects present of the given type. 1098 * 1099 * @param name the name of the type to remove 1100 * @since 6.10.0 1101 */ 1102 public abstract void removeEffect(@NonNull String name); 1103 1104 @FunctionalInterface 1105 public interface PlotPlayerConverter<BaseObject> { 1106 1107 PlotPlayer<?> convert(BaseObject object); 1108 1109 } 1110 1111}