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