This ensures that color picks that have multiple issues will have them all checked at the same time, including ensuring that the fix for one issue doesn't cause another issue. Handling of the onError action has been changed from being called at once to collecting the potential errors in a HashSet to deduplicate them and then calling onError after a valid color has been found. (Otherwise you would in the worst case get 256 error messages logged!)
196 lines
5.9 KiB
C#
196 lines
5.9 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2020 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, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Primitives;
|
|
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;
|
|
public readonly float[] HsvSaturationRange = new[] { 0.25f, 1f };
|
|
public readonly float[] HsvValueRange = new[] { 0.2f, 1.0f };
|
|
public readonly Color[] TeamColorPresets = { };
|
|
|
|
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<Color> 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<Color> terrainColors, IEnumerable<Color> playerColors, HashSet<string> errorMessages = null)
|
|
{
|
|
// Validate color against HSV
|
|
float h, s, v;
|
|
int a;
|
|
|
|
askedColor.ToAhsv(out a, out h, out s, out v);
|
|
if (s < HsvSaturationRange[0] || s > HsvSaturationRange[1] || v < HsvValueRange[0] || v > HsvValueRange[1])
|
|
{
|
|
if (errorMessages != null)
|
|
errorMessages.Add("Color was adjusted to be inside the allowed range.");
|
|
forbiddenColor = askedColor;
|
|
return false;
|
|
}
|
|
|
|
// Validate color against the current map tileset
|
|
if (!IsValid(askedColor, terrainColors, out forbiddenColor))
|
|
{
|
|
if (errorMessages != null)
|
|
errorMessages.Add("Color was adjusted to be less similar to the terrain.");
|
|
return false;
|
|
}
|
|
|
|
// Validate color against other clients
|
|
if (!IsValid(askedColor, playerColors, out forbiddenColor))
|
|
{
|
|
if (errorMessages != null)
|
|
errorMessages.Add("Color was adjusted to be less similar to another player.");
|
|
return false;
|
|
}
|
|
|
|
// Color is valid
|
|
forbiddenColor = default(Color);
|
|
|
|
return true;
|
|
}
|
|
|
|
public Color RandomPresetColor(MersenneTwister random, IEnumerable<Color> terrainColors, IEnumerable<Color> playerColors)
|
|
{
|
|
if (TeamColorPresets.Any())
|
|
{
|
|
Color forbidden;
|
|
foreach (var c in TeamColorPresets.Shuffle(random))
|
|
if (IsValid(c, out forbidden, terrainColors, playerColors))
|
|
return c;
|
|
}
|
|
|
|
return RandomValidColor(random, terrainColors, playerColors);
|
|
}
|
|
|
|
public Color RandomValidColor(MersenneTwister random, IEnumerable<Color> terrainColors, IEnumerable<Color> playerColors)
|
|
{
|
|
Color color;
|
|
Color forbidden;
|
|
do
|
|
{
|
|
var h = random.Next(255) / 255f;
|
|
var s = float2.Lerp(HsvSaturationRange[0], HsvSaturationRange[1], random.NextFloat());
|
|
var v = float2.Lerp(HsvValueRange[0], HsvValueRange[1], random.NextFloat());
|
|
color = Color.FromAhsv(h, s, v);
|
|
}
|
|
while (!IsValid(color, out forbidden, terrainColors, playerColors));
|
|
|
|
return color;
|
|
}
|
|
|
|
public Color MakeValid(Color askedColor, MersenneTwister random, IEnumerable<Color> terrainColors, IEnumerable<Color> playerColors, Action<string> onError)
|
|
{
|
|
var errorMessages = new HashSet<string>();
|
|
|
|
Color forbiddenColor;
|
|
if (IsValid(askedColor, out forbiddenColor, terrainColors, playerColors, errorMessages))
|
|
return 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
|
|
|
|
// Create a tiny vector to make the while loop maths work
|
|
vector[0] = 1;
|
|
vector[1] = 1;
|
|
vector[2] = 1;
|
|
}
|
|
|
|
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;
|
|
Color color;
|
|
do
|
|
{
|
|
// If we reached the limit (The ii >= 255 prevents too much calculations)
|
|
if (attempt >= 255)
|
|
{
|
|
color = RandomPresetColor(random, terrainColors, playerColors);
|
|
errorMessages.Add("Color could not be adjusted enough, a new color has been picked.");
|
|
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 = Color.FromArgb(r, g, b);
|
|
|
|
attempt++;
|
|
}
|
|
while (!IsValid(color, out forbiddenColor, terrainColors, playerColors, errorMessages));
|
|
|
|
// Coalesce the error messages to only print one of each type despite up to 255 iterations
|
|
errorMessages.Do(onError);
|
|
|
|
return color;
|
|
}
|
|
}
|
|
}
|