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}