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