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