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/node/commands.d, selery/node/commands.d)
28  */
29 module selery.node.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 sel.format : Format, unformat;
40 
41 import selery.about : Software;
42 import selery.command.command : Command;
43 import selery.command.util : CommandSender, WorldCommandSender, PocketType, SingleEnum, SnakeCaseEnum, Ranged, Position, Target;
44 import selery.config : Config, Gamemode, Difficulty, Dimension;
45 import selery.effect : Effects;
46 import selery.enchantment : Enchantments;
47 import selery.entity.entity : Entity;
48 import selery.lang : Translation, Translatable;
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 : PlayerInfo, Player, PermissionLevel;
53 import selery.plugin : Description, permission, hidden, unimplemented;
54 import selery.util.messages : Messages;
55 import selery.world.group : GroupInfo;
56 import selery.world.world : WorldInfo, 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 final class Commands {
72 
73 	enum list = mixin({
74 		string[] commands;
75 		foreach(member ; __traits(allMembers, typeof(this))) {
76 			static if(member[$-1] == '0') commands ~= member[0..$-1];
77 		}
78 		return "TypeTuple!(" ~ commands.to!string[1..$-1] ~ ")";
79 	}());
80 
81 	static Commands register(shared NodeServer server) {
82 		return new Commands(server).register();
83 	}
84 
85 	private shared NodeServer server;
86 
87 	public this(shared NodeServer server) {
88 		this.server = server;
89 	}
90 
91 	public Commands register() {
92 		auto server = cast()this.server;
93 		const config = this.server.config.node;
94 		foreach(command ; list) {
95 			if(mixin("config." ~ command ~ "Command")) this.registerImpl!(command, 0)(server);
96 		}
97 		return this;
98 	}
99 	
100 	private void registerImpl(string command, size_t count)(NodeServer server) {
101 		mixin("alias C = " ~ command ~ to!string(count) ~ ";");
102 		static if(count == 0) {
103 			static if(hasUDA!(C, vanilla)) enum description = Translatable.fromBedrock("commands." ~ command ~ ".description");
104 			else enum description = Translatable("commands." ~ command ~ ".description");
105 			static if(hasUDA!(C, aliases)) enum aliases = getUDAs!(C, aliases)[0].aliases;
106 			else enum string[] aliases = [];
107 			static if(hasUDA!(C, permission)) enum permissions = getUDAs!(C, permission)[0].permissions;
108 			else enum string[] permissions = [];
109 			server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description(description), aliases, hasUDA!(C, op), permissions, hasUDA!(C, hidden), !hasUDA!(C, unimplemented));
110 		} else {
111 			server.registerCommand!C(mixin("&this." ~ command ~ count.to!string), convertedName!command, Description.init, [], 0, [], false, !hasUDA!(C, unimplemented));
112 		}
113 		static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.registerImpl!(command, count + 1)(server);
114 	}
115 
116 	private void sendUnimplementedMessage(CommandSender sender) {
117 		sender.sendMessage(Format.red, "Not Implemented");
118 	}
119 
120 	public Commands unregister() {
121 		//TODO unregister overloads using delegate's pointers
122 		auto server = cast()this.server;
123 		foreach(command ; list) {
124 			this.unregisterImpl!(command, 0)(server);
125 		}
126 		return this;
127 	}
128 	
129 	private void unregisterImpl(string command, size_t count)(NodeServer server) {
130 		mixin("alias C = " ~ command ~ to!string(count) ~ ";");
131 		//server.unregisterCommandByOverload(mixin("&this." ~ command ~ to!string(count)), convertedName!command);
132 		static if(__traits(hasMember, typeof(this), command ~ to!string(count + 1))) this.unregisterImpl!(command, count + 1)(server);
133 	}
134 
135 	// about
136 
137 	void about0(CommandSender sender) {
138 		sender.sendMessage(Translation(Messages.about.software, Software.name ~ " " ~ Software.fullVersion));
139 		if(this.server.plugins.length) {
140 			sender.sendMessage(Translation(Messages.about.plugins, this.server.plugins.length));
141 			foreach(_plugin ; this.server.plugins) {
142 				auto plugin = cast()_plugin;
143 				sender.sendMessage("* ", Format.green, plugin.name, Format.reset, " ", (!plugin.version_.startsWith("~") ? "v" : ""), plugin.version_);
144 			}
145 		}
146 	}
147 
148 	// help
149 	
150 	@vanilla help0(ServerCommandSender sender) {
151 		Command[] commands;
152 		foreach(name, command; sender.availableCommands) {
153 			if(!command.hidden && name == command.name) {
154 				foreach(overload ; command.overloads) {
155 					if(overload.callableBy(sender)) {
156 						commands ~= command;
157 						break;
158 					}
159 				}
160 			}
161 		}
162 		sort!((a, b) => a.name < b.name)(commands);
163 		foreach(cmd ; commands) {
164 			if(cmd.description.type == Description.EMPTY) sender.sendMessage(Format.yellow, cmd.name, ":");
165 			else if(cmd.description.type == Description.TEXT) sender.sendMessage(Format.yellow, cmd.description.text);
166 			else sender.sendMessage(Format.yellow, Translation(cmd.description.translatable));
167 			foreach(overload ; cmd.overloads) {
168 				if(overload.callableBy(sender)) {
169 					sender.sendMessage("- ", cmd.name, " ", formatArg(overload));
170 				}
171 			}
172 		}
173 	}
174 	
175 	@vanilla help1(ServerCommandSender sender, string command) {
176 		this.helpImpl(sender, "", command);
177 	}
178 	
179 	private void helpImpl(CommandSender sender, string slash, string command) {
180 		auto cmd = command in sender.availableCommands;
181 		if(cmd) {
182 			string[] messages;
183 			foreach(overload ; cmd.overloads) {
184 				if(overload.callableBy(sender)) {
185 					messages ~= ("- " ~ slash ~ cmd.name ~ " " ~ formatArg(overload));
186 				}
187 			}
188 			if(messages.length) {
189 				if(cmd.aliases.length) {
190 					sender.sendMessage(Format.yellow, Translation(Messages.help.commandAliases, cmd.name, cmd.aliases.join(", ")));
191 				} else {
192 					sender.sendMessage(Format.yellow ~ cmd.name ~ ":");
193 				}
194 				if(cmd.description.type == Description.TEXT) {
195 					sender.sendMessage(Format.yellow, cmd.description.text);
196 				} else if(cmd.description.type == Description.TRANSLATABLE) {
197 					sender.sendMessage(Format.yellow, Translation(cmd.description.translatable));
198 				}
199 				sender.sendMessage(Translation(Messages.generic.usage, ""));
200 				foreach(message ; messages) {
201 					sender.sendMessage(message);
202 				}
203 				return;
204 			}
205 		}
206 		sender.sendMessage(Format.red, Translation(Messages.generic.invalidParameter, command));
207 	}
208 
209 	// permission
210 
211 	enum PermissionAction { grant, revoke }
212 
213 	@unimplemented @op permission0(WorldCommandSender sender, PermissionAction action, Player[] target, string permission) {}
214 
215 	@unimplemented void permission1(WorldCommandSender sender, SingleEnum!"list" list, Player target) {}
216 
217 	@unimplemented void permission2(ServerCommandSender sender, PermissionAction action, string target, string permission) {}
218 
219 	// stop
220 	
221 	@vanilla @op stop0(CommandSender sender, bool gracefully=true) {
222 		if(gracefully) {
223 			if(isServerRunning) {
224 				sender.sendMessage(Translation(Messages.stop.start));
225 				this.server.shutdown();
226 			} else {
227 				sender.sendMessage(Format.red, Translation(Messages.stop.failed));
228 			}
229 		} else {
230 			import core.stdc.stdlib : exit;
231 			exit(0);
232 		}
233 	}
234 
235 	// transfer
236 
237 	@unimplemented @op transfer0(WorldCommandSender sender, Player[] target, string node) {}
238 
239 	@unimplemented @op transfer1(ServerCommandSender sender, string target, string node) {}
240 
241 	// world
242 	
243 	void world0(CommandSender sender, SingleEnum!"list" list) {
244 		string[] names;
245 		foreach(group ; sender.server.worldGroups) names ~= group.name;
246 		sender.sendMessage(Translation("commands.world.list", names.length, names.join(", ")));
247 	}
248 
249 	@op world1(CommandSender sender, SingleEnum!"add" add, string name, bool defaultWorld=false) {
250 		auto world = sender.server.addWorld(name);
251 		if(world) {
252 			sender.sendMessage(Translation("commands.world.add.success"));
253 			//if(defaultWorld) sender.server.defaultWorld = world;
254 		} else {
255 			sender.sendMessage(Format.red, Translation("commands.world.add.failed"));
256 		}
257 	}
258 
259 	void world2(CommandSender sender, SingleEnum!"remove" remove, string name) {
260 		executeOnWorlds(sender, name, (shared GroupInfo info){
261 			if(sender.server.removeWorldGroup(info)) sender.sendMessage(Translation("commands.world.remove.success"));
262 		});
263 	}
264 
265 	@unimplemented void world3(CommandSender sender, SingleEnum!"info" info, string name) {}
266 
267 }
268 
269 string convertName(string command, string replacement=" ") {
270 	string ret;
271 	foreach(c ; command) {
272 		if(c >= 'A' && c <= 'Z') ret ~= replacement ~ cast(char)(c + 32);
273 		else ret ~= c;
274 	}
275 	return ret;
276 }
277 
278 private enum convertedName(string command) = convertName(command);
279 
280 private string[] formatArgs(Command command, CommandSender sender) {
281 	string[] ret;
282 	foreach(overload ; command.overloads) {
283 		if(overload.callableBy(sender)) ret ~= formatArg(overload);
284 	}
285 	return ret;
286 }
287 
288 private string formatArg(Command.Overload overload) {
289 	string[] p;
290 	foreach(i, param; overload.params) {
291 		immutable enum_ = overload.pocketTypeOf(i) == PocketType.stringenum;
292 		if(enum_ && overload.enumMembers(i).length == 1) {
293 			p ~= overload.enumMembers(i)[0];
294 		} else {
295 			string full = enum_ && overload.enumMembers(i).length < 5 ? overload.enumMembers(i).join("|") : (param ~ ": " ~ overload.typeOf(i));
296 			if(i < overload.requiredArgs) {
297 				p ~= "<" ~ full ~ ">";
298 			} else {
299 				p ~= "[" ~ full ~ "]";
300 			}
301 		}
302 	}
303 	return p.join(" ");
304 }
305 
306 private void executeOnWorlds(CommandSender sender, string name, void delegate(shared GroupInfo) del) {
307 	auto group = sender.server.getGroupByName(name);
308 	if(group !is null) {
309 		del(group);
310 	} else {
311 		sender.sendMessage(Format.red, Translation("commands.world.notFound", name));
312 	}
313 }
314 
315 private void executeOnPlayers(CommandSender sender, string name, void delegate(shared PlayerInfo) del) {
316 	if(name.startsWith("@")) {
317 		if(name == "@a" || name == "@r") {
318 			auto players = sender.server.players;
319 			if(players.length) {
320 				final switch(name) {
321 					case "@a":
322 						foreach(player ; sender.server.players) {
323 							del(player);
324 						}
325 						break;
326 					case "@r":
327 						del(players[uniform(0, $)]);
328 						break;
329 				}
330 			} else {
331 				sender.sendMessage(Format.red, Translation(Messages.generic.targetNotFound));
332 			}
333 		} else {
334 			sender.sendMessage(Format.red, Translation(Messages.generic.invalidSyntax));
335 		}
336 	} else {
337 		immutable iname = name.toLower();
338 		bool executed = false;
339 		foreach(player ; sender.server.players) {
340 			if(player.lname == iname) {
341 				executed = true;
342 				del(player);
343 			}
344 		}
345 		if(!executed) sender.sendMessage(Format.red, Translation(Messages.generic.playerNotFound, name));
346 	}
347 }