1 /* 2 * Copyright (c) 2017-2019 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: Copyright (c) 2017-2019 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/selery/source/selery/node/commands.d, selery/node/commands.d) 28 */ 29 module selery.node.commands; 30 31 import std.algorithm : sort, clamp, min, filter; 32 import std.conv : to; 33 import std.math : ceil; 34 import std.random : uniform; 35 import std..string : join, toLower, startsWith; 36 import std.traits : hasUDA, getUDAs, Parameters; 37 import std.typetuple : TypeTuple; 38 39 import sel.format : Format, unformat; 40 41 import selery.about : Software; 42 import selery.command.command : Command; 43 import selery.command.util : CommandSender, WorldCommandSender, PocketType, SingleEnum, SnakeCaseEnum, Ranged, Position, Target; 44 import selery.config : Config, Gamemode, Difficulty, Dimension; 45 import selery.effect : Effects; 46 import selery.enchantment : Enchantments; 47 import selery.entity.entity : Entity; 48 import selery.lang : Translation, Translatable; 49 import selery.node.server : isServerRunning, NodeServer, ServerCommandSender; 50 import selery.player.bedrock : BedrockPlayer; 51 import selery.player.java : JavaPlayer; 52 import selery.player.player : PlayerInfo, Player, PermissionLevel; 53 import selery.plugin : Description, permission, hidden, unimplemented; 54 import selery.util.messages : Messages; 55 import selery.world.group : GroupInfo; 56 import selery.world.world : WorldInfo, Time; 57 58 enum vanilla; 59 enum op; 60 61 struct aliases { 62 63 string[] aliases; 64 65 this(string[] aliases...) { 66 this.aliases = aliases; 67 } 68 69 } 70 71 final class Commands { 72 73 enum list = mixin({ 74 string[] commands; 75 foreach(member ; __traits(allMembers, typeof(this))) { 76 static if(member[$-1] == '0') commands ~= member[0..$-1]; 77 } 78 return "TypeTuple!(" ~ commands.to!string[1..$-1] ~ ")"; 79 }()); 80 81 static Commands register(shared NodeServer server) { 82 return new Commands(server).register(); 83 } 84 85 private shared NodeServer server; 86 87 public this(shared NodeServer server) { 88 this.server = server; 89 } 90 91 public Commands register() { 92 auto server = cast()this.server; 93 const config = this.server.config.node; 94 foreach(command ; list) { 95 if(mixin("config." ~ command ~ "Command")) this.registerImpl!(command, 0)(server); 96 } 97 return this; 98 } 99 100 private void registerImpl(string command, size_t count)(NodeServer server) { 101 mixin("alias C = " ~ command ~ to!string(count) ~ ";"); 102 static if(count == 0) { 103 static if(hasUDA!(C, vanilla)) enum description = Translatable.fromBedrock("commands." ~ command ~ ".description"); 104 else enum description = Translatable("commands." ~ command ~ ".description"); 105 static if(hasUDA!(C, aliases)) enum aliases = getUDAs!(C, aliases)[0].aliases; 106 else enum string[] aliases = []; 107 static if(hasUDA!(C, permission)) enum permissions = getUDAs!(C, permission)[0].permissions; 108 else enum string[] permissions = []; 109 server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description(description), aliases, hasUDA!(C, op), permissions, hasUDA!(C, hidden), !hasUDA!(C, unimplemented)); 110 } else { 111 server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description.init, [], 0, [], false, !hasUDA!(C, unimplemented)); 112 } 113 static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.registerImpl!(command, count + 1)(server); 114 } 115 116 private void sendUnimplementedMessage(CommandSender sender) { 117 sender.sendMessage(Format.red, "Not Implemented"); 118 } 119 120 public Commands unregister() { 121 //TODO unregister overloads using delegate's pointers 122 auto server = cast()this.server; 123 foreach(command ; list) { 124 this.unregisterImpl!(command, 0)(server); 125 } 126 return this; 127 } 128 129 private void unregisterImpl(string command, size_t count)(NodeServer server) { 130 mixin("alias C = " ~ command ~ to!string(count) ~ ";"); 131 //server.unregisterCommandByOverload(mixin("&this." ~ command ~ to!string(count)), convertedName!command); 132 static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.unregisterImpl!(command, count + 1)(server); 133 } 134 135 // about 136 137 void about0(CommandSender sender) { 138 sender.sendMessage(Translation(Messages.about.software, Software.name ~ " " ~ Software.fullVersion)); 139 if(this.server.plugins.length) { 140 sender.sendMessage(Translation(Messages.about.plugins, this.server.plugins.length)); 141 foreach(_plugin ; this.server.plugins) { 142 auto plugin = cast()_plugin; 143 sender.sendMessage("* ", Format.green, plugin.name, Format.reset, " ", (!plugin.version_.startsWith("~") ? "v" : ""), plugin.version_); 144 } 145 } 146 } 147 148 // help 149 150 @vanilla help0(ServerCommandSender sender) { 151 Command[] commands; 152 foreach(name, command; sender.availableCommands) { 153 if(!command.hidden && name == command.name) { 154 foreach(overload ; command.overloads) { 155 if(overload.callableBy(sender)) { 156 commands ~= command; 157 break; 158 } 159 } 160 } 161 } 162 sort!((a, b) => a.name < b.name)(commands); 163 foreach(cmd ; commands) { 164 if(cmd.description.type == Description.EMPTY) sender.sendMessage(Format.yellow, cmd.name, ":"); 165 else if(cmd.description.type == Description.TEXT) sender.sendMessage(Format.yellow, cmd.description.text); 166 else sender.sendMessage(Format.yellow, Translation(cmd.description.translatable)); 167 foreach(overload ; cmd.overloads) { 168 if(overload.callableBy(sender)) { 169 sender.sendMessage("- ", cmd.name, " ", formatArg(overload)); 170 } 171 } 172 } 173 } 174 175 @vanilla help1(ServerCommandSender sender, string command) { 176 this.helpImpl(sender, "", command); 177 } 178 179 private void helpImpl(CommandSender sender, string slash, string command) { 180 auto cmd = command in sender.availableCommands; 181 if(cmd) { 182 string[] messages; 183 foreach(overload ; cmd.overloads) { 184 if(overload.callableBy(sender)) { 185 messages ~= ("- " ~ slash ~ cmd.name ~ " " ~ formatArg(overload)); 186 } 187 } 188 if(messages.length) { 189 if(cmd.aliases.length) { 190 sender.sendMessage(Format.yellow, Translation(Messages.help.commandAliases, cmd.name, cmd.aliases.join(", "))); 191 } else { 192 sender.sendMessage(Format.yellow ~ cmd.name ~ ":"); 193 } 194 if(cmd.description.type == Description.TEXT) { 195 sender.sendMessage(Format.yellow, cmd.description.text); 196 } else if(cmd.description.type == Description.TRANSLATABLE) { 197 sender.sendMessage(Format.yellow, Translation(cmd.description.translatable)); 198 } 199 sender.sendMessage(Translation(Messages.generic.usage, "")); 200 foreach(message ; messages) { 201 sender.sendMessage(message); 202 } 203 return; 204 } 205 } 206 sender.sendMessage(Format.red, Translation(Messages.generic.invalidParameter, command)); 207 } 208 209 // permission 210 211 enum PermissionAction { grant, revoke } 212 213 @unimplemented @op permission0(WorldCommandSender sender, PermissionAction action, Player[] target, string permission) {} 214 215 @unimplemented void permission1(WorldCommandSender sender, SingleEnum!"list" list, Player target) {} 216 217 @unimplemented void permission2(ServerCommandSender sender, PermissionAction action, string target, string permission) {} 218 219 // stop 220 221 @vanilla @op stop0(CommandSender sender, bool gracefully=true) { 222 if(gracefully) { 223 if(isServerRunning) { 224 sender.sendMessage(Translation(Messages.stop.start)); 225 this.server.shutdown(); 226 } else { 227 sender.sendMessage(Format.red, Translation(Messages.stop.failed)); 228 } 229 } else { 230 import core.stdc.stdlib : exit; 231 exit(0); 232 } 233 } 234 235 // transfer 236 237 @unimplemented @op transfer0(WorldCommandSender sender, Player[] target, string node) {} 238 239 @unimplemented @op transfer1(ServerCommandSender sender, string target, string node) {} 240 241 // world 242 243 void world0(CommandSender sender, SingleEnum!"list" list) { 244 string[] names; 245 foreach(group ; sender.server.worldGroups) names ~= group.name; 246 sender.sendMessage(Translation("commands.world.list", names.length, names.join(", "))); 247 } 248 249 @op world1(CommandSender sender, SingleEnum!"add" add, string name, bool defaultWorld=false) { 250 auto world = sender.server.addWorld(name); 251 if(world) { 252 sender.sendMessage(Translation("commands.world.add.success")); 253 //if(defaultWorld) sender.server.defaultWorld = world; 254 } else { 255 sender.sendMessage(Format.red, Translation("commands.world.add.failed")); 256 } 257 } 258 259 void world2(CommandSender sender, SingleEnum!"remove" remove, string name) { 260 executeOnWorlds(sender, name, (shared GroupInfo info){ 261 if(sender.server.removeWorldGroup(info)) sender.sendMessage(Translation("commands.world.remove.success")); 262 }); 263 } 264 265 @unimplemented void world3(CommandSender sender, SingleEnum!"info" info, string name) {} 266 267 } 268 269 string convertName(string command, string replacement=" ") { 270 string ret; 271 foreach(c ; command) { 272 if(c >= 'A' && c <= 'Z') ret ~= replacement ~ cast(char)(c + 32); 273 else ret ~= c; 274 } 275 return ret; 276 } 277 278 private enum convertedName(string command) = convertName(command); 279 280 private string[] formatArgs(Command command, CommandSender sender) { 281 string[] ret; 282 foreach(overload ; command.overloads) { 283 if(overload.callableBy(sender)) ret ~= formatArg(overload); 284 } 285 return ret; 286 } 287 288 private string formatArg(Command.Overload overload) { 289 string[] p; 290 foreach(i, param; overload.params) { 291 immutable enum_ = overload.pocketTypeOf(i) == PocketType.stringenum; 292 if(enum_ && overload.enumMembers(i).length == 1) { 293 p ~= overload.enumMembers(i)[0]; 294 } else { 295 string full = enum_ && overload.enumMembers(i).length < 5 ? overload.enumMembers(i).join("|") : (param ~ ": " ~ overload.typeOf(i)); 296 if(i < overload.requiredArgs) { 297 p ~= "<" ~ full ~ ">"; 298 } else { 299 p ~= "[" ~ full ~ "]"; 300 } 301 } 302 } 303 return p.join(" "); 304 } 305 306 private void executeOnWorlds(CommandSender sender, string name, void delegate(shared GroupInfo) del) { 307 auto group = sender.server.getGroupByName(name); 308 if(group !is null) { 309 del(group); 310 } else { 311 sender.sendMessage(Format.red, Translation("commands.world.notFound", name)); 312 } 313 } 314 315 private void executeOnPlayers(CommandSender sender, string name, void delegate(shared PlayerInfo) del) { 316 if(name.startsWith("@")) { 317 if(name == "@a" || name == "@r") { 318 auto players = sender.server.players; 319 if(players.length) { 320 final switch(name) { 321 case "@a": 322 foreach(player ; sender.server.players) { 323 del(player); 324 } 325 break; 326 case "@r": 327 del(players[uniform(0, $)]); 328 break; 329 } 330 } else { 331 sender.sendMessage(Format.red, Translation(Messages.generic.targetNotFound)); 332 } 333 } else { 334 sender.sendMessage(Format.red, Translation(Messages.generic.invalidSyntax)); 335 } 336 } else { 337 immutable iname = name.toLower(); 338 bool executed = false; 339 foreach(player ; sender.server.players) { 340 if(player.lname == iname) { 341 executed = true; 342 del(player); 343 } 344 } 345 if(!executed) sender.sendMessage(Format.red, Translation(Messages.generic.playerNotFound, name)); 346 } 347 }