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