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