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