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