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.command;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.configuration.Settings;
024import com.plotsquared.core.configuration.caption.CaptionUtility;
025import com.plotsquared.core.configuration.caption.StaticCaption;
026import com.plotsquared.core.configuration.caption.TranslatableCaption;
027import com.plotsquared.core.events.PlotFlagAddEvent;
028import com.plotsquared.core.events.PlotFlagRemoveEvent;
029import com.plotsquared.core.events.Result;
030import com.plotsquared.core.location.Location;
031import com.plotsquared.core.permissions.Permission;
032import com.plotsquared.core.player.PlotPlayer;
033import com.plotsquared.core.plot.Plot;
034import com.plotsquared.core.plot.flag.FlagParseException;
035import com.plotsquared.core.plot.flag.GlobalFlagContainer;
036import com.plotsquared.core.plot.flag.InternalFlag;
037import com.plotsquared.core.plot.flag.PlotFlag;
038import com.plotsquared.core.plot.flag.types.IntegerFlag;
039import com.plotsquared.core.plot.flag.types.ListFlag;
040import com.plotsquared.core.util.EventDispatcher;
041import com.plotsquared.core.util.MathMan;
042import com.plotsquared.core.util.StringComparison;
043import com.plotsquared.core.util.StringMan;
044import com.plotsquared.core.util.helpmenu.HelpMenu;
045import com.plotsquared.core.util.task.RunnableVal2;
046import com.plotsquared.core.util.task.RunnableVal3;
047import net.kyori.adventure.text.Component;
048import net.kyori.adventure.text.TextComponent;
049import net.kyori.adventure.text.format.Style;
050import net.kyori.adventure.text.minimessage.tag.Tag;
051import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
052import org.checkerframework.checker.nullness.qual.NonNull;
053import org.checkerframework.checker.nullness.qual.Nullable;
054
055import java.util.ArrayList;
056import java.util.Arrays;
057import java.util.Collection;
058import java.util.Collections;
059import java.util.HashMap;
060import java.util.Iterator;
061import java.util.List;
062import java.util.Locale;
063import java.util.Map;
064import java.util.concurrent.CompletableFuture;
065import java.util.stream.Collectors;
066import java.util.stream.Stream;
067
068@CommandDeclaration(command = "flag",
069        aliases = {"f", "flag"},
070        usage = "/plot flag <set | remove | add | list | info> <flag> <value>",
071        category = CommandCategory.SETTINGS,
072        requiredType = RequiredType.NONE,
073        permission = "plots.flag")
074@SuppressWarnings("unused")
075public final class FlagCommand extends Command {
076
077    private final EventDispatcher eventDispatcher;
078
079    @Inject
080    public FlagCommand(final @NonNull EventDispatcher eventDispatcher) {
081        super(MainCommand.getInstance(), true);
082        this.eventDispatcher = eventDispatcher;
083    }
084
085    private static boolean sendMessage(PlotPlayer<?> player) {
086        player.sendMessage(
087                TranslatableCaption.of("commandconfig.command_syntax"),
088                TagResolver.resolver(
089                        "value",
090                        Tag.inserting(Component.text("/plot flag <set | remove | add | list | info> <flag> <value>"))
091                )
092        );
093        return true;
094    }
095
096    private static boolean checkPermValue(
097            final @NonNull PlotPlayer<?> player,
098            final @NonNull PlotFlag<?, ?> flag, @NonNull String key, @NonNull String value
099    ) {
100        key = key.toLowerCase();
101        value = value.toLowerCase();
102        String perm = Permission.PERMISSION_SET_FLAG_KEY_VALUE.format(key.toLowerCase(), value.toLowerCase());
103        if (flag instanceof IntegerFlag && MathMan.isInteger(value)) {
104            try {
105                int numeric = Integer.parseInt(value);
106                perm = perm.substring(0, perm.length() - value.length() - 1);
107                boolean result = false;
108                if (numeric > 0) {
109                    int checkRange = PlotSquared.get().getPlatform().equalsIgnoreCase("bukkit") ?
110                            numeric :
111                            Settings.Limit.MAX_PLOTS;
112                    result = player.hasPermissionRange(perm, checkRange) >= numeric;
113                }
114                if (!result) {
115                    player.sendMessage(
116                            TranslatableCaption.of("permission.no_permission"),
117                            TagResolver.resolver(
118                                    "node",
119                                    Tag.inserting(Component.text(perm + "." + numeric))
120                            )
121                    );
122                }
123                return result;
124            } catch (NumberFormatException ignore) {
125            }
126        } else if (flag instanceof final ListFlag<?, ?> listFlag) {
127            try {
128                PlotFlag<? extends List<?>, ?> parsedFlag = listFlag.parse(value);
129                for (final Object entry : parsedFlag.getValue()) {
130                    final String permission = Permission.PERMISSION_SET_FLAG_KEY_VALUE.format(
131                            key.toLowerCase(),
132                            entry.toString().toLowerCase()
133                    );
134                    final boolean result = player.hasPermission(permission);
135                    if (!result) {
136                        player.sendMessage(
137                                TranslatableCaption.of("permission.no_permission"),
138                                TagResolver.resolver("node", Tag.inserting(Component.text(permission)))
139                        );
140                        return false;
141                    }
142                }
143            } catch (final FlagParseException e) {
144                player.sendMessage(
145                        TranslatableCaption.of("flag.flag_parse_error"),
146                        TagResolver.builder()
147                                .tag("flag_name", Tag.inserting(Component.text(flag.getName())))
148                                .tag("flag_value", Tag.inserting(Component.text(e.getValue())))
149                                .tag("error", Tag.inserting(e.getErrorMessage().toComponent(player)))
150                                .build()
151                );
152                return false;
153            } catch (final Exception e) {
154                return false;
155            }
156            return true;
157        }
158        boolean result;
159        String basePerm = Permission.PERMISSION_SET_FLAG_KEY.format(key.toLowerCase());
160        if (flag.isValuedPermission()) {
161            result = player.hasKeyedPermission(basePerm, value);
162        } else {
163            result = player.hasPermission(basePerm);
164            perm = basePerm;
165        }
166        if (!result) {
167            player.sendMessage(
168                    TranslatableCaption.of("permission.no_permission"),
169                    TagResolver.resolver("node", Tag.inserting(Component.text(perm)))
170            );
171        }
172        return result;
173    }
174
175    /**
176     * Checks if the player is allowed to modify the flags at their current location
177     *
178     * @return {@code true} if the player is allowed to modify the flags at their current location
179     */
180    private static boolean checkRequirements(final @NonNull PlotPlayer<?> player) {
181        final Location location = player.getLocation();
182        final Plot plot = location.getPlotAbs();
183        if (plot == null) {
184            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
185            return false;
186        }
187        if (!plot.hasOwner()) {
188            player.sendMessage(TranslatableCaption.of("working.plot_not_claimed"));
189            return false;
190        }
191        if (!plot.isOwner(player.getUUID()) && !player.hasPermission(Permission.PERMISSION_SET_FLAG_OTHER)) {
192            player.sendMessage(
193                    TranslatableCaption.of("permission.no_permission"),
194                    TagResolver.resolver("node", Tag.inserting(Permission.PERMISSION_SET_FLAG_OTHER))
195            );
196            return false;
197        }
198        return true;
199    }
200
201    /**
202     * Attempt to extract the plot flag from the command arguments. If the flag cannot
203     * be found, a flag suggestion may be sent to the player.
204     *
205     * @param player Player executing the command
206     * @param arg    String to extract flag from
207     * @return The flag, if found, else null
208     */
209    @Nullable
210    private static PlotFlag<?, ?> getFlag(
211            final @NonNull PlotPlayer<?> player,
212            final @NonNull String arg
213    ) {
214        if (arg.length() > 0) {
215            final PlotFlag<?, ?> flag = GlobalFlagContainer.getInstance().getFlagFromString(arg);
216            if (flag instanceof InternalFlag || flag == null) {
217                boolean suggested = false;
218                try {
219                    final StringComparison<PlotFlag<?, ?>> stringComparison =
220                            new StringComparison<>(
221                                    arg,
222                                    GlobalFlagContainer.getInstance().getFlagMap().values(),
223                                    PlotFlag::getName
224                            );
225                    final String best = stringComparison.getBestMatch();
226                    if (best != null) {
227                        player.sendMessage(
228                                TranslatableCaption.of("flag.not_valid_flag_suggested"),
229                                TagResolver.resolver("value", Tag.inserting(Component.text(best)))
230                        );
231                        suggested = true;
232                    }
233                } catch (final Exception ignored) { /* Happens sometimes because of mean code */ }
234                if (!suggested) {
235                    player.sendMessage(TranslatableCaption.of("flag.not_valid_flag"));
236                }
237                return null;
238            }
239            return flag;
240        }
241        return null;
242    }
243
244    @Override
245    public CompletableFuture<Boolean> execute(
246            PlotPlayer<?> player, String[] args,
247            RunnableVal3<Command, Runnable, Runnable> confirm,
248            RunnableVal2<Command, CommandResult> whenDone
249    ) throws CommandException {
250        if (args.length == 0 || !Arrays
251                .asList("set", "s", "list", "l", "delete", "remove", "r", "add", "a", "info", "i")
252                .contains(args[0].toLowerCase(Locale.ENGLISH))) {
253            new HelpMenu(player).setCategory(CommandCategory.SETTINGS)
254                    .setCommands(this.getCommands()).generateMaxPages()
255                    .generatePage(0, getParent().toString(), player).render();
256            return CompletableFuture.completedFuture(true);
257        }
258        return super.execute(player, args, confirm, whenDone);
259    }
260
261    @Override
262    public Collection<Command> tab(
263            final PlotPlayer<?> player, final String[] args,
264            final boolean space
265    ) {
266        if (args.length == 1) {
267            return Stream
268                    .of("set", "add", "remove", "delete", "info", "list")
269                    .filter(value -> value.startsWith(args[0].toLowerCase(Locale.ENGLISH)))
270                    .map(value -> new Command(null, false, value, "", RequiredType.NONE, null) {
271                    }).collect(Collectors.toList());
272        } else if (Arrays.asList("set", "add", "remove", "delete", "info")
273                .contains(args[0].toLowerCase(Locale.ENGLISH)) && args.length == 2) {
274            return GlobalFlagContainer.getInstance().getRecognizedPlotFlags().stream()
275                    .filter(flag -> !(flag instanceof InternalFlag))
276                    .filter(flag -> flag.getName().startsWith(args[1].toLowerCase(Locale.ENGLISH)))
277                    .map(flag -> new Command(null, false, flag.getName(), "", RequiredType.NONE, null) {
278                    }).collect(Collectors.toList());
279        } else if (Arrays.asList("set", "add", "remove", "delete")
280                .contains(args[0].toLowerCase(Locale.ENGLISH)) && args.length == 3) {
281            try {
282                final PlotFlag<?, ?> flag =
283                        GlobalFlagContainer.getInstance().getFlagFromString(args[1]);
284                if (flag != null) {
285                    Stream<String> stream = flag.getTabCompletions().stream();
286                    if (flag instanceof ListFlag && args[2].contains(",")) {
287                        final String[] split = args[2].split(",");
288                        // Prefix earlier values onto all suggestions
289                        StringBuilder prefix = new StringBuilder();
290                        for (int i = 0; i < split.length - 1; i++) {
291                            prefix.append(split[i]).append(",");
292                        }
293                        final String cmp;
294                        if (!args[2].endsWith(",")) {
295                            cmp = split[split.length - 1];
296                        } else {
297                            prefix.append(split[split.length - 1]).append(",");
298                            cmp = "";
299                        }
300                        return stream
301                                .filter(value -> value.startsWith(cmp.toLowerCase(Locale.ENGLISH))).map(
302                                        value -> new Command(null, false, prefix + value, "",
303                                                RequiredType.NONE, null
304                                        ) {
305                                        }).collect(Collectors.toList());
306                    } else {
307                        return stream
308                                .filter(value -> value.startsWith(args[2].toLowerCase(Locale.ENGLISH)))
309                                .map(value -> new Command(null, false, value, "", RequiredType.NONE,
310                                        null
311                                ) {
312                                }).collect(Collectors.toList());
313                    }
314                }
315            } catch (final Exception ignored) {
316            }
317        }
318        return tabOf(player, args, space);
319    }
320
321    @CommandDeclaration(command = "set",
322            aliases = {"s", "set"},
323            usage = "/plot flag set <flag> <value>",
324            category = CommandCategory.SETTINGS,
325            requiredType = RequiredType.NONE,
326            permission = "plots.set.flag")
327    public void set(
328            final Command command, final PlotPlayer<?> player, final String[] args,
329            final RunnableVal3<Command, Runnable, Runnable> confirm,
330            final RunnableVal2<Command, CommandResult> whenDone
331    ) {
332        if (!checkRequirements(player)) {
333            return;
334        }
335        if (args.length < 2) {
336            player.sendMessage(
337                    TranslatableCaption.of("commandconfig.command_syntax"),
338                    TagResolver.resolver("value", Tag.inserting(Component.text("/plot flag set <flag> <value>")))
339            );
340            return;
341        }
342        final PlotFlag<?, ?> plotFlag = getFlag(player, args[0]);
343        if (plotFlag == null) {
344            return;
345        }
346        Plot plot = player.getLocation().getPlotAbs();
347        PlotFlagAddEvent event = eventDispatcher.callFlagAdd(plotFlag, plot);
348        if (event.getEventResult() == Result.DENY) {
349            player.sendMessage(
350                    TranslatableCaption.of("events.event_denied"),
351                    TagResolver.resolver("value", Tag.inserting(Component.text("Flag set")))
352            );
353            return;
354        }
355        boolean force = event.getEventResult() == Result.FORCE;
356        String value = StringMan.join(Arrays.copyOfRange(args, 1, args.length), " ");
357        if (!force && !checkPermValue(player, plotFlag, args[0], value)) {
358            return;
359        }
360        value = CaptionUtility.stripClickEvents(plotFlag, value);
361        final PlotFlag<?, ?> parsed;
362        try {
363            parsed = plotFlag.parse(value);
364        } catch (final FlagParseException e) {
365            player.sendMessage(
366                    TranslatableCaption.of("flag.flag_parse_error"),
367                    TagResolver.builder()
368                            .tag("flag_name", Tag.inserting(Component.text(plotFlag.getName())))
369                            .tag("flag_value", Tag.inserting(Component.text(e.getValue())))
370                            .tag("error", Tag.inserting(e.getErrorMessage().toComponent(player)))
371                            .build()
372            );
373            return;
374        }
375        plot.setFlag(parsed);
376        player.sendMessage(
377                TranslatableCaption.of("flag.flag_added"),
378                TagResolver.builder()
379                        .tag("flag", Tag.inserting(Component.text(args[0])))
380                        .tag("value", Tag.inserting(Component.text(parsed.toString())))
381                        .build()
382        );
383    }
384
385    @SuppressWarnings({"unchecked", "rawtypes"})
386    @CommandDeclaration(command = "add",
387            aliases = {"a", "add"},
388            usage = "/plot flag add <flag> <value>",
389            category = CommandCategory.SETTINGS,
390            requiredType = RequiredType.NONE,
391            permission = "plots.flag.add")
392    public void add(
393            final Command command, PlotPlayer<?> player, final String[] args,
394            final RunnableVal3<Command, Runnable, Runnable> confirm,
395            final RunnableVal2<Command, CommandResult> whenDone
396    ) {
397        if (!checkRequirements(player)) {
398            return;
399        }
400        if (args.length < 2) {
401            player.sendMessage(
402                    TranslatableCaption.of("commandconfig.command_syntax"),
403                    TagResolver.resolver("value", Tag.inserting(Component.text("/plot flag add <flag> <values>")))
404            );
405            return;
406        }
407        final PlotFlag<?, ?> plotFlag = getFlag(player, args[0]);
408        if (plotFlag == null) {
409            return;
410        }
411        Plot plot = player.getLocation().getPlotAbs();
412        PlotFlagAddEvent event = eventDispatcher.callFlagAdd(plotFlag, plot);
413        if (event.getEventResult() == Result.DENY) {
414            player.sendMessage(
415                    TranslatableCaption.of("events.event_denied"),
416                    TagResolver.resolver("value", Tag.inserting(Component.text("Flag add")))
417            );
418            return;
419        }
420        boolean force = event.getEventResult() == Result.FORCE;
421        final PlotFlag localFlag = player.getLocation().getPlotAbs().getFlagContainer()
422                .getFlag(event.getFlag().getClass());
423        if (!force) {
424            for (String entry : args[1].split(",")) {
425                if (!checkPermValue(player, event.getFlag(), args[0], entry)) {
426                    return;
427                }
428            }
429        }
430        final String value = StringMan.join(Arrays.copyOfRange(args, 1, args.length), " ");
431        final PlotFlag parsed;
432        try {
433            parsed = event.getFlag().parse(value);
434        } catch (FlagParseException e) {
435            player.sendMessage(
436                    TranslatableCaption.of("flag.flag_parse_error"),
437                    TagResolver.builder()
438                            .tag("flag_name", Tag.inserting(Component.text(plotFlag.getName())))
439                            .tag("flag_value", Tag.inserting(Component.text(e.getValue())))
440                            .tag("error", Tag.inserting(e.getErrorMessage().toComponent(player)))
441                            .build()
442            );
443            return;
444        }
445        boolean result =
446                player.getLocation().getPlotAbs().setFlag(localFlag.merge(parsed.getValue()));
447        if (!result) {
448            player.sendMessage(TranslatableCaption.of("flag.flag_not_added"));
449            return;
450        }
451        player.sendMessage(
452                TranslatableCaption.of("flag.flag_added"),
453                TagResolver.builder()
454                        .tag("flag", Tag.inserting(Component.text(args[0])))
455                        .tag("value", Tag.inserting(Component.text(parsed.toString())))
456                        .build()
457        );
458    }
459
460    @SuppressWarnings({"unchecked", "rawtypes"})
461    @CommandDeclaration(command = "remove",
462            aliases = {"r", "remove", "delete"},
463            usage = "/plot flag remove <flag> [values]",
464            category = CommandCategory.SETTINGS,
465            requiredType = RequiredType.NONE,
466            permission = "plots.flag.remove")
467    public void remove(
468            final Command command, PlotPlayer<?> player, final String[] args,
469            final RunnableVal3<Command, Runnable, Runnable> confirm,
470            final RunnableVal2<Command, CommandResult> whenDone
471    ) {
472        if (!checkRequirements(player)) {
473            return;
474        }
475        if (args.length != 1 && args.length != 2) {
476            player.sendMessage(
477                    TranslatableCaption.of("commandconfig.command_syntax"),
478                    TagResolver.resolver("value", Tag.inserting(Component.text("/plot flag remove <flag> [values]")))
479            );
480            return;
481        }
482        PlotFlag<?, ?> flag = getFlag(player, args[0]);
483        if (flag == null) {
484            return;
485        }
486        final Plot plot = player.getLocation().getPlotAbs();
487        final PlotFlag<?, ?> flagWithOldValue = plot.getFlagContainer().getFlag(flag.getClass());
488        PlotFlagRemoveEvent event = eventDispatcher.callFlagRemove(flag, plot);
489        if (event.getEventResult() == Result.DENY) {
490            player.sendMessage(
491                    TranslatableCaption.of("events.event_denied"),
492                    TagResolver.resolver("value", Tag.inserting(Component.text("Flag remove")))
493            );
494            return;
495        }
496        boolean force = event.getEventResult() == Result.FORCE;
497        flag = event.getFlag();
498        if (!force && !player.hasPermission(Permission.PERMISSION_SET_FLAG_KEY.format(args[0].toLowerCase()))) {
499            if (args.length != 2) {
500                player.sendMessage(
501                        TranslatableCaption.of("permission.no_permission"),
502                        TagResolver.resolver(
503                                "node",
504                                Tag.inserting(Component.text(Permission.PERMISSION_SET_FLAG_KEY.format(args[0].toLowerCase())))
505                        )
506                );
507                return;
508            }
509        }
510        if (args.length == 2 && flag instanceof final ListFlag<?, ?> listFlag) {
511            String value = StringMan.join(Arrays.copyOfRange(args, 1, args.length), " ");
512            final List<?> list =
513                    new ArrayList<>(plot.getFlag((Class<? extends ListFlag<?, ?>>) listFlag.getClass()));
514            final PlotFlag parsedFlag;
515            try {
516                parsedFlag = listFlag.parse(value);
517            } catch (final FlagParseException e) {
518                player.sendMessage(
519                        TranslatableCaption.of("flag.flag_parse_error"),
520                        TagResolver.builder()
521                                .tag("flag_name", Tag.inserting(Component.text(flag.getName())))
522                                .tag("flag_value", Tag.inserting(Component.text(e.getValue())))
523                                .tag("error", Tag.inserting(e.getErrorMessage().toComponent(player)))
524                                .build()
525                );
526                return;
527            }
528            if (((List<?>) parsedFlag.getValue()).isEmpty()) {
529                player.sendMessage(TranslatableCaption.of("flag.flag_not_removed"));
530                return;
531            }
532            if (list.removeAll((List) parsedFlag.getValue())) {
533                if (list.isEmpty()) {
534                    if (plot.removeFlag(flag)) {
535                        player.sendMessage(
536                                TranslatableCaption.of("flag.flag_removed"),
537                                TagResolver.builder()
538                                        .tag("flag", Tag.inserting(Component.text(args[0])))
539                                        .tag("value", Tag.inserting(Component.text(flag.toString())))
540                                        .build()
541                        );
542                        return;
543                    } else {
544                        player.sendMessage(TranslatableCaption.of("flag.flag_not_removed"));
545                        return;
546                    }
547                } else {
548                    PlotFlag<?, ?> plotFlag = parsedFlag.createFlagInstance(list);
549                    PlotFlagAddEvent addEvent = eventDispatcher.callFlagAdd(plotFlag, plot);
550                    if (addEvent.getEventResult() == Result.DENY) {
551                        player.sendMessage(
552                                TranslatableCaption.of("events.event_denied"),
553                                TagResolver.resolver(
554                                        "value",
555                                        Tag.inserting(Component.text("Re-addition of " + plotFlag.getName()))
556                                )
557                        );
558                        return;
559                    }
560                    if (plot.setFlag(addEvent.getFlag())) {
561                        player.sendMessage(TranslatableCaption.of("flag.flag_partially_removed"));
562                        return;
563                    } else {
564                        player.sendMessage(TranslatableCaption.of("flag.flag_not_removed"));
565                        return;
566                    }
567                }
568            } else {
569                player.sendMessage(TranslatableCaption.of("flag.flag_not_removed"));
570                return;
571            }
572        } else {
573            boolean result = plot.removeFlag(flag);
574            if (!result) {
575                player.sendMessage(TranslatableCaption.of("flag.flag_not_removed"));
576                return;
577            }
578        }
579        player.sendMessage(
580                TranslatableCaption.of("flag.flag_removed"),
581                TagResolver.builder()
582                        .tag("flag", Tag.inserting(Component.text(args[0])))
583                        .tag("value", Tag.inserting(Component.text(flag.toString())))
584                        .build()
585        );
586    }
587
588    @CommandDeclaration(command = "list",
589            aliases = {"l", "list", "flags"},
590            usage = "/plot flag list",
591            category = CommandCategory.SETTINGS,
592            requiredType = RequiredType.NONE,
593            permission = "plots.flag.list")
594    public void list(
595            final Command command, final PlotPlayer<?> player, final String[] args,
596            final RunnableVal3<Command, Runnable, Runnable> confirm,
597            final RunnableVal2<Command, CommandResult> whenDone
598    ) {
599        if (!checkRequirements(player)) {
600            return;
601        }
602
603        final Map<Component, ArrayList<String>> flags = new HashMap<>();
604        for (PlotFlag<?, ?> plotFlag : GlobalFlagContainer.getInstance().getRecognizedPlotFlags()) {
605            if (plotFlag instanceof InternalFlag) {
606                continue;
607            }
608            final Component category = plotFlag.getFlagCategory().toComponent(player);
609            final Collection<String> flagList = flags.computeIfAbsent(category, k -> new ArrayList<>());
610            flagList.add(plotFlag.getName());
611        }
612
613        for (final Map.Entry<Component, ArrayList<String>> entry : flags.entrySet()) {
614            Collections.sort(entry.getValue());
615            Component category =
616                    MINI_MESSAGE.deserialize(
617                            TranslatableCaption.of("flag.flag_list_categories").getComponent(player),
618                            TagResolver.resolver("category", Tag.inserting(entry.getKey().style(Style.empty())))
619                    );
620            TextComponent.Builder builder = Component.text().append(category);
621            final Iterator<String> flagIterator = entry.getValue().iterator();
622            while (flagIterator.hasNext()) {
623                final String flag = flagIterator.next();
624                builder.append(MINI_MESSAGE
625                        .deserialize(
626                                TranslatableCaption.of("flag.flag_list_flag").getComponent(player),
627                                TagResolver.builder()
628                                        .tag("command", Tag.preProcessParsed("/plot flag info " + flag))
629                                        .tag("flag", Tag.inserting(Component.text(flag)))
630                                        .tag("suffix", Tag.inserting(Component.text(flagIterator.hasNext() ? ", " : "")))
631                                        .build()
632                        ));
633            }
634            player.sendMessage(StaticCaption.of(MINI_MESSAGE.serialize(builder.build())));
635        }
636    }
637
638    @CommandDeclaration(command = "info",
639            aliases = {"i", "info"},
640            usage = "/plot flag info <flag>",
641            category = CommandCategory.SETTINGS,
642            requiredType = RequiredType.NONE,
643            permission = "plots.flag.info")
644    public void info(
645            final Command command, final PlotPlayer<?> player, final String[] args,
646            final RunnableVal3<Command, Runnable, Runnable> confirm,
647            final RunnableVal2<Command, CommandResult> whenDone
648    ) {
649        if (!checkRequirements(player)) {
650            return;
651        }
652        if (args.length < 1) {
653            player.sendMessage(
654                    TranslatableCaption.of("commandconfig.command_syntax"),
655                    TagResolver.resolver("value", Tag.inserting(Component.text("/plot flag info <flag>")))
656            );
657            return;
658        }
659        final PlotFlag<?, ?> plotFlag = getFlag(player, args[0]);
660        if (plotFlag != null) {
661            player.sendMessage(TranslatableCaption.of("flag.flag_info_header"));
662            // Flag name
663            player.sendMessage(
664                    TranslatableCaption.of("flag.flag_info_name"),
665                    TagResolver.resolver("flag", Tag.inserting(Component.text(plotFlag.getName())))
666            );
667            // Flag category
668            player.sendMessage(
669                    TranslatableCaption.of("flag.flag_info_category"),
670                    TagResolver.resolver(
671                            "value",
672                            Tag.inserting(plotFlag.getFlagCategory().toComponent(player))
673                    )
674            );
675            // Flag description
676            // TODO maybe merge and \n instead?
677            player.sendMessage(TranslatableCaption.of("flag.flag_info_description"));
678            player.sendMessage(plotFlag.getFlagDescription());
679            // Flag example
680            player.sendMessage(
681                    TranslatableCaption.of("flag.flag_info_example"),
682                    TagResolver.builder()
683                            .tag("command", Tag.preProcessParsed("/plot flag set"))
684                            .tag("flag", Tag.preProcessParsed(plotFlag.getName()))
685                            .tag("value", Tag.preProcessParsed(plotFlag.getExample()))
686                            .build()
687            );
688            // Default value
689            final String defaultValue = player.getLocation().getPlotArea().getFlagContainer()
690                    .getFlagErased(plotFlag.getClass()).toString();
691            player.sendMessage(
692                    TranslatableCaption.of("flag.flag_info_default_value"),
693                    TagResolver.resolver("value", Tag.inserting(Component.text(defaultValue)))
694            );
695            // Footer. Done this way to prevent the duplicate-message-thingy from catching it
696            player.sendMessage(TranslatableCaption.of("flag.flag_info_footer"));
697        }
698    }
699
700}