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.TranslatableCaption;
025import com.plotsquared.core.events.TeleportCause;
026import com.plotsquared.core.permissions.Permission;
027import com.plotsquared.core.player.PlotPlayer;
028import com.plotsquared.core.plot.Plot;
029import com.plotsquared.core.plot.PlotArea;
030import com.plotsquared.core.plot.flag.implementations.UntrustedVisitFlag;
031import com.plotsquared.core.plot.world.PlotAreaManager;
032import com.plotsquared.core.util.MathMan;
033import com.plotsquared.core.util.PlayerManager;
034import com.plotsquared.core.util.TabCompletions;
035import com.plotsquared.core.util.query.PlotQuery;
036import com.plotsquared.core.util.query.SortingStrategy;
037import com.plotsquared.core.util.task.RunnableVal2;
038import com.plotsquared.core.util.task.RunnableVal3;
039import net.kyori.adventure.text.Component;
040import net.kyori.adventure.text.minimessage.tag.Tag;
041import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
042import org.checkerframework.checker.nullness.qual.NonNull;
043
044import java.util.ArrayList;
045import java.util.Collection;
046import java.util.Collections;
047import java.util.List;
048import java.util.UUID;
049import java.util.concurrent.CompletableFuture;
050import java.util.concurrent.TimeoutException;
051
052@CommandDeclaration(command = "visit",
053        permission = "plots.visit",
054        usage = "/plot visit <player> | <alias> | <plot> [area]|[#] [#]",
055        aliases = {"v", "tp", "teleport", "goto", "warp"},
056        requiredType = RequiredType.PLAYER,
057        category = CommandCategory.TELEPORT)
058public class Visit extends Command {
059
060    private final PlotAreaManager plotAreaManager;
061
062    @Inject
063    public Visit(final @NonNull PlotAreaManager plotAreaManager) {
064        super(MainCommand.getInstance(), true);
065        this.plotAreaManager = plotAreaManager;
066    }
067
068    private void visit(
069            final @NonNull PlotPlayer<?> player, final @NonNull PlotQuery query, final PlotArea sortByArea,
070            final RunnableVal3<Command, Runnable, Runnable> confirm, final RunnableVal2<Command, CommandResult> whenDone, int page
071    ) {
072        // We get the query once,
073        // then we get it another time further on
074        final List<Plot> unsorted = query.asList();
075
076        if (unsorted.size() > 1) {
077            query.whereBasePlot();
078        }
079
080        // without specified argument
081        if (page == Integer.MIN_VALUE) {
082            page = 1;
083        }
084
085        PlotArea relativeArea = sortByArea;
086        if (Settings.Teleport.PER_WORLD_VISIT && sortByArea == null) {
087            relativeArea = player.getApplicablePlotArea();
088        }
089
090        if (relativeArea != null) {
091            query.relativeToArea(relativeArea).withSortingStrategy(SortingStrategy.SORT_BY_CREATION);
092        } else {
093            query.withSortingStrategy(SortingStrategy.SORT_BY_TEMP);
094        }
095
096        final List<Plot> plots = query.asList();
097
098        // Conversion of reversed page argument
099        if (page < 0) {
100            page = (plots.size() + 1) + page;
101        }
102
103        if (plots.isEmpty()) {
104            player.sendMessage(TranslatableCaption.of("invalid.found_no_plots"));
105            return;
106        } else if (page > plots.size() || page < 1) {
107            player.sendMessage(
108                    TranslatableCaption.of("invalid.number_not_in_range"),
109                    TagResolver.builder()
110                            .tag("min", Tag.inserting(Component.text(1)))
111                            .tag("max", Tag.inserting(Component.text(plots.size())))
112                            .build()
113            );
114            return;
115        }
116
117        final Plot plot = plots.get(page - 1);
118        if (!plot.hasOwner()) {
119            if (!player.hasPermission(Permission.PERMISSION_VISIT_UNOWNED)) {
120                player.sendMessage(
121                        TranslatableCaption.of("permission.no_permission"),
122                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.unowned")))
123                );
124                return;
125            }
126        } else if (plot.isOwner(player.getUUID())) {
127            if (!player.hasPermission(Permission.PERMISSION_VISIT_OWNED) && !player.hasPermission(Permission.PERMISSION_HOME)) {
128                player.sendMessage(
129                        TranslatableCaption.of("permission.no_permission"),
130                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.owned")))
131                );
132                return;
133            }
134        } else if (plot.isAdded(player.getUUID())) {
135            if (!player.hasPermission(Permission.PERMISSION_SHARED)) {
136                player.sendMessage(
137                        TranslatableCaption.of("permission.no_permission"),
138                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.shared")))
139                );
140                return;
141            }
142        } else {
143            // allow visit, if UntrustedVisit flag is set, or if the player has either the plot.visit.other or
144            // plot.admin.visit.untrusted permission
145            if (!plot.getFlag(UntrustedVisitFlag.class) && !player.hasPermission(Permission.PERMISSION_VISIT_OTHER)
146                    && !player.hasPermission(Permission.PERMISSION_ADMIN_VISIT_UNTRUSTED)) {
147                player.sendMessage(
148                        TranslatableCaption.of("permission.no_permission"),
149                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.visit.other")))
150                );
151                return;
152            }
153            if (plot.isDenied(player.getUUID())) {
154                if (!player.hasPermission(Permission.PERMISSION_VISIT_DENIED)) {
155                    player.sendMessage(
156                            TranslatableCaption.of("permission.no_permission"),
157                            TagResolver.resolver(
158                                    "node",
159                                    Tag.inserting(Permission.PERMISSION_VISIT_DENIED)
160                            )
161                    );
162                    return;
163                }
164            }
165        }
166
167        confirm.run(this, () -> plot.teleportPlayer(player, TeleportCause.COMMAND_VISIT, result -> {
168            if (result) {
169                whenDone.run(Visit.this, CommandResult.SUCCESS);
170            } else {
171                whenDone.run(Visit.this, CommandResult.FAILURE);
172            }
173        }), () -> whenDone.run(Visit.this, CommandResult.FAILURE));
174    }
175
176    @Override
177    public CompletableFuture<Boolean> execute(
178            final PlotPlayer<?> player,
179            String[] args,
180            final RunnableVal3<Command, Runnable, Runnable> confirm,
181            final RunnableVal2<Command, CommandResult> whenDone
182    ) throws CommandException {
183        if (args.length > 3) {
184            sendUsage(player);
185            return CompletableFuture.completedFuture(false);
186        }
187
188        if (args.length == 1 && args[0].contains(":")) {
189            args = args[0].split(":");
190        }
191
192        PlotArea sortByArea;
193
194        int page = Integer.MIN_VALUE;
195
196        switch (args.length) {
197            // /p v <player> <area> <page>
198            case 3:
199                if (isInvalidPageNr(args[2])) {
200                    sendInvalidPageNrMsg(player);
201                    return CompletableFuture.completedFuture(false);
202                }
203                page = getPageNr(args[2]);
204                // /p v <player> <area> [page]
205                // /p v <player> [page]
206            case 2:
207                // If "case 3" is already through or the argument is not a page number:
208                // -> /p v <player> <area> [page]
209                if (page != Integer.MIN_VALUE || isInvalidPageNr(args[1])) {
210                    sortByArea = this.plotAreaManager.getPlotAreaByString(args[1]);
211                    if (sortByArea == null) {
212                        sendInvalidPageNrMsg(player);
213                        return CompletableFuture.completedFuture(false);
214                    }
215
216                    final PlotArea finalSortByArea = sortByArea;
217                    int finalPage1 = page;
218                    PlayerManager.getUUIDsFromString(args[0], (uuids, throwable) -> {
219                        if (throwable instanceof TimeoutException) {
220                            player.sendMessage(TranslatableCaption.of("players.fetching_players_timeout"));
221                        } else if (throwable != null || uuids.size() != 1) {
222                            player.sendMessage(
223                                    TranslatableCaption.of("commandconfig.command_syntax"),
224                                    TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
225                            );
226                        } else {
227                            final UUID uuid = uuids.toArray(new UUID[0])[0];
228                            PlotQuery query = PlotQuery.newQuery();
229                            if (Settings.Teleport.VISIT_MERGED_OWNERS) {
230                                query.whereBasePlot().ownersInclude(uuid);
231                            } else {
232                                query.whereBasePlot().ownedBy(uuid);
233                            }
234                            this.visit(
235                                    player,
236                                    query,
237                                    finalSortByArea,
238                                    confirm,
239                                    whenDone,
240                                    finalPage1
241                            );
242                        }
243                    });
244                    break;
245                }
246                // -> /p v <player> <page>
247                if (isInvalidPageNr(args[1])) {
248                    sendInvalidPageNrMsg(player);
249                    return CompletableFuture.completedFuture(false);
250                }
251                page = getPageNr(args[1]);
252                // /p v <player> [page]
253                // /p v <uuid> [page]
254                // /p v <plot> [page]
255                // /p v <alias>
256            case 1:
257                final String[] finalArgs = args;
258                int finalPage = page;
259                if (args[0].length() >= 2 && !args[0].contains(";") && !args[0].contains(",")) {
260                    PlotSquared.get().getImpromptuUUIDPipeline().getSingle(args[0], (uuid, throwable) -> {
261                        if (throwable instanceof TimeoutException) {
262                            // The request timed out
263                            player.sendMessage(TranslatableCaption.of("players.fetching_players_timeout"));
264                        } else if (uuid != null && (Settings.Teleport.VISIT_MERGED_OWNERS
265                                ? !PlotQuery.newQuery().ownersInclude(uuid).anyMatch()
266                                : !PlotQuery.newQuery().ownedBy(uuid).anyMatch())) {
267                            // It was a valid UUID but the player has no plots
268                            player.sendMessage(TranslatableCaption.of("errors.player_no_plots"));
269                        } else if (uuid == null) {
270                            // player not found, so we assume it's an alias if no page was provided
271                            if (finalPage == Integer.MIN_VALUE) {
272                                this.visit(
273                                        player,
274                                        PlotQuery.newQuery().withAlias(finalArgs[0]),
275                                        player.getApplicablePlotArea(),
276                                        confirm,
277                                        whenDone,
278                                        1
279                                );
280                            } else {
281                                player.sendMessage(
282                                        TranslatableCaption.of("errors.invalid_player"),
283                                        TagResolver.resolver("value", Tag.inserting(Component.text(finalArgs[0])))
284                                );
285                            }
286                        } else {
287                            this.visit(
288                                    player,
289                                    Settings.Teleport.VISIT_MERGED_OWNERS
290                                            ? PlotQuery.newQuery().ownersInclude(uuid).whereBasePlot()
291                                            : PlotQuery.newQuery().ownedBy(uuid).whereBasePlot(),
292                                    null,
293                                    confirm,
294                                    whenDone,
295                                    finalPage
296                            );
297                        }
298                    });
299                } else {
300                    // Try to parse a plot
301                    final Plot plot = Plot.getPlotFromString(player, finalArgs[0], true);
302                    if (plot != null) {
303                        this.visit(player, PlotQuery.newQuery().withPlot(plot), null, confirm, whenDone, 1);
304                    }
305                }
306                break;
307            case 0:
308                // /p v is invalid
309                player.sendMessage(
310                        TranslatableCaption.of("commandconfig.command_syntax"),
311                        TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
312                );
313                return CompletableFuture.completedFuture(false);
314            default:
315        }
316
317        return CompletableFuture.completedFuture(true);
318    }
319
320    private boolean isInvalidPageNr(String arg) {
321        if (MathMan.isInteger(arg)) {
322            return false;
323        } else if (arg.equals("last") || arg.equals("n")) {
324            return false;
325        }
326        return true;
327    }
328
329    private int getPageNr(String arg) {
330        if (MathMan.isInteger(arg)) {
331            return Integer.parseInt(arg);
332        } else if (arg.equals("last") || arg.equals("n")) {
333            return -1;
334        }
335        return Integer.MIN_VALUE;
336    }
337
338    private void sendInvalidPageNrMsg(PlotPlayer<?> player) {
339        player.sendMessage(
340                TranslatableCaption.of("invalid.not_valid_number"),
341                TagResolver.resolver("value", Tag.inserting(Component.text("(1, ∞)")))
342        );
343        player.sendMessage(
344                TranslatableCaption.of("commandconfig.command_syntax"),
345                TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
346        );
347    }
348
349    @Override
350    public Collection<Command> tab(PlotPlayer<?> player, String[] args, boolean space) {
351        final List<Command> completions = new ArrayList<>();
352        switch (args.length - 1) {
353            case 0 -> completions.addAll(TabCompletions.completePlayers(player, args[0], Collections.emptyList()));
354            case 1 -> {
355                completions.addAll(
356                        TabCompletions.completeAreas(args[1]));
357                completions.addAll(TabCompletions.asCompletions("last"));
358                if (args[1].isEmpty()) {
359                    // if no input is given, only suggest 1 - 3
360                    completions.addAll(
361                            TabCompletions.asCompletions("1", "2", "3"));
362                    break;
363                }
364                completions.addAll(
365                        TabCompletions.completeNumbers(args[1], 10, 999));
366            }
367            case 2 -> {
368                completions.addAll(TabCompletions.asCompletions("last"));
369                if (args[2].isEmpty()) {
370                    // if no input is given, only suggest 1 - 3
371                    completions.addAll(
372                            TabCompletions.asCompletions("1", "2", "3"));
373                    break;
374                }
375                completions.addAll(
376                        TabCompletions.completeNumbers(args[2], 10, 999));
377            }
378        }
379
380        return completions;
381    }
382
383}