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.PlotMergeEvent;
026import com.plotsquared.core.events.Result;
027import com.plotsquared.core.location.Direction;
028import com.plotsquared.core.location.Location;
029import com.plotsquared.core.permissions.Permission;
030import com.plotsquared.core.player.PlotPlayer;
031import com.plotsquared.core.plot.Plot;
032import com.plotsquared.core.plot.PlotArea;
033import com.plotsquared.core.util.EconHandler;
034import com.plotsquared.core.util.EventDispatcher;
035import com.plotsquared.core.util.PlotExpression;
036import com.plotsquared.core.util.StringMan;
037import net.kyori.adventure.text.Component;
038import net.kyori.adventure.text.minimessage.tag.Tag;
039import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
040import org.checkerframework.checker.nullness.qual.NonNull;
041
042import java.util.UUID;
043import java.util.function.Supplier;
044
045@CommandDeclaration(command = "merge",
046        aliases = "m",
047        permission = "plots.merge",
048        usage = "/plot merge <all | n | e | s | w> [removeroads]",
049        category = CommandCategory.SETTINGS,
050        requiredType = RequiredType.NONE,
051        confirmation = true)
052public class Merge extends SubCommand {
053
054    public static final String[] values = new String[]{"north", "east", "south", "west"};
055    public static final String[] aliases = new String[]{"n", "e", "s", "w"};
056
057    private final EventDispatcher eventDispatcher;
058    private final EconHandler econHandler;
059
060    @Inject
061    public Merge(
062            final @NonNull EventDispatcher eventDispatcher,
063            final @NonNull EconHandler econHandler
064    ) {
065        this.eventDispatcher = eventDispatcher;
066        this.econHandler = econHandler;
067    }
068
069    public static String direction(float yaw) {
070        yaw = yaw / 90;
071        int i = Math.round(yaw);
072        return switch (i) {
073            case -4, 0, 4 -> "SOUTH";
074            case -1, 3 -> "EAST";
075            case -2, 2 -> "NORTH";
076            case -3, 1 -> "WEST";
077            default -> "";
078        };
079    }
080
081    @Override
082    public boolean onCommand(final PlotPlayer<?> player, String[] args) {
083        Location location = player.getLocationFull();
084        final Plot plot = location.getPlotAbs();
085        if (plot == null) {
086            player.sendMessage(TranslatableCaption.of("errors.not_in_plot"));
087            return false;
088        }
089        if (!plot.hasOwner()) {
090            player.sendMessage(TranslatableCaption.of("info.plot_unowned"));
091            return false;
092        }
093        if (plot.getVolume() > Integer.MAX_VALUE) {
094            player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
095            return false;
096        }
097        Direction direction = null;
098        if (args.length == 0) {
099            switch (direction(player.getLocationFull().getYaw())) {
100                case "NORTH" -> direction = Direction.NORTH;
101                case "EAST" -> direction = Direction.EAST;
102                case "SOUTH" -> direction = Direction.SOUTH;
103                case "WEST" -> direction = Direction.WEST;
104            }
105        } else {
106            for (int i = 0; i < values.length; i++) {
107                if (args[0].equalsIgnoreCase(values[i]) || args[0].equalsIgnoreCase(aliases[i])) {
108                    direction = Direction.getFromIndex(i);
109                    break;
110                }
111            }
112            if (direction == null && (args[0].equalsIgnoreCase("all") || args[0]
113                    .equalsIgnoreCase("auto")) && player.hasPermission(Permission.PERMISSION_MERGE_ALL)) {
114                direction = Direction.ALL;
115            }
116        }
117        if (direction == null) {
118            player.sendMessage(
119                    TranslatableCaption.of("commandconfig.command_syntax"),
120                    TagResolver.resolver(
121                            "value", Tag.inserting(Component.text(
122                                    "/plot merge <" + StringMan.join(values, " | ") + "> [removeroads]"
123                            ))
124                    )
125            );
126            player.sendMessage(
127                    TranslatableCaption.of("help.direction"),
128                    TagResolver.resolver("dir", Tag.inserting(Component.text(direction(location.getYaw()))))
129            );
130            return false;
131        }
132        final int size = plot.getConnectedPlots().size();
133        int max = player.hasPermissionRange("plots.merge", Settings.Limit.MAX_PLOTS);
134        PlotMergeEvent event =
135                this.eventDispatcher.callMerge(plot, direction, max, player);
136        if (event.getEventResult() == Result.DENY) {
137            player.sendMessage(
138                    TranslatableCaption.of("events.event_denied"),
139                    TagResolver.resolver("value", Tag.inserting(Component.text("Merge")))
140            );
141            return false;
142        }
143        boolean force = event.getEventResult() == Result.FORCE;
144        direction = event.getDir();
145        final int maxSize = event.getMax();
146
147        if (!force && size - 1 > maxSize) {
148            player.sendMessage(
149                    TranslatableCaption.of("permission.no_permission"),
150                    TagResolver.resolver("node", Tag.inserting(Component.text(Permission.PERMISSION_MERGE + "." + (size + 1))))
151            );
152            return false;
153        }
154        final PlotArea plotArea = plot.getArea();
155        PlotExpression priceExr = plotArea.getPrices().getOrDefault("merge", null);
156        final double price = priceExr == null ? 0d : priceExr.evaluate(size);
157
158        UUID uuid = player.getUUID();
159
160        if (!force) {
161            if (!plot.isOwner(uuid)) {
162                if (!player.hasPermission(Permission.PERMISSION_ADMIN_COMMAND_MERGE)) {
163                    player.sendMessage(TranslatableCaption.of("permission.no_plot_perms"));
164                    return false;
165                } else {
166                    uuid = plot.getOwnerAbs();
167                }
168            }
169
170            if (this.econHandler.isEnabled(plotArea) && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON) && price > 0d && this.econHandler.getMoney(
171                    player) < price) {
172                player.sendMessage(
173                        TranslatableCaption.of("economy.cannot_afford_merge"),
174                        TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price))))
175                );
176                return false;
177            }
178        }
179
180        if (direction == Direction.ALL) {
181            boolean terrain = true;
182            if (args.length == 2) {
183                terrain = "true".equalsIgnoreCase(args[1]);
184            }
185            if (!force && !terrain && !player.hasPermission(Permission.PERMISSION_MERGE_KEEP_ROAD)) {
186                player.sendMessage(
187                        TranslatableCaption.of("permission.no_permission"),
188                        TagResolver.resolver(
189                                "node",
190                                Tag.inserting(Permission.PERMISSION_MERGE_KEEP_ROAD)
191                        )
192                );
193                return true;
194            }
195            if (plot.getPlotModificationManager().autoMerge(Direction.ALL, maxSize, uuid, player, terrain)) {
196                this.econHandler.withdrawMoney(player, price);
197                player.sendMessage(
198                        TranslatableCaption.of("economy.removed_balance"),
199                        TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price)))),
200                        TagResolver.resolver(
201                                "balance",
202                                Tag.inserting(Component.text(this.econHandler.format(this.econHandler.getMoney(player))))
203                        )
204                );
205                player.sendMessage(TranslatableCaption.of("merge.success_merge"));
206                eventDispatcher.callPostMerge(player, plot);
207                return true;
208            }
209            player.sendMessage(TranslatableCaption.of("merge.no_available_automerge"));
210            return false;
211        }
212        final boolean terrain;
213        if (args.length == 2) {
214            terrain = "true".equalsIgnoreCase(args[1]);
215        } else {
216            terrain = true;
217        }
218        if (!force && !terrain && !player.hasPermission(Permission.PERMISSION_MERGE_KEEP_ROAD)) {
219            player.sendMessage(
220                    TranslatableCaption.of("permission.no_permission"),
221                    TagResolver.resolver("node", Tag.inserting(Permission.PERMISSION_MERGE_KEEP_ROAD))
222            );
223            return true;
224        }
225        if (plot.getPlotModificationManager().autoMerge(direction, maxSize - size, uuid, player, terrain)) {
226            if (this.econHandler.isEnabled(plotArea) && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON) && price > 0d) {
227                this.econHandler.withdrawMoney(player, price);
228                player.sendMessage(
229                        TranslatableCaption.of("economy.removed_balance"),
230                        TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price))))
231                );
232            }
233            player.sendMessage(TranslatableCaption.of("merge.success_merge"));
234            eventDispatcher.callPostMerge(player, plot);
235            return true;
236        }
237        Plot adjacent = plot.getRelative(direction);
238        if (adjacent == null || !adjacent.hasOwner() || adjacent
239                .isMerged((direction.getIndex() + 2) % 4) || (!force && adjacent.isOwner(uuid))) {
240            player.sendMessage(TranslatableCaption.of("merge.no_available_automerge"));
241            return false;
242        }
243        if (!force && !player.hasPermission(Permission.PERMISSION_MERGE_OTHER)) {
244            player.sendMessage(
245                    TranslatableCaption.of("permission.no_permission"),
246                    TagResolver.resolver("node", Tag.inserting(Permission.PERMISSION_MERGE_OTHER))
247            );
248            return false;
249        }
250        java.util.Set<UUID> uuids = adjacent.getOwners();
251        boolean isOnline = false;
252        if (!force) {
253            for (final UUID owner : uuids) {
254                final PlotPlayer<?> accepter = PlotSquared.platform().playerManager().getPlayerIfExists(owner);
255                if (accepter == null) {
256                    continue;
257                }
258                isOnline = true;
259                final Direction dir = direction;
260                Supplier<Boolean> run = () -> {
261                    accepter.sendMessage(TranslatableCaption.of("merge.merge_accepted"));
262                    if (plot.getPlotModificationManager().autoMerge(dir, maxSize - size, owner, player, terrain)) {
263                        PlotPlayer<?> plotPlayer = PlotSquared.platform().playerManager().getPlayerIfExists(player.getUUID());
264                        if (plotPlayer == null) {
265                            accepter.sendMessage(TranslatableCaption.of("merge.merge_not_valid"));
266                            return false;
267                        }
268                        if (this.econHandler.isEnabled(plotArea) && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON) && price > 0d) {
269                            if (this.econHandler.getMoney(player) < price) {
270                                player.sendMessage(
271                                        TranslatableCaption.of("economy.cannot_afford_merge"),
272                                        TagResolver.resolver(
273                                                "money",
274                                                Tag.inserting(Component.text(this.econHandler.format(price)))
275                                        )
276                                );
277                                return false;
278                            }
279                            this.econHandler.withdrawMoney(player, price);
280                            player.sendMessage(
281                                    TranslatableCaption.of("economy.removed_balance"),
282                                    TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price))))
283                            );
284                        }
285                        player.sendMessage(TranslatableCaption.of("merge.success_merge"));
286                        eventDispatcher.callPostMerge(player, plot);
287                        return true;
288                    }
289                    player.sendMessage(TranslatableCaption.of("merge.no_available_automerge"));
290                    return false;
291                };
292                if (hasConfirmation(player)) {
293                    CmdConfirm.addPending(
294                            accepter, MINI_MESSAGE.serialize(MINI_MESSAGE
295                                    .deserialize(
296                                            TranslatableCaption.of("merge.merge_request_confirm").getComponent(player),
297                                            TagResolver.builder()
298                                                    .tag("player", Tag.inserting(Component.text(player.getName())))
299                                                    .tag(
300                                                            "location",
301                                                            Tag.inserting(Component.text(plot.getWorldName() + " " + plot.getId()))
302                                                    )
303                                                    .build()
304                                    )),
305                            run::get
306                    );
307                } else {
308                    return run.get();
309                }
310                // find first
311                break;
312            }
313        }
314        if (force || !isOnline) {
315            if (force || player.hasPermission(Permission.PERMISSION_ADMIN_COMMAND_MERGE_OTHER_OFFLINE)) {
316                if (plot.getPlotModificationManager().autoMerge(
317                        direction,
318                        maxSize - size,
319                        uuids.iterator().next(),
320                        player,
321                        terrain
322                )) {
323                    if (this.econHandler.isEnabled(plotArea) && !player.hasPermission(Permission.PERMISSION_ADMIN_BYPASS_ECON) && price > 0d) {
324                        if (!force && this.econHandler.getMoney(player) < price) {
325                            player.sendMessage(
326                                    TranslatableCaption.of("economy.cannot_afford_merge"),
327                                    TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price))))
328                            );
329                            return false;
330                        }
331                        this.econHandler.withdrawMoney(player, price);
332                        player.sendMessage(
333                                TranslatableCaption.of("economy.removed_balance"),
334                                TagResolver.resolver("money", Tag.inserting(Component.text(this.econHandler.format(price))))
335                        );
336                    }
337                    player.sendMessage(TranslatableCaption.of("merge.success_merge"));
338                    eventDispatcher.callPostMerge(player, plot);
339                    return true;
340                }
341            }
342            player.sendMessage(TranslatableCaption.of("merge.no_available_automerge"));
343            return false;
344        }
345        player.sendMessage(TranslatableCaption.of("merge.merge_requested"));
346        return true;
347    }
348
349}