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 public @Nullable Plot getCurrentPlot() { 294 try (final MetaDataAccess<Plot> lastPlotAccess = 295 this.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 296 if (lastPlotAccess.get().orElse(null) == null && !Settings.Enabled_Components.EVENTS) { 297 return this.getLocation().getPlot(); 298 } 299 return lastPlotAccess.get().orElse(null); 300 } 301 } 302 303 /** 304 * Get the total number of allowed plots 305 * 306 * @return number of allowed plots within the scope (globally, or in the player's current world as defined in the settings.yml) 307 */ 308 public int getAllowedPlots() { 309 final int calculatedLimit = hasPermissionRange("plots.plot", Settings.Limit.MAX_PLOTS); 310 return this.eventDispatcher.callPlayerPlotLimit(this, calculatedLimit).limit(); 311 } 312 313 /** 314 * Get the number of plots this player owns. 315 * 316 * @return number of plots within the scope (globally, or in the player's current world as defined in the settings.yml) 317 * @see #getPlotCount(String) 318 * @see #getPlots() 319 */ 320 public int getPlotCount() { 321 if (!Settings.Limit.GLOBAL) { 322 return getPlotCount(getContextualWorldName()); 323 } 324 final AtomicInteger count = new AtomicInteger(0); 325 final UUID uuid = getUUID(); 326 this.plotAreaManager.forEachPlotArea(value -> { 327 if (!Settings.Done.COUNTS_TOWARDS_LIMIT) { 328 for (Plot plot : value.getPlotsAbs(uuid)) { 329 if (!DoneFlag.isDone(plot)) { 330 count.incrementAndGet(); 331 } 332 } 333 } else { 334 count.addAndGet(value.getPlotsAbs(uuid).size()); 335 } 336 }); 337 return count.get(); 338 } 339 340 public int getClusterCount() { 341 if (!Settings.Limit.GLOBAL) { 342 return getClusterCount(getContextualWorldName()); 343 } 344 final AtomicInteger count = new AtomicInteger(0); 345 this.plotAreaManager.forEachPlotArea(value -> { 346 for (PlotCluster cluster : value.getClusters()) { 347 if (cluster.isOwner(getUUID())) { 348 count.incrementAndGet(); 349 } 350 } 351 }); 352 return count.get(); 353 } 354 355 /** 356 * {@return the world name at the player's contextual position} 357 * The contextual position can be affected when using a command with 358 * an explicit plot override, e.g., `/plot <id> info`. 359 */ 360 private @NonNull String getContextualWorldName() { 361 Plot current = getCurrentPlot(); 362 if (current != null) { 363 return current.getWorldName(); 364 } 365 return getLocation().getWorldName(); 366 } 367 368 /** 369 * {@return the plot area at the player's contextual position} 370 * The contextual position can be affected when using a command with 371 * an explicit plot override, e.g., `/plot <id> info`. 372 * 373 * @since 7.5.9 374 */ 375 public @Nullable PlotArea getContextualPlotArea() { 376 Plot current = getCurrentPlot(); 377 if (current != null) { 378 return current.getArea(); 379 } 380 return getLocation().getPlotArea(); 381 } 382 383 /** 384 * Get the number of plots this player owns in the world. 385 * 386 * @param world the name of the plotworld to check. 387 * @return plot count 388 */ 389 public int getPlotCount(String world) { 390 UUID uuid = getUUID(); 391 int count = 0; 392 for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) { 393 if (!Settings.Done.COUNTS_TOWARDS_LIMIT) { 394 count += 395 area.getPlotsAbs(uuid).stream().filter(plot -> !DoneFlag.isDone(plot)).count(); 396 } else { 397 count += area.getPlotsAbs(uuid).size(); 398 } 399 } 400 return count; 401 } 402 403 public int getClusterCount(String world) { 404 int count = 0; 405 for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) { 406 for (PlotCluster cluster : area.getClusters()) { 407 if (cluster.isOwner(getUUID())) { 408 count++; 409 } 410 } 411 } 412 return count; 413 } 414 415 /** 416 * Get a {@link Set} of plots owned by this player. 417 * 418 * <p> 419 * Take a look at {@link PlotSquared} for more searching functions. 420 * See {@link #getPlotCount()} for the number of plots. 421 * </p> 422 * 423 * @return a {@link Set} of plots owned by the player 424 */ 425 public Set<Plot> getPlots() { 426 return PlotQuery.newQuery().ownedBy(this).asSet(); 427 } 428 429 /** 430 * Return the PlotArea this player is currently in, or null. 431 * 432 * @return Plot area the player is currently in, or {@code null} 433 */ 434 public @Nullable PlotArea getPlotAreaAbs() { 435 return this.plotAreaManager.getPlotArea(getLocation()); 436 } 437 438 public PlotArea getApplicablePlotArea() { 439 Plot plot = getCurrentPlot(); 440 if (plot == null) { 441 return this.plotAreaManager.getApplicablePlotArea(getLocation()); 442 } 443 return plot.getArea(); 444 } 445 446 @Override 447 public @NonNull RequiredType getSuperCaller() { 448 return RequiredType.PLAYER; 449 } 450 451 /** 452 * Get this player's last recorded location or null if they don't any plot relevant location. 453 * 454 * @return The location 455 */ 456 public @NonNull Location getLocation() { 457 Location location = getMeta("location"); 458 if (location != null) { 459 return location; 460 } 461 return getLocationFull(); 462 } 463 464 /////////////// PLAYER META /////////////// 465 466 ////////////// PARTIALLY IMPLEMENTED /////////// 467 468 /** 469 * Get this player's full location (including yaw/pitch) 470 * 471 * @return location 472 */ 473 public abstract Location getLocationFull(); 474 475 //////////////////////////////////////////////// 476 477 /** 478 * Get this player's UUID. 479 * <p>=== !IMPORTANT ===</p> 480 * The UUID is dependent on the mode chosen in the settings.yml and may not be the same as Bukkit has 481 * (especially if using an old version of Bukkit that does not support UUIDs) 482 * 483 * @return UUID 484 */ 485 @Override 486 public @NonNull 487 abstract UUID getUUID(); 488 489 public boolean canTeleport(final @NonNull Location location) { 490 Preconditions.checkNotNull(location, "Specified location cannot be null"); 491 final Location current = getLocationFull(); 492 teleport(location); 493 boolean result = getLocation().equals(location); 494 teleport(current); 495 return result; 496 } 497 498 /** 499 * Teleport this player to a location. 500 * 501 * @param location the target location 502 */ 503 public void teleport(Location location) { 504 teleport(location, TeleportCause.PLUGIN); 505 } 506 507 /** 508 * Teleport this player to a location. 509 * 510 * @param location the target location 511 * @param cause the cause of the teleport 512 */ 513 public abstract void teleport(Location location, TeleportCause cause); 514 515 /** 516 * Kick this player to a location 517 * 518 * @param location the target location 519 */ 520 public void plotkick(Location location) { 521 setMeta("kick", true); 522 teleport(location, TeleportCause.KICK); 523 deleteMeta("kick"); 524 } 525 526 /** 527 * Set this compass target. 528 * 529 * @param location the target location 530 */ 531 public abstract void setCompassTarget(Location location); 532 533 /** 534 * Set player data that will persist restarts. 535 * - Please note that this is not intended to store large values 536 * - For session only data use meta 537 * 538 * @param key metadata key 539 */ 540 public void setAttribute(String key) { 541 setPersistentMeta("attrib_" + key, new byte[]{(byte) 1}); 542 } 543 544 /** 545 * Retrieves the attribute of this player. 546 * 547 * @param key metadata key 548 * @return the attribute will be either {@code true} or {@code false} 549 */ 550 public boolean getAttribute(String key) { 551 if (!hasPersistentMeta("attrib_" + key)) { 552 return false; 553 } 554 return getPersistentMeta("attrib_" + key)[0] == 1; 555 } 556 557 /** 558 * Remove an attribute from a player. 559 * 560 * @param key metadata key 561 */ 562 public void removeAttribute(String key) { 563 removePersistentMeta("attrib_" + key); 564 } 565 566 /** 567 * Sets the local weather for this Player. 568 * 569 * @param weather the weather visible to the player 570 */ 571 public abstract void setWeather(@NonNull PlotWeather weather); 572 573 /** 574 * Get this player's gamemode. 575 * 576 * @return the gamemode of the player. 577 */ 578 public abstract @NonNull GameMode getGameMode(); 579 580 /** 581 * Set this player's gameMode. 582 * 583 * @param gameMode the gamemode to set 584 */ 585 public abstract void setGameMode(@NonNull GameMode gameMode); 586 587 /** 588 * Set this player's local time (ticks). 589 * 590 * @param time the time visible to the player 591 */ 592 public abstract void setTime(long time); 593 594 /** 595 * Determines whether or not the player can fly. 596 * 597 * @return {@code true} if the player is allowed to fly 598 */ 599 public abstract boolean getFlight(); 600 601 /** 602 * Sets whether or not this player can fly. 603 * 604 * @param fly {@code true} if the player can fly, otherwise {@code false} 605 */ 606 public abstract void setFlight(boolean fly); 607 608 /** 609 * Play music at a location for this player. 610 * 611 * @param location where to play the music 612 * @param id the record item id 613 */ 614 public abstract void playMusic(@NonNull Location location, @NonNull ItemType id); 615 616 /** 617 * Check if this player is banned. 618 * 619 * @return {@code true} if the player is banned, {@code false} otherwise. 620 */ 621 public abstract boolean isBanned(); 622 623 /** 624 * Kick this player from the game. 625 * 626 * @param message the reason for the kick 627 */ 628 public abstract void kick(String message); 629 630 public void refreshDebug() { 631 final boolean debug = this.getAttribute("debug"); 632 if (debug && !debugModeEnabled.contains(this)) { 633 debugModeEnabled.add(this); 634 } else if (!debug) { 635 debugModeEnabled.remove(this); 636 } 637 } 638 639 /** 640 * Called when this player quits. 641 */ 642 public void unregister() { 643 Plot plot = getCurrentPlot(); 644 if (plot != null && Settings.Enabled_Components.PERSISTENT_META && plot 645 .getArea() instanceof SinglePlotArea) { 646 PlotId id = plot.getId(); 647 int x = id.getX(); 648 int z = id.getY(); 649 ByteBuffer buffer = ByteBuffer.allocate(14); 650 buffer.putShort((short) x); 651 buffer.putShort((short) z); 652 Location location = getLocation(); 653 buffer.putInt(location.getX()); 654 buffer.putShort((short) location.getY()); 655 buffer.putInt(location.getZ()); 656 setPersistentMeta("quitLocV2", buffer.array()); 657 } else if (hasPersistentMeta("quitLocV2")) { 658 removePersistentMeta("quitLocV2"); 659 } 660 if (plot != null) { 661 this.eventDispatcher.callLeave(this, plot); 662 } 663 if (Settings.Enabled_Components.BAN_DELETER && isBanned()) { 664 for (Plot owned : getPlots()) { 665 owned.getPlotModificationManager().deletePlot(null, null); 666 LOGGER.info("Plot {} was deleted + cleared due to {} getting banned", owned.getId(), getName()); 667 } 668 } 669 if (PlotSquared.platform().expireManager() != null) { 670 PlotSquared.platform().expireManager().storeDate(getUUID(), System.currentTimeMillis()); 671 } 672 PlotSquared.platform().playerManager().removePlayer(this); 673 PlotSquared.platform().unregister(this); 674 675 debugModeEnabled.remove(this); 676 } 677 678 /** 679 * Get the amount of clusters this player owns in the specific world. 680 * 681 * @param world world 682 * @return number of clusters owned 683 */ 684 public int getPlayerClusterCount(String world) { 685 return PlotSquared.get().getClusters(world).stream() 686 .filter(cluster -> getUUID().equals(cluster.owner)).mapToInt(PlotCluster::getArea) 687 .sum(); 688 } 689 690 /** 691 * Get the amount of clusters this player owns. 692 * 693 * @return the number of clusters this player owns 694 */ 695 public int getPlayerClusterCount() { 696 final AtomicInteger count = new AtomicInteger(); 697 this.plotAreaManager.forEachPlotArea(value -> count.addAndGet(value.getClusters().size())); 698 return count.get(); 699 } 700 701 /** 702 * Return a {@code Set} of all plots this player owns in a certain world. 703 * 704 * @param world the world to retrieve plots from 705 * @return a {@code Set} of plots this player owns in the provided world 706 */ 707 public Set<Plot> getPlots(String world) { 708 return PlotQuery.newQuery().inWorld(world).ownedBy(getUUID()).asSet(); 709 } 710 711 public void populatePersistentMetaMap() { 712 if (Settings.Enabled_Components.PERSISTENT_META) { 713 DBFunc.getPersistentMeta( 714 getUUID(), new RunnableVal<>() { 715 @Override 716 public void run(Map<String, byte[]> value) { 717 try { 718 PlotPlayer.this.metaMap = value; 719 if (value.isEmpty()) { 720 return; 721 } 722 723 if (PlotPlayer.this.getAttribute("debug")) { 724 debugModeEnabled.add(PlotPlayer.this); 725 } 726 727 if (!Settings.Teleport.ON_LOGIN) { 728 return; 729 } 730 PlotAreaManager manager = PlotPlayer.this.plotAreaManager; 731 732 if (!(manager instanceof SinglePlotAreaManager)) { 733 return; 734 } 735 PlotArea area = ((SinglePlotAreaManager) manager).getArea(); 736 boolean V2 = false; 737 byte[] arr = PlotPlayer.this.getPersistentMeta("quitLoc"); 738 if (arr == null) { 739 arr = PlotPlayer.this.getPersistentMeta("quitLocV2"); 740 if (arr == null) { 741 return; 742 } 743 V2 = true; 744 removePersistentMeta("quitLocV2"); 745 } else { 746 removePersistentMeta("quitLoc"); 747 } 748 749 if (!getMeta("teleportOnLogin", true)) { 750 return; 751 } 752 ByteBuffer quitWorld = ByteBuffer.wrap(arr); 753 final int plotX = quitWorld.getShort(); 754 final int plotZ = quitWorld.getShort(); 755 PlotId id = PlotId.of(plotX, plotZ); 756 int x = quitWorld.getInt(); 757 int y = V2 ? quitWorld.getShort() : (quitWorld.get() & 0xFF); 758 int z = quitWorld.getInt(); 759 Plot plot = area.getOwnedPlot(id); 760 761 if (plot == null) { 762 return; 763 } 764 765 final Location location = Location.at(plot.getWorldName(), x, y, z); 766 if (plot.isLoaded()) { 767 TaskManager.runTask(() -> { 768 if (getMeta("teleportOnLogin", true)) { 769 teleport(location, TeleportCause.LOGIN); 770 sendMessage( 771 TranslatableCaption.of("teleport.teleported_to_plot")); 772 } 773 }); 774 } else if (!PlotSquared.get().isMainThread(Thread.currentThread())) { 775 if (getMeta("teleportOnLogin", true)) { 776 plot.teleportPlayer( 777 PlotPlayer.this, 778 result -> TaskManager.runTask(() -> { 779 if (getMeta("teleportOnLogin", true)) { 780 if (plot.isLoaded()) { 781 teleport(location, TeleportCause.LOGIN); 782 sendMessage(TranslatableCaption 783 .of("teleport.teleported_to_plot")); 784 } 785 } 786 }) 787 ); 788 } 789 } 790 } catch (Throwable e) { 791 LOGGER.error("Error populating persistent meta for player {}", PlotPlayer.this.getName(), e); 792 } 793 } 794 } 795 ); 796 } 797 } 798 799 byte[] getPersistentMeta(String key) { 800 return this.metaMap.get(key); 801 } 802 803 Object removePersistentMeta(String key) { 804 final Object old = this.metaMap.remove(key); 805 if (Settings.Enabled_Components.PERSISTENT_META) { 806 DBFunc.removePersistentMeta(getUUID(), key); 807 } 808 return old; 809 } 810 811 /** 812 * Access keyed persistent meta data for this player. This returns a meta data 813 * access instance, that MUST be closed. It is meant to be used with try-with-resources, 814 * like such: 815 * <pre>{@code 816 * try (final MetaDataAccess<Integer> access = player.accessPersistentMetaData(PlayerMetaKeys.GRANTS)) { 817 * int grants = access.get(); 818 * access.set(grants + 1); 819 * } 820 * }</pre> 821 * 822 * @param key Meta data key 823 * @param <T> Meta data type 824 * @return Meta data access. MUST be closed after being used 825 */ 826 public @NonNull <T> MetaDataAccess<T> accessPersistentMetaData(final @NonNull MetaDataKey<T> key) { 827 return new PersistentMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey())); 828 } 829 830 /** 831 * Access keyed temporary meta data for this player. This returns a meta data 832 * access instance, that MUST be closed. It is meant to be used with try-with-resources, 833 * like such: 834 * <pre>{@code 835 * try (final MetaDataAccess<Integer> access = player.accessTemporaryMetaData(PlayerMetaKeys.GRANTS)) { 836 * int grants = access.get(); 837 * access.set(grants + 1); 838 * } 839 * }</pre> 840 * 841 * @param key Meta data key 842 * @param <T> Meta data type 843 * @return Meta data access. MUST be closed after being used 844 */ 845 public @NonNull <T> MetaDataAccess<T> accessTemporaryMetaData(final @NonNull MetaDataKey<T> key) { 846 return new TemporaryMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey())); 847 } 848 849 <T> void setPersistentMeta( 850 final @NonNull MetaDataKey<T> key, 851 final @NonNull T value 852 ) { 853 if (key.getType().getRawType().equals(Integer.class)) { 854 this.setPersistentMeta(key.toString(), Ints.toByteArray((int) (Object) value)); 855 } else if (key.getType().getRawType().equals(Boolean.class)) { 856 this.setPersistentMeta(key.toString(), ByteArrayUtilities.booleanToBytes((boolean) (Object) value)); 857 } else { 858 throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType())); 859 } 860 } 861 862 @SuppressWarnings("unchecked") 863 @Nullable 864 <T> T getPersistentMeta(final @NonNull MetaDataKey<T> key) { 865 final byte[] value = this.getPersistentMeta(key.toString()); 866 if (value == null) { 867 return null; 868 } 869 final Object returnValue; 870 if (key.getType().getRawType().equals(Integer.class)) { 871 returnValue = Ints.fromByteArray(value); 872 } else if (key.getType().getRawType().equals(Boolean.class)) { 873 returnValue = ByteArrayUtilities.bytesToBoolean(value); 874 } else { 875 throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType())); 876 } 877 return (T) returnValue; 878 } 879 880 void setPersistentMeta(String key, byte[] value) { 881 boolean delete = hasPersistentMeta(key); 882 this.metaMap.put(key, value); 883 if (Settings.Enabled_Components.PERSISTENT_META) { 884 DBFunc.addPersistentMeta(getUUID(), key, value, delete); 885 } 886 } 887 888 /** 889 * Send a title to the player that fades in, in 10 ticks, stays for 50 ticks and fades 890 * out in 20 ticks 891 * 892 * @param title Title text 893 * @param subtitle Subtitle text 894 * @param replacements Variable replacements 895 */ 896 public void sendTitle( 897 final @NonNull Caption title, final @NonNull Caption subtitle, 898 final @NonNull TagResolver... replacements 899 ) { 900 sendTitle( 901 title, 902 subtitle, 903 Settings.Titles.TITLES_FADE_IN, 904 Settings.Titles.TITLES_STAY, 905 Settings.Titles.TITLES_FADE_OUT, 906 replacements 907 ); 908 } 909 910 /** 911 * Send a title to the player 912 * 913 * @param title Title 914 * @param subtitle Subtitle 915 * @param fadeIn Fade in time (in ticks) 916 * @param stay The title stays for (in ticks) 917 * @param fadeOut Fade out time (in ticks) 918 * @param replacements Variable replacements 919 */ 920 public void sendTitle( 921 final @NonNull Caption title, final @NonNull Caption subtitle, 922 final int fadeIn, final int stay, final int fadeOut, 923 final @NonNull TagResolver... replacements 924 ) { 925 final Component titleComponent = MiniMessage.miniMessage().deserialize(title.getComponent(this), replacements); 926 final Component subtitleComponent = 927 MiniMessage.miniMessage().deserialize(subtitle.getComponent(this), replacements); 928 final Title.Times times = Title.Times.times( 929 Duration.of(Settings.Titles.TITLES_FADE_IN * 50L, ChronoUnit.MILLIS), 930 Duration.of(Settings.Titles.TITLES_STAY * 50L, ChronoUnit.MILLIS), 931 Duration.of(Settings.Titles.TITLES_FADE_OUT * 50L, ChronoUnit.MILLIS) 932 ); 933 getAudience().showTitle(Title 934 .title(titleComponent, subtitleComponent, times)); 935 } 936 937 /** 938 * Method designed to send an ActionBar to a player. 939 * 940 * @param caption Caption 941 * @param replacements Variable replacements 942 */ 943 public void sendActionBar( 944 final @NonNull Caption caption, 945 final @NonNull TagResolver... replacements 946 ) { 947 String message; 948 try { 949 message = caption.getComponent(this); 950 } catch (final CaptionMap.NoSuchCaptionException exception) { 951 // This sends feedback to the player 952 message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey(); 953 // And this also prints it to the console 954 exception.printStackTrace(); 955 } 956 if (message.isEmpty()) { 957 return; 958 } 959 // Replace placeholders, etc 960 message = CaptionUtility.format(this, message) 961 .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&') 962 .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this)); 963 964 965 final Component component = MiniMessage.miniMessage().deserialize(message, replacements); 966 getAudience().sendActionBar(component); 967 } 968 969 @Override 970 public void sendMessage( 971 final @NonNull Caption caption, 972 final @NonNull TagResolver... replacements 973 ) { 974 String message; 975 try { 976 message = caption.getComponent(this); 977 } catch (final CaptionMap.NoSuchCaptionException exception) { 978 // This sends feedback to the player 979 message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey(); 980 // And this also prints it to the console 981 exception.printStackTrace(); 982 } 983 if (message.isEmpty()) { 984 return; 985 } 986 // Replace placeholders, etc 987 message = CaptionUtility.format(this, message) 988 .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&') 989 .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this)); 990 // Parse the message 991 final Component component = MiniMessage.miniMessage().deserialize(message, replacements); 992 if (!Objects.equal(component, this.getMeta("lastMessage")) 993 || System.currentTimeMillis() - this.<Long>getMeta("lastMessageTime") > 5000) { 994 setMeta("lastMessage", component); 995 setMeta("lastMessageTime", System.currentTimeMillis()); 996 getAudience().sendMessage(component); 997 } 998 } 999 1000 /** 1001 * Sends a message to the command caller, when the future is resolved 1002 * 1003 * @param caption Caption to send 1004 * @param asyncReplacement Async variable replacement 1005 * @return A Future to be resolved, after the message was sent 1006 * @since 7.1.0 1007 */ 1008 public final CompletableFuture<Void> sendMessage( 1009 @NonNull Caption caption, 1010 CompletableFuture<@NonNull TagResolver> asyncReplacement 1011 ) { 1012 return sendMessage(caption, new CompletableFuture[]{asyncReplacement}); 1013 } 1014 1015 /** 1016 * Sends a message to the command caller, when all futures are resolved 1017 * 1018 * @param caption Caption to send 1019 * @param asyncReplacements Async variable replacements 1020 * @param replacements Sync variable replacements 1021 * @return A Future to be resolved, after the message was sent 1022 * @since 7.1.0 1023 */ 1024 public final CompletableFuture<Void> sendMessage( 1025 @NonNull Caption caption, 1026 CompletableFuture<@NonNull TagResolver>[] asyncReplacements, 1027 @NonNull TagResolver... replacements 1028 ) { 1029 return CompletableFuture.allOf(asyncReplacements).whenComplete((unused, throwable) -> { 1030 Set<TagResolver> resolvers = new HashSet<>(Arrays.asList(replacements)); 1031 if (throwable != null) { 1032 sendMessage( 1033 TranslatableCaption.of("errors.error"), 1034 TagResolver.resolver( 1035 "value", Tag.inserting( 1036 Component.text("Failed to resolve asynchronous caption replacements") 1037 ) 1038 ) 1039 ); 1040 LOGGER.error("Failed to resolve asynchronous tagresolver(s) for " + caption, throwable); 1041 } else { 1042 for (final CompletableFuture<TagResolver> asyncReplacement : asyncReplacements) { 1043 resolvers.add(asyncReplacement.join()); 1044 } 1045 } 1046 sendMessage(caption, resolvers.toArray(TagResolver[]::new)); 1047 }); 1048 } 1049 1050 // Redefine from PermissionHolder as it's required from CommandCaller 1051 @Override 1052 public boolean hasPermission(@NonNull String permission) { 1053 return hasPermission(null, permission); 1054 } 1055 1056 boolean hasPersistentMeta(String key) { 1057 return this.metaMap.containsKey(key); 1058 } 1059 1060 /** 1061 * Check if the player is able to see the other player. 1062 * This does not mean that the other player is in line of sight of the player, 1063 * but rather that the player is permitted to see the other player. 1064 * 1065 * @param other Other player 1066 * @return {@code true} if the player is able to see the other player, {@code false} if not 1067 */ 1068 public abstract boolean canSee(PlotPlayer<?> other); 1069 1070 public abstract void stopSpectating(); 1071 1072 public boolean hasDebugMode() { 1073 return this.getAttribute("debug"); 1074 } 1075 1076 @NonNull 1077 @Override 1078 public Locale getLocale() { 1079 if (this.locale == null) { 1080 this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE); 1081 } 1082 return this.locale; 1083 } 1084 1085 @Override 1086 public void setLocale(final @NonNull Locale locale) { 1087 if (!PlotSquared.get().getCaptionMap(TranslatableCaption.DEFAULT_NAMESPACE).supportsLocale(locale)) { 1088 this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE); 1089 } else { 1090 this.locale = locale; 1091 } 1092 } 1093 1094 @Override 1095 public int hashCode() { 1096 if (this.hash == 0 || this.hash == 485) { 1097 this.hash = 485 + this.getUUID().hashCode(); 1098 } 1099 return this.hash; 1100 } 1101 1102 @Override 1103 public boolean equals(final Object obj) { 1104 if (!(obj instanceof final PlotPlayer<?> other)) { 1105 return false; 1106 } 1107 return this.getUUID().equals(other.getUUID()); 1108 } 1109 1110 /** 1111 * Get the {@link Audience} that represents this plot player 1112 * 1113 * @return Player audience 1114 */ 1115 public @NonNull 1116 abstract Audience getAudience(); 1117 1118 /** 1119 * Get this player's {@link LockRepository} 1120 * 1121 * @return Lock repository instance 1122 */ 1123 public @NonNull LockRepository getLockRepository() { 1124 return this.lockRepository; 1125 } 1126 1127 /** 1128 * Removes any effects present of the given type. 1129 * 1130 * @param name the name of the type to remove 1131 * @since 6.10.0 1132 */ 1133 public abstract void removeEffect(@NonNull String name); 1134 1135 @FunctionalInterface 1136 public interface PlotPlayerConverter<BaseObject> { 1137 1138 PlotPlayer<?> convert(BaseObject object); 1139 1140 } 1141 1142}