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