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/command/command.d, selery/command/command.d) 28 */ 29 module selery.command.command; 30 31 import std.algorithm : all; 32 import std.conv : ConvException, to; 33 static import std.math; 34 import std.meta : staticIndexOf, Reverse; 35 import std..string : toLower, startsWith; 36 import std.traits : Parameters, ParameterDefaults, ParameterIdentifierTuple, hasUDA, getUDAs, isIntegral, isFloatingPoint; 37 38 import sel.format : Format; 39 40 import selery.command.args : StringReader; 41 import selery.command.util : PocketType, CommandSender, WorldCommandSender, Ranged, isRanged, Target, Position; 42 import selery.entity.entity : Entity; 43 import selery.event.node.command : CommandNotFoundEvent, CommandFailedEvent; 44 import selery.lang : Translation; 45 import selery.log : Message; 46 import selery.node.server : ServerCommandSender; 47 import selery.player.player : Player; 48 import selery.plugin : Description; 49 import selery.util.messages : Messages; 50 import selery.util.tuple : Tuple; 51 52 struct CommandResult { 53 54 // defaults 55 enum SUCCESS = CommandResult(success); 56 enum UNIMPLEMENTED = CommandResult(unimplemented); 57 enum NOT_FOUND = CommandResult(notFound); 58 enum INVALID_SYNTAX = CommandResult(invalidSyntax); 59 60 enum : ubyte { 61 62 success, 63 64 notFound, 65 66 unimplemented, 67 invalidSyntax, 68 invalidParameter, 69 invalidNumber, 70 invalidBoolean, 71 targetNotPlayer, 72 playerNotFound, 73 targetNotFound, 74 invalidRangeDown, 75 invalidRangeUp 76 77 } 78 79 ubyte result = success; 80 string[] args; 81 82 string command; 83 84 inout pure nothrow @property @safe @nogc bool successful() { 85 return result == success; 86 } 87 88 /** 89 * Returns: whether the commands was successfully executed 90 */ 91 inout bool trigger(CommandSender sender) { 92 if(this.result != success) { 93 if(this.result == notFound) { 94 //TODO call event with actual used command 95 if(!(cast()sender.server).callCancellableIfExists!CommandNotFoundEvent(sender, this.command)) { 96 if(cast(Player)sender) sender.sendMessage(Format.red, Translation(Messages.generic.notFound)); 97 else sender.sendMessage(Format.red, Translation(Messages.generic.notFoundConsole)); 98 } 99 } else { 100 //TODO call event with actual used command 101 if(!(cast()sender.server).callCancellableIfExists!CommandFailedEvent(sender, sender.availableCommands.get(this.command, null))) { 102 const message = (){ 103 final switch(result) with(Messages) { 104 case unimplemented: return generic.notImplemented; 105 case invalidSyntax: return generic.invalidSyntax; 106 case invalidParameter: return generic.invalidParameter; 107 case invalidNumber: return generic.numInvalid; 108 case invalidBoolean: return generic.invalidBoolean; 109 case targetNotPlayer: return generic.targetNotPlayer; 110 case playerNotFound: return generic.playerNotFound; 111 case targetNotFound: return generic.targetNotFound; 112 case invalidRangeDown: return generic.numTooSmall; 113 case invalidRangeUp: return generic.numTooBig; 114 } 115 }(); 116 sender.sendMessage(Format.red, Translation(message, this.args)); 117 } 118 } 119 return false; 120 } else { 121 return true; 122 } 123 } 124 125 } 126 127 class Command { 128 129 /** 130 * Command's overload. 131 */ 132 public class Overload { 133 134 enum : string { 135 136 TARGET = "target", 137 ENTITIES = "entities", 138 PLAYERS = "players", 139 PLAYER = "player", 140 POSITION = "x y z", 141 BOOL = "bool", 142 INT = "int", 143 FLOAT = "float", 144 STRING = "string", 145 UNKNOWN = "unknown" 146 147 } 148 149 /** 150 * Name of the parameters (name of the variables if not specified 151 * by the user). 152 */ 153 public string[] params; 154 155 public abstract @property size_t requiredArgs(); 156 157 public abstract string typeOf(size_t i); 158 159 public abstract PocketType pocketTypeOf(size_t i); 160 161 public abstract string[] enumMembers(size_t i); 162 163 public abstract bool callableBy(CommandSender sender); 164 165 public abstract CommandResult callArgs(CommandSender sender, string args); 166 167 } 168 169 private class OverloadOf(C:CommandSender, bool implemented, E...) : Overload if(areValidArgs!(C, E[0..$/2])) { 170 171 private alias Args = E[0..$/2]; 172 private alias Params = E[$/2..$]; 173 174 private enum size_t minArgs = staticIndexOf!(void, Params) != -1 ? (Params.length - staticIndexOf!(void, Reverse!Params)) : 0; 175 176 public void delegate(C, Args) del; 177 178 public this(void delegate(C, Args) del, string[] params) { 179 this.del = del; 180 this.params = params; 181 } 182 183 public override @property size_t requiredArgs() { 184 return minArgs; 185 } 186 187 public override string typeOf(size_t i) { 188 switch(i) { 189 foreach(immutable j, T; Args) { 190 case j: 191 static if(is(T == Target)) return TARGET; 192 else static if(is(T == Entity[])) return ENTITIES; 193 else static if(is(T == Player[])) return PLAYERS; 194 else static if(is(T == Player)) return PLAYER; 195 else static if(is(T == Position)) return POSITION; 196 else static if(is(T == bool)) return BOOL; 197 else static if(is(T == enum)) return T.stringof; 198 else static if(isIntegral!T || isRanged!T && isIntegral!(T.Type)) return INT; 199 else static if(isFloatingPoint!T || isRanged!T && isFloatingPoint!(T.Type)) return FLOAT; 200 else return STRING; 201 } 202 default: 203 return UNKNOWN; 204 } 205 } 206 207 public override PocketType pocketTypeOf(size_t i) { 208 switch(i) { 209 foreach(immutable j, T; Args) { 210 case j: 211 static if(is(T == Target) || is(T == Entity) || is(T == Entity[]) || is(T == Player[]) || is(T == Player)) return PocketType.target; 212 else static if(is(T == Position)) return PocketType.blockpos; 213 else static if(is(T == bool)) return PocketType.boolean; 214 else static if(is(T == enum)) return PocketType.stringenum; 215 else static if(is(T == string)) return j == Args.length - 1 ? PocketType.rawtext : PocketType..string; 216 else static if(isIntegral!T || isRanged!T && isIntegral!(T.Type)) return PocketType.integer; 217 else static if(isFloatingPoint!T || isRanged!T && isFloatingPoint!(T.Type)) return PocketType.floating; 218 else goto default; 219 } 220 default: 221 return PocketType.rawtext; 222 } 223 } 224 225 public override string[] enumMembers(size_t i) { 226 switch(i) { 227 foreach(immutable j, T; E) { 228 static if(is(T == enum)) { 229 case j: return [__traits(allMembers, T)]; 230 } 231 } 232 default: return []; 233 } 234 } 235 236 public override bool callableBy(CommandSender sender) { 237 static if(is(C == CommandSender)) return true; 238 else return cast(C)sender !is null; 239 } 240 241 public override CommandResult callArgs(CommandSender _sender, string args) { 242 static if(!is(C == CommandSender)) { 243 C sender = cast(C)_sender; 244 // assuming that the control has already been done 245 //if(senderc is null) return CommandResult.NOT_FOUND; 246 } else { 247 alias sender = _sender; 248 } 249 StringReader reader = StringReader(args); 250 Args cargs; 251 foreach(immutable i, T; Args) { 252 if(!reader.eof()) { 253 static if(is(T == Target) || is(T == Entity[]) || is(T == Player[]) || is(T == Player) || is(T == Entity)) { 254 immutable selector = reader.readQuotedString(); 255 auto target = Target.fromString(sender, selector); 256 static if(is(T == Player) || is(T == Player[])) { 257 //TODO this control can be done before querying the entities 258 if(!target.player) return CommandResult(CommandResult.targetNotPlayer); 259 } 260 if(target.entities.length == 0) return CommandResult(selector.startsWith("@") ? CommandResult.targetNotFound : CommandResult.playerNotFound); 261 static if(is(T == Player)) { 262 cargs[i] = target.players[0]; 263 } else static if(is(T == Player[])) { 264 cargs[i] = target.players; 265 } else static if(is(T == Entity)) { 266 cargs[i] = target.entities[0]; 267 } else static if(is(T == Entity[])) { 268 cargs[i] = target.entities; 269 } else { 270 cargs[i] = target; 271 } 272 } else static if(is(T == Position)) { 273 try { 274 cargs[i] = Position(Position.Point.fromString(reader.readString()), Position.Point.fromString(reader.readString()), Position.Point.fromString(reader.readString())); 275 } catch(Exception) { 276 return CommandResult.INVALID_SYNTAX; 277 } 278 } else static if(is(T == bool)) { 279 immutable value = reader.readString(); 280 if(value == "true") cargs[i] = true; 281 else if(value == "false") cargs[i] = false; 282 else return CommandResult(CommandResult.invalidBoolean, [value]); 283 } else static if(is(T == enum)) { 284 immutable value = reader.readString(); 285 switch(value.toLower) { 286 mixin((){ 287 string ret; 288 foreach(immutable member ; __traits(allMembers, T)) { 289 ret ~= `case "` ~ member.toLower ~ `": cargs[i]=T.` ~ member ~ `; break;`; 290 } 291 return ret; 292 }()); 293 default: 294 return CommandResult(CommandResult.invalidParameter, [value]); 295 } 296 } else static if(isIntegral!T || isFloatingPoint!T || isRanged!T) { 297 immutable value = reader.readString(); 298 try { 299 static if(isFloatingPoint!T || isRanged!T && isFloatingPoint!(T.Type)) { 300 // converted numbers cannot be infinite or nan 301 immutable num = to!double(value); 302 } else { 303 immutable num = to!int(value); 304 } 305 // control bounds (on integers and ranged numbers) 306 static if(!isFloatingPoint!T) { 307 enum _min = T.min; 308 static if(!isRanged!T || T.type[0] == '[') { 309 if(num < _min) return CommandResult(CommandResult.invalidRangeDown, [value, to!string(_min)]); 310 } else { 311 if(num <= _min) return CommandResult(CommandResult.invalidRangeDown, [value, to!string(_min)]); 312 } 313 static if(!isRanged!T || T.type[1] == ']') { 314 if(num > T.max) return CommandResult(CommandResult.invalidRangeUp, [value, to!string(T.max)]); 315 } else { 316 if(num >= T.max) return CommandResult(CommandResult.invalidRangeUp, [value, to!string(T.max)]); 317 } 318 } 319 // assign 320 static if(isRanged!T) cargs[i] = T(cast(T.Type)num); 321 else cargs[i] = cast(T)num; 322 } catch(ConvException) { 323 return CommandResult(CommandResult.invalidNumber, [value]); 324 } 325 } else static if(i == Args.length - 1) { 326 immutable value = reader.readText(); 327 if(value.length > 2 && value[0] == '"' && value[$-1] == '"') { 328 cargs[i] = value[1..$-1]; 329 } else { 330 cargs[i] = value; 331 } 332 } else { 333 cargs[i] = reader.readQuotedString(); 334 } 335 } else { 336 static if(!is(Params[i] == void)) cargs[i] = Params[i]; 337 else return CommandResult.INVALID_SYNTAX; 338 } 339 } 340 reader.skip(); 341 if(reader.eof) { 342 static if(implemented) { 343 this.del(sender, cargs); 344 return CommandResult.SUCCESS; 345 } else { 346 return CommandResult.UNIMPLEMENTED; 347 } 348 } else { 349 return CommandResult.INVALID_SYNTAX; 350 } 351 } 352 353 } 354 355 immutable string name; 356 immutable Description description; 357 immutable string[] aliases; 358 359 immutable ubyte permissionLevel; 360 immutable string[] permissions; 361 immutable bool hidden; 362 363 Overload[] overloads; 364 365 this(string name, Description description=Description.init, string[] aliases=[], ubyte permissionLevel=0, string[] permissions=[], bool hidden=false) { 366 assert(checkCommandName(name)); 367 assert(aliases.all!(a => checkCommandName(a))()); 368 this.name = name; 369 this.description = description; 370 this.aliases = aliases.idup; 371 this.permissionLevel = permissionLevel; 372 this.permissions = permissions.idup; 373 this.hidden = hidden; 374 } 375 376 /** 377 * Returns a new command with the same settings (name, description, ...) 378 * but without any overload. 379 */ 380 public Command clone() { 381 return new Command(this.name, cast()this.description, cast(string[])this.aliases, this.permissionLevel, cast(string[])this.permissions, this.hidden); 382 } 383 384 /** 385 * Adds an overload from a function. 386 */ 387 void add(alias func)(void delegate(Parameters!func) del, bool implemented=true) if(Parameters!func.length >= 1 && is(Parameters!func[0] : CommandSender)) { 388 string[] params = [ParameterIdentifierTuple!func][1..$]; 389 //TODO nameable params 390 if(implemented) this.overloads ~= new OverloadOf!(Parameters!func[0], true, Parameters!func[1..$], ParameterDefaults!func[1..$])(del, params); 391 else this.overloads ~= new OverloadOf!(Parameters!func[0], false, Parameters!func[1..$], ParameterDefaults!func[1..$])(del, params); 392 } 393 394 /** 395 * Removes an overload using a function. 396 */ 397 bool remove(alias func)() { 398 foreach(i, overload; this.overloads) { 399 if(cast(OverloadOf!(Parameters!func[0], Parameters!func[1..$], ParameterDefaults!func[1..$]))overload) { 400 this.overloads = this.overloads[0..i] ~ this.overloads[i+1..$]; 401 return true; 402 } 403 } 404 return false; 405 } 406 407 } 408 409 private bool checkCommandName(string name) { 410 if(name.length) { 411 foreach(c ; name) { 412 if((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '?') return false; 413 } 414 } 415 return true; 416 } 417 418 public bool areValidArgs(C:CommandSender, E...)() { 419 foreach(T ; E) { 420 static if(!areValidArgsImpl!(C, T)) return false; 421 } 422 return true; 423 } 424 425 private template areValidArgsImpl(C, T) { 426 static if(is(T == enum) || is(T == string) || is(T == bool) || isIntegral!T || isFloatingPoint!T || isRanged!T) enum areValidArgsImpl = true; 427 else static if(is(T == Target) || is(T == Entity) || is(T == Entity[]) || is(T == Player[]) || is(T == Player) || is(T == Position)) enum areValidArgsImpl = is(C : WorldCommandSender); 428 else enum areValidArgsImpl = false; 429 }