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