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.listener; 020 021import com.plotsquared.core.PlotSquared; 022import com.plotsquared.core.configuration.Settings; 023import com.plotsquared.core.configuration.caption.Caption; 024import com.plotsquared.core.configuration.caption.StaticCaption; 025import com.plotsquared.core.configuration.caption.TranslatableCaption; 026import com.plotsquared.core.events.PlotFlagRemoveEvent; 027import com.plotsquared.core.events.Result; 028import com.plotsquared.core.location.Location; 029import com.plotsquared.core.permissions.Permission; 030import com.plotsquared.core.player.MetaDataAccess; 031import com.plotsquared.core.player.PlayerMetaDataKeys; 032import com.plotsquared.core.player.PlotPlayer; 033import com.plotsquared.core.plot.Plot; 034import com.plotsquared.core.plot.PlotArea; 035import com.plotsquared.core.plot.PlotTitle; 036import com.plotsquared.core.plot.PlotWeather; 037import com.plotsquared.core.plot.comment.CommentManager; 038import com.plotsquared.core.plot.flag.GlobalFlagContainer; 039import com.plotsquared.core.plot.flag.PlotFlag; 040import com.plotsquared.core.plot.flag.implementations.DenyExitFlag; 041import com.plotsquared.core.plot.flag.implementations.FarewellFlag; 042import com.plotsquared.core.plot.flag.implementations.FeedFlag; 043import com.plotsquared.core.plot.flag.implementations.FlyFlag; 044import com.plotsquared.core.plot.flag.implementations.GamemodeFlag; 045import com.plotsquared.core.plot.flag.implementations.GreetingFlag; 046import com.plotsquared.core.plot.flag.implementations.GuestGamemodeFlag; 047import com.plotsquared.core.plot.flag.implementations.HealFlag; 048import com.plotsquared.core.plot.flag.implementations.MusicFlag; 049import com.plotsquared.core.plot.flag.implementations.NotifyEnterFlag; 050import com.plotsquared.core.plot.flag.implementations.NotifyLeaveFlag; 051import com.plotsquared.core.plot.flag.implementations.PlotTitleFlag; 052import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag; 053import com.plotsquared.core.plot.flag.implementations.TimeFlag; 054import com.plotsquared.core.plot.flag.implementations.TitlesFlag; 055import com.plotsquared.core.plot.flag.implementations.WeatherFlag; 056import com.plotsquared.core.plot.flag.types.TimedFlag; 057import com.plotsquared.core.util.EventDispatcher; 058import com.plotsquared.core.util.task.TaskManager; 059import com.plotsquared.core.util.task.TaskTime; 060import com.sk89q.worldedit.world.gamemode.GameMode; 061import com.sk89q.worldedit.world.gamemode.GameModes; 062import com.sk89q.worldedit.world.item.ItemType; 063import com.sk89q.worldedit.world.item.ItemTypes; 064import net.kyori.adventure.text.Component; 065import net.kyori.adventure.text.minimessage.MiniMessage; 066import net.kyori.adventure.text.minimessage.tag.Tag; 067import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 068import org.checkerframework.checker.nullness.qual.NonNull; 069import org.checkerframework.checker.nullness.qual.Nullable; 070 071import java.util.ArrayList; 072import java.util.HashMap; 073import java.util.Iterator; 074import java.util.List; 075import java.util.Map; 076import java.util.Optional; 077import java.util.UUID; 078import java.util.concurrent.CompletableFuture; 079 080public class PlotListener { 081 082 private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); 083 084 private final HashMap<UUID, Interval> feedRunnable = new HashMap<>(); 085 private final HashMap<UUID, Interval> healRunnable = new HashMap<>(); 086 private final Map<UUID, List<StatusEffect>> playerEffects = new HashMap<>(); 087 088 private final EventDispatcher eventDispatcher; 089 090 public PlotListener(final @Nullable EventDispatcher eventDispatcher) { 091 this.eventDispatcher = eventDispatcher; 092 } 093 094 public void startRunnable() { 095 TaskManager.runTaskRepeat(() -> { 096 if (!healRunnable.isEmpty()) { 097 for (Iterator<Map.Entry<UUID, Interval>> iterator = 098 healRunnable.entrySet().iterator(); iterator.hasNext(); ) { 099 Map.Entry<UUID, Interval> entry = iterator.next(); 100 Interval value = entry.getValue(); 101 ++value.count; 102 if (value.count == value.interval) { 103 value.count = 0; 104 final PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(entry.getKey()); 105 if (player == null) { 106 iterator.remove(); 107 continue; 108 } 109 // Don't attempt to heal dead players - they will get stuck in the abyss (#4406) 110 if (PlotSquared.platform().worldUtil().getHealth(player) <= 0) { 111 continue; 112 } 113 double level = PlotSquared.platform().worldUtil().getHealth(player); 114 if (level != value.max) { 115 PlotSquared.platform().worldUtil().setHealth(player, Math.min(level + value.amount, value.max)); 116 } 117 } 118 } 119 } 120 if (!feedRunnable.isEmpty()) { 121 for (Iterator<Map.Entry<UUID, Interval>> iterator = 122 feedRunnable.entrySet().iterator(); iterator.hasNext(); ) { 123 Map.Entry<UUID, Interval> entry = iterator.next(); 124 Interval value = entry.getValue(); 125 ++value.count; 126 if (value.count == value.interval) { 127 value.count = 0; 128 final PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(entry.getKey()); 129 if (player == null) { 130 iterator.remove(); 131 continue; 132 } 133 int level = PlotSquared.platform().worldUtil().getFoodLevel(player); 134 if (level != value.max) { 135 PlotSquared.platform().worldUtil().setFoodLevel(player, Math.min(level + value.amount, value.max)); 136 } 137 } 138 } 139 } 140 141 if (!playerEffects.isEmpty()) { 142 long currentTime = System.currentTimeMillis(); 143 for (Iterator<Map.Entry<UUID, List<StatusEffect>>> iterator = 144 playerEffects.entrySet().iterator(); iterator.hasNext(); ) { 145 Map.Entry<UUID, List<StatusEffect>> entry = iterator.next(); 146 List<StatusEffect> effects = entry.getValue(); 147 effects.removeIf(effect -> currentTime > effect.expiresAt); 148 if (effects.isEmpty()) { 149 iterator.remove(); 150 } 151 } 152 } 153 }, TaskTime.seconds(1L)); 154 } 155 156 public boolean plotEntry(final PlotPlayer<?> player, final Plot plot) { 157 if (plot.isDenied(player.getUUID()) && !player.hasPermission("plots.admin.entry.denied")) { 158 player.sendMessage( 159 TranslatableCaption.of("deny.no_enter"), 160 TagResolver.resolver("plot", Tag.inserting(Component.text(plot.toString()))) 161 ); 162 return false; 163 } 164 try (final MetaDataAccess<Plot> lastPlot = player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 165 Plot last = lastPlot.get().orElse(null); 166 if ((last != null) && !last.getId().equals(plot.getId())) { 167 plotExit(player, last); 168 } 169 if (PlotSquared.platform().expireManager() != null) { 170 PlotSquared.platform().expireManager().handleEntry(player, plot); 171 } 172 lastPlot.set(plot); 173 } 174 this.eventDispatcher.callEntry(player, plot); 175 if (plot.hasOwner()) { 176 // This will inherit values from PlotArea 177 final TitlesFlag.TitlesFlagValue titlesFlag = plot.getFlag(TitlesFlag.class); 178 final boolean titles; 179 if (titlesFlag == TitlesFlag.TitlesFlagValue.NONE) { 180 titles = Settings.Titles.DISPLAY_TITLES; 181 } else { 182 titles = titlesFlag == TitlesFlag.TitlesFlagValue.TRUE; 183 } 184 185 String greeting = plot.getFlag(GreetingFlag.class); 186 if (!greeting.isEmpty()) { 187 if (!Settings.Chat.NOTIFICATION_AS_ACTIONBAR) { 188 plot.format(StaticCaption.of(greeting), player, false).thenAcceptAsync(player::sendMessage); 189 } else { 190 plot.format(StaticCaption.of(greeting), player, false).thenAcceptAsync(player::sendActionBar); 191 } 192 } 193 194 if (plot.getFlag(NotifyEnterFlag.class)) { 195 if (!player.hasPermission("plots.flag.notify-enter.bypass")) { 196 for (UUID uuid : plot.getOwners()) { 197 final PlotPlayer<?> owner = PlotSquared.platform().playerManager().getPlayerIfExists(uuid); 198 if (owner != null && !owner.getUUID().equals(player.getUUID()) && owner.canSee(player)) { 199 Caption caption = TranslatableCaption.of("notification.notify_enter"); 200 notifyPlotOwner(player, plot, owner, caption); 201 } 202 } 203 } 204 } 205 206 final FlyFlag.FlyStatus flyStatus = plot.getFlag(FlyFlag.class); 207 if (!player.hasPermission(Permission.PERMISSION_ADMIN_FLIGHT)) { 208 if (flyStatus != FlyFlag.FlyStatus.DEFAULT) { 209 boolean flight = player.getFlight(); 210 GameMode gamemode = player.getGameMode(); 211 if (flight != (gamemode == GameModes.CREATIVE || gamemode == GameModes.SPECTATOR)) { 212 try (final MetaDataAccess<Boolean> metaDataAccess = player.accessPersistentMetaData(PlayerMetaDataKeys.PERSISTENT_FLIGHT)) { 213 metaDataAccess.set(player.getFlight()); 214 } 215 } 216 player.setFlight(flyStatus == FlyFlag.FlyStatus.ENABLED); 217 } 218 } 219 220 final GameMode gameMode = plot.getFlag(GamemodeFlag.class); 221 if (!gameMode.equals(GamemodeFlag.DEFAULT)) { 222 if (player.getGameMode() != gameMode) { 223 if (!player.hasPermission("plots.gamemode.bypass")) { 224 player.setGameMode(gameMode); 225 } else { 226 player.sendMessage( 227 TranslatableCaption.of("gamemode.gamemode_was_bypassed"), 228 TagResolver.builder() 229 .tag("gamemode", Tag.inserting(Component.text(gameMode.toString()))) 230 .tag("plot", Tag.inserting(Component.text(plot.getId().toString()))) 231 .build() 232 ); 233 } 234 } 235 } 236 237 final GameMode guestGameMode = plot.getFlag(GuestGamemodeFlag.class); 238 if (!guestGameMode.equals(GamemodeFlag.DEFAULT)) { 239 if (player.getGameMode() != guestGameMode && !plot.isAdded(player.getUUID())) { 240 if (!player.hasPermission("plots.gamemode.bypass")) { 241 player.setGameMode(guestGameMode); 242 } else { 243 player.sendMessage( 244 TranslatableCaption.of("gamemode.gamemode_was_bypassed"), 245 TagResolver.builder() 246 .tag("gamemode", Tag.inserting(Component.text(guestGameMode.toString()))) 247 .tag("plot", Tag.inserting(Component.text(plot.getId().toString()))) 248 .build() 249 ); 250 } 251 } 252 } 253 254 long time = plot.getFlag(TimeFlag.class); 255 if (time != TimeFlag.TIME_DISABLED.getValue() && !player.getAttribute("disabletime")) { 256 try { 257 player.setTime(time); 258 } catch (Exception ignored) { 259 PlotFlag<?, ?> plotFlag = 260 GlobalFlagContainer.getInstance().getFlag(TimeFlag.class); 261 PlotFlagRemoveEvent event = 262 this.eventDispatcher.callFlagRemove(plotFlag, plot); 263 if (event.getEventResult() != Result.DENY) { 264 plot.removeFlag(event.getFlag()); 265 } 266 } 267 } 268 269 player.setWeather(plot.getFlag(WeatherFlag.class)); 270 271 ItemType musicFlag = plot.getFlag(MusicFlag.class); 272 273 try (final MetaDataAccess<Location> musicMeta = 274 player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_MUSIC)) { 275 if (musicFlag != null) { 276 final String rawId = musicFlag.getId(); 277 if (rawId.contains("disc") || musicFlag == ItemTypes.AIR) { 278 Location location = player.getLocation(); 279 Location lastLocation = musicMeta.get().orElse(null); 280 if (lastLocation != null) { 281 plot.getCenter(center -> player.playMusic(center.add(0, Short.MAX_VALUE, 0), musicFlag)); 282 if (musicFlag == ItemTypes.AIR) { 283 musicMeta.remove(); 284 } 285 } 286 if (musicFlag != ItemTypes.AIR) { 287 try { 288 musicMeta.set(location); 289 plot.getCenter(center -> player.playMusic(center.add(0, Short.MAX_VALUE, 0), musicFlag)); 290 } catch (Exception ignored) { 291 } 292 } 293 } 294 } else { 295 musicMeta.get().ifPresent(lastLoc -> { 296 musicMeta.remove(); 297 player.playMusic(lastLoc, ItemTypes.AIR); 298 }); 299 } 300 } 301 302 CommentManager.sendTitle(player, plot); 303 304 if (titles && !player.getAttribute("disabletitles")) { 305 String title; 306 String subtitle; 307 PlotTitle titleFlag = plot.getFlag(PlotTitleFlag.class); 308 boolean fromFlag; 309 if (titleFlag.title() != null && titleFlag.subtitle() != null) { 310 title = titleFlag.title(); 311 subtitle = titleFlag.subtitle(); 312 fromFlag = true; 313 } else { 314 title = ""; 315 subtitle = ""; 316 fromFlag = false; 317 } 318 if (fromFlag || !plot.getFlag(ServerPlotFlag.class) || Settings.Titles.DISPLAY_DEFAULT_ON_SERVER_PLOT) { 319 TaskManager.runTaskLaterAsync(() -> { 320 Plot lastPlot; 321 try (final MetaDataAccess<Plot> lastPlotAccess = 322 player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 323 lastPlot = lastPlotAccess.get().orElse(null); 324 } 325 if ((lastPlot != null) && plot.getId().equals(lastPlot.getId()) && plot.hasOwner()) { 326 final UUID plotOwner = plot.getOwnerAbs(); 327 Caption header = fromFlag ? StaticCaption.of(title) : TranslatableCaption.of("titles" + 328 ".title_entered_plot"); 329 Caption subHeader = fromFlag ? StaticCaption.of(subtitle) : TranslatableCaption.of("titles" + 330 ".title_entered_plot_sub"); 331 332 CompletableFuture<TagResolver> future = PlotSquared.platform().playerManager() 333 .getUsernameCaption(plotOwner).thenApply(caption -> TagResolver.builder() 334 .tag("owner", Tag.inserting(caption.toComponent(player))) 335 .tag("plot", Tag.inserting(Component.text(lastPlot.getId().toString()))) 336 .tag("world", Tag.inserting(Component.text(player.getLocation().getWorldName()))) 337 .tag("alias", Tag.inserting(Component.text(plot.getAlias()))) 338 .build() 339 ); 340 341 future.whenComplete((tagResolver, throwable) -> { 342 if (Settings.Titles.TITLES_AS_ACTIONBAR) { 343 player.sendActionBar(header, tagResolver); 344 } else { 345 player.sendTitle(header, subHeader, tagResolver); 346 } 347 }); 348 } 349 }, TaskTime.seconds(1L)); 350 } 351 } 352 353 TimedFlag.Timed<Integer> feed = plot.getFlag(FeedFlag.class); 354 if (feed.interval() != 0 && feed.value() != 0) { 355 feedRunnable 356 .put(player.getUUID(), new Interval(feed.interval(), feed.value(), 20)); 357 } 358 TimedFlag.Timed<Integer> heal = plot.getFlag(HealFlag.class); 359 if (heal.interval() != 0 && heal.value() != 0) { 360 healRunnable 361 .put(player.getUUID(), new Interval(heal.interval(), heal.value(), 20)); 362 } 363 return true; 364 } 365 return true; 366 } 367 368 public boolean plotExit(final PlotPlayer<?> player, Plot plot) { 369 try (final MetaDataAccess<Plot> lastPlot = player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 370 final Plot previous = lastPlot.remove(); 371 372 List<StatusEffect> effects = playerEffects.remove(player.getUUID()); 373 if (effects != null) { 374 long currentTime = System.currentTimeMillis(); 375 effects.forEach(effect -> { 376 if (currentTime <= effect.expiresAt) { 377 player.removeEffect(effect.name); 378 } 379 }); 380 } 381 382 if (plot.hasOwner()) { 383 PlotArea pw = plot.getArea(); 384 if (pw == null) { 385 return true; 386 } 387 try (final MetaDataAccess<Boolean> kickAccess = 388 player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_KICK)) { 389 if (plot.getFlag(DenyExitFlag.class) && !player.hasPermission(Permission.PERMISSION_ADMIN_EXIT_DENIED) && 390 !kickAccess.get().orElse(false)) { 391 if (previous != null) { 392 lastPlot.set(previous); 393 } 394 return false; 395 } 396 } 397 if (!plot.getFlag(GamemodeFlag.class).equals(GamemodeFlag.DEFAULT) || !plot 398 .getFlag(GuestGamemodeFlag.class).equals(GamemodeFlag.DEFAULT)) { 399 if (player.getGameMode() != pw.getGameMode()) { 400 if (!player.hasPermission("plots.gamemode.bypass")) { 401 player.setGameMode(pw.getGameMode()); 402 } else { 403 player.sendMessage( 404 TranslatableCaption.of("gamemode.gamemode_was_bypassed"), 405 TagResolver.builder() 406 .tag("gamemode", Tag.inserting(Component.text(pw.getGameMode().toString()))) 407 .tag("plot", Tag.inserting(Component.text(plot.toString()))) 408 .build() 409 ); 410 } 411 } 412 } 413 414 String farewell = plot.getFlag(FarewellFlag.class); 415 if (!farewell.isEmpty()) { 416 if (!Settings.Chat.NOTIFICATION_AS_ACTIONBAR) { 417 plot.format(StaticCaption.of(farewell), player, false).thenAcceptAsync(player::sendMessage); 418 } else { 419 plot.format(StaticCaption.of(farewell), player, false).thenAcceptAsync(player::sendActionBar); 420 } 421 } 422 423 if (plot.getFlag(NotifyLeaveFlag.class)) { 424 if (!player.hasPermission("plots.flag.notify-leave.bypass")) { 425 for (UUID uuid : plot.getOwners()) { 426 final PlotPlayer<?> owner = PlotSquared.platform().playerManager().getPlayerIfExists(uuid); 427 if ((owner != null) && !owner.getUUID().equals(player.getUUID()) && owner.canSee(player)) { 428 Caption caption = TranslatableCaption.of("notification.notify_leave"); 429 notifyPlotOwner(player, plot, owner, caption); 430 } 431 } 432 } 433 } 434 435 final FlyFlag.FlyStatus flyStatus = plot.getFlag(FlyFlag.class); 436 if (flyStatus != FlyFlag.FlyStatus.DEFAULT) { 437 try (final MetaDataAccess<Boolean> metaDataAccess = player.accessPersistentMetaData(PlayerMetaDataKeys.PERSISTENT_FLIGHT)) { 438 final Optional<Boolean> value = metaDataAccess.get(); 439 if (value.isPresent()) { 440 player.setFlight(value.get()); 441 metaDataAccess.remove(); 442 } else { 443 GameMode gameMode = player.getGameMode(); 444 if (gameMode == GameModes.SURVIVAL || gameMode == GameModes.ADVENTURE) { 445 player.setFlight(false); 446 } else if (!player.getFlight()) { 447 player.setFlight(true); 448 } 449 } 450 } 451 } 452 453 if (plot.getFlag(TimeFlag.class) != TimeFlag.TIME_DISABLED.getValue().longValue()) { 454 player.setTime(Long.MAX_VALUE); 455 } 456 457 final PlotWeather plotWeather = plot.getFlag(WeatherFlag.class); 458 if (plotWeather != PlotWeather.OFF) { 459 player.setWeather(PlotWeather.WORLD); 460 } 461 462 try (final MetaDataAccess<Location> musicAccess = 463 player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_MUSIC)) { 464 musicAccess.get().ifPresent(lastLoc -> { 465 musicAccess.remove(); 466 player.playMusic(lastLoc, ItemTypes.AIR); 467 }); 468 } 469 470 feedRunnable.remove(player.getUUID()); 471 healRunnable.remove(player.getUUID()); 472 } 473 } finally { 474 this.eventDispatcher.callLeave(player, plot); 475 } 476 return true; 477 } 478 479 private void notifyPlotOwner(final PlotPlayer<?> player, final Plot plot, final PlotPlayer<?> owner, final Caption caption) { 480 TagResolver resolver = TagResolver.builder() 481 .tag("player", Tag.inserting(Component.text(player.getName()))) 482 .tag("plot", Tag.inserting(Component.text(plot.getId().toString()))) 483 .tag("area", Tag.inserting(Component.text(String.valueOf(plot.getArea())))) 484 .build(); 485 if (!Settings.Chat.NOTIFICATION_AS_ACTIONBAR) { 486 owner.sendMessage(caption, resolver); 487 } else { 488 owner.sendActionBar(caption, resolver); 489 } 490 } 491 492 public void logout(UUID uuid) { 493 feedRunnable.remove(uuid); 494 healRunnable.remove(uuid); 495 playerEffects.remove(uuid); 496 } 497 498 /** 499 * Marks an effect as a status effect that will be removed on leaving a plot 500 * 501 * @param uuid The uuid of the player the effect belongs to 502 * @param name The name of the status effect 503 * @param expiresAt The time when the effect expires 504 * @since 6.10.0 505 */ 506 public void addEffect(@NonNull UUID uuid, @NonNull String name, long expiresAt) { 507 List<StatusEffect> effects = playerEffects.getOrDefault(uuid, new ArrayList<>()); 508 effects.removeIf(effect -> effect.name.equals(name)); 509 if (expiresAt != -1) { 510 effects.add(new StatusEffect(name, expiresAt)); 511 } 512 playerEffects.put(uuid, effects); 513 } 514 515 private static class Interval { 516 517 final int interval; 518 final int amount; 519 final int max; 520 int count = 0; 521 522 Interval(int interval, int amount, int max) { 523 this.interval = interval; 524 this.amount = amount; 525 this.max = max; 526 } 527 528 } 529 530 private record StatusEffect(@NonNull String name, long expiresAt) { 531 532 private StatusEffect(@NonNull String name, long expiresAt) { 533 this.name = name; 534 this.expiresAt = expiresAt; 535 } 536 537 } 538 539}