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