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