1 /**
2 Copyright: Copyright (c) 2016-2017 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.session.server;
7 
8 import voxelman.log;
9 import std..string : format;
10 import netlib;
11 import pluginlib;
12 import datadriven;
13 import voxelman.math;
14 
15 import voxelman.core.config;
16 import voxelman.core.events;
17 import voxelman.net.events;
18 import voxelman.core.packets;
19 import voxelman.net.packets;
20 import voxelman.world.storage;
21 
22 import voxelman.command.plugin;
23 import voxelman.eventdispatcher.plugin;
24 import voxelman.entity.plugin : EntityComponentRegistry;
25 import voxelman.net.plugin;
26 import voxelman.world.serverworld;
27 
28 import voxelman.session.components;
29 import voxelman.session.clientdb;
30 import voxelman.session.sessionman;
31 
32 
33 struct ClientPositionManager
34 {
35 	ClientManager cm;
36 
37 	void tpToPos(Session* session, ClientDimPos dimPos, DimensionId dim)
38 	{
39 		auto position = cm.db.get!ClientPosition(session.dbKey);
40 
41 		position.dimPos = dimPos;
42 		position.dimension = dim;
43 		++position.positionKey;
44 
45 		sendPositionToClient(*position, session.sessionId);
46 		updateObserverBox(session);
47 	}
48 
49 	void tpToPlayer(Session* session, Session* destination)
50 	{
51 		cm.connection.sendTo(session.sessionId, MessagePacket(
52 			format("Teleporting to %s", destination.name)));
53 		auto position = cm.db.get!ClientPosition(session.dbKey);
54 		auto destposition = cm.db.get!ClientPosition(destination.dbKey);
55 
56 		position.dimPos = destposition.dimPos;
57 		position.dimension = destposition.dimension;
58 		++position.positionKey;
59 
60 		sendPositionToClient(*position, session.sessionId);
61 		updateObserverBox(session);
62 	}
63 
64 	void updateClientViewRadius(
65 		Session* session,
66 		int viewRadius)
67 	{
68 		auto settings = cm.db.get!ClientSettings(session.dbKey);
69 		settings.viewRadius = clamp(viewRadius,
70 			MIN_VIEW_RADIUS, MAX_VIEW_RADIUS);
71 		updateObserverBox(session);
72 	}
73 
74 	// Set client postion on server side, without sending position update to client
75 	void updateClientPosition(
76 		Session* session,
77 		ClientDimPos dimPos,
78 		ushort dimension,
79 		ubyte positionKey,
80 		bool updatePositionKey,
81 		bool sendPosUpdate = false)
82 	{
83 		auto position = cm.db.get!ClientPosition(session.dbKey);
84 
85 		// reject stale position. Dimension already has changed.
86 		if (position.positionKey != positionKey)
87 			return;
88 
89 		position.dimPos = dimPos;
90 		position.dimension = dimension;
91 		position.positionKey += cast(ubyte)updatePositionKey;
92 
93 		if(sendPosUpdate)
94 			sendPositionToClient(*position, session.sessionId);
95 
96 		updateObserverBox(session);
97 	}
98 
99 	void tpToDimension(Session* session, DimensionId dimension)
100 	{
101 		auto dimInfo = cm.serverWorld.dimMan.getOrCreate(dimension);
102 		auto position = cm.db.get!ClientPosition(session.dbKey);
103 		position.dimPos = dimInfo.spawnPos;
104 		position.dimension = dimension;
105 		++position.positionKey;
106 
107 		sendPositionToClient(*position, session.sessionId);
108 		updateObserverBox(session);
109 	}
110 
111 	private void sendPositionToClient(ClientPosition position, SessionId sessionId)
112 	{
113 		cm.serverWorld.dimObserverMan.updateObserver(sessionId, position.dimension);
114 		cm.connection.sendTo(sessionId,
115 			ClientPositionPacket(
116 				position.dimPos,
117 				position.dimension,
118 				position.positionKey));
119 	}
120 
121 	// updates WorldBox of observer in ChunkObserverManager
122 	// must be called after new position was sent to client.
123 	// ChunkObserverManager can initiate chunk sending when observed
124 	// box changes.
125 	private void updateObserverBox(Session* session)
126 	{
127 		if (cm.isSpawned(session)) {
128 			auto position = cm.db.get!ClientPosition(session.dbKey);
129 			auto settings = cm.db.get!ClientSettings(session.dbKey);
130 			auto borders = cm.serverWorld.dimMan.dimensionBorders(position.dimension);
131 			cm.serverWorld.chunkObserverManager.changeObserverBox(
132 				session.sessionId, position.chunk, settings.viewRadius, borders);
133 		}
134 	}
135 }
136 
137 immutable vec3[string] dirToVec;
138 static this()
139 {
140 	dirToVec = [
141 		"u" : vec3( 0, 1, 0),
142 		"d" : vec3( 0,-1, 0),
143 		"l" : vec3(-1, 0, 0),
144 		"r" : vec3( 1, 0, 0),
145 		"f" : vec3( 0, 0,-1),
146 		"b" : vec3( 0, 0, 1),
147 		];
148 }
149 
150 final class ClientManager : IPlugin
151 {
152 private:
153 	EventDispatcherPlugin evDispatcher;
154 	NetServerPlugin connection;
155 	ServerWorld serverWorld;
156 
157 public:
158 	ClientDb db;
159 	SessionManager sessions;
160 	ClientPositionManager clientPosMan;
161 
162 	// IPlugin stuff
163 	mixin IdAndSemverFrom!"voxelman.session.plugininfo";
164 
165 	this() {
166 		clientPosMan.cm = this;
167 	}
168 
169 	override void registerResources(IResourceManagerRegistry resmanRegistry)
170 	{
171 		auto ioman = resmanRegistry.getResourceManager!IoManager;
172 		ioman.registerWorldLoadSaveHandlers(&db.load, &db.save);
173 		auto components = resmanRegistry.getResourceManager!EntityComponentRegistry;
174 		db.eman = components.eman;
175 		registerSessionComponents(db.eman);
176 	}
177 
178 	override void init(IPluginManager pluginman)
179 	{
180 		evDispatcher = pluginman.getPlugin!EventDispatcherPlugin;
181 		connection = pluginman.getPlugin!NetServerPlugin;
182 		serverWorld = pluginman.getPlugin!ServerWorld;
183 
184 		evDispatcher.subscribeToEvent(&handleClientConnected);
185 		evDispatcher.subscribeToEvent(&handleClientDisconnected);
186 
187 		connection.registerPacketHandler!LoginPacket(&handleLoginPacket);
188 		connection.registerPacketHandler!ViewRadiusPacket(&handleViewRadius);
189 		connection.registerPacketHandler!ClientPositionPacket(&handleClientPosition);
190 		connection.registerPacketHandler!GameStartPacket(&handleGameStartPacket);
191 
192 		auto commandPlugin = pluginman.getPlugin!CommandPluginServer;
193 		commandPlugin.registerCommand("spawn", &onSpawnCommand);
194 		commandPlugin.registerCommand("dim_spawn", &onDimensionSpawnCommand);
195 		commandPlugin.registerCommand("tp", &onTeleport);
196 		commandPlugin.registerCommand("dim", &changeDimensionCommand);
197 		commandPlugin.registerCommand("add_active", &onAddActive);
198 		commandPlugin.registerCommand("remove_active", &onRemoveActive);
199 		commandPlugin.registerCommand("preload_box", &onPreloadBox);
200 	}
201 
202 	void onAddActive(CommandParams params) {
203 		Session* session = sessions[params.source];
204 		auto position = db.get!ClientPosition(session.dbKey);
205 		auto cwp = position.chunk;
206 		serverWorld.activeChunks.add(cwp);
207 		infof("add active %s", cwp);
208 	}
209 
210 	void onRemoveActive(CommandParams params) {
211 		Session* session = sessions[params.source];
212 		auto position = db.get!ClientPosition(session.dbKey);
213 		auto cwp = position.chunk;
214 		serverWorld.activeChunks.remove(cwp);
215 		infof("remove active %s", cwp);
216 	}
217 
218 	void onPreloadBox(CommandParams params) {
219 		import std.regex : matchFirst, regex;
220 		import std.conv : to;
221 		Session* session = sessions[params.source];
222 		auto position = db.get!ClientPosition(session.dbKey);
223 		auto cwp = position.chunk;
224 
225 		// preload <radius> // preload box at
226 		auto regexRadius = regex(`(\d+)`, "m");
227 		auto captures = matchFirst(params.rawArgs, regexRadius);
228 
229 		if (!captures.empty)
230 		{
231 			int radius = to!int(captures[1]);
232 			WorldBox box = calcBox(cwp, radius);
233 			auto borders = serverWorld.dimMan.dimensionBorders(cwp.dimension);
234 			serverWorld.chunkObserverManager.addServerObserverBox(box, borders);
235 			infof("Preloading %s", box);
236 		}
237 	}
238 
239 	void onSpawnCommand(CommandParams params)
240 	{
241 		Session* session = sessions[params.source];
242 		if(session is null) return;
243 
244 		auto position = db.get!ClientPosition(session.dbKey);
245 		if (params.args.length > 1 && params.args[1] == "set")
246 		{
247 			setWorldSpawn(position.dimension, session);
248 			return;
249 		}
250 
251 		clientPosMan.tpToPos(
252 			session, serverWorld.worldInfo.spawnPos,
253 			serverWorld.worldInfo.spawnDimension);
254 	}
255 
256 	void setWorldSpawn(DimensionId dimension, Session* session)
257 	{
258 		auto position = db.get!ClientPosition(session.dbKey);
259 		serverWorld.worldInfo.spawnPos = position.dimPos;
260 		serverWorld.worldInfo.spawnDimension = dimension;
261 		connection.sendTo(session.sessionId, MessagePacket(
262 			format(`world spawn is now dim %s pos %s`, dimension, position.dimPos.pos)));
263 	}
264 
265 	void onDimensionSpawnCommand(CommandParams params)
266 	{
267 		Session* session = sessions[params.source];
268 		if(session is null) return;
269 
270 		auto position = db.get!ClientPosition(session.dbKey);
271 		if (params.args.length > 1 && params.args[1] == "set")
272 		{
273 			setDimensionSpawn(position.dimension, session);
274 			return;
275 		}
276 
277 		auto dimInfo = serverWorld.dimMan.getOrCreate(position.dimension);
278 		clientPosMan.tpToPos(session, dimInfo.spawnPos, position.dimension);
279 	}
280 
281 	void setDimensionSpawn(DimensionId dimension, Session* session)
282 	{
283 		auto dimInfo = serverWorld.dimMan.getOrCreate(dimension);
284 		auto position = db.get!ClientPosition(session.dbKey);
285 		dimInfo.spawnPos = position.dimPos;
286 		connection.sendTo(session.sessionId, MessagePacket(
287 			format(`spawn of dimension %s is now %s`, dimension, position.dimPos.pos)));
288 	}
289 
290 	void onTeleport(CommandParams params)
291 	{
292 		import std.regex : matchFirst, regex;
293 		import std.conv : to;
294 		Session* session = sessions[params.source];
295 		if(session is null) return;
296 		auto position = db.get!ClientPosition(session.dbKey);
297 
298 		vec3 pos;
299 
300 		// tp <x> <y> <z>
301 		auto regex3 = regex(`(-?\d+)\W+(-?\d+)\W+(-?\d+)`, "m");
302 		auto captures3 = matchFirst(params.rawArgs, regex3);
303 
304 		if (!captures3.empty)
305 		{
306 			pos.x = to!int(captures3[1]);
307 			pos.y = to!int(captures3[2]);
308 			pos.z = to!int(captures3[3]);
309 			connection.sendTo(session.sessionId, MessagePacket(
310 				format("Teleporting to %s %s %s", pos.x, pos.y, pos.z)));
311 			clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension);
312 			return;
313 		}
314 
315 		// tp u|d|l|r|f|b \d+
316 		auto regexDir = regex(`([udlrfb])\W+(-?\d+)`, "m");
317 		auto capturesDir = matchFirst(params.rawArgs, regexDir);
318 
319 		if (!capturesDir.empty)
320 		{
321 			string dir = capturesDir[1];
322 			if (auto dirVector = dir in dirToVec)
323 			{
324 				int delta = to!int(capturesDir[2]);
325 				pos = position.dimPos.pos + *dirVector * delta;
326 				connection.sendTo(session.sessionId, MessagePacket(
327 					format("Teleporting to %s %s %s", pos.x, pos.y, pos.z)));
328 				clientPosMan.tpToPos(session, ClientDimPos(pos, position.dimPos.heading), position.dimension);
329 				return;
330 			}
331 		}
332 
333 		// tp <x> <z>
334 		auto regex2 = regex(`(-?\d+)\W+(-?\d+)`, "m");
335 		auto captures2 = matchFirst(params.rawArgs, regex2);
336 		if (!captures2.empty)
337 		{
338 			pos.x = to!int(captures2[1]);
339 			pos.y = position.dimPos.pos.y;
340 			pos.z = to!int(captures2[2]);
341 			clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension);
342 			return;
343 		}
344 
345 		// tp <player>
346 		auto regex1 = regex(`[a-Z]+[_a-Z0-9]+`);
347 		auto captures1 = matchFirst(params.rawArgs, regex1);
348 		if (!captures1.empty)
349 		{
350 			string destName = to!string(captures1[1]);
351 			Session* destination = sessions[destName];
352 			if(destination is null)
353 			{
354 				connection.sendTo(params.source, MessagePacket(
355 					format(`Player "%s" is not online`, destName)));
356 				return;
357 			}
358 			clientPosMan.tpToPlayer(session, destination);
359 			return;
360 		}
361 
362 		connection.sendTo(params.source, MessagePacket(
363 			`Wrong syntax: "tp <x> [<y>] <z>" | "tp <player>" | "tp u|d|l|r|f|b <num_blocks>"`));
364 	}
365 
366 	bool isLoggedIn(SessionId sessionId)
367 	{
368 		Session* session = sessions[sessionId];
369 		return session.isLoggedIn;
370 	}
371 
372 	bool isSpawned(SessionId sessionId)
373 	{
374 		Session* session = sessions[sessionId];
375 		return isSpawned(session);
376 	}
377 
378 	bool isSpawned(Session* session)
379 	{
380 		return session.isLoggedIn && db.has!SpawnedFlag(session.dbKey);
381 	}
382 
383 	string[EntityId] clientNames()
384 	{
385 		string[EntityId] names;
386 		foreach(session; sessions.byValue) {
387 			if (session.isLoggedIn)
388 				names[session.dbKey] = session.name;
389 		}
390 
391 		return names;
392 	}
393 
394 	string clientName(SessionId sessionId)
395 	{
396 		auto cl = sessions[sessionId];
397 		return cl ? cl.name : format("%s", sessionId);
398 	}
399 
400 	auto loggedInClients()
401 	{
402 		import std.algorithm : filter, map;
403 		return sessions.byValue.filter!(a=>a.isLoggedIn).map!(a=>a.sessionId);
404 	}
405 
406 	private void handleClientConnected(ref ClientConnectedEvent event)
407 	{
408 		sessions.put(event.sessionId, SessionType.unknownClient);
409 	}
410 
411 	private void handleClientDisconnected(ref ClientDisconnectedEvent event)
412 	{
413 		Session* session = sessions[event.sessionId];
414 		infof("%s %s disconnected", event.sessionId, session.name);
415 
416 		db.remove!LoggedInFlag(session.dbKey);
417 		db.remove!ClientSessionInfo(session.dbKey);
418 		db.remove!SpawnedFlag(session.dbKey);
419 
420 		connection.sendToAll(ClientLoggedOutPacket(session.dbKey));
421 		sessions.remove(event.sessionId);
422 	}
423 
424 	private void changeDimensionCommand(CommandParams params)
425 	{
426 		import std.conv : to, ConvException;
427 
428 		Session* session = sessions[params.source];
429 		if (isSpawned(session))
430 		{
431 			if (params.args.length > 1)
432 			{
433 				auto position = db.get!ClientPosition(session.dbKey);
434 				auto dim = to!DimensionId(params.args[1]);
435 				if (dim == position.dimension)
436 					return;
437 
438 				clientPosMan.tpToDimension(session, dim);
439 			}
440 		}
441 	}
442 
443 	private void handleLoginPacket(ubyte[] packetData, SessionId sessionId)
444 	{
445 		auto packet = unpackPacket!LoginPacket(packetData);
446 		string name = packet.clientName;
447 		Session* session = sessions[sessionId];
448 
449 		bool createdNew;
450 		EntityId clientId = db.getOrCreate(name, createdNew);
451 
452 		if (createdNew)
453 		{
454 			infof("new client registered %s %s", clientId, name);
455 			db.set(clientId, ClientPosition(), ClientSettings());
456 		}
457 		else
458 		{
459 			if (db.has!ClientSessionInfo(clientId))
460 			{
461 				bool hasConflict(string name) {
462 					EntityId clientId = db.getIdForName(name);
463 					if (clientId == 0) return false;
464 					return db.has!ClientSessionInfo(clientId);
465 				}
466 
467 				// already logged in
468 				infof("client with name %s is already logged in %s", name, clientId);
469 				string requestedName = name;
470 				name = db.resolveNameConflict(requestedName, &hasConflict);
471 				infof("Using '%s' instead of '%s'", name, requestedName);
472 				clientId = db.getOrCreate(name, createdNew);
473 
474 				if (createdNew)
475 				{
476 					infof("new client registered %s %s", clientId, name);
477 					db.set(clientId, ClientPosition(), ClientSettings());
478 				}
479 			}
480 			infof("client logged in %s %s", clientId, name);
481 		}
482 
483 		db.set(clientId, ClientSessionInfo(name, session.sessionId));
484 		db.set(clientId, LoggedInFlag());
485 
486 		sessions.identifySession(session.sessionId, name, clientId);
487 
488 		connection.sendTo(sessionId, SessionInfoPacket(session.dbKey, clientNames));
489 		connection.sendToAllExcept(sessionId, ClientLoggedInPacket(session.dbKey, name));
490 
491 		evDispatcher.postEvent(ClientLoggedInEvent(clientId, createdNew));
492 
493 		infof("%s %s logged in", sessionId, sessions[sessionId].name);
494 	}
495 
496 	private void handleGameStartPacket(ubyte[] packetData, SessionId sessionId)
497 	{
498 		Session* session = sessions[sessionId];
499 		if (session.isLoggedIn)
500 		{
501 			auto position = db.get!ClientPosition(session.dbKey);
502 			clientPosMan.sendPositionToClient(*position, session.sessionId);
503 			db.set(session.dbKey, SpawnedFlag());
504 			connection.sendTo(sessionId, SpawnPacket());
505 		}
506 	}
507 
508 	private void handleViewRadius(ubyte[] packetData, SessionId sessionId)
509 	{
510 		import std.algorithm : clamp;
511 		auto packet = unpackPacket!ViewRadiusPacket(packetData);
512 		Session* session = sessions[sessionId];
513 		if (session.isLoggedIn)
514 		{
515 			clientPosMan.updateClientViewRadius(session, packet.viewRadius);
516 		}
517 	}
518 
519 	private void handleClientPosition(ubyte[] packetData, SessionId sessionId)
520 	{
521 		Session* session = sessions[sessionId];
522 		if (isSpawned(session))
523 		{
524 			auto packet = unpackPacket!ClientPositionPacket(packetData);
525 
526 			clientPosMan.updateClientPosition(
527 				session, packet.dimPos, packet.dimension,
528 				packet.positionKey, false);
529 		}
530 	}
531 }