001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.minecraft.command.definition;
018
019import ca.stellardrift.permissionsex.context.ContextValue;
020import ca.stellardrift.permissionsex.minecraft.command.ButtonType;
021import ca.stellardrift.permissionsex.minecraft.command.CommandException;
022import ca.stellardrift.permissionsex.minecraft.command.CommandRegistrationContext;
023import ca.stellardrift.permissionsex.minecraft.command.Commander;
024import ca.stellardrift.permissionsex.minecraft.command.Elements;
025import ca.stellardrift.permissionsex.minecraft.command.Permission;
026import ca.stellardrift.permissionsex.minecraft.command.argument.Parsers;
027import ca.stellardrift.permissionsex.rank.RankLadder;
028import ca.stellardrift.permissionsex.subject.SubjectRef;
029import ca.stellardrift.permissionsex.subject.SubjectType;
030import cloud.commandframework.Command;
031import cloud.commandframework.arguments.CommandArgument;
032import cloud.commandframework.arguments.flags.CommandFlag;
033import cloud.commandframework.arguments.standard.IntegerArgument;
034import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys;
035import cloud.commandframework.minecraft.extras.RichDescription;
036import io.leangen.geantyref.TypeToken;
037import net.kyori.adventure.text.Component;
038import org.checkerframework.checker.nullness.qual.Nullable;
039
040import java.util.ArrayList;
041import java.util.List;
042import java.util.ListIterator;
043import java.util.Set;
044
045import static ca.stellardrift.permissionsex.minecraft.command.Elements.*;
046import static net.kyori.adventure.text.Component.text;
047import static net.kyori.adventure.text.TextComponent.ofChildren;
048
049public final class RankingCommands {
050
051    // All commands
052
053    private static CommandArgument<Commander, RankLadder> ladderArgument(final boolean required) {
054        final CommandArgument.Builder<Commander, RankLadder> ladder = CommandArgument.<Commander, RankLadder>ofType(RankLadder.class, "ladder")
055            .withParser(Parsers.rankLadder());
056        if (!required) {
057            ladder.asOptionalWithDefault("default");
058        }
059        return ladder.build();
060    }
061
062    private static final CommandArgument<Commander, RankLadder> LADDER_ARG = ladderArgument(true);
063
064    // Only for subcommands that modify
065    private static final CommandFlag<Void> FLAG_RELATIVE = CommandFlag.newBuilder("relative")
066        .withDescription(RichDescription.of(Messages.RANK_ARG_RELATIVE_DESCRIPTION))
067        .withAliases("r")
068        .build();
069
070    private RankingCommands() {
071    }
072
073    static void register(final CommandRegistrationContext ctx) {
074        // ctx.register(RankingCommands::list, "")
075        ctx.push(ctx.head().argument(LADDER_ARG, RichDescription.of(Messages.RANK_ARG_LADDER_DESCRIPTION)), child -> {
076            ctx.register(list(ctx.head()));
077            ctx.register(RankingCommands::add, "add", "+");
078            ctx.register(RankingCommands::remove, "remove", "rem", "delete", "del", "-");
079        });
080    }
081
082    static Command.Builder<Commander> list(final Command.Builder<Commander> builder) {
083        final Permission listPerm = Permission.pex("ranking.list");
084        return builder
085            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_LIST_DESCRIPTION.tr())
086            .permission(listPerm)
087            .handler(ctx -> {
088                final Commander source = ctx.getSender();
089                final RankLadder ladder = ctx.get(LADDER_ARG);
090                final List<Component> ranksList = new ArrayList<>();
091                final List<? extends SubjectRef<?>> rawRanks = ladder.ranks();
092
093                // Build list
094                if (rawRanks.size() == 0) {
095                    throw new CommandException(Messages.RANKING_ERROR_EMPTY_LADDER.tr(ladder));
096                } else if (rawRanks.size() == 1) {
097                    ranksList.add(source.formatter().subject(rawRanks.get(0)));
098                } else {
099                    for (final ListIterator<? extends SubjectRef<?>> it = rawRanks.listIterator(rawRanks.size()); it.hasPrevious(); ) {
100                        final int idx = it.previousIndex();
101                        final SubjectRef<?> rank = it.previous();
102                        final Component rendered;
103                        if (idx == rawRanks.size()) { // first
104                            rendered = ofChildren(
105                                source.formatter().subject(rank),
106                                moveDownButton(source, ladder, rank),
107                                deleteButton(source, ladder, rank)
108                            );
109                        } else if (idx == 0) { // last
110                            rendered = ofChildren(
111                                source.formatter().subject(rank),
112                                moveUpButton(source, ladder, rank),
113                                deleteButton(source, ladder, rank)
114                            );
115                        } else { // in-between
116                            rendered = ofChildren(
117                                source.formatter().subject(rank),
118                                moveDownButton(source, ladder, rank),
119                                moveUpButton(source, ladder, rank),
120                                deleteButton(source, ladder, rank)
121                            );
122                        }
123                        ranksList.add(rendered);
124                    }
125                }
126
127                // Send as a pagination
128                source.sendPaginated(
129                    Messages.RANKING_PAGINATION_HEADER.tr(
130                        ladder.name(),
131                        source.formatter().button(
132                            text().content("+"),
133                            ButtonType.POSITIVE,
134                            Messages.RANKING_BUTTON_ADD_DESCRIPTION,
135                            "/pex rank " + ladder.name() + " add ",
136                            false
137                        )
138                    ),
139                    Messages.RANKING_PAGINATION_SUBTITLE,
140                    ranksList
141                );
142            });
143    }
144
145    private static Component deleteButton(
146        final Commander cmd,
147        final RankLadder rank,
148        final SubjectRef<?> subject
149    ) {
150        return cmd.formatter().button(
151            text().content("-"),
152            ButtonType.NEGATIVE,
153            Messages.RANKING_BUTTON_DELETE_DESCRIPTION.tr(),
154            "/pex rank " + rank.name() + " remove " + subject.type().name() + " " + subject.serializedIdentifier(),
155            true
156        );
157    }
158
159    private static Component moveDownButton(
160        final Commander cmd,
161        final RankLadder rank,
162        final SubjectRef<?> subject
163    ) {
164        return cmd.formatter().button(
165            text().content("▼"),
166            ButtonType.NEUTRAL,
167            Messages.RANKING_BUTTON_MOVE_DOWN_DESCRIPTION.tr(),
168            "/pex rank " + rank.name() + " add " + subject.type().name() + " " + subject.serializedIdentifier() + " -r -1",
169            true
170        );
171    }
172
173    private static Component moveUpButton(
174        final Commander cmd,
175        final RankLadder ladder,
176        final SubjectRef<?> subject
177    ) {
178        return cmd.formatter().button(
179            text().content("▲"),
180            ButtonType.NEUTRAL,
181            Messages.RANKING_BUTTON_MOVE_UP_DESCRIPTION.tr(),
182            "/pex rank " + ladder.name() + " add " + subject.type().name() + " " + subject.serializedIdentifier() + " -r 1",
183            true
184        );
185    }
186
187    // Other subcommands
188
189    private static CommandArgument<Commander, SubjectType<?>> rankTypeArgument() {
190        return CommandArgument.<Commander, SubjectType<?>>ofType(new TypeToken<SubjectType<?>>() {}, "type")
191            .withParser(Parsers.subjectType())
192            .build();
193    }
194
195    private static CommandArgument<Commander, ?> rankIdentifierArgument(final CommandArgument<Commander, SubjectType<?>> parentType) {
196        return CommandArgument.<Commander, Object>ofType(Object.class, "identifier")
197            .withParser(Parsers.subjectIdentifier(data -> data.get(parentType)))
198            .build();
199    }
200
201
202    static Command.Builder<Commander> add(final Command.Builder<Commander> builder) {
203        final Permission perm = Permission.pex("rank.add");
204        final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument();
205        final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg);
206        final CommandArgument<Commander, Integer> positionArg = IntegerArgument.optional("position");
207        final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg);
208
209        return builder
210            .flag(FLAG_RELATIVE)
211            .argument(rankTypeArg)
212            .argument(rankIdentifierArg)
213            .argument(positionArg)
214            .permission(perm)
215            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_ADD_DESCRIPTION.tr())
216            .handler(handler((source, engine, ctx) -> {
217                final RankLadder ladder = ctx.get(LADDER_ARG);
218                source.checkPermission(perm.then(ladder.name()));
219                final SubjectRef<?> toAdd = rankProvider.provide(ctx);
220                final @Nullable Integer position = ctx.getOrDefault(positionArg, null);
221
222                if (position != null) {
223                    int addPosition = position;
224                    if (ctx.flags().isPresent(FLAG_RELATIVE.getName())) {
225                        final int currentIndex = ladder.indexOf(toAdd);
226                        if (currentIndex == -1) {
227                            throw new CommandException(Messages.RANKING_ADD_ERROR_RELATIVE_ON_OUTSIDE_SUBJECT.tr());
228                        }
229                        addPosition = (currentIndex + addPosition > 1) ? addPosition + 1 : addPosition; // If we are adding to later, we need to add after the next rank (otherwise we end up staying in the same place)
230                    }
231                    engine.ladders().set(ladder.name(), ladder.with(toAdd, addPosition))
232                        .whenComplete(messageSender(source, Messages.RANKING_ADD_SUCCESS_POSITION.tr(
233                            source.formatter().subject(toAdd),
234                            ladder.asComponent(),
235                            text(addPosition)
236                        )));
237                } else {
238                    // Append
239                    engine.ladders().set(ladder.name(), ladder.with(toAdd))
240                        .whenComplete(messageSender(source, Messages.RANKING_ADD_SUCCESS.tr(
241                            source.formatter().subject(toAdd),
242                            ladder.asComponent()
243                        )));
244                }
245            }));
246    }
247
248    static Command.Builder<Commander> remove(final Command.Builder<Commander> builder) {
249        final Permission perm = Permission.pex("rank.remove");
250        final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument();
251        final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg);
252        final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg);
253
254        return builder
255            .argument(rankTypeArg)
256            .argument(rankIdentifierArg)
257            .permission(perm)
258            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_REMOVE_DESCRIPTION.tr())
259            .handler(handler((source, engine, ctx) -> {
260                final RankLadder ladder = ctx.get(LADDER_ARG);
261                source.checkPermission(perm.then(ladder.name()));
262                final SubjectRef<?> toRemove = rankProvider.provide(ctx);
263                final RankLadder newLadder = ladder.without(toRemove);
264
265                if (newLadder == ladder) {
266                    throw new CommandException(Messages.RANKING_REMOVE_ERROR_NOT_IN_LADDER.tr(
267                        source.formatter().subject(toRemove),
268                        ladder.asComponent()
269                    ));
270                } else {
271                    engine.ladders().set(ladder.name(), newLadder)
272                        .whenComplete(messageSender(source, Messages.RANKING_REMOVE_SUCCESS.tr(
273                            source.formatter().subject(toRemove),
274                            ladder.asComponent()
275                        )));
276                }
277            }));
278    }
279
280    public static Command.Builder<Commander> promote(final Command.Builder<Commander> builder) {
281        final Permission promote = Permission.pex("promote");
282        final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument();
283        final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg);
284        final CommandArgument<Commander, RankLadder> ladderArg = ladderArgument(false);
285        final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg);
286
287        return builder
288            .flag(FLAG_TRANSIENT)
289            .flag(FLAG_CONTEXT)
290            .argument(rankTypeArg)
291            .argument(rankIdentifierArg)
292            .argument(ladderArg)
293            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_PROMOTE_DESCRIPTION.tr())
294            .permission(promote)
295            .handler(handler((source, engine, ctx) -> {
296                final RankLadder ladder = ctx.get(ladderArg);
297                final Set<ContextValue<?>> contexts = Elements.contexts(ctx);
298                final SubjectRef.ToData<?> subject = rankProvider.provideData(ctx, promote);
299                subject.update(data -> ladder.promote(contexts, data))
300                    .thenAccept(change -> {
301                        if (!change.changed()) {
302                            throw new CommandException(Messages.PROMOTE_ERROR_ALREADY_AT_TOP.tr(
303                                source.formatter().subject(subject),
304                                ladder.asComponent()
305                            ));
306                        }
307                    }).whenComplete(messageSender(source, Messages.PROMOTE_SUCCESS.tr(
308                    source.formatter().subject(subject),
309                    ladder.asComponent().style(source.formatter()::hl)
310                )));
311            }));
312    }
313
314    public static Command.Builder<Commander> demote(final Command.Builder<Commander> builder) {
315        final Permission promote = Permission.pex("demote");
316        final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument();
317        final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg);
318        final CommandArgument<Commander, RankLadder> ladderArg = ladderArgument(false);
319        final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg);
320
321        return builder
322            .flag(FLAG_TRANSIENT)
323            .flag(FLAG_CONTEXT)
324            .argument(rankTypeArg)
325            .argument(rankIdentifierArg)
326            .argument(ladderArg)
327            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_DEMOTE_DESCRIPTION.tr())
328            .permission(promote)
329            .handler(handler((source, engine, ctx) -> {
330                final RankLadder ladder = ctx.get(ladderArg);
331                final Set<ContextValue<?>> contexts = Elements.contexts(ctx);
332                final SubjectRef.ToData<?> subject = rankProvider.provideData(ctx, promote);
333                subject.update(data -> ladder.promote(contexts, data))
334                    .thenAccept(change -> {
335                        if (!change.changed()) {
336                            throw new CommandException(Messages.DEMOTE_ERROR_NOT_ON_LADDER.tr(
337                                source.formatter().subject(subject),
338                                ladder.asComponent()
339                            ));
340                        }
341                    }).whenComplete(messageSender(source, Messages.DEMOTE_SUCCESS.tr(
342                    source.formatter().subject(subject),
343                    ladder.asComponent().style(source.formatter()::hl)
344                )));
345            }));
346    }
347
348}