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