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 cloud.commandframework.services.ServicePipeline;
022import com.google.inject.Inject;
023import com.plotsquared.core.PlotSquared;
024import com.plotsquared.core.configuration.Settings;
025import com.plotsquared.core.configuration.caption.TranslatableCaption;
026import com.plotsquared.core.database.DBFunc;
027import com.plotsquared.core.events.PlayerAutoPlotEvent;
028import com.plotsquared.core.events.PlotAutoMergeEvent;
029import com.plotsquared.core.events.Result;
030import com.plotsquared.core.permissions.Permission;
031import com.plotsquared.core.permissions.PermissionHandler;
032import com.plotsquared.core.player.MetaDataAccess;
033import com.plotsquared.core.player.PlayerMetaDataKeys;
034import com.plotsquared.core.player.PlotPlayer;
035import com.plotsquared.core.plot.Plot;
036import com.plotsquared.core.plot.PlotArea;
037import com.plotsquared.core.plot.world.PlotAreaManager;
038import com.plotsquared.core.services.plots.AutoQuery;
039import com.plotsquared.core.services.plots.AutoService;
040import com.plotsquared.core.util.EconHandler;
041import com.plotsquared.core.util.EventDispatcher;
042import com.plotsquared.core.util.PlotExpression;
043import com.plotsquared.core.util.task.AutoClaimFinishTask;
044import com.plotsquared.core.util.task.RunnableVal;
045import com.plotsquared.core.util.task.TaskManager;
046import io.leangen.geantyref.TypeToken;
047import net.kyori.adventure.text.Component;
048import net.kyori.adventure.text.minimessage.tag.Tag;
049import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
050import org.checkerframework.checker.nullness.qual.NonNull;
051import org.checkerframework.checker.nullness.qual.Nullable;
052
053import java.util.Collections;
054import java.util.Iterator;
055import java.util.List;
056import java.util.stream.Collectors;
057
058@CommandDeclaration(command = "auto",
059        permission = "plots.auto",
060        category = CommandCategory.CLAIMING,
061        requiredType = RequiredType.NONE,
062        aliases = "a",
063        usage = "/plot auto [length, width]")
064public class Auto extends SubCommand {
065
066    private final PlotAreaManager plotAreaManager;
067    private final EventDispatcher eventDispatcher;
068    private final EconHandler econHandler;
069    private final ServicePipeline servicePipeline;
070
071    @Inject
072    public Auto(
073            final @NonNull PlotAreaManager plotAreaManager,
074            final @NonNull EventDispatcher eventDispatcher,
075            final @NonNull EconHandler econHandler,
076            final @NonNull ServicePipeline servicePipeline
077    ) {
078        this.plotAreaManager = plotAreaManager;
079        this.eventDispatcher = eventDispatcher;
080        this.econHandler = econHandler;
081        this.servicePipeline = servicePipeline;
082        this.servicePipeline.registerServiceType(TypeToken.get(AutoService.class), new AutoService.DefaultAutoService());
083        final AutoService.MultiPlotService multiPlotService = new AutoService.MultiPlotService();
084        this.servicePipeline.registerServiceImplementation(AutoService.class, multiPlotService,
085                Collections.singletonList(multiPlotService)
086        );
087        final AutoService.SinglePlotService singlePlotService = new AutoService.SinglePlotService();
088        this.servicePipeline.registerServiceImplementation(AutoService.class, singlePlotService,
089                Collections.singletonList(singlePlotService)
090        );
091    }
092
093    public static boolean checkAllowedPlots(
094            PlotPlayer<?> player, PlotArea plotarea,
095            @Nullable Integer allowedPlots, int sizeX, int sizeZ
096    ) {
097        if (allowedPlots == null) {
098            allowedPlots = player.getAllowedPlots();
099        }
100        int currentPlots;
101        if (Settings.Limit.GLOBAL) {
102            currentPlots = player.getPlotCount();
103        } else {
104            currentPlots = player.getPlotCount(plotarea.getWorldName());
105        }
106        int diff = allowedPlots - currentPlots;
107        if (diff - sizeX * sizeZ < 0) {
108            try (final MetaDataAccess<Integer> metaDataAccess = player.accessPersistentMetaData(
109                    PlayerMetaDataKeys.PERSISTENT_GRANTED_PLOTS)) {
110                if (metaDataAccess.isPresent()) {
111                    int grantedPlots = metaDataAccess.get().orElse(0);
112                    if (diff < 0 && grantedPlots < sizeX * sizeZ) {
113                        player.sendMessage(
114                                TranslatableCaption.of("permission.cant_claim_more_plots"),
115                                TagResolver.resolver("amount", Tag.inserting(Component.text(diff + grantedPlots)))
116                        );
117                        return false;
118                    } else if (diff >= 0 && grantedPlots + diff < sizeX * sizeZ) {
119                        player.sendMessage(
120                                TranslatableCaption.of("permission.cant_claim_more_plots"),
121                                TagResolver.resolver("amount", Tag.inserting(Component.text(diff + grantedPlots)))
122                        );
123                        return false;
124                    } else {
125                        int left = grantedPlots + diff < 0 ? 0 : diff - sizeX * sizeZ;
126                        if (left == 0) {
127                            metaDataAccess.remove();
128                        } else {
129                            metaDataAccess.set(left);
130                        }
131                        player.sendMessage(
132                                TranslatableCaption.of("economy.removed_granted_plot"),
133                                TagResolver.builder()
134                                        .tag("usedGrants", Tag.inserting(Component.text(grantedPlots - left)))
135                                        .tag("remainingGrants", Tag.inserting(Component.text(left)))
136                                        .build()
137                        );
138                    }
139                } else {
140                    player.sendMessage(
141                            TranslatableCaption.of("permission.cant_claim_more_plots"),
142                            TagResolver.resolver("amount", Tag.inserting(Component.text(player.getAllowedPlots())))
143                    );
144                    return false;
145                }
146            }
147        }
148        return true;
149    }
150
151    private void claimSingle(
152            final @NonNull PlotPlayer<?> player, final @NonNull Plot plot,
153            final @NonNull PlotArea plotArea, final @Nullable String schematic
154    ) {
155        try (final MetaDataAccess<Boolean> metaDataAccess =
156                     player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_AUTO)) {
157            metaDataAccess.set(true);
158        }
159        plot.setOwnerAbs(player.getUUID());
160
161        final RunnableVal<Plot> runnableVal = new RunnableVal<>() {
162            {
163                this.value = plot;
164            }
165
166            @Override
167            public void run(final Plot plot) {
168                try {
169                    TaskManager.getPlatformImplementation().sync(
170                            new AutoClaimFinishTask(player, plot, plotArea, schematic,
171                                    PlotSquared.get().getEventDispatcher()
172                            ));
173                } catch (final Exception e) {
174                    e.printStackTrace();
175                }
176            }
177        };
178
179        DBFunc.createPlotSafe(plot, runnableVal, () -> claimSingle(player, plot, plotArea, schematic));
180
181    }
182
183    @Override
184    public boolean onCommand(final PlotPlayer<?> player, String[] args) {
185        PlotArea plotarea = player.getApplicablePlotArea();
186        if (plotarea == null) {
187            final PermissionHandler permissionHandler = PlotSquared.platform().permissionHandler();
188            if (permissionHandler.hasCapability(
189                    PermissionHandler.PermissionHandlerCapability.PER_WORLD_PERMISSIONS)) {
190                for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
191                    if (player.hasPermission(area.getWorldName(), "plots.auto")) {
192                        if (plotarea != null) {
193                            plotarea = null;
194                            break;
195                        }
196                        plotarea = area;
197                    }
198                }
199            }
200            if (this.plotAreaManager.getAllPlotAreas().length == 1) {
201                plotarea = this.plotAreaManager.getAllPlotAreas()[0];
202            }
203            if (plotarea == null) {
204                player.sendMessage(TranslatableCaption.of("errors.not_in_plot_world"));
205                return false;
206            }
207        }
208        int sizeX = 1;
209        int sizeZ = 1;
210        String schematic = null;
211        boolean mega = false;
212        if (args.length > 0) {
213            try {
214                String[] split = args[0].split("[,;]");
215                if (split.length == 2) {
216                    sizeX = Integer.parseInt(split[0]);
217                    sizeZ = Integer.parseInt(split[1]);
218                } else {
219                    player.sendMessage(
220                            TranslatableCaption.of("commandconfig.command_syntax"),
221                            TagResolver.resolver("value", Tag.inserting(Component.text(getUsage())))
222                    );
223                    return true;
224                }
225                if (sizeX < 1 || sizeZ < 1) {
226                    player.sendMessage(TranslatableCaption.of("error.plot_size_negative"));
227                    return true;
228                }
229                if (args.length > 1) {
230                    schematic = args[1];
231                }
232                mega = true;
233            } catch (NumberFormatException ignored) {
234                sizeX = 1;
235                sizeZ = 1;
236                schematic = args[0];
237            }
238        }
239        PlayerAutoPlotEvent event = this.eventDispatcher
240                .callAuto(player, plotarea, schematic, sizeX, sizeZ);
241        if (event.getEventResult() == Result.DENY) {
242            player.sendMessage(
243                    TranslatableCaption.of("events.event_denied"),
244                    TagResolver.resolver("value", Tag.inserting(Component.text("Auto claim")))
245            );
246            return true;
247        }
248        boolean force = event.getEventResult() == Result.FORCE;
249        sizeX = event.getSizeX();
250        sizeZ = event.getSizeZ();
251        schematic = event.getSchematic();
252        if (!force && mega && !player.hasPermission(Permission.PERMISSION_AUTO_MEGA)) {
253            player.sendMessage(
254                    TranslatableCaption.of("permission.no_permission"),
255                    TagResolver.resolver("node", Tag.inserting(Permission.PERMISSION_AUTO_MEGA))
256            );
257            return false;
258        }
259        if (!force && sizeX * sizeZ > Settings.Claim.MAX_AUTO_AREA) {
260            player.sendMessage(
261                    TranslatableCaption.of("permission.cant_claim_more_plots_num"),
262                    TagResolver.resolver("amount", Tag.inserting(Component.text(Settings.Claim.MAX_AUTO_AREA)))
263            );
264            return false;
265        }
266        final int allowed_plots = player.getAllowedPlots();
267        try (final MetaDataAccess<Boolean> metaDataAccess =
268                     player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_AUTO)) {
269            if (!force && (metaDataAccess.get().orElse(false) || !checkAllowedPlots(player,
270                    plotarea, allowed_plots, sizeX, sizeZ
271            ))) {
272                return false;
273            }
274        }
275
276        if (schematic != null && !schematic.isEmpty()) {
277            if (!plotarea.hasSchematic(schematic)) {
278                player.sendMessage(
279                        TranslatableCaption.of("schematics.schematic_invalid_named"),
280                        TagResolver.builder()
281                                .tag("schemname", Tag.inserting(Component.text(schematic)))
282                                .tag("reason", Tag.inserting(Component.text("non-existent")))
283                                .build()
284                );
285                return true;
286            }
287            if (!force && !player.hasPermission(
288                    Permission.PERMISSION_CLAIM_SCHEMATIC.format(schematic)
289            ) && !player.hasPermission("plots.admin.command.schematic")) {
290                player.sendMessage(
291                        TranslatableCaption.of("permission.no_permission"),
292                        TagResolver.resolver("node", Tag.inserting(Component.text("plots.claim.%s0")))
293                );
294                return true;
295            }
296        }
297        if (this.econHandler != null && plotarea.useEconomy() && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON)) {
298            PlotExpression costExp = plotarea.getPrices().get("claim");
299            PlotExpression mergeCostExp = plotarea.getPrices().get("merge");
300            int size = sizeX * sizeZ;
301            double mergeCost = size <= 1 || mergeCostExp == null ? 0d : mergeCostExp.evaluate(size);
302            double cost = costExp.evaluate(Settings.Limit.GLOBAL ?
303                    player.getPlotCount() :
304                    player.getPlotCount(plotarea.getWorldName()));
305            cost = size * cost + mergeCost;
306            if (cost > 0d) {
307                if (!this.econHandler.isSupported()) {
308                    player.sendMessage(TranslatableCaption.of("economy.vault_or_consumer_null"));
309                    return false;
310                }
311                if (!force && this.econHandler.getMoney(player) < cost) {
312                    player.sendMessage(
313                            TranslatableCaption.of("economy.cannot_afford_plot"),
314                            TagResolver.builder()
315                                    .tag("money", Tag.inserting(Component.text(this.econHandler.format(cost))))
316                                    .tag(
317                                            "balance",
318                                            Tag.inserting(Component.text(this.econHandler.format(this.econHandler.getMoney(player))))
319                                    )
320                                    .build()
321                    );
322                    return false;
323                }
324                this.econHandler.withdrawMoney(player, cost);
325                player.sendMessage(
326                        TranslatableCaption.of("economy.removed_balance"),
327                        TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(cost))))
328                );
329            }
330        }
331
332        List<Plot> plots = this.servicePipeline
333                .pump(new AutoQuery(player, null, sizeX, sizeZ, plotarea))
334                .through(AutoService.class)
335                .getResult();
336
337        plots = this.eventDispatcher.callAutoPlotsChosen(player, plots).getPlots();
338
339        if (plots.isEmpty()) {
340            player.sendMessage(TranslatableCaption.of("errors.no_free_plots"));
341            return false;
342        } else if (plots.size() == 1) {
343            this.claimSingle(player, plots.get(0), plotarea, schematic);
344        } else {
345            final Iterator<Plot> plotIterator = plots.iterator();
346            while (plotIterator.hasNext()) {
347                Plot plot = plotIterator.next();
348                if (!plot.canClaim(player)) {
349                    continue;
350                }
351                plot.claim(player, !plotIterator.hasNext(), null, true, true);
352                eventDispatcher.callPostAuto(player, plot);
353            }
354            final PlotAutoMergeEvent mergeEvent = this.eventDispatcher.callAutoMerge(
355                    plots.get(0),
356                    plots.stream().map(Plot::getId).collect(Collectors.toList())
357            );
358            if (!force && mergeEvent.getEventResult() == Result.DENY) {
359                player.sendMessage(
360                        TranslatableCaption.of("events.event_denied"),
361                        TagResolver.resolver("value", Tag.inserting(Component.text("Auto merge")))
362                );
363                return false;
364            }
365            return plotarea.mergePlots(mergeEvent.getPlots(), true);
366        }
367        return true;
368    }
369
370}