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}