1 /**
2 Copyright: Copyright (c) 2016-2018 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 		cm.evDispatcher.postEvent(ClientMovedEvent(session.dbKey, dimPos, dimension));
99 	}
100 
101 	void tpToDimension(Session* session, DimensionId dimension)
102 	{
103 		auto dimInfo = cm.serverWorld.dimMan.getOrCreate(dimension);
104 		auto position = cm.db.get!ClientPosition(session.dbKey);
105 		position.dimPos = dimInfo.spawnPos;
106 		position.dimension = dimension;
107 		++position.positionKey;
108 
109 		sendPositionToClient(*position, session.sessionId);
110 		updateObserverBox(session);
111 	}
112 
113 	private void sendPositionToClient(ClientPosition position, SessionId sessionId)
114 	{
115 		cm.serverWorld.dimObserverMan.updateObserver(sessionId, position.dimension);
116 		cm.connection.sendTo(sessionId,
117 			ClientPositionPacket(
118 				position.dimPos,
119 				position.dimension,
120 				position.positionKey));
121 	}
122 
123 	// updates WorldBox of observer in ChunkObserverManager
124 	// must be called after new position was sent to client.
125 	// ChunkObserverManager can initiate chunk sending when observed
126 	// box changes.
127 	private void updateObserverBox(Session* session)
128 	{
129 		if (cm.isSpawned(session)) {
130 			auto position = cm.db.get!ClientPosition(session.dbKey);
131 			auto settings = cm.db.get!ClientSettings(session.dbKey);
132 			auto borders = cm.serverWorld.dimMan.dimensionBorders(position.dimension);
133 			cm.serverWorld.chunkObserverManager.changeObserverBox(
134 				session.sessionId, position.chunk, settings.viewRadius, borders);
135 		}
136 	}
137 }
138 
139 immutable vec3[string] dirToVec;
140 static this()
141 {
142 	dirToVec = [
143 		"u" : vec3( 0, 1, 0),
144 		"d" : vec3( 0,-1, 0),
145 		"l" : vec3(-1, 0, 0),
146 		"r" : vec3( 1, 0, 0),
147 		"f" : vec3( 0, 0,-1),
148 		"b" : vec3( 0, 0, 1),
149 		];
150 }
151 
152 final class ClientManager : IPlugin
153 {
154 private:
155 	EventDispatcherPlugin evDispatcher;
156 	NetServerPlugin connection;
157 	ServerWorld serverWorld;
158 
159 public:
160 	ClientDb db;
161 	SessionManager sessions;
162 	ClientPositionManager clientPosMan;
163 
164 	// IPlugin stuff
165 	mixin IdAndSemverFrom!"voxelman.session.plugininfo";
166 
167 	this() {
168 		clientPosMan.cm = this;
169 	}
170 
171 	override void registerResources(IResourceManagerRegistry resmanRegistry)
172 	{
173 		auto ioman = resmanRegistry.getResourceManager!IoManager;
174 		ioman.registerWorldLoadSaveHandlers(&db.load, &db.save);
175 		auto components = resmanRegistry.getResourceManager!EntityComponentRegistry;
176 		db.eman = components.eman;
177 		registerSessionComponents(db.eman);
178 	}
179 
180 	override void init(IPluginManager pluginman)
181 	{
182 		evDispatcher = pluginman.getPlugin!EventDispatcherPlugin;
183 		connection = pluginman.getPlugin!NetServerPlugin;
184 		serverWorld = pluginman.getPlugin!ServerWorld;
185 
186 		evDispatcher.subscribeToEvent(&handleClientConnected);
187 		evDispatcher.subscribeToEvent(&handleClientDisconnected);
188 
189 		connection.registerPacketHandler!LoginPacket(&handleLoginPacket);
190 		connection.registerPacketHandler!ViewRadiusPacket(&handleViewRadius);
191 		connection.registerPacketHandler!ClientPositionPacket(&handleClientPosition);
192 		connection.registerPacketHandler!GameStartPacket(&handleGameStartPacket);
193 
194 		auto commandPlugin = pluginman.getPlugin!CommandPluginServer;
195 		commandPlugin.registerCommand(CommandInfo("spawn", &onSpawnCommand, ["[set]"], "Teleports user to or set a world spawn point"));
196 		commandPlugin.registerCommand(CommandInfo("dim_spawn", &onDimensionSpawnCommand, ["[set]"], "Teleports user to or set a dimension spawn point"));
197 		commandPlugin.registerCommand(CommandInfo("tp", &onTeleport, ["<x> [<y>] <z>", "<player_name>", "u|d|l|r|f|b <num_blocks>"], "Teleports user to coordinates, to another player, or in choosen direction"));
198 		commandPlugin.registerCommand(CommandInfo("dim", &changeDimensionCommand, ["<dimension_number>"], "Teleports user to the spawn point of specified dimension"));
199 		commandPlugin.registerCommand(CommandInfo("add_active", &onAddActive, null, "Marks user's current chunk as active. (Chunk is always loaded)"));
200 		commandPlugin.registerCommand(CommandInfo("remove_active", &onRemoveActive, null, "Unmarks user's current chunk as active. (Chunk is no longer always loaded)"));
201 		commandPlugin.registerCommand(CommandInfo("preload_box", &onPreloadBox, ["<radius>"], "Adds server observer to all chunks in specified radius. Chunks will stay loaded, until server restarts"));
202 	}
203 
204 	void onAddActive(CommandParams params) {
205 		Session* session = sessions[params.sourceSession];
206 		auto position = db.get!ClientPosition(session.dbKey);
207 		auto cwp = position.chunk;
208 		serverWorld.activeChunks.add(cwp);
209 		infof("add active %s", cwp);
210 	}
211 
212 	void onRemoveActive(CommandParams params) {
213 		Session* session = sessions[params.sourceSession];
214 		auto position = db.get!ClientPosition(session.dbKey);
215 		auto cwp = position.chunk;
216 		serverWorld.activeChunks.remove(cwp);
217 		infof("remove active %s", cwp);
218 	}
219 
220 	void onPreloadBox(CommandParams params) {
221 		import std.regex : matchFirst, regex;
222 		import std.conv : to;
223 		Session* session = sessions[params.sourceSession];
224 		auto position = db.get!ClientPosition(session.dbKey);
225 		auto cwp = position.chunk;
226 
227 		// preload <radius> // preload box at
228 		auto regexRadius = regex(`(\d+)`, "m");
229 		auto captures = matchFirst(params.rawArgs, regexRadius);
230 
231 		if (!captures.empty)
232 		{
233 			int radius = to!int(captures[1]);
234 			WorldBox box = calcBox(cwp, radius);
235 			auto borders = serverWorld.dimMan.dimensionBorders(cwp.dimension);
236 			serverWorld.chunkObserverManager.addServerObserverBox(box, borders);
237 			infof("Preloading %s", box);
238 		}
239 	}
240 
241 	void onSpawnCommand(CommandParams params)
242 	{
243 		Session* session = sessions[params.sourceSession];
244 		if(session is null) return;
245 
246 		auto position = db.get!ClientPosition(session.dbKey);
247 		if (params.args.length > 1 && params.args[1] == "set")
248 		{
249 			setWorldSpawn(position.dimension, session);
250 			return;
251 		}
252 
253 		clientPosMan.tpToPos(
254 			session, serverWorld.worldInfo.spawnPos,
255 			serverWorld.worldInfo.spawnDimension);
256 	}
257 
258 	void setWorldSpawn(DimensionId dimension, Session* session)
259 	{
260 		auto position = db.get!ClientPosition(session.dbKey);
261 		serverWorld.worldInfo.spawnPos = position.dimPos;
262 		serverWorld.worldInfo.spawnDimension = dimension;
263 		connection.sendTo(session.sessionId, MessagePacket(
264 			format(`world spawn is now dim %s pos %s`, dimension, position.dimPos.pos)));
265 	}
266 
267 	void onDimensionSpawnCommand(CommandParams params)
268 	{
269 		Session* session = sessions[params.sourceSession];
270 		if(session is null) return;
271 
272 		auto position = db.get!ClientPosition(session.dbKey);
273 		if (params.args.length > 1 && params.args[1] == "set")
274 		{
275 			setDimensionSpawn(position.dimension, session);
276 			return;
277 		}
278 
279 		auto dimInfo = serverWorld.dimMan.getOrCreate(position.dimension);
280 		clientPosMan.tpToPos(session, dimInfo.spawnPos, position.dimension);
281 	}
282 
283 	void setDimensionSpawn(DimensionId dimension, Session* session)
284 	{
285 		auto dimInfo = serverWorld.dimMan.getOrCreate(dimension);
286 		auto position = db.get!ClientPosition(session.dbKey);
287 		dimInfo.spawnPos = position.dimPos;
288 		connection.sendTo(session.sessionId, MessagePacket(
289 			format(`spawn of dimension %s is now %s`, dimension, position.dimPos.pos)));
290 	}
291 
292 	void onTeleport(CommandParams params)
293 	{
294 		import std.regex : matchFirst, regex;
295 		import std.conv : to;
296 		Session* session = sessions[params.sourceSession];
297 		if(session is null) return;
298 		auto position = db.get!ClientPosition(session.dbKey);
299 
300 		vec3 pos;
301 
302 		// tp <x> <y> <z>
303 		auto regex3 = regex(`(-?\d+)\W+(-?\d+)\W+(-?\d+)`, "m");
304 		auto captures3 = matchFirst(params.rawArgs, regex3);
305 
306 		if (!captures3.empty)
307 		{
308 			pos.x = to!int(captures3[1]);
309 			pos.y = to!int(captures3[2]);
310 			pos.z = to!int(captures3[3]);
311 			connection.sendTo(session.sessionId, MessagePacket(
312 				format("Teleporting to %s %s %s", pos.x, pos.y, pos.z)));
313 			clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension);
314 			return;
315 		}
316 
317 		// tp u|d|l|r|f|b \d+
318 		auto regexDir = regex(`([udlrfb])\W+(-?\d+)`, "m");
319 		auto capturesDir = matchFirst(params.rawArgs, regexDir);
320 
321 		if (!capturesDir.empty)
322 		{
323 			string dir = capturesDir[1];
324 			if (auto dirVector = dir in dirToVec)
325 			{
326 				int delta = to!int(capturesDir[2]);
327 				pos = position.dimPos.pos + *dirVector * delta;
328 				connection.sendTo(session.sessionId, MessagePacket(
329 					format("Teleporting to %s %s %s", pos.x, pos.y, pos.z)));
330 				clientPosMan.tpToPos(session, ClientDimPos(pos, position.dimPos.heading), position.dimension);
331 				return;
332 			}
333 		}
334 
335 		// tp <x> <z>
336 		auto regex2 = regex(`(-?\d+)\W+(-?\d+)`, "m");
337 		auto captures2 = matchFirst(params.rawArgs, regex2);
338 		if (!captures2.empty)
339 		{
340 			pos.x = to!int(captures2[1]);
341 			pos.y = position.dimPos.pos.y;
342 			pos.z = to!int(captures2[2]);
343 			clientPosMan.tpToPos(session, ClientDimPos(pos), position.dimension);
344 			return;
345 		}
346 
347 		// tp <player>
348 		auto regex1 = regex(`[a-Z]+[_a-Z0-9]+`);
349 		auto captures1 = matchFirst(params.rawArgs, regex1);
350 		if (!captures1.empty)
351 		{
352 			string destName = to!string(captures1[1]);
353 			Session* destination = sessions[destName];
354 			if(destination is null)
355 			{
356 				connection.sendTo(params.sourceSession, MessagePacket(
357 					format(`Player "%s" is not online`, destName)));
358 				return;
359 			}
360 			clientPosMan.tpToPlayer(session, destination);
361 			return;
362 		}
363 
364 		connection.sendTo(params.sourceSession, MessagePacket(
365 			`Wrong syntax: "tp <x> [<y>] <z>" | "tp <player>" | "tp u|d|l|r|f|b <num_blocks>"`));
366 	}
367 
368 	bool isLoggedIn(SessionId sessionId)
369 	{
370 		Session* session = sessions[sessionId];
371 		return session.isLoggedIn;
372 	}
373 
374 	/// Returns ClientId for specified SessionId
375 	EntityId sessionClientId(SessionId sessionId)
376 	{
377 		Session* session = sessions[sessionId];
378 		if (session)
379 		{
380 			return session.dbKey;
381 		}
382 		return EntityId(0);
383 	}
384 
385 	bool isSpawned(SessionId sessionId)
386 	{
387 		Session* session = sessions[sessionId];
388 		return isSpawned(session);
389 	}
390 
391 	bool isSpawned(Session* session)
392 	{
393 		return session.isLoggedIn && db.has!SpawnedFlag(session.dbKey);
394 	}
395 
396 	string[EntityId] clientNames()
397 	{
398 		string[EntityId] names;
399 		foreach(session; sessions.byValue) {
400 			if (session.isLoggedIn)
401 				names[session.dbKey] = session.name;
402 		}
403 
404 		return names;
405 	}
406 
407 	string clientName(SessionId sessionId)
408 	{
409 		auto cl = sessions[sessionId];
410 		return cl ? cl.name : format("%s", sessionId);
411 	}
412 
413 	auto loggedInClients()
414 	{
415 		import std.algorithm : filter, map;
416 		return sessions.byValue.filter!(a=>a.isLoggedIn).map!(a=>a.sessionId);
417 	}
418 
419 	private void handleClientConnected(ref ClientConnectedEvent event)
420 	{
421 		sessions.put(event.sessionId, SessionType.unknownClient);
422 	}
423 
424 	private void handleClientDisconnected(ref ClientDisconnectedEvent event)
425 	{
426 		Session* session = sessions[event.sessionId];
427 		infof("%s %s disconnected", event.sessionId, session.name);
428 
429 		db.remove!LoggedInFlag(session.dbKey);
430 		db.remove!ClientSessionInfo(session.dbKey);
431 		db.remove!SpawnedFlag(session.dbKey);
432 
433 		connection.sendToAll(ClientLoggedOutPacket(session.dbKey));
434 		sessions.remove(event.sessionId);
435 	}
436 
437 	// dim <dimension_number>
438 	private void changeDimensionCommand(CommandParams params)
439 	{
440 		import std.conv : to, ConvException;
441 
442 		Session* session = sessions[params.sourceSession];
443 		if (isSpawned(session))
444 		{
445 			if (params.args.length > 1)
446 			{
447 				auto position = db.get!ClientPosition(session.dbKey);
448 				auto dim = to!DimensionId(params.args[1]);
449 				if (dim == position.dimension)
450 					return;
451 
452 				clientPosMan.tpToDimension(session, dim);
453 			}
454 		}
455 	}
456 
457 	private void handleLoginPacket(ubyte[] packetData, SessionId sessionId)
458 	{
459 		auto packet = unpackPacket!LoginPacket(packetData);
460 		string name = packet.clientName;
461 		Session* session = sessions[sessionId];
462 
463 		bool createdNew;
464 		EntityId clientId = db.getOrCreate(name, createdNew);
465 
466 		if (createdNew)
467 		{
468 			onCreatedNewClientRecord(clientId, name);
469 		}
470 		else
471 		{
472 			if (db.has!ClientSessionInfo(clientId))
473 			{
474 				bool hasConflict(string name) {
475 					EntityId clientId = db.getIdForName(name);
476 					if (clientId == EntityId(0)) return false;
477 					return db.has!ClientSessionInfo(clientId);
478 				}
479 
480 				// already logged in
481 				infof("client with name %s is already logged in %s", name, clientId);
482 				string requestedName = name;
483 				name = db.resolveNameConflict(requestedName, &hasConflict);
484 				infof("Using '%s' instead of '%s'", name, requestedName);
485 
486 				clientId = db.getOrCreate(name, createdNew);
487 
488 				if (createdNew) onCreatedNewClientRecord(clientId, name);
489 			}
490 			infof("client logged in %s %s", clientId, name);
491 		}
492 
493 		db.set(clientId, ClientSessionInfo(name, session.sessionId));
494 		db.set(clientId, LoggedInFlag());
495 
496 		sessions.identifySession(session.sessionId, name, clientId);
497 
498 		connection.sendTo(sessionId, SessionInfoPacket(session.dbKey, clientNames));
499 		connection.sendToAllExcept(sessionId, ClientLoggedInPacket(session.dbKey, name));
500 
501 		evDispatcher.postEvent(ClientLoggedInEvent(clientId, createdNew));
502 
503 		infof("%s %s logged in", sessionId, sessions[sessionId].name);
504 	}
505 
506 	private void onCreatedNewClientRecord(EntityId clientId, string name) {
507 		infof("new client registered %s %s", clientId, name);
508 		db.set(clientId, ClientPosition(), ClientSettings());
509 	}
510 
511 	private void handleGameStartPacket(ubyte[] packetData, SessionId sessionId)
512 	{
513 		Session* session = sessions[sessionId];
514 		if (session.isLoggedIn)
515 		{
516 			auto position = db.get!ClientPosition(session.dbKey);
517 			clientPosMan.sendPositionToClient(*position, session.sessionId);
518 			db.set(session.dbKey, SpawnedFlag());
519 			connection.sendTo(sessionId, SpawnPacket());
520 		}
521 	}
522 
523 	private void handleViewRadius(ubyte[] packetData, SessionId sessionId)
524 	{
525 		import std.algorithm : clamp;
526 		auto packet = unpackPacket!ViewRadiusPacket(packetData);
527 		Session* session = sessions[sessionId];
528 		if (session.isLoggedIn)
529 		{
530 			clientPosMan.updateClientViewRadius(session, packet.viewRadius);
531 		}
532 	}
533 
534 	private void handleClientPosition(ubyte[] packetData, SessionId sessionId)
535 	{
536 		Session* session = sessions[sessionId];
537 		if (isSpawned(session))
538 		{
539 			auto packet = unpackPacket!ClientPositionPacket(packetData);
540 
541 			clientPosMan.updateClientPosition(
542 				session, packet.dimPos, packet.dimension,
543 				packet.positionKey, false);
544 		}
545 	}
546 }