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 }