diff --git a/Content.Tests/DMProject/Broken Tests/Const/ConstProc.dm b/Content.Tests/DMProject/Tests/Const/const_rgb_proc.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Const/ConstProc.dm rename to Content.Tests/DMProject/Tests/Const/const_rgb_proc.dm diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 4b7ec230b6..5b073d4430 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -188,6 +188,53 @@ public override void EmitPushValue(ExpressionContext ctx) { ctx.Proc.Rgb(argInfo.Type, argInfo.StackSize); } + + // TODO: This needs to have full parity with the rgb opcode. This is a simplified implementation for the most common case rgb(R, G, B) + public override bool TryAsConstant(DMCompiler compiler, [NotNullWhen(true)] out Constant? constant) { + (string?, float?)[] values = new (string?, float?)[arguments.Length]; + + bool validArgs = true; + + if (arguments.Length < 3 || arguments.Length > 5) { + compiler.Emit(WarningCode.BadExpression, Location, $"rgb: expected 3 to 5 arguments (found {arguments.Length})"); + constant = null; + return false; + } + + for (var index = 0; index < arguments.Expressions.Length; index++) { + var (name, expr) = arguments.Expressions[index]; + if (!expr.TryAsConstant(compiler, out var constExpr)) { + constant = null; + return false; + } + + if (constExpr is not Number num) { + validArgs = false; + values[index] = (name, null); + continue; + } + + values[index] = (name, num.Value); + } + + if (!validArgs) { + compiler.Emit(WarningCode.FallbackBuiltinArgument, Location, + "Non-numerical rgb argument(s) will always return \"00\""); + } + + string result; + try { + result = SharedOperations.ParseRgb(values); + } catch (Exception e) { + compiler.Emit(WarningCode.BadExpression, Location, e.Message); + constant = null; + return false; + } + + constant = new String(Location, result); + + return true; + } } // pick(prob(50);x, prob(200);y) diff --git a/DMCompiler/DMCompiler.csproj b/DMCompiler/DMCompiler.csproj index 77d24af309..a32f4cae6a 100644 --- a/DMCompiler/DMCompiler.csproj +++ b/DMCompiler/DMCompiler.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/DMCompiler/Optimizer/CompactorOptimizations.cs b/DMCompiler/Optimizer/CompactorOptimizations.cs index 9a6525e2fa..a143dc1366 100644 --- a/DMCompiler/Optimizer/CompactorOptimizations.cs +++ b/DMCompiler/Optimizer/CompactorOptimizations.cs @@ -375,4 +375,45 @@ public void Apply(DMCompiler compiler, List input, int index } } +// PushNFloats [count] [float] ... [float] +// Rgb [argType] [count] +// -> PushString [result] +// Only works when [argType] is FromStack and the [count] of both opcodes matches +internal sealed class EvalRgb : IOptimization { + public OptPass OptimizationPass => OptPass.ListCompactor; + + public ReadOnlySpan GetOpcodes() { + return [ + DreamProcOpcode.PushNFloats, + DreamProcOpcode.Rgb + ]; + } + + public bool CheckPreconditions(List input, int index) { + var floatCount = ((AnnotatedBytecodeInstruction)input[index]).GetArg(0).Value; + var rgbInst = (AnnotatedBytecodeInstruction)input[index + 1]; + var argType = rgbInst.GetArg(0).Value; + var stackDelta = rgbInst.GetArg(1).Delta; + + return argType == DMCallArgumentsType.FromStack && floatCount == stackDelta; + } + + public void Apply(DMCompiler compiler, List input, int index) { + var floats = (AnnotatedBytecodeInstruction)(input[index]); + var floatArgs = floats.GetArgs(); + (string?, float?)[] values = new (string?, float?)[floatArgs.Count - 1]; + for (int i = 1; i < floatArgs.Count; i++) { // skip the first value since it's the [count] of floats + values[i - 1] = (null, ((AnnotatedBytecodeFloat)floatArgs[i]).Value); + } + + var resultStr = SharedOperations.ParseRgb(values); + var resultId = compiler.DMObjectTree.AddString(resultStr); + + List args = [new AnnotatedBytecodeString(resultId, floats.Location)]; + + input.RemoveRange(index, 2); + input.Insert(index, new AnnotatedBytecodeInstruction(DreamProcOpcode.PushString, 1, args)); + } +} + #endregion diff --git a/DMCompiler/SharedOperations.cs b/DMCompiler/SharedOperations.cs index 3e5f012020..6c327dc749 100644 --- a/DMCompiler/SharedOperations.cs +++ b/DMCompiler/SharedOperations.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Robust.Shared.Maths; namespace DMCompiler; @@ -61,4 +62,108 @@ public static float Log(float y, float baseValue) { public static float Abs(float a) { return MathF.Abs(a); } + + public enum ColorSpace { + RGB = 0, + HSV = 1, + HSL = 2 + } + + public static string ParseRgb((string? Name, float? Value)[] arguments) { + string result; + float? color1 = null; + float? color2 = null; + float? color3 = null; + float? a = null; + ColorSpace space = ColorSpace.RGB; + + if (arguments[0].Name is null) { + if (arguments.Length is < 3 or > 5) + throw new Exception("Expected 3 to 5 arguments for rgb()"); + + color1 = arguments[0].Value; + color2 = arguments[1].Value; + color3 = arguments[2].Value; + a = (arguments.Length >= 4) ? arguments[3].Value : null; + if (arguments.Length == 5) + space = arguments[4].Value is null ? ColorSpace.RGB : (ColorSpace)(int)arguments[4].Value!; + } else { + foreach (var arg in arguments) { + var name = arg.Name ?? string.Empty; + + if (name.StartsWith("r", StringComparison.InvariantCultureIgnoreCase) && color1 is null) { + color1 = arg.Value; + space = ColorSpace.RGB; + } else if (name.StartsWith("g", StringComparison.InvariantCultureIgnoreCase) && color2 is null) { + color2 = arg.Value; + space = ColorSpace.RGB; + } else if (name.StartsWith("b", StringComparison.InvariantCultureIgnoreCase) && color3 is null) { + color3 = arg.Value; + space = ColorSpace.RGB; + } else if (name.StartsWith("h", StringComparison.InvariantCultureIgnoreCase) && color1 is null) { + color1 = arg.Value; + space = ColorSpace.HSV; + } else if (name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 is null) { + color2 = arg.Value; + space = ColorSpace.HSV; + } else if (name.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) && color3 is null) { + color3 = arg.Value; + space = ColorSpace.HSV; + } else if (name.StartsWith("l", StringComparison.InvariantCultureIgnoreCase) && color3 is null) { + color3 = arg.Value; + space = ColorSpace.HSL; + } else if (name.StartsWith("a", StringComparison.InvariantCultureIgnoreCase) && a is null) + a = arg.Value; + else if (name == "space" && space == default) + space = (ColorSpace)(int)arg.Value!; + else + throw new Exception($"Invalid or double arg \"{name}\""); + } + } + + color1 ??= 0; + color2 ??= 0; + color3 ??= 0; + byte aValue = a is null ? (byte)255 : (byte)Math.Clamp((int)a, 0, 255); + Color color; + + switch (space) { + case ColorSpace.RGB: { + byte r = (byte)Math.Clamp(color1.Value, 0, 255); + byte g = (byte)Math.Clamp(color2.Value, 0, 255); + byte b = (byte)Math.Clamp(color3.Value, 0, 255); + + color = new Color(r, g, b, aValue); + break; + } + case ColorSpace.HSV: { + // TODO: Going beyond the max defined in the docs returns a different value. Don't know why. + float h = Math.Clamp(color1.Value, 0, 360) / 360f; + float s = Math.Clamp(color2.Value, 0, 100) / 100f; + float v = Math.Clamp(color3.Value, 0, 100) / 100f; + + color = Color.FromHsv((h, s, v, aValue / 255f)); + break; + } + case ColorSpace.HSL: { + float h = Math.Clamp(color1.Value, 0, 360) / 360f; + float s = Math.Clamp(color2.Value, 0, 100) / 100f; + float l = Math.Clamp(color3.Value, 0, 100) / 100f; + + color = Color.FromHsl((h, s, l, aValue / 255f)); + break; + } + default: + throw new Exception($"Unimplemented color space {space}"); + } + + // TODO: There is a difference between passing null and not passing a fourth arg at all + if (a is null) { + result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}".ToLower(); + } else { + result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}{color.AByte:X2}".ToLower(); + } + + return result; + } } diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 29f372e2e1..3eca92a72c 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1947,31 +1947,34 @@ public static ProcStatus Rgb(DMProcState state) { var argumentValues = state.PopCount(argumentInfo.StackSize); var arguments = state.CollectProcArguments(argumentValues, argumentInfo.Type, argumentInfo.StackSize); - DreamValue color1 = default; - DreamValue color2 = default; - DreamValue color3 = default; - DreamValue a = DreamValue.Null; - ColorHelpers.ColorSpace space = ColorHelpers.ColorSpace.RGB; - - if (arguments.Item1 != null) { + string result = "#000000"; + if (arguments.Item1 is not null) { if (arguments.Item1.Length is < 3 or > 5) throw new Exception("Expected 3 to 5 arguments for rgb()"); + (string?, float?)[] values = new (string?, float?)[arguments.Item1.Length]; + for (int i = 0; i < arguments.Item1.Length; i++) { + var val = arguments.Item1[i].UnsafeGetValueAsFloat(); + values[i] = (null, val); + } - color1 = arguments.Item1[0]; - color2 = arguments.Item1[1]; - color3 = arguments.Item1[2]; - a = (arguments.Item1.Length >= 4) ? arguments.Item1[3] : DreamValue.Null; - if (arguments.Item1.Length == 5) - space = (ColorHelpers.ColorSpace)(int)arguments.Item1[4].UnsafeGetValueAsFloat(); + result = SharedOperations.ParseRgb(values); } else if (arguments.Item2 != null) { + if (arguments.Item2.Count is < 3 or > 5) + throw new Exception("Expected 3 to 5 arguments for rgb()"); + (string?, float?)[] values = new (string?, float?)[5]; + DreamValue color1 = default; + DreamValue color2 = default; + DreamValue color3 = default; + DreamValue a = DreamValue.Null; + SharedOperations.ColorSpace space = SharedOperations.ColorSpace.RGB; foreach (var arg in arguments.Item2) { if (arg.Key.TryGetValueAsInteger(out var position)) { switch (position) { - case 1: color1 = arg.Value; break; - case 2: color2 = arg.Value; break; - case 3: color3 = arg.Value; break; - case 4: a = arg.Value; break; - case 5: space = (ColorHelpers.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); break; + case 1: color1 = arg.Value; continue; + case 2: color2 = arg.Value; continue; + case 3: color3 = arg.Value; continue; + case 4: a = arg.Value; continue; + case 5: space = (SharedOperations.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); continue; default: throw new Exception($"Invalid argument key {position}"); } } else { @@ -1979,88 +1982,49 @@ public static ProcStatus Rgb(DMProcState state) { if (name.StartsWith("r", StringComparison.InvariantCultureIgnoreCase) && color1 == default) { color1 = arg.Value; - space = ColorHelpers.ColorSpace.RGB; + space = SharedOperations.ColorSpace.RGB; } else if (name.StartsWith("g", StringComparison.InvariantCultureIgnoreCase) && color2 == default) { color2 = arg.Value; - space = ColorHelpers.ColorSpace.RGB; + space = SharedOperations.ColorSpace.RGB; } else if (name.StartsWith("b", StringComparison.InvariantCultureIgnoreCase) && color3 == default) { color3 = arg.Value; - space = ColorHelpers.ColorSpace.RGB; + space = SharedOperations.ColorSpace.RGB; } else if (name.StartsWith("h", StringComparison.InvariantCultureIgnoreCase) && color1 == default) { color1 = arg.Value; - space = ColorHelpers.ColorSpace.HSV; - } else if (name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 == default) { + space = SharedOperations.ColorSpace.HSV; + } else if (name != "space" && name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 == default) { color2 = arg.Value; - space = ColorHelpers.ColorSpace.HSV; + space = SharedOperations.ColorSpace.HSV; } else if (name.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) && color3 == default) { color3 = arg.Value; - space = ColorHelpers.ColorSpace.HSV; + space = SharedOperations.ColorSpace.HSV; } else if (name.StartsWith("l", StringComparison.InvariantCultureIgnoreCase) && color3 == default) { color3 = arg.Value; - space = ColorHelpers.ColorSpace.HSL; + space = SharedOperations.ColorSpace.HSL; } else if (name.StartsWith("a", StringComparison.InvariantCultureIgnoreCase) && a == default) a = arg.Value; else if (name == "space" && space == default) - space = (ColorHelpers.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); + space = (SharedOperations.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); else throw new Exception($"Invalid or double arg \"{name}\""); } - } - if (color1 == default) - throw new Exception("Missing first component"); - if (color2 == default) - throw new Exception("Missing second color component"); - if (color3 == default) - throw new Exception("Missing third color component"); - } else { - state.Push(DreamValue.Null); - return ProcStatus.Continue; - } - - float color1Value = color1.UnsafeGetValueAsFloat(); - float color2Value = color2.UnsafeGetValueAsFloat(); - float color3Value = color3.UnsafeGetValueAsFloat(); - byte aValue = a.IsNull ? (byte)255 : (byte)Math.Clamp((int)a.UnsafeGetValueAsFloat(), 0, 255); - Color color; + values[0] = (null, color1.UnsafeGetValueAsFloat()); + values[1] = (null, color2.UnsafeGetValueAsFloat()); + values[2] = (null, color3.UnsafeGetValueAsFloat()); + if(a.TryGetValueAsFloat(out var aVal)) + values[3] = (null, aVal); + else + values[3] = (null, null); + values[4] = (null, (float)space); - switch (space) { - case ColorHelpers.ColorSpace.RGB: { - byte r = (byte)Math.Clamp(color1Value, 0, 255); - byte g = (byte)Math.Clamp(color2Value, 0, 255); - byte b = (byte)Math.Clamp(color3Value, 0, 255); - - color = new Color(r, g, b, aValue); - break; + result = SharedOperations.ParseRgb(values); } - case ColorHelpers.ColorSpace.HSV: { - // TODO: Going beyond the max defined in the docs returns a different value. Don't know why. - float h = Math.Clamp(color1Value, 0, 360) / 360f; - float s = Math.Clamp(color2Value, 0, 100) / 100f; - float v = Math.Clamp(color3Value, 0, 100) / 100f; - - color = Color.FromHsv((h, s, v, aValue / 255f)); - break; - } - case ColorHelpers.ColorSpace.HSL: { - float h = Math.Clamp(color1Value, 0, 360) / 360f; - float s = Math.Clamp(color2Value, 0, 100) / 100f; - float l = Math.Clamp(color3Value, 0, 100) / 100f; - - color = Color.FromHsl((h, s, l, aValue / 255f)); - break; - } - default: - throw new Exception($"Unimplemented color space {space}"); - } - - // TODO: There is a difference between passing null and not passing a fourth arg at all - if (a.IsNull) { - state.Push(new DreamValue($"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}".ToLower())); } else { - state.Push(new DreamValue($"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}{color.AByte:X2}".ToLower())); + result = "#000000"; } + state.Push(new DreamValue(result)); return ProcStatus.Continue; } diff --git a/OpenDreamShared/Dream/ColorHelpers.cs b/OpenDreamShared/Dream/ColorHelpers.cs index 98f5e413eb..c210a2cdef 100644 --- a/OpenDreamShared/Dream/ColorHelpers.cs +++ b/OpenDreamShared/Dream/ColorHelpers.cs @@ -27,12 +27,6 @@ public static class ColorHelpers { {"cyan", new Color(0, 255, 255)} }; - public enum ColorSpace { - RGB = 0, - HSV = 1, - HSL = 2 - } - public static bool TryParseColor(string color, out Color colorOut, string defaultAlpha = "ff") { if (color.StartsWith("#")) { if (color.Length == 4 || color.Length == 5) { //4-bit color; repeat each digit