diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixel.dm b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm new file mode 100644 index 0000000000..19146ee031 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm @@ -0,0 +1,33 @@ +/proc/RunTest() + var/icon/I = icon('hanoi.dmi') + ASSERT(I.GetPixel(5,5) == "#ff0000") + var/list/targetx_pixels = list("#e8083f", "#ff0000", "#6aee1d", "#f1b004", "#ffa308", "#ff34e2", "#6aeeff",\ + "#f4f14d", "#fbed01", "#757121", "#c07760", "#cc7d66", "#c98164", "#5fed26", "#43fc13", "#43fc13", "#43fc13",\ + "#43fc13", "#43fc13", "#48fa16", "#bf885e", "#625230", "#4d4b26", "#4c5234", "#09f5f3", "#00ffff", "#00ffff","#f44dfb",\ + "#2c14ff", "#001bf9", "#00ef58", "#f74d04") + var/list/targety_pixels = list("#e8083f","#ff0000","#6bee1a","#1452f2","#e00bfc","#5be5fe","#00ffff","#f2ef4d",\ + "#6b6622","#4b4b25","#c07760","#cb8065","#57f020","#43fc13","#43fc13","#43fc13","#43fc13","#43fc13",\ + "#42fc12","#c68c63","#c87c64","#625230","#4c4a26","#f5e921","#2ffef1","#00f9ff","#ee54fc","#ffa04b",\ + "#f8a225","#3530e4","#00ef58","#f74d04") + for(var/x in 1 to 32) + ASSERT(I.GetPixel(x,16,"target") == targetx_pixels[x]) + for(var/y in 1 to 32) + ASSERT(I.GetPixel(16,y,"target") == targety_pixels[y]) + + //chose by fair dice roll, guaranteed to be random + var/list/random_pixels = list(\ + list(9, 2, "#ff0000"),\ + list(31, 8, "#00ef58"),\ + list(32, 32, "#fe1100"),\ + list(1, 1, "#d3105d"),\ + list(2, 13, "#ff0000"),\ + list(0, 0, null),\ + list(31, 31, "#00fe28"),\ + list(-1, 17, null),\ + ) + for(var/list/tuple in random_pixels) + ASSERT(I.GetPixel(tuple[1], tuple[2], "target") == tuple[3]) + +//byond bugs: +//- invalid iconstate causes error.log to be created but it's empty and no runtime triggers +//- invalid dir value (ie, 100) causes a hard crash \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm new file mode 100644 index 0000000000..396dca8d1f --- /dev/null +++ b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm @@ -0,0 +1,7 @@ +/proc/RunTest() + var/icon/I = icon('hanoi.dmi') + ASSERT(I.GetPixel(1,1,"invalid iconstate") == null) //iconstate that doesn't exist + ASSERT(I.GetPixel(1,1,"0",dir=EAST) == null) //not a dir in this file + ASSERT(I.GetPixel(1,1,"0",dir=100) == null) //nonsense dir + ASSERT(I.GetPixel(1,1,"0",frame=-100) == "#ff0000") //frame is clipped at 1 + ASSERT(I.GetPixel(1,1,"0",frame=100) == null) //there's only 1 frame in the file diff --git a/Content.Tests/DMProject/Tests/Icon/hanoi.dmi b/Content.Tests/DMProject/Tests/Icon/hanoi.dmi new file mode 100644 index 0000000000..3215d27a7e Binary files /dev/null and b/Content.Tests/DMProject/Tests/Icon/hanoi.dmi differ diff --git a/DMCompiler/DMStandard/Types/Icon.dm b/DMCompiler/DMStandard/Types/Icon.dm index bd211b7de7..7b0f87bc4a 100644 --- a/DMCompiler/DMStandard/Types/Icon.dm +++ b/DMCompiler/DMStandard/Types/Icon.dm @@ -16,7 +16,6 @@ set opendream_unimplemented = TRUE proc/GetPixel(x, y, icon_state, dir = 0, frame = 0, moving = -1) - set opendream_unimplemented = TRUE proc/Height() diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index 2cd97d84e2..95a6bc3621 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -177,6 +177,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Blend); objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Scale); objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Turn); + objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_GetPixel); objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_ExportText); objectTree.SetNativeProc(objectTree.Savefile, DreamProcNativeSavefile.NativeProc_Flush); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index 2817d08bb9..aa01508780 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -1,3 +1,4 @@ +using System.Linq; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Resources; @@ -76,6 +77,56 @@ public static DreamValue NativeProc_Blend(NativeProc.Bundle bundle, DreamObject? return DreamValue.Null; } +//x, y, icon_state, dir = 0, frame = 0, moving = -1 + [DreamProc("GetPixel")] + [DreamProcParameter("x", Type = DreamValueTypeFlag.Float)] + [DreamProcParameter("y", Type = DreamValueTypeFlag.Float)] + [DreamProcParameter("icon_state", Type = DreamValueTypeFlag.String, DefaultValue = "")] + [DreamProcParameter("dir", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("frame", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] + [DreamProcParameter("moving", Type = DreamValueTypeFlag.Float, DefaultValue = -1)] + public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { + var srcDreamIcon = (DreamObjectIcon)src!; + + //arg validation + int x = bundle.GetArgument(0, "x").MustGetValueAsInteger(); + int y = bundle.GetArgument(1, "y").MustGetValueAsInteger(); + + //outside valid bounds returns null + if(x < 1 || x > srcDreamIcon.Icon.Width || y < 1 || y > srcDreamIcon.Icon.Height) + return DreamValue.Null; + + string iconState = bundle.GetArgument(2, "icon_state").MustGetValueAsString(); + if(!srcDreamIcon.Icon.GenerateDMI().DMI.States.TryGetValue(iconState, out var iconStateObject)){ + if(iconState == string.Empty) + iconStateObject = srcDreamIcon.Icon.GenerateDMI().DMI.States.First().Value; + else //invalid icon state causes BYOND to create error.log but it's empty + return DreamValue.Null; //throw new ArgumentException($"Invalid icon_state {iconState} passed to /icon.GetPixel()"); + } + + AtomDirection dir = (AtomDirection)bundle.GetArgument(3, "dir").MustGetValueAsInteger(); + if(dir == AtomDirection.None) + dir = iconStateObject.Directions.Keys.First(); + else if(!iconStateObject.Directions.ContainsKey(dir)) + return DreamValue.Null; + + int frame = Math.Max(1, bundle.GetArgument(4, "frame").MustGetValueAsInteger())-1; + + if (iconStateObject.FrameCount < frame) + return DreamValue.Null; + + DreamValue moving = bundle.GetArgument(5, "moving"); // TODO: implement movement states + + var stateDirFrame = iconStateObject.Directions[dir][frame]; + + var pixel = srcDreamIcon.Icon.GenerateDMI().Texture[stateDirFrame.X+x-1,stateDirFrame.Y+(srcDreamIcon.Icon.Height-y)]; + return pixel.A switch { + 0 => DreamValue.Null, + 255 => new DreamValue($"#{pixel.R:x2}{pixel.G:x2}{pixel.B:x2}"), + _ => new DreamValue($"#{pixel.R:x2}{pixel.G:x2}{pixel.B:x2}{pixel.A:x2}") + }; + } + [DreamProc("Scale")] [DreamProcParameter("width", Type = DreamValueTypeFlag.Float)] [DreamProcParameter("height", Type = DreamValueTypeFlag.Float)]