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/commands.d, selery/commands.d)
28  */
29 module selery.commands;
30 
31 import std.algorithm : sort, clamp, min, filter;
32 import std.conv : to;
33 import std.math : ceil;
34 import std.random : uniform;
35 import std.string : join, toLower, startsWith;
36 import std.traits : hasUDA, getUDAs, Parameters;
37 import std.typetuple : TypeTuple;
38 
39 import selery.about : Software;
40 import selery.command.command : Command;
41 import selery.command.util : CommandSender, WorldCommandSender, PocketType, SingleEnum, SnakeCaseEnum, Ranged, Position, Target;
42 import selery.config : Config, Gamemode, Difficulty, Dimension;
43 import selery.effect : Effects;
44 import selery.enchantment : Enchantments;
45 import selery.entity.entity : Entity;
46 import selery.lang : Translation, Translatable;
47 import selery.log : Format;
48 import selery.node.info : PlayerInfo, WorldInfo;
49 import selery.node.server : isServerRunning, NodeServer, ServerCommandSender;
50 import selery.player.bedrock : BedrockPlayer;
51 import selery.player.java : JavaPlayer;
52 import selery.player.player : Player, InputMode, PermissionLevel;
53 import selery.plugin : Description, permission, hidden, unimplemented;
54 import selery.util.messages : Messages;
55 import selery.util.util : unformat;
56 import selery.world.world : Time;
57 
58 enum vanilla;
59 enum op;
60 
61 struct aliases {
62 
63 	string[] aliases;
64 
65 	this(string[] aliases...) {
66 		this.aliases = aliases;
67 	}
68 
69 }
70 
71 /**
72  * Supported vanilla commands:
73  * [ ] clear
74  * [ ] clone
75  * [ ] defaultgamemode
76  * [x] deop
77  * [ ] difficulty
78  * [ ] effect
79  * [ ] enchant
80  * [ ] execute
81  * [ ] fill
82  * [x] gamemode
83  * [ ] gamerule
84  * [ ] give
85  * [x] help
86  * [x] kick
87  * [ ] kill
88  * [x] list
89  * [ ] locate
90  * [x] me
91  * [x] op
92  * [ ] playsound
93  * [ ] replaceitem
94  * [x] say
95  * [ ] setblock
96  * [x] setmaxplayers
97  * [ ] setworldspawn
98  * [ ] spawnpoint
99  * [ ] spreadplayers
100  * [x] stop
101  * [ ] stopsound
102  * [ ] summon
103  * [x] tell
104  * [ ] testfor
105  * [ ] testforblock
106  * [ ] testforblocks
107  * [ ] time
108  * [ ] title
109  * [x] toggledownfall
110  * [ ] tp (teleport)
111  * [x] transferserver
112  * [x] weather
113  * 
114  * Supported multiplayer commands:
115  * [ ] ban
116  * [ ] ban-ip
117  * [ ] banlist
118  * [ ] pardon
119  * [x] stop
120  * [ ] whitelist
121  */
122 final class Commands {
123 
124 	enum list = mixin({
125 		string[] commands;
126 		foreach(member ; __traits(allMembers, typeof(this))) {
127 			static if(member[$-1] == '0') commands ~= member[0..$-1];
128 		}
129 		return "TypeTuple!(" ~ commands.to!string[1..$-1] ~ ")";
130 	}());
131 
132 	static Commands register(shared NodeServer server) {
133 		return new Commands(server).register();
134 	}
135 
136 	private shared NodeServer server;
137 
138 	public this(shared NodeServer server) {
139 		this.server = server;
140 	}
141 
142 	public Commands register() {
143 		auto server = cast()this.server;
144 		const config = this.server.config.node;
145 		foreach(command ; list) {
146 			if(mixin("config." ~ command ~ "Command")) this.registerImpl!(command, 0)(server);
147 		}
148 		return this;
149 	}
150 	
151 	private void registerImpl(string command, size_t count)(NodeServer server) {
152 		mixin("alias C = " ~ command ~ to!string(count) ~ ";");
153 		static if(count == 0) {
154 			static if(hasUDA!(C, vanilla)) enum description = Translatable.fromBedrock("commands." ~ command ~ ".description");
155 			else enum description = Translatable("commands." ~ command ~ ".description");
156 			static if(hasUDA!(C, aliases)) enum aliases = getUDAs!(C, aliases)[0].aliases;
157 			else enum string[] aliases = [];
158 			static if(hasUDA!(C, permission)) enum permissions = getUDAs!(C, permission)[0].permissions;
159 			else enum string[] permissions = [];
160 			server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description(description), aliases, hasUDA!(C, op), permissions, hasUDA!(C, hidden), !hasUDA!(C, unimplemented));
161 		} else {
162 			server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description.init, [], 0, [], false, !hasUDA!(C, unimplemented));
163 		}
164 		static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.registerImpl!(command, count + 1)(server);
165 	}
166 
167 	private void sendUnimplementedMessage(CommandSender sender) {
168 		sender.sendMessage(Format.red, "Not Implemented");
169 	}
170 
171 	public Commands unregister() {
172 		//TODO unregister overloads using delegate's pointers
173 		auto server = cast()this.server;
174 		foreach(command ; list) {
175 			this.unregisterImpl!(command, 0)(server);
176 		}
177 		return this;
178 	}
179 	
180 	private void unregisterImpl(string command, size_t count)(NodeServer server) {
181 		mixin("alias C = " ~ command ~ to!string(count) ~ ";");
182 		//server.unregisterCommandByOverload(mixin("&this." ~ command ~ to!string(count)), convertedName!command);
183 		static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.unregisterImpl!(command, count + 1)(server);
184 	}
185 
186 	// about
187 
188 	void about0(CommandSender sender) {
189 		sender.sendMessage(Translation(Messages.about.software, Software.name ~ " " ~ Software.fullVersion));
190 		if(this.server.plugins.length) {
191 			sender.sendMessage(Translation(Messages.about.plugins, this.server.plugins.length));
192 			foreach(_plugin ; this.server.plugins) {
193 				auto plugin = cast()_plugin;
194 				sender.sendMessage("* ", Format.green, plugin.name, Format.reset, " ", (!plugin.vers.startsWith("~") ? "v" : ""), plugin.vers);
195 			}
196 		}
197 	}
198 
199 	// clear
200 
201 	@vanilla @op clear0(Player sender) {
202 		this.clear1(sender, [sender]);
203 	}
204 
205 	@unimplemented @vanilla clear1(WorldCommandSender sender, Player[] target) {}
206 
207 	@unimplemented @vanilla clear2(WorldCommandSender sender, Player[] target, string itemName) {}
208 
209 	// clone
210 
211 	enum MaskMode { masked, replace }
212 
213 	enum CloneMode { force, move, normal }
214 	
215 	@unimplemented @vanilla @op clone0(WorldCommandSender sender, Position begin, Position end, Position destination, MaskMode maskMode=MaskMode.replace, CloneMode cloneMode=CloneMode.normal) {}
216 	
217 	@unimplemented @vanilla clone0(WorldCommandSender sender, Position begin, Position end, Position destination, SingleEnum!"filtered" maskMode, CloneMode cloneMode, string tileName) {}
218 
219 	// defaultgamemode
220 
221 	@unimplemented @vanilla @op defaultgamemode0(WorldCommandSender sender, Gamemode gamemode) {}
222 
223 	// deop
224 
225 	@vanilla @op deop0(WorldCommandSender sender, Player player) {
226 		if(player.permissionLevel <= PermissionLevel.operator) {
227 			if(player.operator) {
228 				player.operator = false;
229 				player.sendMessage(Translation(Messages.deop.message));
230 			}
231 			sender.sendMessage(Translation(Messages.deop.success, player.displayName));
232 		} else {
233 			sender.sendMessage(Translation(Messages.deop.failed, player.displayName));
234 		}
235 	}
236 
237 	@vanilla deop1(ServerCommandSender sender, string player) {
238 		executeOnPlayers(sender, player, (shared PlayerInfo info){
239 			if(info.permissionLevel <= PermissionLevel.operator) {
240 				if(info.permissionLevel == PermissionLevel.operator) {
241 					sender.server.updatePlayerPermissionLevel(info, PermissionLevel.user);
242 					//TODO send message to the player
243 				}
244 				sender.sendMessage(Translation(Messages.deop.success, info.displayName));
245 			} else {
246 				sender.sendMessage(Format.red, Translation(Messages.deop.failed, info.displayName));
247 			}
248 		});
249 	}
250 
251 	// difficulty
252 	
253 	@vanilla @op difficulty0(WorldCommandSender sender, Difficulty difficulty) {
254 		sender.world.difficulty = difficulty;
255 		sender.sendMessage(Translation(Messages.difficulty.success, difficulty));
256 	}
257 	
258 	@vanilla difficulty1(WorldCommandSender sender, Ranged!(ubyte, 0, 3) difficulty) {
259 		this.difficulty0(sender, cast(Difficulty)difficulty.value);
260 	}
261 
262 	@vanilla difficulty2(ServerCommandSender sender, string world, Difficulty difficulty) {
263 		executeOnWorlds(sender, world, (shared WorldInfo info){
264 			sender.server.updateWorldDifficulty(info, difficulty);
265 			sender.sendMessage(Translation(Messages.difficulty.success, difficulty));
266 		});
267 
268 	}
269 
270 	@vanilla difficulty3(ServerCommandSender sender, string world, Ranged!(ubyte, 0, 3) difficulty) {
271 		this.difficulty2(sender, world, cast(Difficulty)difficulty.value);
272 	}
273 
274 	// effect
275 
276 	@unimplemented @vanilla @op effect0(WorldCommandSender sender, SingleEnum!"clear" clear, Entity[] target) {}
277 
278 	@unimplemented @vanilla effect1(WorldCommandSender sender, SingleEnum!"clear" clear, Entity[] target, SnakeCaseEnum!Effects effect) {}
279 
280 	alias Duration = Ranged!(uint, 0, 1_000_000);
281 
282 	@unimplemented @vanilla effect2(WorldCommandSender sender, SingleEnum!"give" give, Entity[] target, SnakeCaseEnum!Effects effect, Duration duration=Duration(30), ubyte amplifier=0, bool hideParticles=false) {}
283 
284 	// enchant
285 
286 	alias Level = Ranged!(ubyte, 1, ubyte.max);
287 
288 	@unimplemented @vanilla @op enchant0(WorldCommandSender sender, Player[] target, SnakeCaseEnum!Enchantments enchantment, Level level=Level(1)) {}
289 
290 	@vanilla enchant1(Player sender, SnakeCaseEnum!Enchantments enchantment, Level level=Level(1)) {
291 		this.enchant0(sender, [sender], enchantment, level);
292 	}
293 
294 	// experience
295 
296 	enum ExperienceAction { add, set }
297 
298 	enum ExperienceType { points, levels }
299 
300 	@unimplemented @vanilla @op @aliases("xp") experience0(WorldCommandSender sender, ExperienceAction action, Player[] target, uint amount, ExperienceType type=ExperienceType.levels) {}
301 
302 	@vanilla experience1(Player sender, ExperienceAction action, uint amount, ExperienceType type=ExperienceType.levels) {
303 		this.experience0(sender, action, [sender], amount, type);
304 	}
305 
306 	@unimplemented @vanilla experience2(WorldCommandSender sender, SingleEnum!"query" query, Player target, ExperienceType type) {}
307 
308 	@vanilla experience3(Player sender, SingleEnum!"query" query, ExperienceType type) {
309 		this.experience2(sender, query, sender, type);
310 	}
311 
312 	// execute
313 
314 	//class ExecuteCommand : WorldCommandSender {}
315 
316 	@unimplemented @vanilla @op execute0(WorldCommandSender sender, Entity[] origin, Position position, string command) {}
317 
318 	// fill
319 
320 	enum OldBlockHandling { destroy, hollow, keep, outline, replace }
321 
322 	@unimplemented @vanilla @op fill0(WorldCommandSender sender, Position from, Position to, string block, OldBlockHandling oldBlockHandling=OldBlockHandling.replace) {}
323 
324 	// gamemode
325 
326 	@vanilla @op @aliases("gm") gamemode0(WorldCommandSender sender, Gamemode gamemode, Player[] target) {
327 		foreach(player ; target) {
328 			player.gamemode = gamemode;
329 			sender.sendMessage(Translation(Messages.gamemode.successOther, player.displayName, gamemode));
330 		}
331 	}
332 
333 	@vanilla gamemode1(Player sender, Gamemode gamemode) {
334 		sender.gamemode = gamemode;
335 		sender.sendMessage(Translation(Messages.gamemode.successSelf, gamemode));
336 	}
337 
338 	@vanilla gamemode2(ServerCommandSender sender, Gamemode gamemode, string target) {
339 		executeOnPlayers(sender, target, (shared PlayerInfo info){
340 			sender.server.updatePlayerGamemode(info, gamemode);
341 			sender.sendMessage(Translation(Messages.gamemode.successOther, info.displayName, gamemode));
342 		});
343 	}
344 
345 	// gamerule
346 
347 	enum Gamerule { depleteHunger, doDaylightCycle, doWeatherCycle, naturalRegeneration, pvp, randomTickSpeed }
348 
349 	@vanilla @op gamerule0(WorldCommandSender sender) {
350 		sender.sendMessage(join([__traits(allMembers, Gamerule)], ", "));
351 	}
352 
353 	@vanilla gamerule1(WorldCommandSender sender, Gamerule rule) {
354 		//TODO
355 		sender.sendMessage(rule, " = ", {
356 			final switch(rule) with(Gamerule) {
357 				case depleteHunger: return sender.world.depleteHunger.to!string;
358 				case doDaylightCycle: return sender.world.time.cycle.to!string;
359 				case doWeatherCycle: return sender.world.weather.cycle.to!string;
360 				case naturalRegeneration: return sender.world.naturalRegeneration.to!string;
361 				case pvp: return sender.world.pvp.to!string;
362 				case randomTickSpeed: return sender.world.randomTickSpeed.to!string;
363 			}
364 		}());
365 	}
366 
367 	@vanilla gamerule2(WorldCommandSender sender, Gamerule rule, bool value) {
368 		//TODO
369 		switch(rule) with(Gamerule) {
370 			case depleteHunger: sender.world.depleteHunger = value; break;
371 			case doDaylightCycle: sender.world.time.cycle = value; break;
372 			case doWeatherCycle: sender.world.weather.cycle = value; break;
373 			case naturalRegeneration: sender.world.naturalRegeneration = value; break;
374 			case pvp: sender.world.pvp = value; break;
375 			default:
376 				sender.sendMessage(Format.red, Translation(Messages.gamerule.invalidType, rule));
377 				return;
378 		}
379 		sender.sendMessage(Translation(Messages.gamerule.success, rule, value));
380 	}
381 
382 	@vanilla gamerule3(WorldCommandSender sender, Gamerule rule, Ranged!(int, 0, int.max) value) {
383 		//TODO
384 		switch(rule) with(Gamerule) {
385 			case randomTickSpeed: sender.world.randomTickSpeed = value; break;
386 			default:
387 				sender.sendMessage(Format.red, Translation(Messages.gamerule.invalidType, rule));
388 				return;
389 		}
390 		sender.sendMessage(Translation(Messages.gamerule.success, rule, value.value));
391 	}
392 	
393 	// give
394 	
395 	@unimplemented @vanilla @op give0(WorldCommandSender sender, Player[] target, string item, ubyte amount=1) {}
396 	
397 	@vanilla give1(Player sender, string item, ubyte amount=1) {
398 		this.give0(sender, [sender], item, amount);
399 	}
400 
401 	// help
402 
403 	@vanilla @aliases("?") help0(JavaPlayer sender, int page=1) {
404 		// pocket players have the help command client-side
405 		Command[] commands;
406 		foreach(name, command; sender.availableCommands) {
407 			sender.server.logger.log(name);
408 			if(command.name == name && !command.hidden) commands ~= command;
409 		}
410 		sort!((a, b) => a.name < b.name)(commands);
411 		immutable pages = cast(size_t)ceil(commands.length.to!float / 7); // commands.length should always be at least 1 (help command)
412 		page = clamp(--page, 0, pages - 1);
413 		sender.sendMessage(Format.darkGreen, Translation(Messages.help.header, page+1, pages));
414 		foreach(command ; commands[page*7..min($, (page+1)*7)]) {
415 			if(command.description.type == Description.EMPTY) sender.sendMessage(command.name);
416 			else if(command.description.type == Description.TEXT) sender.sendMessage(command.name, " - ", command.description.text);
417 			else sender.sendMessage(command.name, " - ", Translation(command.description.translatable));
418 		}
419 		sender.sendMessage(Format.green, Translation(Messages.help.footer));
420 	}
421 	
422 	@vanilla help1(ServerCommandSender sender) {
423 		Command[] commands;
424 		foreach(name, command; sender.availableCommands) {
425 			if(!command.hidden && name == command.name) {
426 				foreach(overload ; command.overloads) {
427 					if(overload.callableBy(sender)) {
428 						commands ~= command;
429 						break;
430 					}
431 				}
432 			}
433 		}
434 		sort!((a, b) => a.name < b.name)(commands);
435 		foreach(cmd ; commands) {
436 			if(cmd.description.type == Description.EMPTY) sender.sendMessage(Format.yellow, cmd.name, ":");
437 			else if(cmd.description.type == Description.TEXT) sender.sendMessage(Format.yellow, cmd.description.text);
438 			else sender.sendMessage(Format.yellow, Translation(cmd.description.translatable));
439 			foreach(overload ; cmd.overloads) {
440 				if(overload.callableBy(sender)) {
441 					sender.sendMessage("- ", cmd.name, " ", formatArg(overload));
442 				}
443 			}
444 		}
445 	}
446 	
447 	@vanilla help2(JavaPlayer sender, string command) {
448 		this.helpImpl(sender, "/", command);
449 	}
450 	
451 	@vanilla help3(ServerCommandSender sender, string command) {
452 		this.helpImpl(sender, "", command);
453 	}
454 	
455 	private void helpImpl(CommandSender sender, string slash, string command) {
456 		auto cmd = command in sender.availableCommands;
457 		if(cmd) {
458 			string[] messages;
459 			foreach(overload ; cmd.overloads) {
460 				if(overload.callableBy(sender)) {
461 					messages ~= ("- " ~ slash ~ cmd.name ~ " " ~ formatArg(overload));
462 				}
463 			}
464 			if(messages.length) {
465 				if(cmd.aliases.length) {
466 					sender.sendMessage(Format.yellow, Translation(Messages.help.commandAliases, cmd.name, cmd.aliases.join(", ")));
467 				} else {
468 					sender.sendMessage(Format.yellow ~ cmd.name ~ ":");
469 				}
470 				if(cmd.description.type == Description.TEXT) {
471 					sender.sendMessage(Format.yellow, cmd.description.text);
472 				} else if(cmd.description.type == Description.TRANSLATABLE) {
473 					sender.sendMessage(Format.yellow, Translation(cmd.description.translatable));
474 				}
475 				sender.sendMessage(Translation(Messages.generic.usage, ""));
476 				foreach(message ; messages) {
477 					sender.sendMessage(message);
478 				}
479 				return;
480 			}
481 		}
482 		sender.sendMessage(Format.red, Translation(Messages.generic.invalidParameter, command));
483 	}
484 
485 	// kick
486 
487 	@vanilla @op kick0(WorldCommandSender sender, Player[] target, string message) {
488 		string[] kicked;
489 		foreach(player ; target) {
490 			player.kick(message);
491 			kicked ~= player.displayName;
492 		}
493 		sender.sendMessage(Translation(Messages.kick.successReason, kicked.join(", "), message));
494 	}
495 
496 	@vanilla kick1(WorldCommandSender sender, Player[] target) {
497 		string[] kicked;
498 		foreach(player ; target) {
499 			player.kick();
500 			kicked ~= player.name;
501 		}
502 		sender.sendMessage(Translation(Messages.kick.success, kicked.join(", ")));
503 	}
504 
505 	@vanilla kick2(ServerCommandSender sender, string player, string message) {
506 		executeOnPlayers(sender, player, (shared PlayerInfo info){
507 			sender.server.kick(info.hubId, message);
508 			sender.sendMessage(Translation(Messages.kick.successReason, info.displayName, message));
509 		});
510 	}
511 
512 	@vanilla kick3(ServerCommandSender sender, string player) {
513 		executeOnPlayers(sender, player, (shared PlayerInfo info){
514 			server.kick(info.hubId, "disconnect.closed", []);
515 			sender.sendMessage(Translation(Messages.kick.success, info.displayName));
516 		});
517 	}
518 
519 	// kill
520 
521 	@unimplemented @vanilla @op kill0(WorldCommandSender sender, Entity[] target) {}
522 
523 	@vanilla kill1(Player sender) {
524 		this.kill0(sender, [sender]);
525 	}
526 
527 	// list
528 
529 	@vanilla list0(CommandSender sender) {
530 		// list players on the current node
531 		sender.sendMessage(Translation(Messages.list.players, sender.server.online, sender.server.max));
532 		if(sender.server.online) {
533 			string[] names;
534 			foreach(player ; server.players) {
535 				names ~= player.displayName;
536 			}
537 			sender.sendMessage(names.join(", "));
538 		}
539 	}
540 
541 	// locate
542 
543 	enum StructureType { endcity, fortress, mansion, mineshaft, monument, stronghold, temple, village }
544 
545 	@unimplemented @vanilla @op locate0(WorldCommandSender sender, StructureType structureType) {}
546 
547 	// me
548 
549 	@vanilla me0(Player sender, string message) {
550 		//TODO replace target selectors with names
551 		sender.world.broadcast("* " ~ sender.displayName ~ Format.reset ~ " " ~ unformat(message));
552 	}
553 
554 	// op
555 
556 	@vanilla @op op0(WorldCommandSender sender, Player player) {
557 		if(!player.operator) {
558 			player.operator = true;
559 			player.sendMessage(Translation(Messages.op.message));
560 			sender.sendMessage(Translation(Messages.op.success, player.displayName));
561 		} else {
562 			sender.sendMessage(Format.red, Translation(Messages.op.failed, player.displayName));
563 		}
564 	}
565 
566 	@vanilla op1(ServerCommandSender sender, string player) {
567 		executeOnPlayers(sender, player, (shared PlayerInfo info){
568 			if(info.permissionLevel < PermissionLevel.operator) {
569 				sender.server.updatePlayerPermissionLevel(info, PermissionLevel.operator);
570 				//TODO send message to the player
571 				sender.sendMessage(Translation(Messages.op.success, info.displayName));
572 			} else {
573 				sender.sendMessage(Format.red, Translation(Messages.op.failed, info.displayName));
574 			}
575 		});
576 	}
577 
578 	// permission
579 
580 	enum PermissionAction { grant, revoke }
581 
582 	@unimplemented @op permission0(WorldCommandSender sender, PermissionAction action, Player[] target, string permission) {}
583 
584 	@unimplemented void permission1(WorldCommandSender sender, SingleEnum!"list" list, Player target) {}
585 
586 	@unimplemented void permission2(ServerCommandSender sender, PermissionAction action, string target, string permission) {}
587 
588 	// say
589 
590 	@vanilla @op say0(WorldCommandSender sender, string message) {
591 		auto player = cast(Player)sender;
592 		immutable name = player is null ? "@" : player.displayName ~ Format.reset;
593 		//TODO convert targets into strings
594 		sender.world.broadcast("[" ~ name ~ "] " ~ message); //TODO unformat
595 	}
596 
597 	@vanilla say1(ServerCommandSender sender, string message) {
598 		sender.server.broadcast("[@] " ~ message);
599 	}
600 
601 	// seed
602 
603 	@vanilla @op seed0(WorldCommandSender sender) {
604 		sender.sendMessage(Translation(Messages.seed.success, sender.world.seed));
605 	}
606 	
607 	// setmaxplayers
608 	
609 	@vanilla @op setmaxplayers0(CommandSender sender, uint players) {
610 		sender.server.max = players;
611 		sender.sendMessage(Translation(Messages.setmaxplayers.success, players));
612 	}
613 
614 	// setworldspawn
615 
616 	@unimplemented @vanilla @op setworldspawn0(WorldCommandSender sender, Position position) {}
617 
618 	@vanilla setworldspawn1(WorldCommandSender sender) {
619 		this.setworldspawn0(sender, Position(Position.Point(true, sender.position.x), Position.Point(true, sender.position.y), Position.Point(true, sender.position.z)));
620 	}
621 
622 	// spawnpoint
623 
624 	@unimplemented @vanilla @op spawnpoint0(WorldCommandSender sender, Player[] target, Position position) {}
625 
626 	@vanilla spawnpoint1(WorldCommandSender sender, Player[] target) {
627 		this.spawnpoint0(sender, target, Position(Position.Point(true, sender.position.x), Position.Point(true, sender.position.y), Position.Point(true, sender.position.z)));
628 	}
629 
630 	@vanilla spawnpoint2(Player sender) {
631 		this.spawnpoint1(sender, [sender]);
632 	}
633 
634 	// spreadplayers
635 
636 	//TODO implement Rotation
637 	//@unimplemented @vanilla @op spreadplayers0(WorldCommandSender sender, Rotation x, Rotation z, double spreadDistance, double maxRange, Entity[] target) {}
638 
639 	// stop
640 	
641 	@vanilla @op stop0(CommandSender sender, bool gracefully=true) {
642 		if(gracefully) {
643 			if(isServerRunning) {
644 				sender.sendMessage(Translation(Messages.stop.start));
645 				this.server.shutdown();
646 			} else {
647 				sender.sendMessage(Format.red, Translation(Messages.stop.failed));
648 			}
649 		} else {
650 			import std.c.stdlib : exit;
651 			exit(0);
652 		}
653 	}
654 
655 	// summon
656 
657 	@unimplemented @vanilla @op summon0(WorldCommandSender sender, string entityType, Position position) {}
658 
659 	@unimplemented @vanilla summon1(WorldCommandSender sender, string entityType) {}
660 
661 	// tell
662 
663 	@vanilla @aliases("msg", "w") tell0(Player sender, Player[] recipient, string message) {
664 		string[] sent;
665 		foreach(player ; recipient) {
666 			if(player.id != sender.id) {
667 				player.sendMessage(Format.italic, Translation(Messages.message.incoming, sender.displayName, message));
668 				sent ~= player.displayName;
669 			}
670 		}
671 		if(sent.length) sender.sendMessage(Format.italic, Translation(Messages.message.outcoming, sent.join(", "), message));
672 		else sender.sendMessage(Format.red, Translation(Messages.message.sameTarget));
673 	}
674 
675 	@vanilla @op time0(WorldCommandSender sender, SingleEnum!"add" add, uint amount) {
676 		uint time = sender.world.time.time + amount;
677 		if(time >= 24000) sender.world.time.day += time / 24000;
678 		sender.world.time.time = time;
679 		sender.sendMessage(Translation(Messages.time.added, amount));
680 	}
681 
682 	// time
683 
684 	enum TimeQuery { day, daytime, gametime }
685 
686 	@vanilla @op time1(WorldCommandSender sender, SingleEnum!"query" query, TimeQuery time) {
687 		final switch(time) with(TimeQuery) {
688 			case day:
689 				sender.sendMessage(Translation(Messages.time.queryDay, sender.world.time.day));
690 				break;
691 			case daytime:
692 				sender.sendMessage(Translation(Messages.time.queryDaytime, sender.world.time.time));
693 				break;
694 			case gametime:
695 				sender.sendMessage(Translation(Messages.time.queryGametime, sender.world.ticks));
696 				break;
697 		}
698 	}
699 
700 	@vanilla @op time2(WorldCommandSender sender, SingleEnum!"set" set, uint amount) {
701 		sender.sendMessage(Translation(Messages.time.set, (sender.world.time.time = amount)));
702 	}
703 
704 	@vanilla @op time3(WorldCommandSender sender, SingleEnum!"set" set, Time amount) {
705 		this.time2(sender, set, cast(uint)amount);
706 	}
707 
708 	// title
709 
710 	@vanilla @op title0(WorldCommandSender sender, Player[] target, SingleEnum!"clear" clear) {
711 		foreach(player ; target) player.clearTitle();
712 		//TODO send message
713 	}
714 
715 	@vanilla title1(WorldCommandSender sender, Player[] target, SingleEnum!"reset" reset) {
716 		foreach(player ; target) player.resetTitle();
717 		//TODO send message
718 	}
719 
720 	@unimplemented @vanilla title2(WorldCommandSender sender, Player[] target, SingleEnum!"title" title, string text) {}
721 
722 	@unimplemented @vanilla title3(WorldCommandSender sender, Player[] target, SingleEnum!"subtitle" subtitle, string text) {}
723 
724 	@unimplemented @vanilla title4(WorldCommandSender sender, Player[] target, SingleEnum!"actionbar" actionbar, string text) {
725 		foreach(player ; target) player.sendTip(text);
726 		//TODO send message
727 	}
728 
729 	@unimplemented @vanilla title5(WorldCommandSender sender, Player[] target, SingleEnum!"times" times, uint fadeIn, uint stay, uint fadeOut) {}
730 
731 	// toggledownfall
732 
733 	@vanilla @op toggledownfall0(WorldCommandSender sender) {
734 		if(sender.world.weather.raining) sender.world.weather.clear();
735 		else sender.world.weather.start();
736 		sender.sendMessage(Translation(Messages.toggledownfall.success));
737 	}
738 
739 	// tp
740 
741 	@vanilla @op @permission("minecraft:teleport") @aliases("teleport") tp0(Player sender, Entity destination) {
742 		this.tp2(sender, [sender], destination);
743 	}
744 
745 	@vanilla tp1(Player sender, Position destination) {
746 		this.tp3(sender, [sender], destination);
747 	}
748 
749 	@unimplemented @vanilla tp2(WorldCommandSender sender, Entity[] victim, Entity destination) {}
750 
751 	@unimplemented @vanilla tp3(WorldCommandSender sender, Entity[] victim, Position destination) {}
752 
753 	// transfer
754 
755 	@unimplemented @op transfer0(WorldCommandSender sender, Player[] target, string node) {}
756 
757 	@unimplemented @op transfer1(ServerCommandSender sender, string target, string node) {}
758 
759 	// transferserver
760 	
761 	@vanilla @op transferserver0(Player sender, string ip, int port=19132) {
762 		immutable _port = cast(ushort)port;
763 		if(_port == port) {
764 			try {
765 				sender.transfer(ip, _port);
766 			} catch(Exception) {}
767 		} else {
768 			sender.sendMessage(Format.red, Translation(Messages.transferserver.invalidPort));
769 		}
770 	}
771 
772 	@vanilla @op transferserver1(WorldCommandSender sender, Player[] target, string ip, int port=19132) {
773 		immutable _port = cast(ushort)port;
774 		if(_port == port) {
775 			bool success = false;
776 			foreach(player ; target) {
777 				try {
778 					player.transfer(ip, _port);
779 					success = true;
780 				} catch(Exception) {}
781 			}
782 			if(success) sender.sendMessage(Translation(Messages.transferserver.success));
783 		} else {
784 			sender.sendMessage(Format.red, Translation(Messages.transferserver.invalidPort));
785 		}
786 	}
787 
788 	// weather
789 
790 	enum Weather { clear, rain, thunder }
791 
792 	@vanilla @op weather0(WorldCommandSender sender, Weather type, int duration=0) {
793 		if(type == Weather.clear) {
794 			if(duration <= 0) sender.world.weather.clear();
795 			else sender.world.weather.clear(duration);
796 			sender.sendMessage(Translation(Messages.weather.clear));
797 		} else {
798 			if(duration <= 0 || duration > 1_000_000) duration = uniform!"[]"(6000, 18000, sender.world.random);
799 			if(type == Weather.rain) {
800 				sender.world.weather.start(duration, false);
801 				sender.sendMessage(Translation(Messages.weather.rain));
802 			} else {
803 				sender.world.weather.start(duration, true);
804 				sender.sendMessage(Translation(Messages.weather.thunder));
805 			}
806 		}
807 	}
808 
809 	// world
810 	
811 	void world0(CommandSender sender, SingleEnum!"list" list) {
812 		string[] names;
813 		foreach(world ; sender.server.worlds) names ~= world.name;
814 		sender.sendMessage(Translation("commands.world.list", names.length, names.join(", ")));
815 	}
816 
817 	@op world1(CommandSender sender, SingleEnum!"add" add, string name, bool defaultWorld=false) {
818 		auto world = sender.server.addWorld(name);
819 		if(world) {
820 			sender.sendMessage(Translation("commands.world.add.success"));
821 			if(defaultWorld) sender.server.defaultWorld = world;
822 		} else {
823 			sender.sendMessage(Format.red, Translation("commands.world.add.failed"));
824 		}
825 	}
826 
827 	void world2(CommandSender sender, SingleEnum!"remove" remove, string name) {
828 		executeOnWorlds(sender, name, (shared WorldInfo info){
829 			if(sender.server.removeWorld(info.id)) sender.sendMessage(Translation("commands.world.remove.success"));
830 		});
831 	}
832 
833 	@unimplemented void world3(CommandSender sender, SingleEnum!"info" info, string name) {}
834 
835 }
836 
837 string convertName(string command, string replacement=" ") {
838 	string ret;
839 	foreach(c ; command) {
840 		if(c >= 'A' && c <= 'Z') ret ~= replacement ~ cast(char)(c + 32);
841 		else ret ~= c;
842 	}
843 	return ret;
844 }
845 
846 private enum convertedName(string command) = convertName(command);
847 
848 private string[] formatArgs(Command command, CommandSender sender) {
849 	string[] ret;
850 	foreach(overload ; command.overloads) {
851 		if(overload.callableBy(sender)) ret ~= formatArg(overload);
852 	}
853 	return ret;
854 }
855 
856 private string formatArg(Command.Overload overload) {
857 	string[] p;
858 	foreach(i, param; overload.params) {
859 		immutable enum_ = overload.pocketTypeOf(i) == PocketType.stringenum;
860 		if(enum_ && overload.enumMembers(i).length == 1) {
861 			p ~= overload.enumMembers(i)[0];
862 		} else {
863 			string full = enum_ && overload.enumMembers(i).length < 5 ? overload.enumMembers(i).join("|") : (param ~ ": " ~ overload.typeOf(i));
864 			if(i < overload.requiredArgs) {
865 				p ~= "<" ~ full ~ ">";
866 			} else {
867 				p ~= "[" ~ full ~ "]";
868 			}
869 		}
870 	}
871 	return p.join(" ");
872 }
873 
874 private void executeOnWorlds(CommandSender sender, string name, void delegate(shared WorldInfo) del) {
875 	auto world = sender.server.getWorldByName(name);
876 	if(world !is null) {
877 		del(world);
878 	} else {
879 		sender.sendMessage(Format.red, Translation("commands.world.notFound", name));
880 	}
881 }
882 
883 private void executeOnPlayers(CommandSender sender, string name, void delegate(shared PlayerInfo) del) {
884 	if(name.startsWith("@")) {
885 		if(name == "@a" || name == "@r") {
886 			auto players = sender.server.players;
887 			if(players.length) {
888 				final switch(name) {
889 					case "@a":
890 						foreach(player ; sender.server.players) {
891 							del(player);
892 						}
893 						break;
894 					case "@r":
895 						del(players[uniform(0, $)]);
896 						break;
897 				}
898 			} else {
899 				sender.sendMessage(Format.red, Translation(Messages.generic.targetNotFound));
900 			}
901 		} else {
902 			sender.sendMessage(Format.red, Translation(Messages.generic.invalidSyntax));
903 		}
904 	} else {
905 		immutable iname = name.toLower();
906 		bool executed = false;
907 		foreach(player ; sender.server.players) {
908 			if(player.lname == iname) {
909 				executed = true;
910 				del(player);
911 			}
912 		}
913 		if(!executed) sender.sendMessage(Format.red, Translation(Messages.generic.playerNotFound, name));
914 	}
915 }