1 /**
2 Copyright: Copyright (c) 2015-2018 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.command.plugin;
7 
8 import netlib;
9 import pluginlib;
10 public import netlib : SessionId;
11 public import std.getopt;
12 import voxelman.log;
13 import std..string : format;
14 import voxelman.text.textsink : TextSink;
15 import voxelman.core.packets : CommandPacket;
16 public import voxelman.core.packets : CommandSourceType;
17 
18 // On client side source == 0
19 // On server if command is issued locally source == 0
20 // First argument is command name (useful for std.getopt)
21 alias CommandHandler = void delegate(CommandParams params);
22 
23 struct CommandInfo
24 {
25 	string names;
26 	CommandHandler handler;
27 	string[] paramUsage;
28 	string helpMessage;
29 }
30 
31 struct CommandParams
32 {
33 	string rawArgs; // without command name
34 	auto rawStrippedArgs() @property
35 	{
36 		import std..string : strip;
37 		return rawArgs.strip;
38 	}
39 	string[] args; // first arg is command name. Use with getopt.
40 	SessionId sourceSession;
41 	CommandSourceType sourceType;
42 	TextSink* textOutput;
43 }
44 
45 enum ExecStatus
46 {
47 	success,
48 	noCommandGiven, // can be safely ignored
49 	notRegistered,
50 	notRegisteredRedirected, // redirect to server
51 	//invalidArgs,
52 	error
53 }
54 
55 final class CommandPluginClient : IPlugin
56 {
57 	mixin CommandPluginCommon!false;
58 
59 	import voxelman.net.plugin : NetClientPlugin;
60 	NetClientPlugin connection;
61 
62 	override void preInit()
63 	{
64 		executorName = "CL";
65 		registerCommand(CommandInfo("help|cl_help", &onHelpCommand));
66 	}
67 
68 	override void init(IPluginManager pluginman)
69 	{
70 		connection = pluginman.getPlugin!NetClientPlugin;
71 	}
72 }
73 
74 final class CommandPluginServer : IPlugin
75 {
76 	mixin CommandPluginCommon!true;
77 
78 	import voxelman.net.plugin : NetServerPlugin;
79 	import voxelman.net.packets : MessagePacket, commandSourceToMsgEndpoint;
80 	import voxelman.session.server;
81 
82 	NetServerPlugin connection;
83 	ClientManager clientMan;
84 
85 	override void preInit()
86 	{
87 		executorName = "SV";
88 	}
89 
90 	override void init(IPluginManager pluginman)
91 	{
92 		connection = pluginman.getPlugin!NetServerPlugin;
93 		connection.registerPacketHandler!CommandPacket(&handleCommandPacket);
94 		clientMan = pluginman.getPlugin!ClientManager;
95 
96 		registerCommand(CommandInfo("help|sv_help", &onHelpCommand));
97 	}
98 
99 	// server command entry point. Command can come from network or from launcher (stdio)
100 	void handleCommandPacket(ubyte[] packetData, SessionId sessionId)
101 	{
102 		if (sessionId != 0) // not server
103 		{
104 			if (!clientMan.isLoggedIn(sessionId))
105 			{
106 				connection.sendTo(sessionId, MessagePacket("Log in to use commands"));
107 				return;
108 			}
109 		}
110 		auto packet = unpackPacket!CommandPacket(packetData);
111 
112 		if (packet.sourceType == CommandSourceType.localLauncher)
113 			packet.sourceType = CommandSourceType.clientLauncher;
114 
115 		execute(packet.command, packet.sourceType, sessionId);
116 		connection.sendTo(sessionId, MessagePacket(commandTextOutput.text, 0, commandSourceToMsgEndpoint[packet.sourceType]));
117 	}
118 }
119 
120 mixin template CommandPluginCommon(bool isServer)
121 {
122 	// IPlugin stuff
123 	mixin IdAndSemverFrom!"voxelman.command.plugininfo";
124 
125 	CommandInfo[] commands;
126 	CommandInfo[string] commandMap;
127 	TextSink commandTextOutput;
128 	string executorName; // SV or CL
129 
130 	void registerCommand(CommandInfo command)
131 	{
132 		import std.algorithm : splitter;
133 
134 		foreach(comAlias; command.names.splitter('|'))
135 		{
136 			assert(comAlias !in commandMap, comAlias ~ " command is already registered");
137 			commandMap[comAlias] = command;
138 		}
139 		commands ~= command;
140 	}
141 
142 	void onHelpCommand(CommandParams params)
143 	{
144 		foreach (ref command; commands)
145 		{
146 			params.textOutput.putfln("% 20s  %s  %s", command.names, command.paramUsage, command.helpMessage);
147 		}
148 
149 		// Also redirect to server
150 		static if(!isServer)
151 		{
152 			if (connection.isConnected) connection.send(CommandPacket("help", params.sourceType));
153 		}
154 	}
155 
156 	// Command output is given in commandTextOutput
157 	ExecStatus execute(const(char)[] input, CommandSourceType sourceType, SessionId source = SessionId(0))
158 	{
159 		import std.regex : regex, splitter;
160 		import std..string : strip;
161 		import std.array : array;
162 
163 		commandTextOutput.clear;
164 
165 		string stripped = cast(string)input.strip;
166 		string[] args = splitter(stripped, regex(`\s+`)).array;
167 
168 		if (args.length == 0)
169 		{
170 			return ExecStatus.noCommandGiven;
171 		}
172 
173 		string comName = args[0];
174 		string rawArgs = stripped[args[0].length..$];
175 
176 		//infof("%s %s> %s", executorName, sourceType, stripped);
177 
178 		commandTextOutput.put(executorName);
179 		commandTextOutput.put(">");
180 		commandTextOutput.putln(stripped);
181 
182 		if (auto command = comName in commandMap)
183 		{
184 			try
185 			{
186 				command.handler(CommandParams(rawArgs, args, source, sourceType, &commandTextOutput));
187 			}
188 			catch(Exception e)
189 			{
190 				commandTextOutput.putf("Error executing command '%s': %s", stripped, e.msg);
191 				return ExecStatus.error;
192 			}
193 		}
194 		else
195 		{
196 			static if(isServer)
197 			{
198 				commandTextOutput.putf("Unknown command '%s'", comName);
199 				return ExecStatus.notRegistered;
200 			}
201 			else
202 			{
203 				if (connection.isConnected)
204 				{
205 					// Redirect unknown command to the server.
206 					// Server will send response with corresponding endpoint, so results are shown in the right console
207 					connection.send(CommandPacket(stripped, sourceType));
208 
209 					// Prevent extra output of typed command, since it will be printed when results come back
210 					commandTextOutput.clear;
211 
212 					return ExecStatus.notRegisteredRedirected;
213 				}
214 				else
215 				{
216 					commandTextOutput.putf("Unknown command '%s', no server connection for redirect", stripped);
217 					return ExecStatus.notRegistered;
218 				}
219 			}
220 		}
221 
222 		return ExecStatus.success;
223 	}
224 }