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