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