1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Generic;
14 using System.Linq;
15 using OpenRA.Graphics;
16 using OpenRA.Mods.Common.Traits;
17 using OpenRA.Network;
18 using OpenRA.Primitives;
19 using OpenRA.Server;
20 using OpenRA.Traits;
21 using S = OpenRA.Server.Server;
22 
23 namespace OpenRA.Mods.Common.Server
24 {
25 	public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined
26 	{
27 		readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
28 		{
29 			{ "state", State },
30 			{ "startgame", StartGame },
31 			{ "slot", Slot },
32 			{ "allow_spectators", AllowSpectators },
33 			{ "spectate", Specate },
34 			{ "slot_close", SlotClose },
35 			{ "slot_open", SlotOpen },
36 			{ "slot_bot", SlotBot },
37 			{ "map", Map },
38 			{ "option", Option },
39 			{ "assignteams", AssignTeams },
40 			{ "kick", Kick },
41 			{ "make_admin", MakeAdmin },
42 			{ "make_spectator", MakeSpectator },
43 			{ "name", Name },
44 			{ "faction", Faction },
45 			{ "team", Team },
46 			{ "spawn", Spawn },
47 			{ "color", PlayerColor },
48 			{ "sync_lobby", SyncLobby }
49 		};
50 
ValidateSlotCommand(S server, Connection conn, Session.Client client, string arg, bool requiresHost)51 		static bool ValidateSlotCommand(S server, Connection conn, Session.Client client, string arg, bool requiresHost)
52 		{
53 			if (!server.LobbyInfo.Slots.ContainsKey(arg))
54 			{
55 				Log.Write("server", "Invalid slot: {0}", arg);
56 				return false;
57 			}
58 
59 			if (requiresHost && !client.IsAdmin)
60 			{
61 				server.SendOrderTo(conn, "Message", "Only the host can do that.");
62 				return false;
63 			}
64 
65 			return true;
66 		}
67 
ValidateCommand(S server, Connection conn, Session.Client client, string cmd)68 		public static bool ValidateCommand(S server, Connection conn, Session.Client client, string cmd)
69 		{
70 			// Kick command is always valid for the host
71 			if (cmd.StartsWith("kick "))
72 				return true;
73 
74 			if (server.State == ServerState.GameStarted)
75 			{
76 				server.SendOrderTo(conn, "Message", "Cannot change state when game started. ({0})".F(cmd));
77 				return false;
78 			}
79 			else if (client.State == Session.ClientState.Ready && !(cmd.StartsWith("state") || cmd == "startgame"))
80 			{
81 				server.SendOrderTo(conn, "Message", "Cannot change state when marked as ready.");
82 				return false;
83 			}
84 
85 			return true;
86 		}
87 
InterpretCommand(S server, Connection conn, Session.Client client, string cmd)88 		public bool InterpretCommand(S server, Connection conn, Session.Client client, string cmd)
89 		{
90 			if (server == null || conn == null || client == null || !ValidateCommand(server, conn, client, cmd))
91 				return false;
92 
93 			var cmdName = cmd.Split(' ').First();
94 			var cmdValue = cmd.Split(' ').Skip(1).JoinWith(" ");
95 
96 			Func<S, Connection, Session.Client, string, bool> a;
97 			if (!commandHandlers.TryGetValue(cmdName, out a))
98 				return false;
99 
100 			return a(server, conn, client, cmdValue);
101 		}
102 
CheckAutoStart(S server)103 		static void CheckAutoStart(S server)
104 		{
105 			var nonBotPlayers = server.LobbyInfo.NonBotPlayers;
106 
107 			// Are all players and admin (could be spectating) ready?
108 			if (nonBotPlayers.Any(c => c.State != Session.ClientState.Ready) ||
109 				server.LobbyInfo.Clients.First(c => c.IsAdmin).State != Session.ClientState.Ready)
110 				return;
111 
112 			// Does server have at least 2 human players?
113 			if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer && nonBotPlayers.Count() < 2)
114 				return;
115 
116 			// Are the map conditions satisfied?
117 			if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && server.LobbyInfo.ClientInSlot(sl.Key) == null))
118 				return;
119 
120 			server.StartGame();
121 		}
122 
State(S server, Connection conn, Session.Client client, string s)123 		static bool State(S server, Connection conn, Session.Client client, string s)
124 		{
125 			var state = Session.ClientState.Invalid;
126 			if (!Enum<Session.ClientState>.TryParse(s, false, out state))
127 			{
128 				server.SendOrderTo(conn, "Message", "Malformed state command");
129 				return true;
130 			}
131 
132 			client.State = state;
133 
134 			Log.Write("server", "Player @{0} is {1}",
135 				conn.Socket.RemoteEndPoint, client.State);
136 
137 			server.SyncLobbyClients();
138 
139 			CheckAutoStart(server);
140 
141 			return true;
142 		}
143 
StartGame(S server, Connection conn, Session.Client client, string s)144 		static bool StartGame(S server, Connection conn, Session.Client client, string s)
145 		{
146 			if (!client.IsAdmin)
147 			{
148 				server.SendOrderTo(conn, "Message", "Only the host can start the game.");
149 				return true;
150 			}
151 
152 			if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required &&
153 												 server.LobbyInfo.ClientInSlot(sl.Key) == null))
154 			{
155 				server.SendOrderTo(conn, "Message", "Unable to start the game until required slots are full.");
156 				return true;
157 			}
158 
159 			if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer && server.LobbyInfo.NonBotPlayers.Count() < 2)
160 			{
161 				server.SendOrderTo(conn, "Message", server.TwoHumansRequiredText);
162 				return true;
163 			}
164 
165 			server.StartGame();
166 			return true;
167 		}
168 
Slot(S server, Connection conn, Session.Client client, string s)169 		static bool Slot(S server, Connection conn, Session.Client client, string s)
170 		{
171 			if (!server.LobbyInfo.Slots.ContainsKey(s))
172 			{
173 				Log.Write("server", "Invalid slot: {0}", s);
174 				return false;
175 			}
176 
177 			var slot = server.LobbyInfo.Slots[s];
178 
179 			if (slot.Closed || server.LobbyInfo.ClientInSlot(s) != null)
180 				return false;
181 
182 			// If the previous slot had a locked spawn then we must not carry that to the new slot
183 			var oldSlot = client.Slot != null ? server.LobbyInfo.Slots[client.Slot] : null;
184 			if (oldSlot != null && oldSlot.LockSpawn)
185 				client.SpawnPoint = 0;
186 
187 			client.Slot = s;
188 			S.SyncClientToPlayerReference(client, server.Map.Players.Players[s]);
189 
190 			if (!slot.LockColor)
191 				client.PreferredColor = client.Color = SanitizePlayerColor(server, client.Color, client.Index, conn);
192 
193 			server.SyncLobbyClients();
194 			CheckAutoStart(server);
195 
196 			return true;
197 		}
198 
AllowSpectators(S server, Connection conn, Session.Client client, string s)199 		static bool AllowSpectators(S server, Connection conn, Session.Client client, string s)
200 		{
201 			if (bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllowSpectators))
202 			{
203 				server.SyncLobbyGlobalSettings();
204 				return true;
205 			}
206 			else
207 			{
208 				server.SendOrderTo(conn, "Message", "Malformed allow_spectate command");
209 				return true;
210 			}
211 		}
212 
Specate(S server, Connection conn, Session.Client client, string s)213 		static bool Specate(S server, Connection conn, Session.Client client, string s)
214 		{
215 			if (server.LobbyInfo.GlobalSettings.AllowSpectators || client.IsAdmin)
216 			{
217 				client.Slot = null;
218 				client.SpawnPoint = 0;
219 				client.Team = 0;
220 				client.Color = Color.White;
221 				server.SyncLobbyClients();
222 				CheckAutoStart(server);
223 				return true;
224 			}
225 			else
226 				return false;
227 		}
228 
SlotClose(S server, Connection conn, Session.Client client, string s)229 		static bool SlotClose(S server, Connection conn, Session.Client client, string s)
230 		{
231 			if (!ValidateSlotCommand(server, conn, client, s, true))
232 				return false;
233 
234 			// kick any player that's in the slot
235 			var occupant = server.LobbyInfo.ClientInSlot(s);
236 			if (occupant != null)
237 			{
238 				if (occupant.Bot != null)
239 				{
240 					server.LobbyInfo.Clients.Remove(occupant);
241 					server.SyncLobbyClients();
242 					var ping = server.LobbyInfo.PingFromClient(occupant);
243 					if (ping != null)
244 					{
245 						server.LobbyInfo.ClientPings.Remove(ping);
246 						server.SyncClientPing();
247 					}
248 				}
249 				else
250 				{
251 					var occupantConn = server.Conns.FirstOrDefault(c => c.PlayerIndex == occupant.Index);
252 					if (occupantConn != null)
253 					{
254 						server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host.");
255 						server.DropClient(occupantConn);
256 					}
257 				}
258 			}
259 
260 			server.LobbyInfo.Slots[s].Closed = true;
261 			server.SyncLobbySlots();
262 
263 			return true;
264 		}
265 
SlotOpen(S server, Connection conn, Session.Client client, string s)266 		static bool SlotOpen(S server, Connection conn, Session.Client client, string s)
267 		{
268 			if (!ValidateSlotCommand(server, conn, client, s, true))
269 				return false;
270 
271 			var slot = server.LobbyInfo.Slots[s];
272 			slot.Closed = false;
273 			server.SyncLobbySlots();
274 
275 			// Slot may have a bot in it
276 			var occupant = server.LobbyInfo.ClientInSlot(s);
277 			if (occupant != null && occupant.Bot != null)
278 			{
279 				server.LobbyInfo.Clients.Remove(occupant);
280 				var ping = server.LobbyInfo.PingFromClient(occupant);
281 				if (ping != null)
282 				{
283 					server.LobbyInfo.ClientPings.Remove(ping);
284 					server.SyncClientPing();
285 				}
286 			}
287 
288 			server.SyncLobbyClients();
289 
290 			return true;
291 		}
292 
SlotBot(S server, Connection conn, Session.Client client, string s)293 		static bool SlotBot(S server, Connection conn, Session.Client client, string s)
294 		{
295 			var parts = s.Split(' ');
296 
297 			if (parts.Length < 3)
298 			{
299 				server.SendOrderTo(conn, "Message", "Malformed slot_bot command");
300 				return true;
301 			}
302 
303 			if (!ValidateSlotCommand(server, conn, client, parts[0], true))
304 				return false;
305 
306 			var slot = server.LobbyInfo.Slots[parts[0]];
307 			var bot = server.LobbyInfo.ClientInSlot(parts[0]);
308 			int controllerClientIndex;
309 			if (!Exts.TryParseIntegerInvariant(parts[1], out controllerClientIndex))
310 			{
311 				Log.Write("server", "Invalid bot controller client index: {0}", parts[1]);
312 				return false;
313 			}
314 
315 			// Invalid slot
316 			if (bot != null && bot.Bot == null)
317 			{
318 				server.SendOrderTo(conn, "Message", "Can't add bots to a slot with another client.");
319 				return true;
320 			}
321 
322 			var botType = parts[2];
323 			var botInfo = server.Map.Rules.Actors["player"].TraitInfos<IBotInfo>()
324 				.FirstOrDefault(b => b.Type == botType);
325 
326 			if (botInfo == null)
327 			{
328 				server.SendOrderTo(conn, "Message", "Invalid bot type.");
329 				return true;
330 			}
331 
332 			slot.Closed = false;
333 			if (bot == null)
334 			{
335 				// Create a new bot
336 				bot = new Session.Client()
337 				{
338 					Index = server.ChooseFreePlayerIndex(),
339 					Name = botInfo.Name,
340 					Bot = botType,
341 					Slot = parts[0],
342 					Faction = "Random",
343 					SpawnPoint = 0,
344 					Team = 0,
345 					State = Session.ClientState.NotReady,
346 					BotControllerClientIndex = controllerClientIndex
347 				};
348 
349 				// Pick a random color for the bot
350 				var validator = server.ModData.Manifest.Get<ColorValidator>();
351 				var tileset = server.Map.Rules.TileSet;
352 				var terrainColors = tileset.TerrainInfo.Where(ti => ti.RestrictPlayerColor).Select(ti => ti.Color);
353 				var playerColors = server.LobbyInfo.Clients.Select(c => c.Color)
354 					.Concat(server.Map.Players.Players.Values.Select(p => p.Color));
355 				bot.Color = bot.PreferredColor = validator.RandomPresetColor(server.Random, terrainColors, playerColors);
356 
357 				server.LobbyInfo.Clients.Add(bot);
358 			}
359 			else
360 			{
361 				// Change the type of the existing bot
362 				bot.Name = botInfo.Name;
363 				bot.Bot = botType;
364 			}
365 
366 			S.SyncClientToPlayerReference(bot, server.Map.Players.Players[parts[0]]);
367 			server.SyncLobbyClients();
368 			server.SyncLobbySlots();
369 
370 			return true;
371 		}
372 
Map(S server, Connection conn, Session.Client client, string s)373 		static bool Map(S server, Connection conn, Session.Client client, string s)
374 		{
375 			if (!client.IsAdmin)
376 			{
377 				server.SendOrderTo(conn, "Message", "Only the host can change the map.");
378 				return true;
379 			}
380 
381 			var lastMap = server.LobbyInfo.GlobalSettings.Map;
382 			Action<MapPreview> selectMap = map =>
383 			{
384 				// Make sure the map hasn't changed in the meantime
385 				if (server.LobbyInfo.GlobalSettings.Map != lastMap)
386 					return;
387 
388 				server.LobbyInfo.GlobalSettings.Map = map.Uid;
389 
390 				var oldSlots = server.LobbyInfo.Slots.Keys.ToArray();
391 				server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map];
392 
393 				server.LobbyInfo.Slots = server.Map.Players.Players
394 					.Select(p => MakeSlotFromPlayerReference(p.Value))
395 					.Where(ss => ss != null)
396 					.ToDictionary(ss => ss.PlayerReference, ss => ss);
397 
398 				LoadMapSettings(server, server.LobbyInfo.GlobalSettings, server.Map.Rules);
399 
400 				// Reset client states
401 				var selectableFactions = server.Map.Rules.Actors["world"].TraitInfos<FactionInfo>()
402 					.Where(f => f.Selectable)
403 					.Select(f => f.InternalName)
404 					.ToList();
405 
406 				foreach (var c in server.LobbyInfo.Clients)
407 				{
408 					c.State = Session.ClientState.Invalid;
409 					if (!selectableFactions.Contains(c.Faction))
410 						c.Faction = "Random";
411 				}
412 
413 				// Reassign players into new slots based on their old slots:
414 				//  - Observers remain as observers
415 				//  - Players who now lack a slot are made observers
416 				//  - Bots who now lack a slot are dropped
417 				//  - Bots who are not defined in the map rules are dropped
418 				var botTypes = server.Map.Rules.Actors["player"].TraitInfos<IBotInfo>().Select(t => t.Type);
419 				var slots = server.LobbyInfo.Slots.Keys.ToArray();
420 				var i = 0;
421 				foreach (var os in oldSlots)
422 				{
423 					var c = server.LobbyInfo.ClientInSlot(os);
424 					if (c == null)
425 						continue;
426 
427 					c.SpawnPoint = 0;
428 					c.Slot = i < slots.Length ? slots[i++] : null;
429 					if (c.Slot != null)
430 					{
431 						// Remove Bot from slot if slot forbids bots
432 						if (c.Bot != null && (!server.Map.Players.Players[c.Slot].AllowBots || !botTypes.Contains(c.Bot)))
433 							server.LobbyInfo.Clients.Remove(c);
434 						S.SyncClientToPlayerReference(c, server.Map.Players.Players[c.Slot]);
435 					}
436 					else if (c.Bot != null)
437 						server.LobbyInfo.Clients.Remove(c);
438 					else
439 						c.Color = Color.White;
440 				}
441 
442 				// Validate if color is allowed and get an alternative if it isn't
443 				foreach (var c in server.LobbyInfo.Clients)
444 					if (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor)
445 						c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn);
446 
447 				server.SyncLobbyInfo();
448 
449 				server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title));
450 
451 				if (server.Map.DefinesUnsafeCustomRules)
452 					server.SendMessage("This map contains custom rules. Game experience may change.");
453 
454 				if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer)
455 					server.SendMessage(server.TwoHumansRequiredText);
456 				else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
457 					server.SendMessage("Bots have been disabled on this map.");
458 
459 				var briefing = MissionBriefingOrDefault(server);
460 				if (briefing != null)
461 					server.SendMessage(briefing);
462 			};
463 
464 			Action queryFailed = () =>
465 				server.SendOrderTo(conn, "Message", "Map was not found on server.");
466 
467 			var m = server.ModData.MapCache[s];
468 			if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
469 				selectMap(m);
470 			else if (server.Settings.QueryMapRepository)
471 			{
472 				server.SendOrderTo(conn, "Message", "Searching for map on the Resource Center...");
473 				var mapRepository = server.ModData.Manifest.Get<WebServices>().MapRepository;
474 				server.ModData.MapCache.QueryRemoteMapDetails(mapRepository, new[] { s }, selectMap, queryFailed);
475 			}
476 			else
477 				queryFailed();
478 
479 			return true;
480 		}
481 
Option(S server, Connection conn, Session.Client client, string s)482 		static bool Option(S server, Connection conn, Session.Client client, string s)
483 		{
484 			if (!client.IsAdmin)
485 			{
486 				server.SendOrderTo(conn, "Message", "Only the host can change the configuration.");
487 				return true;
488 			}
489 
490 			var allOptions = server.Map.Rules.Actors["player"].TraitInfos<ILobbyOptions>()
491 				.Concat(server.Map.Rules.Actors["world"].TraitInfos<ILobbyOptions>())
492 				.SelectMany(t => t.LobbyOptions(server.Map.Rules));
493 
494 			// Overwrite keys with duplicate ids
495 			var options = new Dictionary<string, LobbyOption>();
496 			foreach (var o in allOptions)
497 				options[o.Id] = o;
498 
499 			var split = s.Split(' ');
500 			LobbyOption option;
501 			if (split.Length < 2 || !options.TryGetValue(split[0], out option) ||
502 				!option.Values.ContainsKey(split[1]))
503 			{
504 				server.SendOrderTo(conn, "Message", "Invalid configuration command.");
505 				return true;
506 			}
507 
508 			if (option.IsLocked)
509 			{
510 				server.SendOrderTo(conn, "Message", "{0} cannot be changed.".F(option.Name));
511 				return true;
512 			}
513 
514 			var oo = server.LobbyInfo.GlobalSettings.LobbyOptions[option.Id];
515 			if (oo.Value == split[1])
516 				return true;
517 
518 			oo.Value = oo.PreferredValue = split[1];
519 
520 			if (option.Id == "gamespeed")
521 			{
522 				var speed = server.ModData.Manifest.Get<GameSpeeds>().Speeds[oo.Value];
523 				server.LobbyInfo.GlobalSettings.Timestep = speed.Timestep;
524 				server.LobbyInfo.GlobalSettings.OrderLatency = speed.OrderLatency;
525 			}
526 
527 			server.SyncLobbyGlobalSettings();
528 			server.SendMessage(option.ValueChangedMessage(client.Name, split[1]));
529 
530 			return true;
531 		}
532 
AssignTeams(S server, Connection conn, Session.Client client, string s)533 		static bool AssignTeams(S server, Connection conn, Session.Client client, string s)
534 		{
535 			if (!client.IsAdmin)
536 			{
537 				server.SendOrderTo(conn, "Message", "Only the host can set that option.");
538 				return true;
539 			}
540 
541 			int teamCount;
542 			if (!Exts.TryParseIntegerInvariant(s, out teamCount))
543 			{
544 				server.SendOrderTo(conn, "Message", "Number of teams could not be parsed: {0}".F(s));
545 				return true;
546 			}
547 
548 			var maxTeams = (server.LobbyInfo.Clients.Count(c => c.Slot != null) + 1) / 2;
549 			teamCount = teamCount.Clamp(0, maxTeams);
550 			var clients = server.LobbyInfo.Slots
551 				.Select(slot => server.LobbyInfo.ClientInSlot(slot.Key))
552 				.Where(c => c != null && !server.LobbyInfo.Slots[c.Slot].LockTeam);
553 
554 			var assigned = 0;
555 			var clientCount = clients.Count();
556 			foreach (var player in clients)
557 			{
558 				// Free for all
559 				if (teamCount == 0)
560 					player.Team = 0;
561 
562 				// Humans vs Bots
563 				else if (teamCount == 1)
564 					player.Team = player.Bot == null ? 1 : 2;
565 				else
566 					player.Team = assigned++ * teamCount / clientCount + 1;
567 			}
568 
569 			server.SyncLobbyClients();
570 
571 			return true;
572 		}
573 
Kick(S server, Connection conn, Session.Client client, string s)574 		static bool Kick(S server, Connection conn, Session.Client client, string s)
575 		{
576 			if (!client.IsAdmin)
577 			{
578 				server.SendOrderTo(conn, "Message", "Only the host can kick players.");
579 				return true;
580 			}
581 
582 			var split = s.Split(' ');
583 			if (split.Length < 2)
584 			{
585 				server.SendOrderTo(conn, "Message", "Malformed kick command");
586 				return true;
587 			}
588 
589 			int kickClientID;
590 			Exts.TryParseIntegerInvariant(split[0], out kickClientID);
591 
592 			var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c) != null && server.GetClient(c).Index == kickClientID);
593 			if (kickConn == null)
594 			{
595 				server.SendOrderTo(conn, "Message", "No-one in that slot.");
596 				return true;
597 			}
598 
599 			var kickClient = server.GetClient(kickConn);
600 			if (server.State == ServerState.GameStarted && !kickClient.IsObserver)
601 			{
602 				server.SendOrderTo(conn, "Message", "Only spectators can be kicked after the game has started.");
603 				return true;
604 			}
605 
606 			Log.Write("server", "Kicking client {0}.", kickClientID);
607 			server.SendMessage("{0} kicked {1} from the server.".F(client.Name, kickClient.Name));
608 			server.SendOrderTo(kickConn, "ServerError", "You have been kicked from the server.");
609 			server.DropClient(kickConn);
610 
611 			bool tempBan;
612 			bool.TryParse(split[1], out tempBan);
613 
614 			if (tempBan)
615 			{
616 				Log.Write("server", "Temporarily banning client {0} ({1}).", kickClientID, kickClient.IPAddress);
617 				server.SendMessage("{0} temporarily banned {1} from the server.".F(client.Name, kickClient.Name));
618 				server.TempBans.Add(kickClient.IPAddress);
619 			}
620 
621 			server.SyncLobbyClients();
622 			server.SyncLobbySlots();
623 
624 			return true;
625 		}
626 
MakeAdmin(S server, Connection conn, Session.Client client, string s)627 		static bool MakeAdmin(S server, Connection conn, Session.Client client, string s)
628 		{
629 			if (!client.IsAdmin)
630 			{
631 				server.SendOrderTo(conn, "Message", "Only admins can transfer admin to another player.");
632 				return true;
633 			}
634 
635 			int newAdminId;
636 			Exts.TryParseIntegerInvariant(s, out newAdminId);
637 			var newAdminConn = server.Conns.SingleOrDefault(c => server.GetClient(c) != null && server.GetClient(c).Index == newAdminId);
638 
639 			if (newAdminConn == null)
640 			{
641 				server.SendOrderTo(conn, "Message", "No-one in that slot.");
642 				return true;
643 			}
644 
645 			var newAdminClient = server.GetClient(newAdminConn);
646 			client.IsAdmin = false;
647 			newAdminClient.IsAdmin = true;
648 			server.SendMessage("{0} is now the admin.".F(newAdminClient.Name));
649 			Log.Write("server", "{0} is now the admin.".F(newAdminClient.Name));
650 			server.SyncLobbyClients();
651 
652 			return true;
653 		}
654 
MakeSpectator(S server, Connection conn, Session.Client client, string s)655 		static bool MakeSpectator(S server, Connection conn, Session.Client client, string s)
656 		{
657 			if (!client.IsAdmin)
658 			{
659 				server.SendOrderTo(conn, "Message", "Only the host can move players to spectators.");
660 				return true;
661 			}
662 
663 			int targetId;
664 			Exts.TryParseIntegerInvariant(s, out targetId);
665 			var targetConn = server.Conns.SingleOrDefault(c => server.GetClient(c) != null && server.GetClient(c).Index == targetId);
666 
667 			if (targetConn == null)
668 			{
669 				server.SendOrderTo(conn, "Message", "No-one in that slot.");
670 				return true;
671 			}
672 
673 			var targetClient = server.GetClient(targetConn);
674 			targetClient.Slot = null;
675 			targetClient.SpawnPoint = 0;
676 			targetClient.Team = 0;
677 			targetClient.Color = Color.White;
678 			targetClient.State = Session.ClientState.NotReady;
679 			server.SendMessage("{0} moved {1} to spectators.".F(client.Name, targetClient.Name));
680 			Log.Write("server", "{0} moved {1} to spectators.".F(client.Name, targetClient.Name));
681 			server.SyncLobbyClients();
682 			CheckAutoStart(server);
683 
684 			return true;
685 		}
686 
Name(S server, Connection conn, Session.Client client, string s)687 		static bool Name(S server, Connection conn, Session.Client client, string s)
688 		{
689 			var sanitizedName = Settings.SanitizedPlayerName(s);
690 			if (sanitizedName == client.Name)
691 				return true;
692 
693 			Log.Write("server", "Player@{0} is now known as {1}.", conn.Socket.RemoteEndPoint, sanitizedName);
694 			server.SendMessage("{0} is now known as {1}.".F(client.Name, sanitizedName));
695 			client.Name = sanitizedName;
696 			server.SyncLobbyClients();
697 
698 			return true;
699 		}
700 
Faction(S server, Connection conn, Session.Client client, string s)701 		static bool Faction(S server, Connection conn, Session.Client client, string s)
702 		{
703 			var parts = s.Split(' ');
704 			var targetClient = server.LobbyInfo.ClientWithIndex(Exts.ParseIntegerInvariant(parts[0]));
705 
706 			// Only the host can change other client's info
707 			if (targetClient.Index != client.Index && !client.IsAdmin)
708 				return true;
709 
710 			// Map has disabled faction changes
711 			if (server.LobbyInfo.Slots[targetClient.Slot].LockFaction)
712 				return true;
713 
714 			var factions = server.Map.Rules.Actors["world"].TraitInfos<FactionInfo>()
715 				.Where(f => f.Selectable).Select(f => f.InternalName);
716 
717 			if (!factions.Contains(parts[1]))
718 			{
719 				server.SendOrderTo(conn, "Message", "Invalid faction selected: {0}".F(parts[1]));
720 				server.SendOrderTo(conn, "Message", "Supported values: {0}".F(factions.JoinWith(", ")));
721 				return true;
722 			}
723 
724 			targetClient.Faction = parts[1];
725 			server.SyncLobbyClients();
726 
727 			return true;
728 		}
729 
Team(S server, Connection conn, Session.Client client, string s)730 		static bool Team(S server, Connection conn, Session.Client client, string s)
731 		{
732 			var parts = s.Split(' ');
733 			var targetClient = server.LobbyInfo.ClientWithIndex(Exts.ParseIntegerInvariant(parts[0]));
734 
735 			// Only the host can change other client's info
736 			if (targetClient.Index != client.Index && !client.IsAdmin)
737 				return true;
738 
739 			// Map has disabled team changes
740 			if (server.LobbyInfo.Slots[targetClient.Slot].LockTeam)
741 				return true;
742 
743 			int team;
744 			if (!Exts.TryParseIntegerInvariant(parts[1], out team))
745 			{
746 				Log.Write("server", "Invalid team: {0}", s);
747 				return false;
748 			}
749 
750 			targetClient.Team = team;
751 			server.SyncLobbyClients();
752 
753 			return true;
754 		}
755 
Spawn(S server, Connection conn, Session.Client client, string s)756 		static bool Spawn(S server, Connection conn, Session.Client client, string s)
757 		{
758 			var parts = s.Split(' ');
759 			var targetClient = server.LobbyInfo.ClientWithIndex(Exts.ParseIntegerInvariant(parts[0]));
760 
761 			// Only the host can change other client's info
762 			if (targetClient.Index != client.Index && !client.IsAdmin)
763 				return true;
764 
765 			// Spectators don't need a spawnpoint
766 			if (targetClient.Slot == null)
767 				return true;
768 
769 			// Map has disabled spawn changes
770 			if (server.LobbyInfo.Slots[targetClient.Slot].LockSpawn)
771 				return true;
772 
773 			int spawnPoint;
774 			if (!Exts.TryParseIntegerInvariant(parts[1], out spawnPoint)
775 				|| spawnPoint < 0 || spawnPoint > server.Map.SpawnPoints.Length)
776 			{
777 				Log.Write("server", "Invalid spawn point: {0}", parts[1]);
778 				return true;
779 			}
780 
781 			if (server.LobbyInfo.Clients.Where(cc => cc != client).Any(cc => (cc.SpawnPoint == spawnPoint) && (cc.SpawnPoint != 0)))
782 			{
783 				server.SendOrderTo(conn, "Message", "You cannot occupy the same spawn point as another player.");
784 				return true;
785 			}
786 
787 			// Check if any other slot has locked the requested spawn
788 			if (spawnPoint > 0)
789 			{
790 				var spawnLockedByAnotherSlot = server.LobbyInfo.Slots.Where(ss => ss.Value.LockSpawn).Any(ss =>
791 				{
792 					var pr = PlayerReferenceForSlot(server, ss.Value);
793 					return pr != null && pr.Spawn == spawnPoint;
794 				});
795 
796 				if (spawnLockedByAnotherSlot)
797 				{
798 					server.SendOrderTo(conn, "Message", "The spawn point is locked to another player slot.");
799 					return true;
800 				}
801 			}
802 
803 			targetClient.SpawnPoint = spawnPoint;
804 			server.SyncLobbyClients();
805 
806 			return true;
807 		}
808 
PlayerColor(S server, Connection conn, Session.Client client, string s)809 		static bool PlayerColor(S server, Connection conn, Session.Client client, string s)
810 		{
811 			var parts = s.Split(' ');
812 			var targetClient = server.LobbyInfo.ClientWithIndex(Exts.ParseIntegerInvariant(parts[0]));
813 
814 			// Only the host can change other client's info
815 			if (targetClient.Index != client.Index && !client.IsAdmin)
816 				return true;
817 
818 			// Spectator or map has disabled color changes
819 			if (targetClient.Slot == null || server.LobbyInfo.Slots[targetClient.Slot].LockColor)
820 				return true;
821 
822 			// Validate if color is allowed and get an alternative it isn't
823 			var newColor = FieldLoader.GetValue<Color>("(value)", parts[1]);
824 			targetClient.Color = SanitizePlayerColor(server, newColor, targetClient.Index, conn);
825 
826 			// Only update player's preferred color if new color is valid
827 			if (newColor == targetClient.Color)
828 				targetClient.PreferredColor = targetClient.Color;
829 
830 			server.SyncLobbyClients();
831 
832 			return true;
833 		}
834 
SyncLobby(S server, Connection conn, Session.Client client, string s)835 		static bool SyncLobby(S server, Connection conn, Session.Client client, string s)
836 		{
837 			if (!client.IsAdmin)
838 			{
839 				server.SendOrderTo(conn, "Message", "Only the host can set lobby info");
840 				return true;
841 			}
842 
843 			var lobbyInfo = Session.Deserialize(s);
844 			if (lobbyInfo == null)
845 			{
846 				server.SendOrderTo(conn, "Message", "Invalid Lobby Info Sent");
847 				return true;
848 			}
849 
850 			server.LobbyInfo = lobbyInfo;
851 
852 			server.SyncLobbyInfo();
853 
854 			return true;
855 		}
856 
ServerStarted(S server)857 		public void ServerStarted(S server)
858 		{
859 			// Remote maps are not supported for the initial map
860 			var uid = server.LobbyInfo.GlobalSettings.Map;
861 			server.Map = server.ModData.MapCache[uid];
862 			if (server.Map.Status != MapStatus.Available)
863 				throw new InvalidOperationException("Map {0} not found".F(uid));
864 
865 			server.LobbyInfo.Slots = server.Map.Players.Players
866 				.Select(p => MakeSlotFromPlayerReference(p.Value))
867 				.Where(s => s != null)
868 				.ToDictionary(s => s.PlayerReference, s => s);
869 
870 			LoadMapSettings(server, server.LobbyInfo.GlobalSettings, server.Map.Rules);
871 		}
872 
MakeSlotFromPlayerReference(PlayerReference pr)873 		static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr)
874 		{
875 			if (!pr.Playable) return null;
876 			return new Session.Slot
877 			{
878 				PlayerReference = pr.Name,
879 				Closed = false,
880 				AllowBots = pr.AllowBots,
881 				LockFaction = pr.LockFaction,
882 				LockColor = pr.LockColor,
883 				LockTeam = pr.LockTeam,
884 				LockSpawn = pr.LockSpawn,
885 				Required = pr.Required,
886 			};
887 		}
888 
LoadMapSettings(S server, Session.Global gs, Ruleset rules)889 		public static void LoadMapSettings(S server, Session.Global gs, Ruleset rules)
890 		{
891 			var options = rules.Actors["player"].TraitInfos<ILobbyOptions>()
892 				.Concat(rules.Actors["world"].TraitInfos<ILobbyOptions>())
893 				.SelectMany(t => t.LobbyOptions(rules));
894 
895 			foreach (var o in options)
896 			{
897 				var value = o.DefaultValue;
898 				var preferredValue = o.DefaultValue;
899 				Session.LobbyOptionState state;
900 				if (gs.LobbyOptions.TryGetValue(o.Id, out state))
901 				{
902 					// Propagate old state on map change
903 					if (!o.IsLocked)
904 					{
905 						if (o.Values.Keys.Contains(state.PreferredValue))
906 							value = state.PreferredValue;
907 						else if (o.Values.Keys.Contains(state.Value))
908 							value = state.Value;
909 					}
910 
911 					preferredValue = state.PreferredValue;
912 				}
913 				else
914 					state = new Session.LobbyOptionState();
915 
916 				state.IsLocked = o.IsLocked;
917 				state.Value = value;
918 				state.PreferredValue = preferredValue;
919 				gs.LobbyOptions[o.Id] = state;
920 
921 				if (o.Id == "gamespeed")
922 				{
923 					var speed = server.ModData.Manifest.Get<GameSpeeds>().Speeds[value];
924 					gs.Timestep = speed.Timestep;
925 					gs.OrderLatency = speed.OrderLatency;
926 				}
927 			}
928 		}
929 
SanitizePlayerColor(S server, Color askedColor, int playerIndex, Connection connectionToEcho = null)930 		static Color SanitizePlayerColor(S server, Color askedColor, int playerIndex, Connection connectionToEcho = null)
931 		{
932 			var validator = server.ModData.Manifest.Get<ColorValidator>();
933 			var askColor = askedColor;
934 
935 			Action<string> onError = message =>
936 			{
937 				if (connectionToEcho != null)
938 					server.SendOrderTo(connectionToEcho, "Message", message);
939 			};
940 
941 			var tileset = server.Map.Rules.TileSet;
942 			var terrainColors = tileset.TerrainInfo.Where(ti => ti.RestrictPlayerColor).Select(ti => ti.Color).ToList();
943 			var playerColors = server.LobbyInfo.Clients.Where(c => c.Index != playerIndex).Select(c => c.Color)
944 				.Concat(server.Map.Players.Players.Values.Select(p => p.Color)).ToList();
945 
946 			return validator.MakeValid(askColor, server.Random, terrainColors, playerColors, onError);
947 		}
948 
MissionBriefingOrDefault(S server)949 		static string MissionBriefingOrDefault(S server)
950 		{
951 			var missionData = server.Map.Rules.Actors["world"].TraitInfoOrDefault<MissionDataInfo>();
952 			if (missionData != null && !string.IsNullOrEmpty(missionData.Briefing))
953 				return missionData.Briefing.Replace("\\n", "\n");
954 
955 			return null;
956 		}
957 
ClientJoined(S server, Connection conn)958 		public void ClientJoined(S server, Connection conn)
959 		{
960 			var client = server.GetClient(conn);
961 
962 			// Validate whether color is allowed and get an alternative if it isn't
963 			if (client.Slot != null && !server.LobbyInfo.Slots[client.Slot].LockColor)
964 				client.Color = SanitizePlayerColor(server, client.Color, client.Index);
965 
966 			// Report any custom map details
967 			// HACK: this isn't the best place for this to live, but if we move it somewhere else
968 			// then we need a larger hack to hook the map change event.
969 			var briefing = MissionBriefingOrDefault(server);
970 			if (briefing != null)
971 				server.SendOrderTo(conn, "Message", briefing);
972 		}
973 
INotifyServerEmpty.ServerEmpty(S server)974 		void INotifyServerEmpty.ServerEmpty(S server)
975 		{
976 			// Expire any temporary bans
977 			server.TempBans.Clear();
978 
979 			// Re-enable spectators
980 			server.LobbyInfo.GlobalSettings.AllowSpectators = true;
981 
982 			// Reset player slots
983 			server.LobbyInfo.Slots = server.Map.Players.Players
984 				.Select(p => MakeSlotFromPlayerReference(p.Value))
985 				.Where(ss => ss != null)
986 				.ToDictionary(ss => ss.PlayerReference, ss => ss);
987 		}
988 
PlayerReferenceForSlot(S server, Session.Slot slot)989 		public static PlayerReference PlayerReferenceForSlot(S server, Session.Slot slot)
990 		{
991 			if (slot == null)
992 				return null;
993 
994 			return server.Map.Players.Players[slot.PlayerReference];
995 		}
996 	}
997 }
998