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 }