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