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/util.d, selery/command/util.d)
28  */
29 module selery.command.util;
30 
31 import std.algorithm : sort;
32 import std.conv : to, ConvException;
33 import std.random : uniform;
34 import std.string : split, join, toLower, startsWith, replace;
35 import std.traits : isIntegral, isFloatingPoint;
36 import std.typecons : Tuple;
37 
38 import selery.command.command : Command;
39 import selery.entity.entity : Entity;
40 import selery.log : Message;
41 import selery.math.vector : EntityPosition, isVector, distance;
42 import selery.node.server : NodeServer;
43 import selery.player.player : Player, Gamemode;
44 import selery.world.world : World;
45 
46 import transforms.snake;
47 
48 /**
49  * Interface for command senders.
50  */
51 interface CommandSender {
52 
53 	/**
54 	 * Gets the command sender's current server.
55 	 */
56 	public pure nothrow @property @safe @nogc shared(NodeServer) server();
57 
58 	/**
59 	 * Gets the commands that can be called by the command sender
60 	 * in its current status.
61 	 * Aliases are included in the list and the Command object is
62 	 * the same as the non-aliased command.
63 	 * Example:
64 	 * ---
65 	 * assert(sender.availableCommands["help"] is sender.availableCommands["?"]);
66 	 * ---
67 	 */
68 	public @property Command[string] availableCommands();
69 
70 	/**
71 	 * Sends a message to the command sender.
72 	 * The message can contain formatting codes and translations.
73 	 * Example:
74 	 * ---
75 	 * sender.sendMessage("Hello");
76 	 * sender.sendMessage(Format.blue, "This is a blue message");
77 	 * sender.sendMessage(Format.yellow, Translation("multiplayer.player.joined", "Steve"));
78 	 * ---
79 	 */
80 	public final void sendMessage(E...)(E args) {
81 		this.sendMessageImpl(Message.convert(args));
82 	}
83 
84 	protected void sendMessageImpl(Message[] messages);
85 	
86 }
87 
88 /**
89  * Interface for a command sender that is spawned in a world.
90  */
91 interface WorldCommandSender : CommandSender {
92 	
93 	/**
94 	 * Gets the command sender's world.
95 	 */
96 	public pure nothrow @property @safe @nogc World world();
97 	
98 	/**
99 	 * Gets the command sender's current position.
100 	 */
101 	public @property EntityPosition position();
102 	
103 	/**
104 	 * Gets the list of the entities visible by the
105 	 * command sender.
106 	 */
107 	public @property Entity[] visibleEntities();
108 	
109 	/**
110 	 * Gets the list of the players visible by the
111 	 * command sender.
112 	 */
113 	public @property Player[] visiblePlayers();
114 	
115 }
116 
117 enum PocketType {
118 	
119 	target,
120 	blockpos,
121 	stringenum,
122 	string,
123 	rawtext,
124 	integer,
125 	floating,
126 	boolean,
127 	
128 }
129 
130 /**
131  * Created an enum with a single value that can be used in
132  * commands with a single argument.
133  * Example:
134  * ---
135  * // test add @a
136  * @command("test") test0(SingleEnum!"add", Target target) {}
137  * 
138  * // test remove @a
139  * @command("test") test1(SingleEnum!"remove", Target target) {}
140  * ---
141  */
142 template SingleEnum(string value) {
143 	
144 	mixin("enum SingleEnum { " ~ value ~ " }");
145 	
146 }
147 
148 /**
149  * Example:
150  * ---
151  * enum Example : int {
152  *    plain = 12,
153  *    camelCase = 44,
154  *    PascalCase = 100,
155  *    ALL_UPPERCASE = 200
156  * }
157  * alias Snake = SnakeCaseEnum!Example;
158  * assert(Example.plain == Snake.plain);
159  * assert(Example.camelCase == Snake.camel_case);
160  * assert(Example.PascalCase == Snake.pascal_case);
161  * assert(Example.ALL_UPPERCASE == Snake.all_uppercase);
162  * ---
163  */
164 template SnakeCaseEnum(T) if(is(T == enum)) {
165 	
166 	mixin("enum SnakeCaseEnum {" ~ (){
167 		string ret;
168 		foreach(immutable member ; __traits(allMembers, T)) {
169 			ret ~= member.snakeCaseCT ~ "=T." ~ member ~ ",";
170 		}
171 		return ret;
172 	}() ~ "}");
173 	
174 }
175 
176 struct Ranged(T, string _type, T _min, T _max) if((isIntegral!T || isFloatingPoint!T) && _min < _max && (_type == "[]" || _type == "(]" || _type == "[)" || _type == "()")) {
177 
178 	enum __is_range;
179 
180 	alias Type = T;
181 
182 	enum type = _type;
183 
184 	enum min = _min;
185 	enum max = _max;
186 
187 	T value;
188 
189 	alias value this;
190 
191 }
192 
193 alias Ranged(T, T min, T max) = Ranged!(T, "[]", min, max);
194 
195 enum isRanged(T) = __traits(hasMember, T, "__is_range");
196 
197 template minImpl(T) {
198 	static if(isIntegral!T) enum minImpl = T.min;
199 	else enum minImpl = T.min_normal;
200 }
201 
202 /**
203  * Indicates a position with absolutes and/or relatives coordinates.
204  * Example:
205  * ---
206  * auto pos = Position(Position.Point.fromString("~"), Position.Point.fromString("1"), Position.Point.fromString("~10"));
207  * auto res = pos.from(BlockPosition(1, 10, 100));
208  * assert(res == BlockPosition(1, 1, 110));
209  * ---
210  */
211 struct PositionImpl(V) if(isVector!V) {
212 
213 	alias T = V.Type;
214 	
215 	static struct Point {
216 		
217 		private bool absolute;
218 		private T _value;
219 		private T function(T, T) _apply;
220 		
221 		public this(bool absolute, immutable T v) {
222 			this.absolute = absolute;
223 			this._value = v;
224 			if(absolute) {
225 				this._apply = &applyAbsolute;
226 			} else {
227 				this._apply = &applyRelative;
228 			}
229 		}
230 		
231 		private static T applyAbsolute(T a, T b) {
232 			return a;
233 		}
234 		
235 		private static T applyRelative(T a, T b) {
236 			return b + a;
237 		}
238 		
239 		public T apply(T value) {
240 			return this._apply(this._value, value);
241 		}
242 		
243 		public string toString() {
244 			if(this.absolute) {
245 				return to!string(this._value);
246 			} else if(this._value == 0) {
247 				return "~";
248 			} else {
249 				return "~" ~ to!string(this._value);		
250 			}
251 		}
252 		
253 		public static Point fromString(string str) {
254 			if(str.length) {
255 				if(str[0] == '~') {
256 					if(str.length == 1) {
257 						return Point(false, 0);
258 					} else {
259 						return Point(false, to!T(str[1..$]));
260 					}
261 				} else {
262 					return Point(true, to!T(str));
263 				}
264 			} else {
265 				return Point(true, T.init);
266 			}
267 		}
268 		
269 	}
270 
271 	public static typeof(this) fromString(string str) {
272 		auto spl = str.split(" ");
273 		if(spl.length != 3) throw new ConvException("Wrong format");
274 		else return typeof(this)(Point.fromString(spl[0]), Point.fromString(spl[1]), Point.fromString(spl[2]));
275 	}
276 	
277 	mixin((){
278 		string ret;
279 		foreach(c ; V.coords) {
280 			ret ~= "public Point " ~ c ~ ";";
281 		}
282 		return ret;
283 	}());
284 	
285 	/**
286 	 * Creates a vector from an initial position (used for
287 	 * relative values).
288 	 */
289 	public @property V from(V position) {
290 		T[V.coords.length] ret;
291 		foreach(i, c; V.coords) {
292 			mixin("ret[i] = this." ~ c ~ ".apply(position." ~ c ~ ");");
293 		}
294 		return V(ret);
295 	}
296 	
297 	public string toCoordsString(string glue=", ") {
298 		string[] ret;
299 		foreach(c ; V.coords) {
300 			ret ~= mixin("this." ~ c ~ ".toString()");
301 		}
302 		return ret.join(glue);
303 	}
304 
305 	public string toString() {
306 		return "Position(" ~ this.toCoordsString() ~ ")";
307 	}
308 	
309 }
310 
311 /// ditto
312 alias Position = PositionImpl!EntityPosition;
313 
314 /**
315  * Indicates a target selected using a username or a target selector.
316  * For reference see $(LINK2 https://minecraft.gamepedia.com/Commands#Target_selector_variables, Command on Minecraft Wiki).
317  */
318 struct Target {
319 
320 	/**
321 	 * Raw input of the selector used.
322 	 */
323 	public string input;
324 	
325 	public Entity[] entities;
326 	public Player[] players;
327 
328 	/**
329 	 * Indicates whether the target was a player or an entity.
330 	 * Example:
331 	 * ---
332 	 * "Steve" = true
333 	 * "@a" = true
334 	 * "@e" = false
335 	 * "@e[type=player]" = true
336 	 * "@r" = true
337 	 * "@r[type=creeper]" = false
338 	 * ---
339 	 */
340 	public bool player = true;
341 
342 	public this(string input) {
343 		this.input = input;
344 	}
345 	
346 	public this(string input, Entity[] entities, bool player=true) {
347 		this(input);
348 		this.entities = entities;
349 		foreach(entity ; entities) {
350 			if(cast(Player)entity) this.players ~= cast(Player)entity;
351 		}
352 		this.player = player;
353 	}
354 	
355 	public this(string input, Player[] players, bool player=true) {
356 		this(input);
357 		this.entities = cast(Entity[])players;
358 		this.players = players;
359 	}
360 	
361 	/**
362 	 * Creates a target from a username or a selector string.
363 	 */
364 	public static Target fromString(WorldCommandSender sender, string str) {
365 		if(str.length >= 2 && str[0] == '@') {
366 			string[string] selectors;
367 			if(str.length >= 4 && str[2] == '[' && str[$-1] == ']') {
368 				foreach(sel ; str[3..$-1].split(",")) {
369 					auto spl = sel.split("=");
370 					if(spl.length == 2) selectors[spl[0]] = spl[1];
371 				}
372 			}
373 			switch(str[1]) {
374 				case 's':
375 					if(cast(Entity)sender) {
376 						return Target(str, [cast(Entity)sender]);
377 					} else {
378 						return Target(str);
379 					}
380 				case 'p':
381 					auto players = sender.visiblePlayers;
382 					if(players.length) {
383 						if("c" !in selectors) {
384 							selectors["c"] = "1";
385 						}
386 						filter(sender, players, selectors);
387 						//TODO sort per distance
388 						return Target(str);
389 					} else {
390 						return Target(str);
391 					}
392 				case 'r':
393 					size_t amount = 1;
394 					auto c = "c" in selectors;
395 					if(c) {
396 						try {
397 							amount = to!size_t(*c);
398 						} catch(ConvException) {}
399 						selectors.remove("c");
400 					}
401 					Target rImpl(T:Entity)(T[] data) {
402 						filter(sender, data, selectors);
403 						if(amount >= data.length) {
404 							return Target(str, data);
405 						} else {
406 							T[] selected;
407 							while(--amount) {
408 								size_t index = uniform(0, data.length);
409 								selected ~= data[index];
410 								data = data[0..index] ~ data[index+1..$];
411 							}
412 							return Target(str, selected, is(T == Player));
413 						}
414 					}
415 					auto type = "type" in selectors;
416 					if(type && *type != "player") {
417 						return rImpl(sender.visibleEntities);
418 					} else {
419 						return rImpl(sender.visiblePlayers);
420 					}
421 				case 'a':
422 					auto players = sender.visiblePlayers;
423 					filter(sender, players, selectors);
424 					return Target(str, players, true);
425 				case 'e':
426 					auto entities = sender.visibleEntities;
427 					filter(sender, entities, selectors);
428 					return Target(str, entities, false);
429 				default:
430 					return Target(str);
431 			}
432 		} else {
433 			immutable sel = str.toLower;
434 			Player[] ret;
435 			foreach(player ; sender.visiblePlayers) {
436 				if(player.lname == sel) ret ~= player;
437 			}
438 			return Target(str, ret);
439 		}
440 	}
441 	
442 }
443 
444 private struct Res {
445 	
446 	bool exists;
447 	bool inverted;
448 	string value;
449 	
450 	alias exists this;
451 	
452 }
453 
454 private void filter(T:Entity)(WorldCommandSender sender, ref T[] entities, string[string] selectors) {
455 	Res data(string key) {
456 		auto p = key in selectors;
457 		if(p) {
458 			if((*p).startsWith("!")) return Res(true, true, (*p)[1..$]);
459 			else return Res(true, false, *p);
460 		} else {
461 			return Res(false);
462 		}
463 	}
464 	auto type = data("type");
465 	if(type) {
466 		// filter type
467 		if(!type.inverted) filterImpl!("entity.type == a")(entities, type.value);
468 		else filterImpl!("entity.type != a")(entities, type.value);
469 	}
470 	auto name = data("name");
471 	if(name) {
472 		// filter by nametag
473 		if(!name.inverted) filterImpl!("entity.nametag == a")(entities, name.value);
474 		else filterImpl!("entity.nametag != a")(entities, name.value);
475 	}
476 	auto rx = data("rx");
477 	if(rx) {
478 		// filter by max pitch
479 		try { filterImpl!("entity.pitch <= a")(entities, to!float(name.value)); } catch(ConvException) {}
480 	}
481 	auto rxm = data("rxm");
482 	if(rxm) {
483 		// filter by min pitch
484 		try { filterImpl!("entity.pitch >= a")(entities, to!float(name.value)); } catch(ConvException) {}
485 	}
486 	auto ry = data("ry");
487 	if(ry) {
488 		// filter by max yaw
489 		try { filterImpl!("entity.yaw <= a")(entities, to!float(name.value)); } catch(ConvException) {}
490 	}
491 	auto rym = data("rym");
492 	if(rym) {
493 		// filter by min yaw
494 		try { filterImpl!("entity.yaw >= a")(entities, to!float(name.value)); } catch(ConvException) {}
495 	}
496 	auto m = data("m");
497 	auto l = data("l");
498 	auto lm = data("lm");
499 	if(m || l || lm) {
500 		static if(is(T : Player)) {
501 			alias players = entities;
502 		} else {
503 			// filter out non-players
504 			Player[] players;
505 			foreach(entity ; entities) {
506 				auto player = cast(Player)entity;
507 				if(player !is null) players ~= player;
508 			}
509 		}
510 		if(m) {
511 			// filter gamemode
512 			int gamemode = (){
513 				switch(m.value) {
514 					case "0": case "s": case "survival": return 0;
515 					case "1": case "c": case "creative": return 1;
516 					case "2": case "a": case "adventure": return 2;
517 					case "3": case "sp": case "spectator": return 3;
518 					default: return -1;
519 				}
520 			}();
521 			if(gamemode >= 0) {
522 				if(!m.inverted) filterImpl!("entity.gamemode == a")(players, gamemode);
523 				else filterImpl!("entity.gamemode != a")(players, gamemode);
524 			}
525 		}
526 		if(l) {
527 			// filter xp (min)
528 			try {
529 				filterImpl!("entity.level <= a")(players, to!uint(l.value));
530 			} catch(ConvException) {}
531 		}
532 		if(lm) {
533 			// filter xp (max)
534 			try {
535 				filterImpl!("entity.level >= a")(players, to!uint(l.value));
536 			} catch(ConvException) {}
537 		}
538 		static if(!is(T : Player)) {
539 			entities = cast(Entity[])players;
540 		}
541 	}
542 	auto c = data("c");
543 	if(c) {
544 		try {
545 			auto amount = to!ptrdiff_t(c.value);
546 			if(amount > 0) {
547 				entities = filterDistance!false(sender.position, entities, amount);
548 			} else if(amount < 0) {
549 				entities = filterDistance!true(sender.position, entities, -amount);
550 			} else {
551 				entities.length = 0;
552 			}
553 		} catch(ConvException) {}
554 	}
555 }
556 
557 private void filterImpl(string query, T:Entity, A)(ref T[] entities, A a) {
558 	T[] ret;
559 	foreach(entity ; entities) {
560 		if(mixin(query)) ret ~= entity;
561 	}
562 	if(ret.length != entities.length) entities = ret;
563 }
564 
565 private T[] filterDistance(bool inverted, T:Entity)(EntityPosition position, T[] entities, size_t count) {
566 	if(count >= entities.length) return entities;
567 	Tuple!(T, double)[] distances;
568 	foreach(entity ; entities) {
569 		distances ~= Tuple!(T, double)(entity, distance(position, entity.position));
570 	}
571 	sort!((a, b) => a[1] == b[1] ? a[0].id < b[0].id : a[1] < b[1])(distances);
572 	T[] ret;
573 	foreach(i ; 0..count) {
574 		static if(inverted) ret ~= distances[$-i-1][0];
575 		else ret ~= distances[i][0];
576 	}
577 	return ret;
578 }