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 }