From 6e5984ba1570f00be0338b6576f2b5dd0e18bfc5 Mon Sep 17 00:00:00 2001 From: amy Date: Sat, 16 May 2026 16:36:29 +0100 Subject: [PATCH 01/12] getpixel --- .../DMProject/Tests/Icon/GetPixel.dm | 10 ++++ Content.Tests/DMProject/Tests/Icon/hanoi.dmi | Bin 0 -> 1006 bytes DMCompiler/DMStandard/Types/Icon.dm | 1 - .../Procs/Native/DreamProcNative.cs | 1 + .../Procs/Native/DreamProcNativeIcon.cs | 51 ++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 Content.Tests/DMProject/Tests/Icon/GetPixel.dm create mode 100644 Content.Tests/DMProject/Tests/Icon/hanoi.dmi diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixel.dm b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm new file mode 100644 index 0000000000..f9134d2f21 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm @@ -0,0 +1,10 @@ +/proc/RunTest() + var/icon/I = icon('hanoi.dmi') + // ASSERT(I.GetPixel(5,5) == "#ff0000") + var/list/target_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") + for(var/x in 1 to 32) + world.log << I.GetPixel(x,16,"target") + ASSERT(I.GetPixel(x,16,"target") == target_pixels[x]) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Icon/hanoi.dmi b/Content.Tests/DMProject/Tests/Icon/hanoi.dmi new file mode 100644 index 0000000000000000000000000000000000000000..3215d27a7e7965b7ab12c091344b0614b1513f3e GIT binary patch literal 1006 zcmV1IpXn6{O(lz`x5&s6#xG<;%eyr0Qvv)OZ)#c;%n~yEdKH@{_!sU@dN+<^iBI} z?*H=fQ~dBB_Ua+^=^^szFf9G?^zlvm?E!XiB1}pqOHnil_48j*FJe+KVNx(pOeaiI zG|YWwb#WoRc3#MQWW{=7z;|HAietu%W5|7EL;MrTfn;CpCPMrYNBb7Ph+atg7Ri8Q zZQ?Fh@E}_5BEW}V%YS9dfMs@ZA+>T^OiLw8OC|j60f~YhYi1&9YcUV@^Xl?y^YBgb z?@jRVRR1>OhSvUD<^Egd{`Exr*rP$^lt<+><>3kZ;0yfY1pFry|7-3V5^v)aQ|}Z~ z@f1?>0aEe+RP2u48q*M6n>&Oj00001bW%=J06^y0W&i*HjCxd9bVOxyV{&P5bZKvH z004NLQ&w}aHJ3*o{;uC_5h~D z5P$##AOHafKma^IHthh7u@As7hBF&2R+-WP05-crDrbfj0FarLO)e)l&j|srB%1KpJLIJ9(YibGA)q?>V8k-0;!vR`c?$*TG+BvQR5YXAx-P0?suOArT z85kTA=N*Oz`20eF5!0v&03S~cU@RCC8V^tbgn52qGS-w|FTnJ7Qh=G+xoGnX^Z>Et zMFb##8enO8CEDtmZU90F=fL_#tPmBz=2loJ=u-tySNty6-ucNy0kFHbFYe$F7;xk{ zJ`s27h6T7t&v!t;+4;raF0Y^f*EhF!_k srcDreamIcon.Icon.Width || y < 1 || y > srcDreamIcon.Icon.Height) + return DreamValue.Null; + + string iconState = bundle.GetArgument(2, "icon_state").MustGetValueAsString(); + if(!srcDreamIcon.Icon.States.TryGetValue(iconState, out var iconStateObject)){ + if(iconState == string.Empty) + iconStateObject = srcDreamIcon.Icon.States.First().Value; + else //invalid icon state causes BYOND to create error.log but it's empty + 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(0, bundle.GetArgument(4, "frame").MustGetValueAsInteger()); + if (iconStateObject.Frames < frame) + return DreamValue.Null; + + DreamValue moving = bundle.GetArgument(5, "moving"); //TODO what does this do? + + var stateDirFrame = iconStateObject.Directions[dir][frame]; + var pixel = stateDirFrame.Image![stateDirFrame.DMIFrame.X+x-1,stateDirFrame.DMIFrame.Y+y-1]; + if(pixel.A == 255) + return new DreamValue(new Color(pixel.ToVector4()).ToHexNoAlpha().ToLower()); + else + return new DreamValue(pixel.ToHex().ToLower()); + } + [DreamProc("Scale")] [DreamProcParameter("width", Type = DreamValueTypeFlag.Float)] [DreamProcParameter("height", Type = DreamValueTypeFlag.Float)] From 30ae0c96618bf99b5901c06a1d96d3fd3f1cc01f Mon Sep 17 00:00:00 2001 From: amy Date: Sat, 16 May 2026 17:12:31 +0100 Subject: [PATCH 02/12] more comprehensive test --- .../DMProject/Tests/Icon/GetPixel.dm | 31 ++++++++++++++++--- .../Procs/Native/DreamProcNativeIcon.cs | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixel.dm b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm index f9134d2f21..19146ee031 100644 --- a/Content.Tests/DMProject/Tests/Icon/GetPixel.dm +++ b/Content.Tests/DMProject/Tests/Icon/GetPixel.dm @@ -1,10 +1,33 @@ /proc/RunTest() var/icon/I = icon('hanoi.dmi') - // ASSERT(I.GetPixel(5,5) == "#ff0000") - var/list/target_pixels = list("#e8083f", "#ff0000", "#6aee1d", "#f1b004", "#ffa308", "#ff34e2", "#6aeeff",\ + 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) - world.log << I.GetPixel(x,16,"target") - ASSERT(I.GetPixel(x,16,"target") == target_pixels[x]) \ No newline at end of file + 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/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index f6aadc4e83..711f15d6bf 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -120,7 +120,7 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje DreamValue moving = bundle.GetArgument(5, "moving"); //TODO what does this do? var stateDirFrame = iconStateObject.Directions[dir][frame]; - var pixel = stateDirFrame.Image![stateDirFrame.DMIFrame.X+x-1,stateDirFrame.DMIFrame.Y+y-1]; + var pixel = stateDirFrame.Image![stateDirFrame.DMIFrame.X+x-1,stateDirFrame.DMIFrame.Y+(srcDreamIcon.Icon.Height-y)]; if(pixel.A == 255) return new DreamValue(new Color(pixel.ToVector4()).ToHexNoAlpha().ToLower()); else From 950e7a6d5fc9045190489660df12d351926783db Mon Sep 17 00:00:00 2001 From: amy Date: Sat, 16 May 2026 17:33:12 +0100 Subject: [PATCH 03/12] test that crashes byond lmao --- Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm | 7 +++++++ OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm 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/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index 711f15d6bf..97a8a8aa08 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -113,11 +113,11 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje else if(!iconStateObject.Directions.ContainsKey(dir)) return DreamValue.Null; - int frame = Math.Max(0, bundle.GetArgument(4, "frame").MustGetValueAsInteger()); + int frame = Math.Max(1, bundle.GetArgument(4, "frame").MustGetValueAsInteger()); if (iconStateObject.Frames < frame) return DreamValue.Null; - DreamValue moving = bundle.GetArgument(5, "moving"); //TODO what does this do? + DreamValue moving = bundle.GetArgument(5, "moving"); // TODO: implement movement states var stateDirFrame = iconStateObject.Directions[dir][frame]; var pixel = stateDirFrame.Image![stateDirFrame.DMIFrame.X+x-1,stateDirFrame.DMIFrame.Y+(srcDreamIcon.Icon.Height-y)]; From 8731acbd53c3baf6e68f5e95a93084300fd37eb5 Mon Sep 17 00:00:00 2001 From: amy Date: Sun, 17 May 2026 15:19:06 +0100 Subject: [PATCH 04/12] fixes --- .../Procs/Native/DreamProcNativeIcon.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index 97a8a8aa08..849a402a30 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -1,11 +1,8 @@ -using System.ComponentModel; using System.Linq; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Processing; using BlendType = OpenDreamRuntime.Objects.DreamIconOperationBlend.BlendType; using DreamValueTypeFlag = OpenDreamRuntime.DreamValue.DreamValueTypeFlag; @@ -100,11 +97,11 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje return DreamValue.Null; string iconState = bundle.GetArgument(2, "icon_state").MustGetValueAsString(); - if(!srcDreamIcon.Icon.States.TryGetValue(iconState, out var iconStateObject)){ + if(!srcDreamIcon.Icon.GenerateDMI().DMI.States.TryGetValue(iconState, out var iconStateObject)){ if(iconState == string.Empty) - iconStateObject = srcDreamIcon.Icon.States.First().Value; + iconStateObject = srcDreamIcon.Icon.GenerateDMI().DMI.States.First().Value; else //invalid icon state causes BYOND to create error.log but it's empty - throw new ArgumentException($"Invalid icon_state {iconState} passed to /icon.GetPixel()"); + return DreamValue.Null; //throw new ArgumentException($"Invalid icon_state {iconState} passed to /icon.GetPixel()"); } AtomDirection dir = (AtomDirection)bundle.GetArgument(3, "dir").MustGetValueAsInteger(); @@ -113,14 +110,16 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje else if(!iconStateObject.Directions.ContainsKey(dir)) return DreamValue.Null; - int frame = Math.Max(1, bundle.GetArgument(4, "frame").MustGetValueAsInteger()); - if (iconStateObject.Frames < frame) + 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 = stateDirFrame.Image![stateDirFrame.DMIFrame.X+x-1,stateDirFrame.DMIFrame.Y+(srcDreamIcon.Icon.Height-y)]; + + var pixel = srcDreamIcon.Icon.GenerateDMI().Texture[stateDirFrame.X+x-1,stateDirFrame.Y+(srcDreamIcon.Icon.Height-y)]; if(pixel.A == 255) return new DreamValue(new Color(pixel.ToVector4()).ToHexNoAlpha().ToLower()); else From 2f4e4071f8f3e2ffbc92b1997add5e461c896b4f Mon Sep 17 00:00:00 2001 From: amy Date: Sun, 17 May 2026 15:22:00 +0100 Subject: [PATCH 05/12] steal the return from #2524 --- OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index 849a402a30..aa01508780 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -120,10 +120,11 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje var stateDirFrame = iconStateObject.Directions[dir][frame]; var pixel = srcDreamIcon.Icon.GenerateDMI().Texture[stateDirFrame.X+x-1,stateDirFrame.Y+(srcDreamIcon.Icon.Height-y)]; - if(pixel.A == 255) - return new DreamValue(new Color(pixel.ToVector4()).ToHexNoAlpha().ToLower()); - else - return new DreamValue(pixel.ToHex().ToLower()); + 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")] From 2feffe145797a988ed6aab306b2f32861aac5a8c Mon Sep 17 00:00:00 2001 From: Amy <3855802+amylizzle@users.noreply.github.com> Date: Mon, 25 May 2026 21:48:43 +0100 Subject: [PATCH 06/12] add // NOBYOND to test --- Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm index 396dca8d1f..6c701dfe07 100644 --- a/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm +++ b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm @@ -1,3 +1,5 @@ +// NOBYOND +// this unit test crashed byond until https://www.byond.com/forum/post/2987385 and https://www.byond.com/forum/post/2987386 are fixed /proc/RunTest() var/icon/I = icon('hanoi.dmi') ASSERT(I.GetPixel(1,1,"invalid iconstate") == null) //iconstate that doesn't exist From b783e342fb2ed78c805e5dfc16b06eca62908fae Mon Sep 17 00:00:00 2001 From: amy Date: Mon, 25 May 2026 22:53:49 +0100 Subject: [PATCH 07/12] comments --- .../DMProject/Tests/Icon/GetPixelBadArgs.dm | 2 ++ .../Procs/Native/DreamProcNative.cs | 2 +- .../Procs/Native/DreamProcNativeIcon.cs | 25 +++++++++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm index 6c701dfe07..b04cd3e9f4 100644 --- a/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm +++ b/Content.Tests/DMProject/Tests/Icon/GetPixelBadArgs.dm @@ -2,6 +2,8 @@ // this unit test crashed byond until https://www.byond.com/forum/post/2987385 and https://www.byond.com/forum/post/2987386 are fixed /proc/RunTest() var/icon/I = icon('hanoi.dmi') + ASSERT(I.GetPixel(1,1,"0",dir=new /datum()) == "#ff0000") //what if dir was weird + ASSERT(I.GetPixel(1,1,"0",dir="south") == "#ff0000") //what if dir was dumb 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 diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index 95a6bc3621..8ca5bcde31 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -175,9 +175,9 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Height); objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Insert); objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Blend); + objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_GetPixel); 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 aa01508780..530b6dacd9 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -81,7 +81,7 @@ public static DreamValue NativeProc_Blend(NativeProc.Bundle bundle, DreamObject? [DreamProc("GetPixel")] [DreamProcParameter("x", Type = DreamValueTypeFlag.Float)] [DreamProcParameter("y", Type = DreamValueTypeFlag.Float)] - [DreamProcParameter("icon_state", Type = DreamValueTypeFlag.String, DefaultValue = "")] + [DreamProcParameter("icon_state", Type = DreamValueTypeFlag.String, DefaultValue = null)] [DreamProcParameter("dir", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] [DreamProcParameter("frame", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] [DreamProcParameter("moving", Type = DreamValueTypeFlag.Float, DefaultValue = -1)] @@ -89,22 +89,31 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje var srcDreamIcon = (DreamObjectIcon)src!; //arg validation - int x = bundle.GetArgument(0, "x").MustGetValueAsInteger(); - int y = bundle.GetArgument(1, "y").MustGetValueAsInteger(); + int x = 0; + int y = 0; + if(bundle.GetArgument(0, "x").TryGetValueAsFloatCoerceNull(out float floatx)) + x = (int) floatx; + if(bundle.GetArgument(1, "y").TryGetValueAsFloatCoerceNull(out float floaty)) + y = (int) floaty; //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(!bundle.GetArgument(2, "icon_state").TryGetValueAsString(out string? iconState)) + iconState = ""; + + var generatedDMI = srcDreamIcon.Icon.GenerateDMI(); + if(!generatedDMI.DMI.States.TryGetValue(iconState, out var iconStateObject)){ if(iconState == string.Empty) - iconStateObject = srcDreamIcon.Icon.GenerateDMI().DMI.States.First().Value; + iconStateObject = generatedDMI.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(!bundle.GetArgument(3, "dir").TryGetValueAsFloatCoerceNull(out float dirFloat)) + dirFloat = 0; + AtomDirection dir = (AtomDirection) dirFloat; if(dir == AtomDirection.None) dir = iconStateObject.Directions.Keys.First(); else if(!iconStateObject.Directions.ContainsKey(dir)) @@ -119,7 +128,7 @@ public static DreamValue NativeProc_GetPixel(NativeProc.Bundle bundle, DreamObje var stateDirFrame = iconStateObject.Directions[dir][frame]; - var pixel = srcDreamIcon.Icon.GenerateDMI().Texture[stateDirFrame.X+x-1,stateDirFrame.Y+(srcDreamIcon.Icon.Height-y)]; + var pixel = generatedDMI.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}"), From 21705db497f7a2f0705addc497228b66b57992b3 Mon Sep 17 00:00:00 2001 From: amy Date: Mon, 25 May 2026 22:54:28 +0100 Subject: [PATCH 08/12] othercomment --- OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index 530b6dacd9..450bb620a6 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -77,7 +77,6 @@ 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)] From 5bf8965926f99e3829d9bea1cee951b9259f14b5 Mon Sep 17 00:00:00 2001 From: amy Date: Tue, 26 May 2026 00:00:05 +0100 Subject: [PATCH 09/12] test --- .../DMProject/Tests/Icon/IconBlends.dm | 70 ++++++++++++++++++ Content.Tests/DMProject/Tests/Icon/hanoi.dmi | Bin 1006 -> 1904 bytes 2 files changed, 70 insertions(+) create mode 100644 Content.Tests/DMProject/Tests/Icon/IconBlends.dm diff --git a/Content.Tests/DMProject/Tests/Icon/IconBlends.dm b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm new file mode 100644 index 0000000000..a838d132df --- /dev/null +++ b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm @@ -0,0 +1,70 @@ +/proc/RunTest() + // ICON_ADD + var/icon/A = icon('hanoi.dmi',"reddot") + var/icon/B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_ADD) + var/list/target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "ADD [json_encode(target_pixels)]" + + // ICON_SUBTRACT + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_SUBTRACT) + target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#741900","#741900","#741900","#741900",null,null,null,null,"#ff1900","#ff1900","#ff1900","#ff1900",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#741900","#741900","#741900","#741900",null,null,null,null,"#ff1900","#ff1900","#ff1900","#ff1900",null,null,null,null,null,null,null,null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "SUB [json_encode(target_pixels)]" + + // ICON_MULTIPLY + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_MULTIPLY) + target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#8b0035","#8b0035","#8b0035","#8b0035",null,null,null,null,"#000035","#000035","#000035","#000035",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#8b0035","#8b0035","#8b0035","#8b0035",null,null,null,null,"#000035","#000035","#000035","#000035",null,null,null,null,null,null,null,null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "MULT [json_encode(target_pixels)]" + + // ICON_OVERLAY + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_OVERLAY) + target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "OVERLAY [json_encode(target_pixels)]" + + // ICON_AND + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_AND) + target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "AND [json_encode(target_pixels)]" + + // ICON_OR + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_OR) + target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "OR [json_encode(target_pixels)]" + + // ICON_UNDERLAY + A = icon('hanoi.dmi',"reddot") + B = icon('hanoi.dmi',"bluedot") + A.Blend(B, ICON_UNDERLAY) + target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + for(var/x in 1 to 32) + ASSERT(A.GetPixel(x,x) == target_pixels[x]) + // target_pixels[x] = A.GetPixel(x,x) + // world.log << "UNDERLAY [json_encode(target_pixels)]" diff --git a/Content.Tests/DMProject/Tests/Icon/hanoi.dmi b/Content.Tests/DMProject/Tests/Icon/hanoi.dmi index 3215d27a7e7965b7ab12c091344b0614b1513f3e..c120fc61cb92b56cbdcb949696226f6b3aeba642 100644 GIT binary patch literal 1904 zcmbuAX*Amj7smf0!Y~M>+Rz9Yj4%l#1dxMoM+S$70YFqi${<_xosLZVmSHr-zQvw% zh+t)%UiO_>)OswYl$E=|q zMp0oEnj+5YsWnYjuPQxS@s zb48t((7Kg{Yutcks5p4&fYQZ}83`^%j0oEswho-dq4|M|4Q1sshdHTQ>#G0)49A#h3j=lb5FICIoM4w_5^Gvm3KA(Gtg0xkH9LGN{GJS42{eTNZ{Q_1 zAfIv~-va_h)V*H}BSCm;0XGA9{Xh@78G?#{L-GU#{(?4P=Sv_o1#sRR64G)uF4*mL z82{df^ii1$r(A!T^!2#uiM@2_w>BTp$SL07ig5oOW#d#A<&ctM#o(l}0HGRE){a;of{Q2=wDUO2#AyIq1@C#k;z^=9?N;g@3HoF^wLYUc_UB1w4x_ zck08>M&5onHD#2@so)t`yQRV)4e!#Z8VR8}P??vpqa%KF{Rmno#IG^6w(U>pwk|vT#+xPz! zXHh+Gzsoq7kYAde@)--&o-AuhG_`!0d)nWiZW9HK`b`EFh`rlNKsHQd6fRUrT_tnG zM|Q$C$=>R%=}l$>dx;4fo1K}@5;X&cQeI{kZf zMlb>de{@&OWV6dw2YFe@TLRu~KiIV#Z9~7y{h*QTF$ulAPGrDWBZ0v6eD3z#?!`8_rK{QC@1}hp(+MpM{uyP%|ydhVP z<&+*Xros)sHCx>IhX(`@ynqdfr$y^-0Zn!-Va(kr)+^pEyMZ1RZ6eu0Ot)Ji zR1Hd3>M98x{ylT+02s`>xz8YlEg8(k>2XzFF!pTm13K_~y!$7?ui1aJRQ%^0W8KM9 zHEq7}@_q6_!VkB-#5P5t^sTQWFQ1quv!L471m#IBh%?YV$luaZzgGl#TRUZCY_y;w0CHhgQxX?UdOv05Y+91g ze~C>{Rhm+n%#+re&S8(HnV8o_oo-!Tu6Q6h$WIL~tAdVe29>4ztfT3DDQLwduoa6`$xDy9erD~4K#Rao4tD&L~g7@wZ9g_6>bWl9X#M-=Y;6u zGtKbbML2Beaj!`2Q!$R%xw|0UT$O5g9&){B=6-Rh1YGu{EpVqtX$5o8EtxkbJ({!B-jXEZ}W#om=Iy^2-l~R*-#8_ zm5Qr*Q^wqy;Gv=+W^%ax%@LYOertXFPe6B)uvOke2h=~0`H^6$QJYjQi)DNALIFE# LM{JFyZ_XrFkw?#6d}PIX zV!(G`#)@OcjDKUuePl!Y6Ul*OU+pGB{1Qj|7QcvINctAZfMjjrE?4j%TJ9pihhNKo zWy^qNc5xxKa#~DFB}+>s{OtjWf*xyTB5G?f5BBrw@@n(&P4e$e@bFasHsXfX{#)h# zTju`tMElsILFJT3V=-0C=2JR&a84_w-Y6 z@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+(=$pSoZ^zil2jm5$v}yVGbOXA z7|1r{;(tslO3Y1#u;D5U392w6sKS__3KN1VObMzmBdEfhpb86uDl7@AC`l|zPc6Zv zOj*Iz&jlP;0RC5U%LC*CO8@`?gh@m}RA_#66#$T#l}#=uH_r(Hu;mw!D=aE@fB;HL%gRYr zRDVJNs;X;h3Dwnu0U8>c2sOh2T3qhd#M;_9t^*Lz+11_CE3U5}7~mNg91`aph6nik zLV*#}s0sidPYqx!7!n!}PyvK_equ7#lwU8v^mkH#nc2B$^9%FY8o29u)DV}?%)s@aO62Y5qIi_1-MAh zcR;||`NiKZub=?eH@A29gdQGY0FkHXmsetM?{)~lM?^ZC1pq+#`~TA{pVIy>H_9&U T_b6km00000NkvXXu0mjf24l%k From ba7d5612e2f9a5f80999817233b73923cf034f7c Mon Sep 17 00:00:00 2001 From: amy Date: Tue, 26 May 2026 00:53:04 +0100 Subject: [PATCH 10/12] slightly better reporting --- .../DMProject/Tests/Icon/IconBlends.dm | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Icon/IconBlends.dm b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm index a838d132df..4a6209e340 100644 --- a/Content.Tests/DMProject/Tests/Icon/IconBlends.dm +++ b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm @@ -4,30 +4,39 @@ var/icon/B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_ADD) var/list/target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null) + var/list/actual_pixels = list() + var/matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "ADD [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_ADD did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_SUBTRACT A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_SUBTRACT) target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#741900","#741900","#741900","#741900",null,null,null,null,"#ff1900","#ff1900","#ff1900","#ff1900",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#741900","#741900","#741900","#741900",null,null,null,null,"#ff1900","#ff1900","#ff1900","#ff1900",null,null,null,null,null,null,null,null,null,null) + actual_pixels = list() + matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "SUB [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_SUBTRACT did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_MULTIPLY A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_MULTIPLY) target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#8b0035","#8b0035","#8b0035","#8b0035",null,null,null,null,"#000035","#000035","#000035","#000035",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#8b0035","#8b0035","#8b0035","#8b0035",null,null,null,null,"#000035","#000035","#000035","#000035",null,null,null,null,null,null,null,null,null,null) + actual_pixels = list() + matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "MULT [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_MULTIPLY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_OVERLAY A = icon('hanoi.dmi',"reddot") @@ -35,36 +44,46 @@ A.Blend(B, ICON_OVERLAY) target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "OVERLAY [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_OVERLAY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_AND A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_AND) target_pixels = list(null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,"#ff19ff","#ff19ff","#ff19ff","#ff19ff",null,null,null,null,null,null,null,null,null,null) + actual_pixels = list() + matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "AND [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_AND did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_OR A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_OR) target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff19ff","#ff19ff","#ff19ff","#ff19ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + actual_pixels = list() + matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "OR [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_OR did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") // ICON_UNDERLAY A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_UNDERLAY) target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + actual_pixels = list() + matching = TRUE for(var/x in 1 to 32) - ASSERT(A.GetPixel(x,x) == target_pixels[x]) - // target_pixels[x] = A.GetPixel(x,x) - // world.log << "UNDERLAY [json_encode(target_pixels)]" + matching &= (A.GetPixel(x,x) == target_pixels[x]) + actual_pixels += A.GetPixel(x,x) + if(!matching) + CRASH("ICON_UNDERLAY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") From 67a69a3edeea4253b77d5625abbdb86e84e3eb5e Mon Sep 17 00:00:00 2001 From: amy Date: Wed, 27 May 2026 00:12:35 +0100 Subject: [PATCH 11/12] mmm parity --- .../DMProject/Tests/Icon/IconBlends.dm | 3 +- .../DMProject/Tests/Icon/IconBlendsColor.dm | 83 +++++++++++++++++++ OpenDreamRuntime/Objects/DreamIcon.cs | 32 ++++--- 3 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Icon/IconBlendsColor.dm diff --git a/Content.Tests/DMProject/Tests/Icon/IconBlends.dm b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm index 4a6209e340..8d802650c5 100644 --- a/Content.Tests/DMProject/Tests/Icon/IconBlends.dm +++ b/Content.Tests/DMProject/Tests/Icon/IconBlends.dm @@ -42,7 +42,8 @@ A = icon('hanoi.dmi',"reddot") B = icon('hanoi.dmi',"bluedot") A.Blend(B, ICON_OVERLAY) - target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null,null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + target_pixels = list(null,null,null,"#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#8b00ff","#ff1935","#ff1935","#ff1935","#ff1935","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff","#0000ff",null,null,null) + actual_pixels = list() for(var/x in 1 to 32) matching &= (A.GetPixel(x,x) == target_pixels[x]) actual_pixels += A.GetPixel(x,x) diff --git a/Content.Tests/DMProject/Tests/Icon/IconBlendsColor.dm b/Content.Tests/DMProject/Tests/Icon/IconBlendsColor.dm new file mode 100644 index 0000000000..87b98f5e7e --- /dev/null +++ b/Content.Tests/DMProject/Tests/Icon/IconBlendsColor.dm @@ -0,0 +1,83 @@ +/proc/RunTest() + // ICON_ADD + var/icon/B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200, 10, 0, 170), ICON_ADD) + var/list/target_pixels = list("#c80affaa","#c80affaa","#c80affa9","#c80aff9f","#c80aff95","#c80aff8c","#c80aff83","#c80aff7b","#c80aff73","#c80aff6c","#c80aff65","#c80aff5f","#c80aff58","#c80aff52","#c80aff4d","#c80aff47","#c80aff42","#c80aff3d","#c80aff39","#c80aff34","#c80aff30","#c80aff2c","#c80aff28","#c80aff25","#c80aff21","#c80aff1f","#c80aff1b","#c80aff19","#c80aff16","#c80aff14","#c80aff11","#c80aff0f") + var/list/actual_pixels = list() + var/matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_ADD did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_SUBTRACT + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_SUBTRACT) + target_pixels = list("#0000ffaa","#0000ffaa","#0000ffa9","#0000ff9f","#0000ff95","#0000ff8c","#0000ff83","#0000ff7b","#0000ff73","#0000ff6c","#0000ff65","#0000ff5f","#0000ff58","#0000ff52","#0000ff4d","#0000ff47","#0000ff42","#0000ff3d","#0000ff39","#0000ff34","#0000ff30","#0000ff2c","#0000ff28","#0000ff25","#0000ff21","#0000ff1f","#0000ff1b","#0000ff19","#0000ff16","#0000ff14","#0000ff11","#0000ff0f") + actual_pixels = list() + matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_SUBTRACT did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_MULTIPLY + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_MULTIPLY) + target_pixels = list("#000000aa","#000000aa","#000000a9","#0000009f","#00000095","#0000008c","#00000083","#0000007b","#00000073","#0000006c","#00000065","#0000005f","#00000058","#00000052","#0000004d","#00000047","#00000042","#0000003d","#00000039","#00000034","#00000030","#0000002c","#00000028","#00000025","#00000021","#0000001f","#0000001b","#00000019","#00000016","#00000014","#00000011","#0000000f") + actual_pixels = list() + matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_MULTIPLY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_OVERLAY + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_OVERLAY) + target_pixels = list("#850755","#850755","#850755","#850755f9","#850755f5","#850755f0","#850755ec","#850755e8","#850755e4","#850755e0","#850755dc","#850755d9","#850755d6","#850755d3","#850755d0","#850755cd","#850755cb","#850755c9","#850755c6","#850755c4","#850755c2","#850755c0","#850755be","#850755bc","#850755bb","#850755b9","#850755b8","#850755b6","#850755b5","#850755b4","#850755b3","#850755b2") + actual_pixels = list() + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_OVERLAY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_AND + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_AND) + target_pixels = list("#c80affaa","#c80affaa","#c80affa9","#c80aff9f","#c80aff95","#c80aff8c","#c80aff83","#c80aff7b","#c80aff73","#c80aff6c","#c80aff65","#c80aff5f","#c80aff58","#c80aff52","#c80aff4d","#c80aff47","#c80aff42","#c80aff3d","#c80aff39","#c80aff34","#c80aff30","#c80aff2c","#c80aff28","#c80aff25","#c80aff21","#c80aff1f","#c80aff1b","#c80aff19","#c80aff16","#c80aff14","#c80aff11","#c80aff0f") + actual_pixels = list() + matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_AND did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_OR + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_OR) + target_pixels = list("#c80aff","#c80aff","#c80aff","#c80afff9","#c80afff5","#c80afff0","#c80affec","#c80affe8","#c80affe4","#c80affe0","#c80affdc","#c80affd9","#c80affd6","#c80affd3","#c80affd0","#c80affcd","#c80affcb","#c80affc9","#c80affc6","#c80affc4","#c80affc2","#c80affc0","#c80affbe","#c80affbc","#c80affbb","#c80affb9","#c80affb8","#c80affb6","#c80affb5","#c80affb4","#c80affb3","#c80affb2") + actual_pixels = list() + matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_OR did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") + + // ICON_UNDERLAY + B = icon('hanoi.dmi',"gradient") + B.Blend(rgb(200,10,0,170), ICON_UNDERLAY) + target_pixels = list("#0000ff","#0000ff","#0100fe","#0d01eef9","#1801e0f5","#2302d2f0","#2d02c5ec","#3703b9e8","#4003ade4","#4904a2e0","#520497dc","#59048ed9","#600584d6","#68057bd3","#6e0573d0","#75066acd","#7a0663cb","#80065cc9","#850755c6","#8b074ec4","#900748c2","#940742c0","#99083cbe","#9d0837bc","#a10832bb","#a4082eb9","#a80829b8","#ab0925b6","#ae0921b5","#b0091eb4","#b4091ab3","#b60917b2") + actual_pixels = list() + matching = TRUE + for(var/x in 1 to 32) + matching &= (B.GetPixel(x,x) == target_pixels[x]) + actual_pixels += B.GetPixel(x,x) + if(!matching) + CRASH("ICON_UNDERLAY did not match, expected [json_encode(target_pixels)] but got [json_encode(actual_pixels)]") diff --git a/OpenDreamRuntime/Objects/DreamIcon.cs b/OpenDreamRuntime/Objects/DreamIcon.cs index 81b5676fe7..e864457488 100644 --- a/OpenDreamRuntime/Objects/DreamIcon.cs +++ b/OpenDreamRuntime/Objects/DreamIcon.cs @@ -327,8 +327,7 @@ protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) { pixels[dstPixelPosition].G = (byte)Math.Min(dst.G + src.G, byte.MaxValue); pixels[dstPixelPosition].B = (byte)Math.Min(dst.B + src.B, byte.MaxValue); - // BYOND uses the smaller of the two alphas - pixels[dstPixelPosition].A = Math.Min(dst.A, src.A); + pixels[dstPixelPosition].A = (byte)Math.Round((dst.A * src.A)/255.0); break; } case BlendType.Subtract: { @@ -336,8 +335,7 @@ protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) { pixels[dstPixelPosition].G = (byte)Math.Max(dst.G - src.G, byte.MinValue); pixels[dstPixelPosition].B = (byte)Math.Max(dst.B - src.B, byte.MinValue); - // BYOND uses the smaller of the two alphas - pixels[dstPixelPosition].A = Math.Min(dst.A, src.A); + pixels[dstPixelPosition].A = (byte)Math.Round((dst.A * src.A)/255.0); break; } @@ -360,30 +358,28 @@ protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) { break; } - pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255); - pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255); - pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255); + pixels[dstPixelPosition].R = (byte) Math.Round(dst.R + (src.R - dst.R) * src.A / 255.0); + pixels[dstPixelPosition].G = (byte) Math.Round(dst.G + (src.G - dst.G) * src.A / 255.0); + pixels[dstPixelPosition].B = (byte) Math.Round(dst.B + (src.B - dst.B) * src.A / 255.0); - byte highAlpha = Math.Max(dst.A, src.A); - byte lowAlpha = Math.Min(dst.A, src.A); - pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255)); + pixels[dstPixelPosition].A = (byte) Math.Round(dst.A + src.A - (dst.A * src.A)/255.0); break; } - + case BlendType.Underlay: { + // Opposite of overlay + (dst, src) = (src, dst); + goto case BlendType.Overlay; + } case BlendType.Or: { pixels[dstPixelPosition].R = (byte)(dst.R | src.R); pixels[dstPixelPosition].G = (byte)(dst.G | src.G); pixels[dstPixelPosition].B = (byte)(dst.B | src.B); - pixels[dstPixelPosition].A = (byte)(dst.A | src.A); + pixels[dstPixelPosition].A = (byte) Math.Round(dst.A + src.A - (dst.A * src.A)/255.0); break; } - - case BlendType.Underlay: { - // Opposite of overlay - (dst, src) = (src, dst); - goto case BlendType.Overlay; - } + default: + throw new NotImplementedException("Blend type not implemented"); } } } From ece0db587e4f51496f5b43ff14a98dc9958b6fa9 Mon Sep 17 00:00:00 2001 From: amy Date: Wed, 27 May 2026 00:12:45 +0100 Subject: [PATCH 12/12] oh this too --- Content.Tests/DMProject/Tests/Icon/hanoi.dmi | Bin 1904 -> 1986 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Content.Tests/DMProject/Tests/Icon/hanoi.dmi b/Content.Tests/DMProject/Tests/Icon/hanoi.dmi index c120fc61cb92b56cbdcb949696226f6b3aeba642..a61ebe9b851fe1ceb665ee13f0c42c6578004e22 100644 GIT binary patch literal 1986 zcmbu=c{JOJ76OPPhiRDCyrf)i798*o8mj6B1^}^Vu(VD30??3pOd{ESJLF6nA&S9=VAZ4=OYF zbH*GKVVoLt?j$dB4A-D1JIJaY+j(93PY`cB+ zXN6)7Qq`~2If?&u?;Fgw_UPdw9CI{PZZ6p@4c04V;ySvIinQW`K0!>SgMm2Ie>7-- zxoxzdgl0ed>4-l(Qfs$(H*)d)EJ%nPlrIPsGH`DmrHZD za>hFL-4dNbP;9Dd{MmCozQwyJ%+ye-3KIY&fvw{gbB%LJ@x%n%SSbyMIBPbgo^@|2 zhhbGxf5~!bUSViFGfB@-l#O&X$1DLD!rAy^83rHQaHS0kuX67AC zeg3(SwMzHC)$u${i3W_|-g=J^ANV!XvpW2*gW;s%hPxKEzxR5ExlyE)K_Ht@584@# z-w1!9alL;{nD)U?W$uyuG?Afw?xb#&62CMauN3P(&WfNktZLhCiOY*qTp3Mde7TNPC%JM6+HVmdYDP+u=OKcN&lJt($8)!>)=L4> zl*SC-j8!u^$|KMRoM+FZ8??63TafaYTFqStUPe$sV{7RoIXg~o4u07^wP+S%{ z*#2ozz@Fw1lQE>~&wGb3Iac$~HA3Hh%_pbLSIbpto(X#Nv9dPL6op3KloeJ%LYPU8 zXN7pPvM_6zPIZ#oP?6S10%kJwW3war{To1{VYNe8XdX)sX^+mObGQ|+uY~Y68=gL4 zN{sVPx|??5qjbTm3WR>{i_I<}w*^}k{W7tY|T`31Wvic1KN~+m? zG?&dm#*G$7=4)Swi(08(?#Mi^85chshDi@O(#a(ex8Bfs_mEYsy@;qB+_RF z+!Oc%^?E7cJE{w^M$%@|5+;AA^8#2|_~}UE#`dJkh>w@QPS)WUDya+bC@b6P&pXGh zZyEI%9v+@P+gS3WQ;Tb6EV^KuUfEKPQq*z{=sof(-DLVfap(v05= zy$Z|8VCu`3{QKr4jL}V*i38HQ0ZJ9yr0eZBVIK06HO*!TU8eHy7-HvFYPxt@z{6yy&j$bm!u?qiBCeHIGaz}W0PW#9x3?_A>)n))x8xs=@i literal 1904 zcmbuAX*Amj7smf0!Y~M>+Rz9Yj4%l#1dxMoM+S$70YFqi${<_xosLZVmSHr-zQvw% zh+t)%UiO_>)OswYl$E=|q zMp0oEnj+5YsWnYjuPQxS@s zb48t((7Kg{Yutcks5p4&fYQZ}83`^%j0oEswho-dq4|M|4Q1sshdHTQ>#G0)49A#h3j=lb5FICIoM4w_5^Gvm3KA(Gtg0xkH9LGN{GJS42{eTNZ{Q_1 zAfIv~-va_h)V*H}BSCm;0XGA9{Xh@78G?#{L-GU#{(?4P=Sv_o1#sRR64G)uF4*mL z82{df^ii1$r(A!T^!2#uiM@2_w>BTp$SL07ig5oOW#d#A<&ctM#o(l}0HGRE){a;of{Q2=wDUO2#AyIq1@C#k;z^=9?N;g@3HoF^wLYUc_UB1w4x_ zck08>M&5onHD#2@so)t`yQRV)4e!#Z8VR8}P??vpqa%KF{Rmno#IG^6w(U>pwk|vT#+xPz! zXHh+Gzsoq7kYAde@)--&o-AuhG_`!0d)nWiZW9HK`b`EFh`rlNKsHQd6fRUrT_tnG zM|Q$C$=>R%=}l$>dx;4fo1K}@5;X&cQeI{kZf zMlb>de{@&OWV6dw2YFe@TLRu~KiIV#Z9~7y{h*QTF$ulAPGrDWBZ0v6eD3z#?!`8_rK{QC@1}hp(+MpM{uyP%|ydhVP z<&+*Xros)sHCx>IhX(`@ynqdfr$y^-0Zn!-Va(kr)+^pEyMZ1RZ6eu0Ot)Ji zR1Hd3>M98x{ylT+02s`>xz8YlEg8(k>2XzFF!pTm13K_~y!$7?ui1aJRQ%^0W8KM9 zHEq7}@_q6_!VkB-#5P5t^sTQWFQ1quv!L471m#IBh%?YV$luaZzgGl#TRUZCY_y;w0CHhgQxX?UdOv05Y+91g ze~C>{Rhm+n%#+re&S8(HnV8o_oo-!Tu6Q6h$WIL~tAdVe29>4ztfT3DDQLwduoa6`$xDy9erD~4K#Rao4tD&L~g7@wZ9g_6>bWl9X#M-=Y;6u zGtKbbML2Beaj!`2Q!$R%xw|0UT$O5g9&){B=6-Rh1YGu{EpVqtX$5o8EtxkbJ({!B-jXEZ}W#om=Iy^2-l~R*-#8_ zm5Qr*Q^wqy;Gv=+W^%ax%@LYOertXFPe6B)uvOke2h=~0`H^6$QJYjQi)DNALIFE# LM{JFyZ_