diff --git a/OpenDreamRuntime/Objects/DreamIcon.cs b/OpenDreamRuntime/Objects/DreamIcon.cs index d44741dee2..662117da63 100644 --- a/OpenDreamRuntime/Objects/DreamIcon.cs +++ b/OpenDreamRuntime/Objects/DreamIcon.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using Color = Robust.Shared.Maths.Color; using ParsedDMIDescription = OpenDreamShared.Resources.DMIParser.ParsedDMIDescription; using ParsedDMIState = OpenDreamShared.Resources.DMIParser.ParsedDMIState; using ParsedDMIFrame = OpenDreamShared.Resources.DMIParser.ParsedDMIFrame; @@ -280,7 +281,8 @@ public interface IDreamIconOperation { public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds); } -public sealed class DreamIconOperationBlend : IDreamIconOperation { +[Virtual] +public class DreamIconOperationBlend : IDreamIconOperation { // With the same values as the ICON_* defines in DMStandard public enum BlendType { Add = 0, @@ -294,26 +296,76 @@ public enum BlendType { private readonly BlendType _type; private readonly int _xOffset, _yOffset; - private readonly Image _blending; - private readonly ParsedDMIDescription _blendingDescription; - public DreamIconOperationBlend(BlendType type, DreamValue blending, int xOffset, int yOffset) { + protected DreamIconOperationBlend(BlendType type, int xOffset, int yOffset) { _type = type; _xOffset = xOffset; _yOffset = yOffset; + if (_type is not BlendType.Overlay and not BlendType.Underlay and not BlendType.Add and not BlendType.Subtract) + throw new NotImplementedException($"\"{_type}\" blending is not implemented"); + } + + public virtual void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) { + throw new NotImplementedException(); + } + + protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) { + Rgba32 dst = pixels[dstPixelPosition]; + + switch (_type) { + case BlendType.Add: { + pixels[dstPixelPosition].R = (byte)Math.Min(dst.R + src.R, byte.MaxValue); + 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); + break; + } + case BlendType.Subtract: { + pixels[dstPixelPosition].R = (byte)Math.Max(dst.R - src.R, byte.MinValue); + 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); + break; + } + + case BlendType.Overlay: { + 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); + + byte highAlpha = Math.Max(dst.A, src.A); + byte lowAlpha = Math.Min(dst.A, src.A); + pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255)); + break; + } + case BlendType.Underlay: { + // Opposite of overlay + (dst, src) = (src, dst); + goto case BlendType.Overlay; + } + } + } +} + +public sealed class DreamIconOperationBlendImage : DreamIconOperationBlend { + private readonly Image _blending; + private readonly ParsedDMIDescription _blendingDescription; + + public DreamIconOperationBlendImage(BlendType type, int xOffset, int yOffset, DreamValue blending) : base(type, xOffset, yOffset) { //TODO: Find a way to get rid of this! var objectTree = IoCManager.Resolve(); var resourceManager = IoCManager.Resolve(); (var blendingResource, _blendingDescription) = DreamMetaObjectIcon.GetIconResourceAndDescription(objectTree, resourceManager, blending); _blending = resourceManager.LoadImage(blendingResource); - - if (_type is not BlendType.Overlay and not BlendType.Underlay) - throw new NotImplementedException($"\"{_type}\" blending is not implemented"); } - public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) { + public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) { _blending.ProcessPixelRows(accessor => { // The first frame of the source image blends with the first frame of the destination image // The second frame blends with the second, and so on @@ -332,23 +384,7 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun Rgba32 dst = pixels[dstPixelPosition]; Rgba32 src = row[srcFramePos.Value.X + x - bounds.Left]; - switch (_type) { - case BlendType.Overlay: { - 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); - - byte highAlpha = Math.Max(dst.A, src.A); - byte lowAlpha = Math.Min(dst.A, src.A); - pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255)); - break; - } - case BlendType.Underlay: { - // Opposite of overlay - (dst, src) = (src, dst); - goto case BlendType.Overlay; - } - } + BlendPixel(pixels, dstPixelPosition, src); } } }); @@ -371,3 +407,23 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun return (column * _blendingDescription.Width, row * _blendingDescription.Height); } } + +public sealed class DreamIconOperationBlendColor : DreamIconOperationBlend { + private readonly Rgba32 _color; + + public DreamIconOperationBlendColor(BlendType type, int xOffset, int yOffset, Color color) : base(type, xOffset, yOffset) { + _color = new Rgba32(color.RByte, color.GByte, color.BByte, color.AByte); + } + + public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) { + // TODO: x & y offsets + + for (int y = bounds.Top; y < bounds.Bottom; y++) { + for (int x = bounds.Left; x < bounds.Right; x++) { + int dstPixelPosition = (y * imageSpan) + x; + + BlendPixel(pixels, dstPixelPosition, _color); + } + } + } +} diff --git a/OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs b/OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs index 570a82f466..1221a3b691 100644 --- a/OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs +++ b/OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs @@ -30,8 +30,7 @@ public void OnObjectCreated(DreamObject dreamObject, DreamProcArguments creation DreamValue frame = creationArguments.GetArgument(3, "frame"); DreamValue moving = creationArguments.GetArgument(4, "moving"); - DreamIcon dreamIcon = new(_rscMan); - ObjectToDreamIcon.Add(dreamObject, dreamIcon); + var dreamIcon = InitializeIcon(_rscMan, dreamObject); if (icon != DreamValue.Null) { // TODO: Could maybe have an alternative path for /icon values so the DMI doesn't have to be generated @@ -61,6 +60,18 @@ public void OnVariableSet(DreamObject dreamObject, string varName, DreamValue va } } + /// + /// A fast path for initializing an /icon object + /// + /// Doesn't call any DM code + /// The /icon's DreamIcon + public static DreamIcon InitializeIcon(DreamResourceManager rscMan, DreamObject icon) { + DreamIcon dreamIcon = new(rscMan); + + ObjectToDreamIcon.Add(icon, dreamIcon); + return dreamIcon; + } + public static (DreamResource Resource, ParsedDMIDescription Description) GetIconResourceAndDescription( IDreamObjectTree objectTree, DreamResourceManager resourceManager, DreamValue value) { if (value.TryGetValueAsDreamObjectOfType(objectTree.Icon, out var iconObj)) { diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index a61fa95dd6..088eee116a 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.MetaObjects; +using OpenDreamRuntime.Procs.Native; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; using OpenDreamShared.Dream.Procs; @@ -617,11 +618,21 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO return null; } else { - throw new Exception("Invalid append operation on " + first + " and " + second); + throw new Exception($"Invalid append operation on {first} and {second}"); } } else { result = second; } + } else if (first.TryGetValueAsDreamResource(out _) || first.TryGetValueAsDreamObjectOfType(state.Proc.ObjectTree.Icon, out _)) { + // Implicitly create a new /icon and ICON_ADD blend it + // Note that BYOND creates something other than an /icon, but it behaves the same as one in most reasonable interactions + DreamObject iconObj = state.Proc.ObjectTree.CreateObject(DreamPath.Icon); + var icon = DreamMetaObjectIcon.InitializeIcon(state.Proc.DreamResourceManager, iconObj); + var from = DreamMetaObjectIcon.GetIconResourceAndDescription(state.Proc.ObjectTree, state.Proc.DreamResourceManager, first); + + icon.InsertStates(from.Resource, from.Description, DreamValue.Null, DreamValue.Null, DreamValue.Null); + DreamProcNativeIcon.Blend(icon, second, DreamIconOperationBlend.BlendType.Add, 0, 0); + result = new DreamValue(iconObj); } else if (second.Value != null) { switch (first.Type) { case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float: @@ -630,11 +641,6 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO case DreamValue.DreamValueType.String when second.Type == DreamValue.DreamValueType.String: result = new DreamValue(first.GetValueAsString() + second.GetValueAsString()); break; - case DreamValue.DreamValueType.DreamResource when (second.Type == DreamValue.DreamValueType.String && first.TryGetValueAsDreamResource(out var rsc) && rsc.ResourcePath.EndsWith("dmi")): - // TODO icon += hexcolor is the same as Blend() - state.DreamManager.WriteWorldLog("Appending colors to DMIs is not implemented", LogLevel.Warning, "opendream.unimplemented"); - result = first; - break; default: throw new Exception("Invalid append operation on " + first + " and " + second); } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs index d6dbe8d430..044794603b 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs @@ -1,6 +1,8 @@ using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.MetaObjects; using OpenDreamRuntime.Resources; +using OpenDreamShared.Dream; +using BlendType = OpenDreamRuntime.Objects.DreamIconOperationBlend.BlendType; namespace OpenDreamRuntime.Procs.Native { static class DreamProcNativeIcon { @@ -46,6 +48,17 @@ public static DreamValue NativeProc_Insert(DreamObject instance, DreamObject usr return DreamValue.Null; } + public static void Blend(DreamIcon icon, DreamValue blend, BlendType function, int x, int y) { + if (blend.TryGetValueAsString(out var colorStr)) { + if (!ColorHelpers.TryParseColor(colorStr, out var color)) + throw new Exception($"Invalid color {colorStr}"); + + icon.ApplyOperation(new DreamIconOperationBlendColor(function, x, y, color)); + } else { + icon.ApplyOperation(new DreamIconOperationBlendImage(function, x, y, blend)); + } + } + [DreamProc("Blend")] [DreamProcParameter("icon", Type = DreamValue.DreamValueType.DreamObject)] [DreamProcParameter("function", Type = DreamValue.DreamValueType.Float)] @@ -63,10 +76,7 @@ public static DreamValue NativeProc_Blend(DreamObject instance, DreamObject usr, if (!function.TryGetValueAsInteger(out var functionValue)) throw new Exception($"Invalid 'function' argument {function}"); - var blendType = (DreamIconOperationBlend.BlendType) functionValue; - - DreamIcon iconObj = DreamMetaObjectIcon.ObjectToDreamIcon[instance]; - iconObj.ApplyOperation(new DreamIconOperationBlend(blendType, icon, x, y)); + Blend(DreamMetaObjectIcon.ObjectToDreamIcon[instance], icon, (BlendType)functionValue, x, y); return DreamValue.Null; }