diff --git a/OpenRA.Game/Network/Session.cs b/OpenRA.Game/Network/Session.cs index ca635729bb..0756da7fed 100644 --- a/OpenRA.Game/Network/Session.cs +++ b/OpenRA.Game/Network/Session.cs @@ -127,6 +127,7 @@ namespace OpenRA.Network public int RandomSeed = 0; public bool FragileAlliances = false; // Allow diplomatic stance changes after game start. public bool AllowCheats = false; + public bool AllowSpectate = true; public bool Dedicated; public string Difficulty; public bool Crates = true; diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 8e5781d691..276540470b 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -276,6 +276,13 @@ namespace OpenRA.Server IsAdmin = !LobbyInfo.Clients.Any(c1 => c1.IsAdmin) }; + if (client.IsObserver && !LobbyInfo.GlobalSettings.AllowSpectate) + { + SendOrderTo(newConn, "ServerError", "The game is full"); + DropClient(newConn); + return; + } + if (client.Slot != null) SyncClientToPlayerReference(client, Map.Players[client.Slot]); else @@ -317,7 +324,7 @@ namespace OpenRA.Server LobbyInfo.Clients.Add(client); Log.Write("server", "Client {0}: Accepted connection from {1}.", - newConn.PlayerIndex, newConn.socket.RemoteEndPoint); + newConn.PlayerIndex, newConn.socket.RemoteEndPoint); foreach (var t in serverTraits.WithInterface()) t.ClientJoined(this, newConn); diff --git a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs index 1746fd7211..8252e4346a 100644 --- a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs @@ -1,725 +1,743 @@ -#region Copyright & License Information -/* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation. For more information, - * see COPYING. - */ -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; -using OpenRA.Network; -using OpenRA.FileFormats; -using OpenRA.Server; -using S = OpenRA.Server.Server; - -namespace OpenRA.Mods.RA.Server -{ - public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart - { - static bool ValidateSlotCommand(S server, Connection conn, Session.Client client, string arg, bool requiresHost) - { - if (!server.LobbyInfo.Slots.ContainsKey(arg)) - { - Log.Write("server", "Invalid slot: {0}", arg); - return false; - } - - if (requiresHost && !client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can do that"); - return false; - } - - return true; - } - - public static bool ValidateCommand(S server, Connection conn, Session.Client client, string cmd) - { - if (server.State == ServerState.GameStarted) - { - server.SendOrderTo(conn, "Message", "Cannot change state when game started. ({0})".F(cmd)); - return false; - } - else if (client.State == Session.ClientState.Ready && !(cmd == "ready" || cmd == "startgame")) - { - server.SendOrderTo(conn, "Message", "Cannot change state when marked as ready."); - return false; - } - - return true; - } - - void CheckAutoStart(S server, Connection conn, Session.Client client) - { - var playerClients = server.LobbyInfo.Clients.Where(c => c.Bot == null && c.Slot != null); - - // Are all players ready? - if (playerClients.Count() == 0 || playerClients.Any(c => c.State != Session.ClientState.Ready)) - return; - - // Are the map conditions satisfied? - if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && server.LobbyInfo.ClientInSlot(sl.Key) == null)) - return; - - server.StartGame(); - } - - public bool InterpretCommand(S server, Connection conn, Session.Client client, string cmd) - { - if (!ValidateCommand(server, conn, client, cmd)) - return false; - - var dict = new Dictionary> - { - { "ready", - s => - { - // if we're downloading, we can't ready up. - if (client.State == Session.ClientState.NotReady) - client.State = Session.ClientState.Ready; - else if (client.State == Session.ClientState.Ready) - client.State = Session.ClientState.NotReady; - - Log.Write("server", "Player @{0} is {1}", - conn.socket.RemoteEndPoint, client.State); - - server.SyncLobbyInfo(); - - CheckAutoStart(server, conn, client); - - return true; - }}, - { "startgame", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can start the game"); - return true; - } - - if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && - server.LobbyInfo.ClientInSlot(sl.Key) == null)) - { - server.SendOrderTo(conn, "Message", "Unable to start the game until required slots are full."); - return true; - } - server.StartGame(); - return true; - }}, - { "slot", - s => - { - if (!server.LobbyInfo.Slots.ContainsKey(s)) - { - Log.Write("server", "Invalid slot: {0}", s ); - return false; - } - var slot = server.LobbyInfo.Slots[s]; - - if (slot.Closed || server.LobbyInfo.ClientInSlot(s) != null) - return false; - - client.Slot = s; - S.SyncClientToPlayerReference(client, server.Map.Players[s]); - - server.SyncLobbyInfo(); - CheckAutoStart(server, conn, client); - - return true; - }}, - { "spectate", - s => - { - client.Slot = null; - client.SpawnPoint = 0; - client.Color = HSLColor.FromRGB(255, 255, 255); - server.SyncLobbyInfo(); - return true; - }}, - { "slot_close", - s => - { - if (!ValidateSlotCommand( server, conn, client, s, true )) - return false; - - // kick any player that's in the slot - var occupant = server.LobbyInfo.ClientInSlot(s); - if (occupant != null) - { - if (occupant.Bot != null) - server.LobbyInfo.Clients.Remove(occupant); - else - { - var occupantConn = server.Conns.FirstOrDefault( c => c.PlayerIndex == occupant.Index ); - if (occupantConn != null) - { - server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host"); - server.DropClient(occupantConn); - } - } - } - - server.LobbyInfo.Slots[s].Closed = true; - server.SyncLobbyInfo(); - return true; - }}, - { "slot_open", - s => - { - if (!ValidateSlotCommand( server, conn, client, s, true )) - return false; - - var slot = server.LobbyInfo.Slots[s]; - slot.Closed = false; - - // Slot may have a bot in it - var occupant = server.LobbyInfo.ClientInSlot(s); - if (occupant != null && occupant.Bot != null) - server.LobbyInfo.Clients.Remove(occupant); - - server.SyncLobbyInfo(); - return true; - }}, - { "slot_bot", - s => - { - var parts = s.Split(' '); - - if (parts.Length < 3) - { - server.SendOrderTo(conn, "Message", "Malformed slot_bot command"); - return true; - } - - if (!ValidateSlotCommand(server, conn, client, parts[0], true)) - return false; - - var slot = server.LobbyInfo.Slots[parts[0]]; - var bot = server.LobbyInfo.ClientInSlot(parts[0]); - int controllerClientIndex; - if (!int.TryParse(parts[1], out controllerClientIndex)) - { - Log.Write("server", "Invalid bot controller client index: {0}", parts[1]); - return false; - } - var botType = parts.Skip(2).JoinWith(" "); - - // Invalid slot - if (bot != null && bot.Bot == null) - { - server.SendOrderTo(conn, "Message", "Can't add bots to a slot with another client"); - return true; - } - - slot.Closed = false; - if (bot == null) - { - // Create a new bot - bot = new Session.Client() - { - Index = server.ChooseFreePlayerIndex(), - Name = botType, - Bot = botType, - Slot = parts[0], - Country = "random", - SpawnPoint = 0, - Team = 0, - State = Session.ClientState.NotReady, - BotControllerClientIndex = controllerClientIndex - }; - - // pick a random color for the bot - var hue = (byte)server.Random.Next(255); - var sat = (byte)server.Random.Next(255); - var lum = (byte)server.Random.Next(51,255); - bot.Color = bot.PreferredColor = new HSLColor(hue, sat, lum); - - server.LobbyInfo.Clients.Add(bot); - } - else - { - // Change the type of the existing bot - bot.Name = botType; - bot.Bot = botType; - } - - S.SyncClientToPlayerReference(bot, server.Map.Players[parts[0]]); - server.SyncLobbyInfo(); - return true; - }}, - { "map", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can change the map"); - return true; - } - - if (!server.ModData.AvailableMaps.ContainsKey(s)) - { - server.SendOrderTo(conn, "Message", "Map was not found on server"); - return true; - } - server.LobbyInfo.GlobalSettings.Map = s; - var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); - LoadMap(server); - SetDefaultDifficulty(server); - - // Reassign players into new slots based on their old slots: - // - Observers remain as observers - // - Players who now lack a slot are made observers - // - Bots who now lack a slot are dropped - var slots = server.LobbyInfo.Slots.Keys.ToArray(); - int i = 0; - foreach (var os in oldSlots) - { - var c = server.LobbyInfo.ClientInSlot(os); - if (c == null) - continue; - - c.SpawnPoint = 0; - c.State = Session.ClientState.NotReady; - c.Slot = i < slots.Length ? slots[i++] : null; - if (c.Slot != null) - { - // Remove Bot from slot if slot forbids bots - if (c.Bot != null && !server.Map.Players[c.Slot].AllowBots) - server.LobbyInfo.Clients.Remove(c); - S.SyncClientToPlayerReference(c, server.Map.Players[c.Slot]); - } - else if (c.Bot != null) - server.LobbyInfo.Clients.Remove(c); - } - - server.SyncLobbyInfo(); - return true; - }}, - { "fragilealliance", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.FragileAlliances.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled alliance configuration"); - return true; - } - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.FragileAlliances); - server.SyncLobbyInfo(); - return true; - }}, - { "allowcheats", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.Cheats.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled cheat configuration"); - return true; - } - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllowCheats); - server.SyncLobbyInfo(); - return true; - }}, - { "shroud", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.Shroud.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled shroud configuration"); - return true; - } - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Shroud); - server.SyncLobbyInfo(); - return true; - }}, - { "fog", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.Fog.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled fog configuration"); - return true; - } - - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Fog); - server.SyncLobbyInfo(); - return true; - }}, - { "assignteams", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - int teamCount; - if (!int.TryParse(s, out teamCount)) - { - server.SendOrderTo(conn, "Message", "Number of teams could not be parsed: {0}".F(s)); - return true; - } - - var maxTeams = (server.LobbyInfo.Clients.Count(c => c.Slot != null) + 1) / 2; - teamCount = teamCount.Clamp(0, maxTeams); - var players = server.LobbyInfo.Slots - .Select(slot => server.LobbyInfo.ClientInSlot(slot.Key)) - .Where(c => c != null && !server.LobbyInfo.Slots[c.Slot].LockTeam); - - var assigned = 0; - var playerCount = players.Count(); - foreach (var player in players) - { - // Free for all - if (teamCount == 0) - player.Team = 0; - - // Humans vs Bots - else if (teamCount == 1) - player.Team = player.Bot == null ? 1 : 2; - - else - player.Team = assigned++ * teamCount / playerCount + 1; - } - - server.SyncLobbyInfo(); - return true; - }}, - { "crates", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.Crates.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled crate configuration"); - return true; - } - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Crates); - server.SyncLobbyInfo(); - return true; - }}, - { "allybuildradius", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.AllyBuildRadius.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled ally build radius configuration"); - return true; - } - - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllyBuildRadius); - server.SyncLobbyInfo(); - return true; - }}, - { "difficulty", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (s != null && !server.Map.Options.Difficulties.Contains(s)) - { - server.SendOrderTo(conn, "Message", "Unsupported difficulty selected: {0}".F(s)); - server.SendOrderTo(conn, "Message", "Supported difficulties: {0}".F(server.Map.Options.Difficulties.JoinWith(","))); - return true; - } - - server.LobbyInfo.GlobalSettings.Difficulty = s; - server.SyncLobbyInfo(); - return true; - }}, - { "startingunits", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (!server.Map.Options.ConfigurableStartingUnits) - { - server.SendOrderTo(conn, "Message", "Map has disabled start unit configuration"); - return true; - } - - server.LobbyInfo.GlobalSettings.StartingUnitsClass = s; - server.SyncLobbyInfo(); - return true; - }}, - { "startingcash", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can set that option"); - return true; - } - - if (server.Map.Options.StartingCash.HasValue) - { - server.SendOrderTo(conn, "Message", "Map has disabled cash configuration"); - return true; - } - - server.LobbyInfo.GlobalSettings.StartingCash = int.Parse(s); - server.SyncLobbyInfo(); - return true; - }}, - { "kick", - s => - { - if (!client.IsAdmin) - { - server.SendOrderTo(conn, "Message", "Only the host can kick players"); - return true; - } - - var split = s.Split(' '); - if (split.Length < 2) - { - server.SendOrderTo(conn, "Message", "Malformed kick command"); - return true; - } - - int kickClientID; - int.TryParse(split[0], out kickClientID); - - var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c) != null && server.GetClient(c).Index == kickClientID); - if (kickConn == null) - { - server.SendOrderTo(conn, "Message", "Noone in that slot."); - return true; - } - - var kickConnIP = server.GetClient(kickConn).IpAddress; - - Log.Write("server", "Kicking client {0} as requested", kickClientID); - server.SendOrderTo(kickConn, "ServerError", "You have been kicked from the server"); - server.DropClient(kickConn); - - bool tempBan; - bool.TryParse(split[1], out tempBan); - - if (tempBan) - { - Log.Write("server", "Temporarily banning client {0} ({1}) as requested", kickClientID, kickConnIP); - server.TempBans.Add(kickConnIP); - } - - server.SyncLobbyInfo(); - return true; - }}, - { "name", - s => - { - Log.Write("server", "Player@{0} is now known as {1}", conn.socket.RemoteEndPoint, s); - client.Name = s; - server.SyncLobbyInfo(); - return true; - }}, - { "race", - s => - { - var parts = s.Split(' '); - var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); - - // Only the host can change other client's info - if (targetClient.Index != client.Index && !client.IsAdmin) - return true; - - // Map has disabled race changes - if (server.LobbyInfo.Slots[targetClient.Slot].LockRace) - return true; - - targetClient.Country = parts[1]; - server.SyncLobbyInfo(); - return true; - }}, - { "team", - s => - { - var parts = s.Split(' '); - var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); - - // Only the host can change other client's info - if (targetClient.Index != client.Index && !client.IsAdmin) - return true; - - // Map has disabled team changes - if (server.LobbyInfo.Slots[targetClient.Slot].LockTeam) - return true; - - int team; - if (!int.TryParse(parts[1], out team)) - { - Log.Write("server", "Invalid team: {0}", s ); - return false; - } - - targetClient.Team = team; - server.SyncLobbyInfo(); - return true; - }}, - { "spawn", - s => - { - var parts = s.Split(' '); - var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); - - // Only the host can change other client's info - if (targetClient.Index != client.Index && !client.IsAdmin) - return true; - - // Spectators don't need a spawnpoint - if (targetClient.Slot == null) - return true; - - // Map has disabled spawn changes - if (server.LobbyInfo.Slots[targetClient.Slot].LockSpawn) - return true; - - int spawnPoint; - if (!int.TryParse(parts[1], out spawnPoint) || spawnPoint < 0 || spawnPoint > server.Map.GetSpawnPoints().Length) - { - Log.Write("server", "Invalid spawn point: {0}", parts[1]); - return true; - } - - if (server.LobbyInfo.Clients.Where( cc => cc != client ).Any( cc => (cc.SpawnPoint == spawnPoint) && (cc.SpawnPoint != 0) )) - { - server.SendOrderTo(conn, "Message", "You can't be at the same spawn point as another player"); - return true; - } - - targetClient.SpawnPoint = spawnPoint; - server.SyncLobbyInfo(); - return true; - }}, - { "color", - s => - { - var parts = s.Split(' '); - var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); - - // Only the host can change other client's info - if (targetClient.Index != client.Index && !client.IsAdmin) - return true; - - // Spectator or map has disabled color changes - if (targetClient.Slot == null || server.LobbyInfo.Slots[targetClient.Slot].LockColor) - return true; - - var ci = parts[1].Split(',').Select(cc => int.Parse(cc)).ToArray(); - targetClient.Color = targetClient.PreferredColor = new HSLColor((byte)ci[0], (byte)ci[1], (byte)ci[2]); - server.SyncLobbyInfo(); - return true; - }} - }; - - var cmdName = cmd.Split(' ').First(); - var cmdValue = cmd.Split(' ').Skip(1).JoinWith(" "); - - Func a; - if (!dict.TryGetValue(cmdName, out a)) - return false; - - return a(cmdValue); - } - - public void ServerStarted(S server) - { - LoadMap(server); - SetDefaultDifficulty(server); - } - - static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr) - { - if (!pr.Playable) return null; - if (Game.Settings.Server.LockBots) - pr.AllowBots = false; - return new Session.Slot - { - PlayerReference = pr.Name, - Closed = false, - AllowBots = pr.AllowBots, - LockRace = pr.LockRace, - LockColor = pr.LockColor, - LockTeam = pr.LockTeam, - LockSpawn = pr.LockSpawn, - Required = pr.Required, - }; - } - - static void LoadMap(S server) - { - server.Map = new Map(server.ModData.AvailableMaps[server.LobbyInfo.GlobalSettings.Map].Path); - server.LobbyInfo.Slots = server.Map.Players - .Select(p => MakeSlotFromPlayerReference(p.Value)) - .Where(s => s != null) - .ToDictionary(s => s.PlayerReference, s => s); - - server.Map.Options.UpdateServerSettings(server.LobbyInfo.GlobalSettings); - } - - static void SetDefaultDifficulty(S server) - { - if (!server.Map.Options.Difficulties.Any()) - { - server.LobbyInfo.GlobalSettings.Difficulty = null; - return; - } - - if (!server.Map.Options.Difficulties.Contains(server.LobbyInfo.GlobalSettings.Difficulty)) - server.LobbyInfo.GlobalSettings.Difficulty = server.Map.Options.Difficulties.First(); - } - } -} +#region Copyright & License Information +/* + * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Network; +using OpenRA.FileFormats; +using OpenRA.Server; +using S = OpenRA.Server.Server; + +namespace OpenRA.Mods.RA.Server +{ + public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart + { + static bool ValidateSlotCommand(S server, Connection conn, Session.Client client, string arg, bool requiresHost) + { + if (!server.LobbyInfo.Slots.ContainsKey(arg)) + { + Log.Write("server", "Invalid slot: {0}", arg); + return false; + } + + if (requiresHost && !client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can do that"); + return false; + } + + return true; + } + + public static bool ValidateCommand(S server, Connection conn, Session.Client client, string cmd) + { + if (server.State == ServerState.GameStarted) + { + server.SendOrderTo(conn, "Message", "Cannot change state when game started. ({0})".F(cmd)); + return false; + } + else if (client.State == Session.ClientState.Ready && !(cmd == "ready" || cmd == "startgame")) + { + server.SendOrderTo(conn, "Message", "Cannot change state when marked as ready."); + return false; + } + + return true; + } + + void CheckAutoStart(S server, Connection conn, Session.Client client) + { + var playerClients = server.LobbyInfo.Clients.Where(c => c.Bot == null && c.Slot != null); + + // Are all players ready? + if (playerClients.Count() == 0 || playerClients.Any(c => c.State != Session.ClientState.Ready)) + return; + + // Are the map conditions satisfied? + if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && server.LobbyInfo.ClientInSlot(sl.Key) == null)) + return; + + server.StartGame(); + } + + public bool InterpretCommand(S server, Connection conn, Session.Client client, string cmd) + { + if (!ValidateCommand(server, conn, client, cmd)) + return false; + + var dict = new Dictionary> + { + { "ready", + s => + { + // if we're downloading, we can't ready up. + if (client.State == Session.ClientState.NotReady) + client.State = Session.ClientState.Ready; + else if (client.State == Session.ClientState.Ready) + client.State = Session.ClientState.NotReady; + + Log.Write("server", "Player @{0} is {1}", + conn.socket.RemoteEndPoint, client.State); + + server.SyncLobbyInfo(); + + CheckAutoStart(server, conn, client); + + return true; + }}, + { "startgame", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can start the game"); + return true; + } + + if (server.LobbyInfo.Slots.Any(sl => sl.Value.Required && + server.LobbyInfo.ClientInSlot(sl.Key) == null)) + { + server.SendOrderTo(conn, "Message", "Unable to start the game until required slots are full."); + return true; + } + server.StartGame(); + return true; + }}, + { "slot", + s => + { + if (!server.LobbyInfo.Slots.ContainsKey(s)) + { + Log.Write("server", "Invalid slot: {0}", s ); + return false; + } + var slot = server.LobbyInfo.Slots[s]; + + if (slot.Closed || server.LobbyInfo.ClientInSlot(s) != null) + return false; + + client.Slot = s; + S.SyncClientToPlayerReference(client, server.Map.Players[s]); + + server.SyncLobbyInfo(); + CheckAutoStart(server, conn, client); + + return true; + }}, + { "allow_spectate", + s => + { + s = s.Trim(); + if(s.Equals("True") || s.Equals("False")){ + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllowSpectate); + + server.SyncLobbyInfo(); + return true; + }else{ + server.SendOrderTo(conn, "Message", "Malformed allow_spectate command"); + return true; + } + }}, + { "spectate", + s => + { + if(server.LobbyInfo.GlobalSettings.AllowSpectate){ + client.Slot = null; + client.SpawnPoint = 0; + client.Color = HSLColor.FromRGB(255, 255, 255); + server.SyncLobbyInfo(); + return true; + }else{ + return false; + } + }}, + { "slot_close", + s => + { + if (!ValidateSlotCommand( server, conn, client, s, true )) + return false; + + // kick any player that's in the slot + var occupant = server.LobbyInfo.ClientInSlot(s); + if (occupant != null) + { + if (occupant.Bot != null) + server.LobbyInfo.Clients.Remove(occupant); + else + { + var occupantConn = server.Conns.FirstOrDefault( c => c.PlayerIndex == occupant.Index ); + if (occupantConn != null) + { + server.SendOrderTo(occupantConn, "ServerError", "Your slot was closed by the host"); + server.DropClient(occupantConn); + } + } + } + + server.LobbyInfo.Slots[s].Closed = true; + server.SyncLobbyInfo(); + return true; + }}, + { "slot_open", + s => + { + if (!ValidateSlotCommand( server, conn, client, s, true )) + return false; + + var slot = server.LobbyInfo.Slots[s]; + slot.Closed = false; + + // Slot may have a bot in it + var occupant = server.LobbyInfo.ClientInSlot(s); + if (occupant != null && occupant.Bot != null) + server.LobbyInfo.Clients.Remove(occupant); + + server.SyncLobbyInfo(); + return true; + }}, + { "slot_bot", + s => + { + var parts = s.Split(' '); + + if (parts.Length < 3) + { + server.SendOrderTo(conn, "Message", "Malformed slot_bot command"); + return true; + } + + if (!ValidateSlotCommand(server, conn, client, parts[0], true)) + return false; + + var slot = server.LobbyInfo.Slots[parts[0]]; + var bot = server.LobbyInfo.ClientInSlot(parts[0]); + int controllerClientIndex; + if (!int.TryParse(parts[1], out controllerClientIndex)) + { + Log.Write("server", "Invalid bot controller client index: {0}", parts[1]); + return false; + } + var botType = parts.Skip(2).JoinWith(" "); + + // Invalid slot + if (bot != null && bot.Bot == null) + { + server.SendOrderTo(conn, "Message", "Can't add bots to a slot with another client"); + return true; + } + + slot.Closed = false; + if (bot == null) + { + // Create a new bot + bot = new Session.Client() + { + Index = server.ChooseFreePlayerIndex(), + Name = botType, + Bot = botType, + Slot = parts[0], + Country = "random", + SpawnPoint = 0, + Team = 0, + State = Session.ClientState.NotReady, + BotControllerClientIndex = controllerClientIndex + }; + + // pick a random color for the bot + var hue = (byte)server.Random.Next(255); + var sat = (byte)server.Random.Next(255); + var lum = (byte)server.Random.Next(51,255); + bot.Color = bot.PreferredColor = new HSLColor(hue, sat, lum); + + server.LobbyInfo.Clients.Add(bot); + } + else + { + // Change the type of the existing bot + bot.Name = botType; + bot.Bot = botType; + } + + S.SyncClientToPlayerReference(bot, server.Map.Players[parts[0]]); + server.SyncLobbyInfo(); + return true; + }}, + { "map", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can change the map"); + return true; + } + + if (!server.ModData.AvailableMaps.ContainsKey(s)) + { + server.SendOrderTo(conn, "Message", "Map was not found on server"); + return true; + } + server.LobbyInfo.GlobalSettings.Map = s; + var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); + LoadMap(server); + SetDefaultDifficulty(server); + + // Reassign players into new slots based on their old slots: + // - Observers remain as observers + // - Players who now lack a slot are made observers + // - Bots who now lack a slot are dropped + var slots = server.LobbyInfo.Slots.Keys.ToArray(); + int i = 0; + foreach (var os in oldSlots) + { + var c = server.LobbyInfo.ClientInSlot(os); + if (c == null) + continue; + + c.SpawnPoint = 0; + c.State = Session.ClientState.NotReady; + c.Slot = i < slots.Length ? slots[i++] : null; + if (c.Slot != null) + { + // Remove Bot from slot if slot forbids bots + if (c.Bot != null && !server.Map.Players[c.Slot].AllowBots) + server.LobbyInfo.Clients.Remove(c); + S.SyncClientToPlayerReference(c, server.Map.Players[c.Slot]); + } + else if (c.Bot != null) + server.LobbyInfo.Clients.Remove(c); + } + + server.SyncLobbyInfo(); + return true; + }}, + { "fragilealliance", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.FragileAlliances.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled alliance configuration"); + return true; + } + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.FragileAlliances); + server.SyncLobbyInfo(); + return true; + }}, + { "allowcheats", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.Cheats.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled cheat configuration"); + return true; + } + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllowCheats); + server.SyncLobbyInfo(); + return true; + }}, + { "shroud", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.Shroud.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled shroud configuration"); + return true; + } + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Shroud); + server.SyncLobbyInfo(); + return true; + }}, + { "fog", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.Fog.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled fog configuration"); + return true; + } + + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Fog); + server.SyncLobbyInfo(); + return true; + }}, + { "assignteams", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + int teamCount; + if (!int.TryParse(s, out teamCount)) + { + server.SendOrderTo(conn, "Message", "Number of teams could not be parsed: {0}".F(s)); + return true; + } + + var maxTeams = (server.LobbyInfo.Clients.Count(c => c.Slot != null) + 1) / 2; + teamCount = teamCount.Clamp(0, maxTeams); + var players = server.LobbyInfo.Slots + .Select(slot => server.LobbyInfo.ClientInSlot(slot.Key)) + .Where(c => c != null && !server.LobbyInfo.Slots[c.Slot].LockTeam); + + var assigned = 0; + var playerCount = players.Count(); + foreach (var player in players) + { + // Free for all + if (teamCount == 0) + player.Team = 0; + + // Humans vs Bots + else if (teamCount == 1) + player.Team = player.Bot == null ? 1 : 2; + + else + player.Team = assigned++ * teamCount / playerCount + 1; + } + + server.SyncLobbyInfo(); + return true; + }}, + { "crates", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.Crates.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled crate configuration"); + return true; + } + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Crates); + server.SyncLobbyInfo(); + return true; + }}, + { "allybuildradius", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.AllyBuildRadius.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled ally build radius configuration"); + return true; + } + + bool.TryParse(s, out server.LobbyInfo.GlobalSettings.AllyBuildRadius); + server.SyncLobbyInfo(); + return true; + }}, + { "difficulty", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (s != null && !server.Map.Options.Difficulties.Contains(s)) + { + server.SendOrderTo(conn, "Message", "Unsupported difficulty selected: {0}".F(s)); + server.SendOrderTo(conn, "Message", "Supported difficulties: {0}".F(server.Map.Options.Difficulties.JoinWith(","))); + return true; + } + + server.LobbyInfo.GlobalSettings.Difficulty = s; + server.SyncLobbyInfo(); + return true; + }}, + { "startingunits", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (!server.Map.Options.ConfigurableStartingUnits) + { + server.SendOrderTo(conn, "Message", "Map has disabled start unit configuration"); + return true; + } + + server.LobbyInfo.GlobalSettings.StartingUnitsClass = s; + server.SyncLobbyInfo(); + return true; + }}, + { "startingcash", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can set that option"); + return true; + } + + if (server.Map.Options.StartingCash.HasValue) + { + server.SendOrderTo(conn, "Message", "Map has disabled cash configuration"); + return true; + } + + server.LobbyInfo.GlobalSettings.StartingCash = int.Parse(s); + server.SyncLobbyInfo(); + return true; + }}, + { "kick", + s => + { + if (!client.IsAdmin) + { + server.SendOrderTo(conn, "Message", "Only the host can kick players"); + return true; + } + + var split = s.Split(' '); + if (split.Length < 2) + { + server.SendOrderTo(conn, "Message", "Malformed kick command"); + return true; + } + + int kickClientID; + int.TryParse(split[0], out kickClientID); + + var kickConn = server.Conns.SingleOrDefault(c => server.GetClient(c) != null && server.GetClient(c).Index == kickClientID); + if (kickConn == null) + { + server.SendOrderTo(conn, "Message", "Noone in that slot."); + return true; + } + + var kickConnIP = server.GetClient(kickConn).IpAddress; + + Log.Write("server", "Kicking client {0} as requested", kickClientID); + server.SendOrderTo(kickConn, "ServerError", "You have been kicked from the server"); + server.DropClient(kickConn); + + bool tempBan; + bool.TryParse(split[1], out tempBan); + + if (tempBan) + { + Log.Write("server", "Temporarily banning client {0} ({1}) as requested", kickClientID, kickConnIP); + server.TempBans.Add(kickConnIP); + } + + server.SyncLobbyInfo(); + return true; + }}, + { "name", + s => + { + Log.Write("server", "Player@{0} is now known as {1}", conn.socket.RemoteEndPoint, s); + client.Name = s; + server.SyncLobbyInfo(); + return true; + }}, + { "race", + s => + { + var parts = s.Split(' '); + var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); + + // Only the host can change other client's info + if (targetClient.Index != client.Index && !client.IsAdmin) + return true; + + // Map has disabled race changes + if (server.LobbyInfo.Slots[targetClient.Slot].LockRace) + return true; + + targetClient.Country = parts[1]; + server.SyncLobbyInfo(); + return true; + }}, + { "team", + s => + { + var parts = s.Split(' '); + var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); + + // Only the host can change other client's info + if (targetClient.Index != client.Index && !client.IsAdmin) + return true; + + // Map has disabled team changes + if (server.LobbyInfo.Slots[targetClient.Slot].LockTeam) + return true; + + int team; + if (!int.TryParse(parts[1], out team)) + { + Log.Write("server", "Invalid team: {0}", s ); + return false; + } + + targetClient.Team = team; + server.SyncLobbyInfo(); + return true; + }}, + { "spawn", + s => + { + var parts = s.Split(' '); + var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); + + // Only the host can change other client's info + if (targetClient.Index != client.Index && !client.IsAdmin) + return true; + + // Spectators don't need a spawnpoint + if (targetClient.Slot == null) + return true; + + // Map has disabled spawn changes + if (server.LobbyInfo.Slots[targetClient.Slot].LockSpawn) + return true; + + int spawnPoint; + if (!int.TryParse(parts[1], out spawnPoint) || spawnPoint < 0 || spawnPoint > server.Map.GetSpawnPoints().Length) + { + Log.Write("server", "Invalid spawn point: {0}", parts[1]); + return true; + } + + if (server.LobbyInfo.Clients.Where( cc => cc != client ).Any( cc => (cc.SpawnPoint == spawnPoint) && (cc.SpawnPoint != 0) )) + { + server.SendOrderTo(conn, "Message", "You can't be at the same spawn point as another player"); + return true; + } + + targetClient.SpawnPoint = spawnPoint; + server.SyncLobbyInfo(); + return true; + }}, + { "color", + s => + { + var parts = s.Split(' '); + var targetClient = server.LobbyInfo.ClientWithIndex(int.Parse(parts[0])); + + // Only the host can change other client's info + if (targetClient.Index != client.Index && !client.IsAdmin) + return true; + + // Spectator or map has disabled color changes + if (targetClient.Slot == null || server.LobbyInfo.Slots[targetClient.Slot].LockColor) + return true; + + var ci = parts[1].Split(',').Select(cc => int.Parse(cc)).ToArray(); + targetClient.Color = targetClient.PreferredColor = new HSLColor((byte)ci[0], (byte)ci[1], (byte)ci[2]); + server.SyncLobbyInfo(); + return true; + }} + }; + + var cmdName = cmd.Split(' ').First(); + var cmdValue = cmd.Split(' ').Skip(1).JoinWith(" "); + + Func a; + if (!dict.TryGetValue(cmdName, out a)) + return false; + + return a(cmdValue); + } + + public void ServerStarted(S server) + { + LoadMap(server); + SetDefaultDifficulty(server); + } + + static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr) + { + if (!pr.Playable) return null; + if (Game.Settings.Server.LockBots) + pr.AllowBots = false; + return new Session.Slot + { + PlayerReference = pr.Name, + Closed = false, + AllowBots = pr.AllowBots, + LockRace = pr.LockRace, + LockColor = pr.LockColor, + LockTeam = pr.LockTeam, + LockSpawn = pr.LockSpawn, + Required = pr.Required, + }; + } + + static void LoadMap(S server) + { + server.Map = new Map(server.ModData.AvailableMaps[server.LobbyInfo.GlobalSettings.Map].Path); + server.LobbyInfo.Slots = server.Map.Players + .Select(p => MakeSlotFromPlayerReference(p.Value)) + .Where(s => s != null) + .ToDictionary(s => s.PlayerReference, s => s); + + server.Map.Options.UpdateServerSettings(server.LobbyInfo.GlobalSettings); + } + + static void SetDefaultDifficulty(S server) + { + if (!server.Map.Options.Difficulties.Any()) + { + server.LobbyInfo.GlobalSettings.Difficulty = null; + return; + } + + if (!server.Map.Options.Difficulties.Contains(server.LobbyInfo.GlobalSettings.Difficulty)) + server.LobbyInfo.GlobalSettings.Difficulty = server.Map.Options.Difficulties.First(); + } + } +} diff --git a/OpenRA.Mods.RA/ServerTraits/MasterServerPinger.cs b/OpenRA.Mods.RA/ServerTraits/MasterServerPinger.cs index 227be96376..35381d3ce5 100644 --- a/OpenRA.Mods.RA/ServerTraits/MasterServerPinger.cs +++ b/OpenRA.Mods.RA/ServerTraits/MasterServerPinger.cs @@ -75,7 +75,8 @@ namespace OpenRA.Mods.RA.Server numBots, "{0}@{1}".F(mod.Id, mod.Version), server.LobbyInfo.GlobalSettings.Map, - server.Map.PlayerCount)); + server.Map.PlayerCount, + server.LobbyInfo.GlobalSettings.AllowSpectate)); if (isInitialPing) { diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index 3d75b073f0..681efd37f9 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -173,6 +173,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic }; } + var slotsButton = lobby.GetOrNull("SLOTS_DROPDOWNBUTTON"); if (slotsButton != null) { @@ -613,6 +614,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic idx++; } + // Add spectators foreach (var client in orderManager.LobbyInfo.Clients.Where(client => client.Slot == null)) { @@ -653,27 +655,48 @@ namespace OpenRA.Mods.RA.Widgets.Logic idx++; } - // Spectate button - if (orderManager.LocalClient.Slot != null) - { - Widget spec = null; - if (idx < Players.Children.Count) - spec = Players.Children[idx]; - if (spec == null || spec.Id != NewSpectatorTemplate.Id) - spec = NewSpectatorTemplate.Clone(); + // Spectate button + if (orderManager.LocalClient.Slot != null) + { - var btn = spec.Get("SPECTATE"); - btn.OnClick = () => orderManager.IssueOrder(Order.Command("spectate")); - btn.IsDisabled = () => orderManager.LocalClient.IsReady; - spec.IsVisible = () => true; + Widget spec = null; + if (idx < Players.Children.Count) + spec = Players.Children[idx]; + if (spec == null || spec.Id != NewSpectatorTemplate.Id) + spec = NewSpectatorTemplate.Clone(); - if (idx >= Players.Children.Count) - Players.AddChild(spec); - else if (Players.Children[idx].Id != spec.Id) - Players.ReplaceChild(Players.Children[idx], spec); + var block = spec.Get("BLOCK_SPECTATE"); + block.OnClick = () => + { + orderManager.IssueOrder(Order.Command("allow_spectate False")); + orderManager.IssueOrders( + orderManager.LobbyInfo.Clients.Where( + c => c.IsObserver && !c.IsAdmin).Select( + client => Order.Command(String.Format("kick {0} {1}", client.Index, client.Name + ))).ToArray()); + }; + block.IsVisible = () => orderManager.LocalClient.IsAdmin && orderManager.LobbyInfo.GlobalSettings.AllowSpectate; + block.IsDisabled = () => !orderManager.LobbyInfo.GlobalSettings.AllowSpectate; + + var allow = spec.Get("ALLOW_SPECTATE"); + allow.OnClick = () => orderManager.IssueOrder(Order.Command("allow_spectate True")); + allow.IsVisible = () => orderManager.LocalClient.IsAdmin && !orderManager.LobbyInfo.GlobalSettings.AllowSpectate; + allow.IsDisabled = () => orderManager.LobbyInfo.GlobalSettings.AllowSpectate; + + var btn = spec.Get("SPECTATE"); + btn.OnClick = () => orderManager.IssueOrder(Order.Command("spectate")); + btn.IsDisabled = () => orderManager.LocalClient.IsReady; + btn.IsVisible = () => orderManager.LobbyInfo.GlobalSettings.AllowSpectate; + spec.IsVisible = () => true; + + if (idx >= Players.Children.Count) + Players.AddChild(spec); + else if (Players.Children[idx].Id != spec.Id) + Players.ReplaceChild(Players.Children[idx], spec); + + idx++; + } - idx++; - } while (Players.Children.Count > idx) Players.RemoveChild(Players.Children[idx]); diff --git a/mods/cnc/chrome/lobby-playerbin.yaml b/mods/cnc/chrome/lobby-playerbin.yaml index 83c877e61d..451fcfa9f9 100644 --- a/mods/cnc/chrome/lobby-playerbin.yaml +++ b/mods/cnc/chrome/lobby-playerbin.yaml @@ -307,10 +307,24 @@ ScrollPanel@LOBBY_PLAYER_BIN: Height:25 Visible:false Children: + Button@ALLOW_SPECTATE: + Text:Allow Spectators + Font:Regular + Width:190 + Height:25 + Y:0 + X:15 + Button@BLOCK_SPECTATE: + Text:Block Spectators + Font:Regular + Width:190 + Height:25 + Y:0 + X:15 Button@SPECTATE: Text:Spectate Font:Regular - Width:453 + Width:257 Height:25 - X:15 + X:210 Y:0 \ No newline at end of file diff --git a/mods/d2k/chrome/lobby-playerbin.yaml b/mods/d2k/chrome/lobby-playerbin.yaml index f64d6ed24f..2d37501d56 100644 --- a/mods/d2k/chrome/lobby-playerbin.yaml +++ b/mods/d2k/chrome/lobby-playerbin.yaml @@ -298,6 +298,20 @@ ScrollPanel@LOBBY_PLAYER_BIN: Height:25 Visible:false Children: + Button@ALLOW_SPECTATE: + Text:Allow Spectators + Font:Regular + Width:165 + Height:25 + X:15 + Y:0 + Button@BLOCK_SPECTATE: + Text:Block Spectators + Font:Regular + Width:165 + Height:25 + X:15 + Y:0 Button@SPECTATE: Text:Spectate Font:Regular diff --git a/mods/ra/chrome/lobby-playerbin.yaml b/mods/ra/chrome/lobby-playerbin.yaml index c6d2c6b88c..2396bb125c 100644 --- a/mods/ra/chrome/lobby-playerbin.yaml +++ b/mods/ra/chrome/lobby-playerbin.yaml @@ -298,6 +298,20 @@ ScrollPanel@LOBBY_PLAYER_BIN: Height:25 Visible:false Children: + Button@ALLOW_SPECTATE: + Text:Allow Spectators + Font:Regular + Width:165 + Height:25 + X:15 + Y:0 + Button@BLOCK_SPECTATE: + Text:Block Spectators + Font:Regular + Width:165 + Height:25 + X:15 + Y:0 Button@SPECTATE: Text:Spectate Font:Regular