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}