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