diff --git a/OpenRA.Mods.Common/ColorValidator.cs b/OpenRA.Mods.Common/ColorValidator.cs new file mode 100644 index 0000000000..43e6d7d149 --- /dev/null +++ b/OpenRA.Mods.Common/ColorValidator.cs @@ -0,0 +1,151 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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.Support; + +namespace OpenRA.Mods.Common +{ + public class ColorValidator : IGlobalModData + { + // The bigger the color threshold, the less permissive is the algorithm + public readonly int Threshold = 0x50; + + 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); + } + + bool IsValid(Color askedColor, IEnumerable forbiddenColors, out Color forbiddenColor) + { + var blockingColors = forbiddenColors + .Where(playerColor => GetColorDelta(askedColor, playerColor) < Threshold) + .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 bool IsValid(Color askedColor, out Color forbiddenColor, IEnumerable terrainColors, IEnumerable playerColors, Action onError) + { + // Validate color against the current map tileset + if (!IsValid(askedColor, terrainColors, out forbiddenColor)) + { + onError("Color was adjusted to be less similar to the terrain."); + return false; + } + + // Validate color against other clients + if (!IsValid(askedColor, playerColors, out forbiddenColor)) + { + onError("Color was adjusted to be less similar to another player."); + return false; + } + + // Color is valid + forbiddenColor = default(Color); + + return true; + } + + public HSLColor RandomValidColor(MersenneTwister random, IEnumerable terrainColors, IEnumerable playerColors) + { + HSLColor color; + Color forbidden; + Action ignoreError = _ => { }; + do + { + var hue = (byte)random.Next(255); + var sat = (byte)random.Next(255); + var lum = (byte)random.Next(129, 255); + color = new HSLColor(hue, sat, lum); + } while (!IsValid(color.RGB, out forbidden, terrainColors, playerColors, ignoreError)); + + return color; + } + + public HSLColor MakeValid(Color askedColor, MersenneTwister random, IEnumerable terrainColors, IEnumerable playerColors, Action onError) + { + Color forbiddenColor; + if (IsValid(askedColor, out forbiddenColor, terrainColors, playerColors, onError)) + return new HSLColor(askedColor); + + // 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 division 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 attempt = 1; + var allForbidden = terrainColors.Concat(playerColors); + HSLColor color; + do + { + // If we reached the limit (The ii >= 255 prevents too much calculations) + if (attempt >= 255) + { + color = RandomValidColor(random, terrainColors, playerColors); + break; + } + + // Apply vector to forbidden color + var r = (forbiddenColor.R + (int)(vector[0] * weightVector[0] * attempt)).Clamp(0, 255); + var g = (forbiddenColor.G + (int)(vector[1] * weightVector[1] * attempt)).Clamp(0, 255); + var b = (forbiddenColor.B + (int)(vector[2] * weightVector[2] * attempt)).Clamp(0, 255); + + // Get the alternative color attempt + color = new HSLColor(Color.FromArgb(r, g, b)); + + attempt++; + } while (!IsValid(color.RGB, allForbidden, out forbiddenColor)); + + return color; + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index b0c42ec9f9..a6952e89d7 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -212,7 +212,6 @@ - @@ -733,6 +732,7 @@ + diff --git a/OpenRA.Mods.Common/ServerTraits/ColorValidator.cs b/OpenRA.Mods.Common/ServerTraits/ColorValidator.cs deleted file mode 100644 index d6f4ef4814..0000000000 --- a/OpenRA.Mods.Common/ServerTraits/ColorValidator.cs +++ /dev/null @@ -1,206 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2015 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.Server; -using S = OpenRA.Server.Server; - -namespace OpenRA.Mods.Common.Server -{ - public class ColorValidator : ServerTrait, IClientJoined - { - // The bigger the color threshold, the less permissive is the algorithm - const int ColorThreshold = 0x70; - const byte ColorLowerBound = 0x80; - const byte ColorHigherBound = 0xFF; - - 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 division 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 doesn't 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(129, 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 - var tileset = server.Map.Rules.TileSets[server.Map.Tileset]; - var forbiddenColors = tileset.TerrainInfo.Select(terrainInfo => terrainInfo.Color); - - if (!ValidateColorAgainstForbidden(askedColor, forbiddenColors, out forbiddenColor)) - { - if (connectionToEcho != null) - server.SendOrderTo(connectionToEcho, "Message", "Color was too similar to the terrain, and has been adjusted."); - - return false; - } - - // Validate color against other clients - var playerColors = server.LobbyInfo.Clients.Where(c => c.Index != playerIndex).Select(c => c.Color.RGB); - if (!ValidateColorAgainstForbidden(askedColor, playerColors, out forbiddenColor)) - { - if (connectionToEcho != null) - server.SendOrderTo(connectionToEcho, "Message", "Color was too similar to another player's color, and has been adjusted."); - - return false; - } - - var mapPlayerColors = server.MapPlayers.Players.Values.Select(p => p.Color.RGB); - - if (!ValidateColorAgainstForbidden(askedColor, mapPlayerColors, out forbiddenColor)) - { - if (connectionToEcho != null) - server.SendOrderTo(connectionToEcho, "Message", "Color was too similar to a non-combatant player, and has been adjusted."); - - return false; - } - - // Color 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 whether color is allowed and get an alternative if it isn't - if (client.Slot == null || !server.LobbyInfo.Slots[client.Slot].LockColor) - client.Color = ValidatePlayerColorAndGetAlternative(server, client.Color, client.Index); - } - - #endregion - } -} diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index eda67f531b..62b5a02d36 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; @@ -19,7 +20,7 @@ using S = OpenRA.Server.Server; namespace OpenRA.Mods.Common.Server { - public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart + public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, IClientJoined { static bool ValidateSlotCommand(S server, Connection conn, Session.Client client, string arg, bool requiresHost) { @@ -136,10 +137,7 @@ namespace OpenRA.Mods.Common.Server S.SyncClientToPlayerReference(client, server.MapPlayers.Players[s]); if (!slot.LockColor) - { - var validatedColor = ColorValidator.ValidatePlayerColorAndGetAlternative(server, client.Color, client.Index, conn); - client.PreferredColor = client.Color = validatedColor; - } + client.PreferredColor = client.Color = SanitizePlayerColor(server, client.Color, client.Index, conn); server.SyncLobbyClients(); CheckAutoStart(server); @@ -291,16 +289,12 @@ namespace OpenRA.Mods.Common.Server }; // 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; + var validator = server.ModData.Manifest.Get(); + var tileset = server.Map.Rules.TileSets[server.Map.Tileset]; + var terrainColors = tileset.TerrainInfo.Select(ti => ti.Color); + var playerColors = server.LobbyInfo.Clients.Select(c => c.Color.RGB) + .Concat(server.MapPlayers.Players.Values.Select(p => p.Color.RGB)); + bot.Color = bot.PreferredColor = validator.RandomValidColor(server.Random, terrainColors, playerColors); server.LobbyInfo.Clients.Add(bot); } @@ -367,12 +361,10 @@ namespace OpenRA.Mods.Common.Server server.LobbyInfo.Clients.Remove(c); } + // Validate if color is allowed and get an alternative it isn't foreach (var c in server.LobbyInfo.Clients) - { - // Validate if color is allowed and get an alternative it isn't if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor)) - c.Color = c.PreferredColor = ColorValidator.ValidatePlayerColorAndGetAlternative(server, c.Color, c.Index, conn); - } + c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn); server.SyncLobbyInfo(); @@ -875,16 +867,13 @@ namespace OpenRA.Mods.Common.Server if (targetClient.Slot == null || server.LobbyInfo.Slots[targetClient.Slot].LockColor) return true; - var newHslColor = FieldLoader.GetValue("(value)", parts[1]); - // Validate if color is allowed and get an alternative it isn't - var altHslColor = ColorValidator.ValidatePlayerColorAndGetAlternative(server, newHslColor, targetClient.Index, conn); - - targetClient.Color = altHslColor; + var newColor = FieldLoader.GetValue("(value)", parts[1]); + targetClient.Color = SanitizePlayerColor(server, newColor, targetClient.Index, conn); // Only update player's preferred color if new color is valid - if (newHslColor == altHslColor) - targetClient.PreferredColor = altHslColor; + if (newColor == targetClient.Color) + targetClient.PreferredColor = targetClient.Color; server.SyncLobbyClients(); return true; @@ -973,5 +962,33 @@ namespace OpenRA.Mods.Common.Server if (!server.Map.Options.Difficulties.Contains(server.LobbyInfo.GlobalSettings.Difficulty)) server.LobbyInfo.GlobalSettings.Difficulty = server.Map.Options.Difficulties.First(); } + + static HSLColor SanitizePlayerColor(S server, HSLColor askedColor, int playerIndex, Connection connectionToEcho = null) + { + var validator = server.ModData.Manifest.Get(); + var askColor = askedColor; + + Action onError = message => + { + if (connectionToEcho != null) + server.SendOrderTo(connectionToEcho, "Message", message); + }; + + var tileset = server.Map.Rules.TileSets[server.Map.Tileset]; + var terrainColors = tileset.TerrainInfo.Select(ti => ti.Color).ToList(); + var playerColors = server.LobbyInfo.Clients.Where(c => c.Index != playerIndex).Select(c => c.Color.RGB) + .Concat(server.MapPlayers.Players.Values.Select(p => p.Color.RGB)).ToList(); + + return validator.MakeValid(askColor.RGB, server.Random, terrainColors, playerColors, onError); + } + + public void ClientJoined(S server, Connection conn) + { + var client = server.GetClient(conn); + + // Validate whether color is allowed and get an alternative if it isn't + if (client.Slot == null || !server.LobbyInfo.Slots[client.Slot].LockColor) + client.Color = SanitizePlayerColor(server, client.Color, client.Index); + } } } diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 17177eae48..73735f3f6e 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -164,7 +164,6 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification - ColorValidator LobbyDefaults: AllowCheats: false @@ -242,3 +241,5 @@ GameSpeeds: Name: Fastest Timestep: 20 OrderLatency: 6 + +ColorValidator: diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 2bace6adeb..27c8b39dc3 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -149,7 +149,6 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification - ColorValidator LobbyDefaults: AllowCheats: false @@ -217,3 +216,5 @@ GameSpeeds: Name: Fastest Timestep: 20 OrderLatency: 6 + +ColorValidator: diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 4b188744e5..20674cec31 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -165,7 +165,6 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification - ColorValidator LobbyDefaults: AllowCheats: false @@ -242,3 +241,5 @@ GameSpeeds: Name: Fastest Timestep: 20 OrderLatency: 6 + +ColorValidator: diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 363ff8ea88..9e2eb7b762 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -211,7 +211,6 @@ ServerTraits: PlayerPinger MasterServerPinger LobbySettingsNotification - ColorValidator LobbyDefaults: AllowCheats: true @@ -281,3 +280,5 @@ GameSpeeds: Name: Fastest Timestep: 20 OrderLatency: 6 + +ColorValidator: