diff --git a/OpenRA.Game/Map/TileSet.cs b/OpenRA.Game/Map/TileSet.cs index 1afa545d16..1c8e2faf88 100644 --- a/OpenRA.Game/Map/TileSet.cs +++ b/OpenRA.Game/Map/TileSet.cs @@ -144,7 +144,7 @@ namespace OpenRA public readonly Dictionary Templates = new Dictionary(); public readonly string[] EditorTemplateOrder; - readonly TerrainTypeInfo[] terrainInfo; + public readonly TerrainTypeInfo[] TerrainInfo; readonly Dictionary terrainIndexByType = new Dictionary(); readonly int defaultWalkableTerrainIndex; @@ -158,13 +158,13 @@ namespace OpenRA FieldLoader.Load(this, yaml["General"]); // TerrainTypes - terrainInfo = yaml["Terrain"].ToDictionary().Values + TerrainInfo = yaml["Terrain"].ToDictionary().Values .Select(y => new TerrainTypeInfo(y)) .OrderBy(tt => tt.Type) .ToArray(); - for (var i = 0; i < terrainInfo.Length; i++) + for (var i = 0; i < TerrainInfo.Length; i++) { - var tt = terrainInfo[i].Type; + var tt = TerrainInfo[i].Type; if (terrainIndexByType.ContainsKey(tt)) throw new InvalidDataException("Duplicate terrain type '{0}' in '{1}'.".F(tt, filepath)); @@ -185,7 +185,7 @@ namespace OpenRA this.Id = id; this.Palette = palette; this.Extensions = extensions; - this.terrainInfo = terrainInfo; + this.TerrainInfo = terrainInfo; for (var i = 0; i < terrainInfo.Length; i++) { @@ -201,12 +201,12 @@ namespace OpenRA public TerrainTypeInfo this[int index] { - get { return terrainInfo[index]; } + get { return TerrainInfo[index]; } } public int TerrainsCount { - get { return terrainInfo.Length; } + get { return TerrainInfo.Length; } } public bool TryGetTerrainIndex(string type, out int index) @@ -254,7 +254,7 @@ namespace OpenRA root.Add(new MiniYamlNode("General", null, gen)); root.Add(new MiniYamlNode("Terrain", null, - terrainInfo.Select(t => new MiniYamlNode("TerrainType@{0}".F(t.Type), t.Save())).ToList())); + TerrainInfo.Select(t => new MiniYamlNode("TerrainType@{0}".F(t.Type), t.Save())).ToList())); root.Add(new MiniYamlNode("Templates", null, Templates.Select(t => new MiniYamlNode("Template@{0}".F(t.Value.Id), t.Value.Save(this))).ToList())); @@ -263,7 +263,7 @@ namespace OpenRA public TerrainTypeInfo GetTerrainInfo(TerrainTile r) { - return terrainInfo[GetTerrainIndex(r)]; + return TerrainInfo[GetTerrainIndex(r)]; } } } diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index e8b98dcb31..0e14ddcf63 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -73,6 +73,30 @@ namespace OpenRA.Server c.Team = pr.Team; } + static void SendData(Socket s, byte[] data) + { + var start = 0; + var length = data.Length; + SocketError error; + + // Non-blocking sends are free to send only part of the data + while (start < length) + { + var sent = s.Send(data, start, length - start, SocketFlags.None, out error); + if (error == SocketError.WouldBlock) + { + Log.Write("server", "Non-blocking send of {0} bytes failed. Falling back to blocking send.", length - start); + s.Blocking = true; + sent = s.Send(data, start, length - start, SocketFlags.None); + s.Blocking = false; + } + else if (error != SocketError.Success) + throw new SocketException((int)error); + + start += sent; + } + } + protected volatile ServerState internalState = new ServerState(); public ServerState State { @@ -683,29 +707,5 @@ namespace OpenRA.Server gameTimeout.Enabled = true; } } - - static void SendData(Socket s, byte[] data) - { - var start = 0; - var length = data.Length; - SocketError error; - - // Non-blocking sends are free to send only part of the data - while (start < length) - { - var sent = s.Send(data, start, length - start, SocketFlags.None, out error); - if (error == SocketError.WouldBlock) - { - Log.Write("server", "Non-blocking send of {0} bytes failed. Falling back to blocking send.", length - start); - s.Blocking = true; - sent = s.Send(data, start, length - start, SocketFlags.None); - s.Blocking = false; - } - else if (error != SocketError.Success) - throw new SocketException((int)error); - - start += sent; - } - } } } diff --git a/OpenRA.Game/Server/TraitInterfaces.cs b/OpenRA.Game/Server/TraitInterfaces.cs index f77e403372..4668c1fb81 100644 --- a/OpenRA.Game/Server/TraitInterfaces.cs +++ b/OpenRA.Game/Server/TraitInterfaces.cs @@ -27,13 +27,13 @@ namespace OpenRA.Server int TickTimeout { get; } } - public abstract class ServerTrait {} + public abstract class ServerTrait { } public class DebugServerTrait : ServerTrait, IInterpretCommand, IStartGame, INotifySyncLobbyInfo, INotifyServerStart, INotifyServerShutdown, IEndGame { public bool InterpretCommand(Server server, Connection conn, Session.Client client, string cmd) { - Console.WriteLine("Server received command from player {1}: {0}",cmd, conn.PlayerIndex); + Console.WriteLine("Server received command from player {1}: {0}", cmd, conn.PlayerIndex); return false; } diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 58f4f5f7c5..aaf689a4dc 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -292,6 +292,7 @@ + diff --git a/OpenRA.Mods.RA/ServerTraits/ColorValidator.cs b/OpenRA.Mods.RA/ServerTraits/ColorValidator.cs new file mode 100644 index 0000000000..5c22218578 --- /dev/null +++ b/OpenRA.Mods.RA/ServerTraits/ColorValidator.cs @@ -0,0 +1,208 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 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.Drawing; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Network; +using OpenRA.Server; +using S = OpenRA.Server.Server; + +namespace OpenRA.Mods.RA.Server +{ + public class ColorValidator : ServerTrait, IClientJoined + { + // The bigger the color threshold, the less permitive is the algorithm + const int ColorThreshold = 0x40; + const byte ColorLowerBound = 0x33; + const byte ColorHigherBound = 0xFF; + + public static bool ValidateColorAgainstOtherPlayers(Color askedColor, int playerIndex, IEnumerable lobbyClients, out Color forbiddenColor) + { + // Get lobby players colors, except from the actual target player + var playerColors = lobbyClients + .Where(lobbyClient => lobbyClient.Index != playerIndex) + .Select(lobbyClient => lobbyClient.Color.RGB); + + // Calculate the difference between each player's color and target color and get the closest forbidden color (if invalid) + return ValidateColorAgainstForbidden(askedColor, playerColors, out forbiddenColor); + } + + public static bool ValidateColorAgainstTileset(Color askedColor, TileSet tileSet, out Color forbiddenColor) + { + // Get colors from the current map terrain info + var forbiddenColors = tileSet.TerrainInfo.Select(terrainInfo => terrainInfo.Color); + + // Calculate the difference between each forbidden color and target color and get the closest forbidden color (if invalid) + return ValidateColorAgainstForbidden(askedColor, forbiddenColors, out forbiddenColor); + } + + private static bool ValidateColorAgainstForbidden(Color askedColor, IEnumerable forbiddenColors, out Color forbiddenColor) + { + var blockingColors = + forbiddenColors + .Where(playerColor => GetColorDelta(askedColor, playerColor) < ColorThreshold) + .Select(playerColor => new { Delta = GetColorDelta(askedColor, playerColor), Color = playerColor }); + + // Return the player that holds with the lowest difference + if (blockingColors.Any()) + { + forbiddenColor = blockingColors.MinBy(aa => aa.Delta).Color; + return false; + } + + forbiddenColor = default(Color); + return true; + } + + public static Color? GetColorAlternative(Color askedColor, Color forbiddenColor) + { + Color? color = null; + + // Vector between the 2 colors + var vector = new double[] + { + askedColor.R - forbiddenColor.R, + askedColor.G - forbiddenColor.G, + askedColor.B - forbiddenColor.B + }; + + // Reduce vector by it's biggest value (more calculations, but more accuracy too) + var vectorMax = vector.Max(vv => Math.Abs(vv)); + if (vectorMax == 0) + vectorMax = 1; // Avoid divison by 0 + vector[0] /= vectorMax; + vector[1] /= vectorMax; + vector[2] /= vectorMax; + + // Color weights + var rmean = (double)(askedColor.R + forbiddenColor.R) / 2; + var weightVector = new[] + { + 2.0 + rmean / 256, + 4.0, + 2.0 + (255 - rmean) / 256, + }; + + var ii = 1; + var alternativeColor = new int[3]; + + do + { + // If we reached the limit (The ii >= 255 prevents too much calculations) + if ((alternativeColor[0] == ColorLowerBound && alternativeColor[1] == ColorLowerBound && alternativeColor[2] == ColorLowerBound) + || (alternativeColor[0] == ColorHigherBound && alternativeColor[1] == ColorHigherBound && alternativeColor[2] == ColorHigherBound) + || ii >= 255) + { + color = null; + break; + } + + // Apply vector to forbidden color + alternativeColor[0] = forbiddenColor.R + (int)(vector[0] * weightVector[0] * ii); + alternativeColor[1] = forbiddenColor.G + (int)(vector[1] * weightVector[1] * ii); + alternativeColor[2] = forbiddenColor.B + (int)(vector[2] * weightVector[2] * ii); + + // Be sure it doesnt go out of bounds (0x33 is the lower limit for HSL picker) + alternativeColor[0] = alternativeColor[0].Clamp(ColorLowerBound, ColorHigherBound); + alternativeColor[1] = alternativeColor[1].Clamp(ColorLowerBound, ColorHigherBound); + alternativeColor[2] = alternativeColor[2].Clamp(ColorLowerBound, ColorHigherBound); + + // Get the alternative color attempt + color = Color.FromArgb(alternativeColor[0], alternativeColor[1], alternativeColor[2]); + + ++ii; + } while (GetColorDelta(color.Value, forbiddenColor) < ColorThreshold); + + return color; + } + + public static double GetColorDelta(Color colorA, Color colorB) + { + var rmean = (colorA.R + colorB.R) / 2.0; + var r = colorA.R - colorB.R; + var g = colorA.G - colorB.G; + var b = colorA.B - colorB.B; + var weightR = 2.0 + rmean / 256; + var weightG = 4.0; + var weightB = 2.0 + (255 - rmean) / 256; + return Math.Sqrt(weightR * r * r + weightG * g * g + weightB * b * b); + } + + public static HSLColor ValidatePlayerColorAndGetAlternative(S server, HSLColor askedColor, int playerIndex, Connection connectionToEcho = null) + { + var askColor = askedColor; + + Color invalidColor; + if (!ValidatePlayerNewColor(server, askColor.RGB, playerIndex, out invalidColor, connectionToEcho)) + { + var altColor = GetColorAlternative(askColor.RGB, invalidColor); + if (altColor == null || !ValidatePlayerNewColor(server, altColor.Value, playerIndex)) + { + // Pick a random color + do + { + var hue = (byte)server.Random.Next(255); + var sat = (byte)server.Random.Next(255); + var lum = (byte)server.Random.Next(51, 255); + askColor = new HSLColor(hue, sat, lum); + } while (!ValidatePlayerNewColor(server, askColor.RGB, playerIndex)); + } + else + askColor = HSLColor.FromRGB(altColor.Value.R, altColor.Value.G, altColor.Value.B); + } + + return askColor; + } + + public static bool ValidatePlayerNewColor(S server, Color askedColor, int playerIndex, out Color forbiddenColor, Connection connectionToEcho = null) + { + // Validate color against the current map tileset + if (!ValidateColorAgainstTileset(askedColor, Game.modData.DefaultRules.TileSets[server.Map.Tileset], out forbiddenColor)) + { + if (connectionToEcho != null) + server.SendOrderTo(connectionToEcho, "Message", "Requested color was too similar to the map terrain, and has been adjusted."); + return false; + } + + // Validate color against the other players colors + if (!ValidateColorAgainstOtherPlayers(askedColor, playerIndex, server.LobbyInfo.Clients, out forbiddenColor)) + { + if (connectionToEcho != null) + server.SendOrderTo(connectionToEcho, "Message", "Requested color was too similar to another player, and has been adjusted."); + return false; + } + + // Else is valid! + forbiddenColor = default(Color); + return true; + } + + public static bool ValidatePlayerNewColor(S server, Color askedColor, int playerIndex, Connection connectionToEcho = null) + { + Color forbiddenColor; + return ValidatePlayerNewColor(server, askedColor, playerIndex, out forbiddenColor, connectionToEcho); + } + + #region IClientJoined + + public void ClientJoined(S server, Connection conn) + { + var client = server.GetClient(conn); + + // Validate if color is allowed and get an alternative if it isn't + client.Color = ColorValidator.ValidatePlayerColorAndGetAlternative(server, client.Color, client.Index); + } + + #endregion + } +} diff --git a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs index 35a011bb56..77e409c1bc 100644 --- a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using OpenRA.Graphics; using OpenRA.Network; @@ -95,7 +96,8 @@ namespace OpenRA.Mods.RA.Server CheckAutoStart(server); return true; - }}, + } + }, { "startgame", s => { @@ -111,17 +113,20 @@ namespace OpenRA.Mods.RA.Server 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 ); + Log.Write("server", "Invalid slot: {0}", s); return false; } + var slot = server.LobbyInfo.Slots[s]; if (slot.Closed || server.LobbyInfo.ClientInSlot(s) != null) @@ -133,7 +138,8 @@ namespace OpenRA.Mods.RA.Server CheckAutoStart(server); return true; - }}, + } + }, { "allow_spectators", s => { @@ -147,7 +153,8 @@ namespace OpenRA.Mods.RA.Server server.SendOrderTo(conn, "Message", "Malformed allow_spectate command"); return true; } - }}, + } + }, { "spectate", s => { @@ -161,7 +168,8 @@ namespace OpenRA.Mods.RA.Server } else return false; - }}, + } + }, { "slot_close", s => { @@ -194,11 +202,12 @@ namespace OpenRA.Mods.RA.Server server.LobbyInfo.Slots[s].Closed = true; server.SyncLobbySlots(); return true; - }}, + } + }, { "slot_open", s => { - if (!ValidateSlotCommand( server, conn, client, s, true )) + if (!ValidateSlotCommand(server, conn, client, s, true)) return false; var slot = server.LobbyInfo.Slots[s]; @@ -213,10 +222,11 @@ namespace OpenRA.Mods.RA.Server var ping = server.LobbyInfo.PingFromClient(occupant); server.LobbyInfo.ClientPings.Remove(ping); } - server.SyncLobbyClients(); + server.SyncLobbyClients(); return true; - }}, + } + }, { "slot_bot", s => { @@ -239,6 +249,7 @@ namespace OpenRA.Mods.RA.Server Log.Write("server", "Invalid bot controller client index: {0}", parts[1]); return false; } + var botType = parts.Skip(2).JoinWith(" "); // Invalid slot @@ -265,11 +276,17 @@ namespace OpenRA.Mods.RA.Server 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); + // Pick a random color for the bot + HSLColor botColor; + do + { + var hue = (byte)server.Random.Next(255); + var sat = (byte)server.Random.Next(255); + var lum = (byte)server.Random.Next(51, 255); + botColor = new HSLColor(hue, sat, lum); + } while (!ColorValidator.ValidatePlayerNewColor(server, botColor.RGB, bot.Index)); + + bot.Color = bot.PreferredColor = botColor; server.LobbyInfo.Clients.Add(bot); } @@ -284,7 +301,8 @@ namespace OpenRA.Mods.RA.Server server.SyncLobbyClients(); server.SyncLobbySlots(); return true; - }}, + } + }, { "map", s => { @@ -335,12 +353,19 @@ namespace OpenRA.Mods.RA.Server server.LobbyInfo.Clients.Remove(c); } + foreach (var c in server.LobbyInfo.Clients) + { + // Validate if color is allowed and get an alternative it it isn't + c.Color = c.PreferredColor = ColorValidator.ValidatePlayerColorAndGetAlternative(server, c.Color, c.Index, conn); + } + server.SyncLobbyInfo(); server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title)); return true; - }}, + } + }, { "fragilealliance", s => { @@ -362,7 +387,8 @@ namespace OpenRA.Mods.RA.Server .F(client.Name, server.LobbyInfo.GlobalSettings.FragileAlliances ? "enabled" : "disabled")); return true; - }}, + } + }, { "allowcheats", s => { @@ -384,7 +410,8 @@ namespace OpenRA.Mods.RA.Server .F(client.Name, server.LobbyInfo.GlobalSettings.AllowCheats ? "allowed" : "disallowed")); return true; - }}, + } + }, { "shroud", s => { @@ -406,7 +433,8 @@ namespace OpenRA.Mods.RA.Server .F(client.Name, server.LobbyInfo.GlobalSettings.Shroud ? "enabled" : "disabled")); return true; - }}, + } + }, { "fog", s => { @@ -422,14 +450,14 @@ namespace OpenRA.Mods.RA.Server return true; } - bool.TryParse(s, out server.LobbyInfo.GlobalSettings.Fog); server.SyncLobbyGlobalSettings(); server.SendMessage("{0} {1} Fog of War." .F(client.Name, server.LobbyInfo.GlobalSettings.Fog ? "enabled" : "disabled")); return true; - }}, + } + }, { "assignteams", s => { @@ -463,14 +491,14 @@ namespace OpenRA.Mods.RA.Server // Humans vs Bots else if (teamCount == 1) player.Team = player.Bot == null ? 1 : 2; - else player.Team = assigned++ * teamCount / playerCount + 1; } server.SyncLobbyClients(); return true; - }}, + } + }, { "crates", s => { @@ -492,7 +520,8 @@ namespace OpenRA.Mods.RA.Server .F(client.Name, server.LobbyInfo.GlobalSettings.Crates ? "enabled" : "disabled")); return true; - }}, + } + }, { "allybuildradius", s => { @@ -514,7 +543,8 @@ namespace OpenRA.Mods.RA.Server .F(client.Name, server.LobbyInfo.GlobalSettings.AllyBuildRadius ? "enabled" : "disabled")); return true; - }}, + } + }, { "difficulty", s => { @@ -536,7 +566,8 @@ namespace OpenRA.Mods.RA.Server server.SendMessage("{0} changed difficulty to {1}.".F(client.Name, s)); return true; - }}, + } + }, { "startingunits", s => { @@ -561,7 +592,8 @@ namespace OpenRA.Mods.RA.Server server.SendMessage("{0} changed Starting Units to {1}.".F(client.Name, className)); return true; - }}, + } + }, { "startingcash", s => { @@ -582,7 +614,8 @@ namespace OpenRA.Mods.RA.Server server.SendMessage("{0} changed Starting Cash to ${1}.".F(client.Name, s)); return true; - }}, + } + }, { "techlevel", s => { @@ -603,7 +636,8 @@ namespace OpenRA.Mods.RA.Server server.SendMessage("{0} changed Tech Level to {1}.".F(client.Name, s)); return true; - }}, + } + }, { "kick", s => { @@ -651,7 +685,8 @@ namespace OpenRA.Mods.RA.Server server.SyncLobbySlots(); return true; - }}, + } + }, { "name", s => { @@ -659,7 +694,8 @@ namespace OpenRA.Mods.RA.Server client.Name = s; server.SyncLobbyClients(); return true; - }}, + } + }, { "race", s => { @@ -677,7 +713,8 @@ namespace OpenRA.Mods.RA.Server targetClient.Country = parts[1]; server.SyncLobbyClients(); return true; - }}, + } + }, { "team", s => { @@ -695,14 +732,15 @@ namespace OpenRA.Mods.RA.Server int team; if (!Exts.TryParseIntegerInvariant(parts[1], out team)) { - Log.Write("server", "Invalid team: {0}", s ); + Log.Write("server", "Invalid team: {0}", s); return false; } targetClient.Team = team; server.SyncLobbyClients(); return true; - }}, + } + }, { "spawn", s => { @@ -738,7 +776,8 @@ namespace OpenRA.Mods.RA.Server targetClient.SpawnPoint = spawnPoint; server.SyncLobbyClients(); return true; - }}, + } + }, { "color", s => { @@ -753,11 +792,21 @@ namespace OpenRA.Mods.RA.Server if (targetClient.Slot == null || server.LobbyInfo.Slots[targetClient.Slot].LockColor) return true; - var ci = parts[1].Split(',').Select(cc => Exts.ParseIntegerInvariant(cc)).ToArray(); - targetClient.Color = targetClient.PreferredColor = new HSLColor((byte)ci[0], (byte)ci[1], (byte)ci[2]); + var newHslColor = FieldLoader.GetValue("(value)", parts[1]); + + // Validate if color is allowed and get an alternative it it isn't + var altHslColor = ColorValidator.ValidatePlayerColorAndGetAlternative(server, newHslColor, targetClient.Index, conn); + + targetClient.Color = altHslColor; + + // Only update player's preferred color if new color is valid + if (newHslColor == altHslColor) + targetClient.PreferredColor = altHslColor; + server.SyncLobbyClients(); return true; - }} + } + } }; var cmdName = cmd.Split(' ').First(); diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 348c62f414..f0be53262e 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -153,6 +153,7 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification + ColorValidator LobbyDefaults: AllowCheats: false diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index ad47ee8a29..d216aabc58 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -135,6 +135,7 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification + ColorValidator LobbyDefaults: AllowCheats: false diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 555af16850..2852236732 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -151,6 +151,7 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification + ColorValidator LobbyDefaults: AllowCheats: false diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 0609f5075f..d31a64bd23 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -180,6 +180,7 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification + ColorValidator LobbyDefaults: AllowCheats: true