From 01edef6c5ebc0c2c4235ee80127e7c18f410cf0a Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:04:54 -0400 Subject: [PATCH 01/10] basic vis_flags implementation --- .../DebugWindows/IconDebugWindow.xaml.cs | 1 + OpenDreamClient/Rendering/DreamViewOverlay.cs | 1 + OpenDreamClient/Rendering/RendererMetaData.cs | 2 ++ OpenDreamRuntime/AtomManager.cs | 13 +++++++++++-- OpenDreamShared/Dream/ImmutableAppearance.cs | 13 +++++++++++++ OpenDreamShared/Dream/MutableAppearance.cs | 18 ++++++++++++++++++ 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index c323caac2a..6c3a17d1b7 100644 --- a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs +++ b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs @@ -58,6 +58,7 @@ private void Update() { AddPropertyIfNotDefault("Plane", appearance.Plane, MutableAppearance.Default.Plane); AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, MutableAppearance.Default.BlendMode); AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, MutableAppearance.Default.AppearanceFlags); + AddPropertyIfNotDefault("Vis Flags", appearance.VisFlags, MutableAppearance.Default.VisFlags); AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, MutableAppearance.Default.Invisibility); AddPropertyIfNotDefault("Opacity", appearance.Opacity, MutableAppearance.Default.Opacity); AddPropertyIfNotDefault("Override", appearance.Override, MutableAppearance.Default.Override); diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 92d59a2a88..ee5e8a8d7a 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -211,6 +211,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u current.TieBreaker = tieBreaker; current.RenderSource = icon.Appearance.RenderSource; current.RenderTarget = icon.Appearance.RenderTarget; + current.VisFlags = icon.Appearance.VisFlags; current.AppearanceFlags = icon.Appearance.AppearanceFlags; current.BlendMode = icon.Appearance.BlendMode; current.Flick = flick; diff --git a/OpenDreamClient/Rendering/RendererMetaData.cs b/OpenDreamClient/Rendering/RendererMetaData.cs index 8d50ea57bc..ce09b272dd 100644 --- a/OpenDreamClient/Rendering/RendererMetaData.cs +++ b/OpenDreamClient/Rendering/RendererMetaData.cs @@ -20,6 +20,7 @@ internal sealed class RendererMetaData : IComparable { public string? RenderSource; public string? RenderTarget; public List? KeepTogetherGroup; + public VisFlags VisFlags; public AppearanceFlags AppearanceFlags; public BlendMode BlendMode; public MouseOpacity MouseOpacity; @@ -53,6 +54,7 @@ public void Reset() { RenderSource = ""; RenderTarget = ""; KeepTogetherGroup = null; //don't actually need to allocate this 90% of the time + VisFlags = VisFlags.None; AppearanceFlags = AppearanceFlags.None; BlendMode = BlendMode.Default; MouseOpacity = MouseOpacity.Transparent; diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index fcf5ede69d..d48066c019 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -142,6 +142,7 @@ public bool IsValidAppearanceVar(string name) { case "plane": case "blend_mode": case "appearance_flags": + case "vis_flags": case "alpha": case "glide_size": case "render_source": @@ -252,8 +253,12 @@ public void SetAppearanceVar(MutableAppearance appearance, string varName, Dream appearance.BlendMode = Enum.IsDefined((BlendMode)blendMode) ? (BlendMode)blendMode : BlendMode.Default; break; case "appearance_flags": - value.TryGetValueAsInteger(out int flagsVar); - appearance.AppearanceFlags = (AppearanceFlags) flagsVar; + value.TryGetValueAsInteger(out int appearanceFlagsVar); + appearance.AppearanceFlags = (AppearanceFlags) appearanceFlagsVar; + break; + case "vis_flags": + value.TryGetValueAsInteger(out int visFlagsVar); + appearance.VisFlags = (VisFlags) visFlagsVar; break; case "alpha": value.TryGetValueAsFloat(out float floatAlpha); @@ -408,6 +413,8 @@ public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varNam return new((int) appearance.BlendMode); case "appearance_flags": return new((int) appearance.AppearanceFlags); + case "vis_flags": + return new((int) appearance.VisFlags); case "alpha": return new(appearance.Alpha); case "glide_size": @@ -641,6 +648,7 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) def.TryGetVariable("render_target", out var renderTargetVar); def.TryGetVariable("blend_mode", out var blendModeVar); def.TryGetVariable("appearance_flags", out var appearanceFlagsVar); + def.TryGetVariable("vis_flags", out var visFlagsVar); def.TryGetVariable("maptext", out var maptextVar); def.TryGetVariable("maptext_width", out var maptextWidthVar); def.TryGetVariable("maptext_height", out var maptextHeightVar); @@ -671,6 +679,7 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) SetAppearanceVar(appearance, "render_target", renderTargetVar); SetAppearanceVar(appearance, "blend_mode", blendModeVar); SetAppearanceVar(appearance, "appearance_flags", appearanceFlagsVar); + SetAppearanceVar(appearance, "vis_flags", visFlagsVar); SetAppearanceVar(appearance, "maptext", maptextVar); SetAppearanceVar(appearance, "maptext_width", maptextWidthVar); SetAppearanceVar(appearance, "maptext_height", maptextHeightVar); diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs index a6682696cd..9112facea6 100644 --- a/OpenDreamShared/Dream/ImmutableAppearance.cs +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -42,6 +42,7 @@ public sealed class ImmutableAppearance : IEquatable { [ViewVariables] public readonly int Plane = MutableAppearance.Default.Plane; [ViewVariables] public readonly BlendMode BlendMode = MutableAppearance.Default.BlendMode; [ViewVariables] public readonly AppearanceFlags AppearanceFlags = MutableAppearance.Default.AppearanceFlags; + [ViewVariables] public readonly VisFlags VisFlags = MutableAppearance.Default.VisFlags; [ViewVariables] public readonly sbyte Invisibility = MutableAppearance.Default.Invisibility; [ViewVariables] public readonly bool Opacity = MutableAppearance.Default.Opacity; [ViewVariables] public readonly bool Override = MutableAppearance.Default.Override; @@ -96,6 +97,7 @@ public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? RenderTarget = appearance.RenderTarget; BlendMode = appearance.BlendMode; AppearanceFlags = appearance.AppearanceFlags; + VisFlags = appearance.VisFlags; Invisibility = appearance.Invisibility; Opacity = appearance.Opacity; MouseOpacity = appearance.MouseOpacity; @@ -169,6 +171,7 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.RenderTarget != RenderTarget) return false; if (immutableAppearance.BlendMode != BlendMode) return false; if (immutableAppearance.AppearanceFlags != AppearanceFlags) return false; + if (immutableAppearance.VisFlags != VisFlags) return false; if (immutableAppearance.Invisibility != Invisibility) return false; if (immutableAppearance.Opacity != Opacity) return false; if (immutableAppearance.MouseOpacity != MouseOpacity) return false; @@ -253,6 +256,7 @@ public override int GetHashCode() { hashCode.Add(RenderTarget); hashCode.Add(BlendMode); hashCode.Add(AppearanceFlags); + hashCode.Add(VisFlags); hashCode.Add(Maptext); hashCode.Add(MaptextOffset); hashCode.Add(MaptextSize); @@ -347,6 +351,9 @@ public ImmutableAppearance(NetBuffer buffer, IRobustSerializer serializer) { case IconAppearanceProperty.AppearanceFlags: AppearanceFlags = (AppearanceFlags)buffer.ReadInt32(); break; + case IconAppearanceProperty.VisFlags: + VisFlags = (VisFlags)buffer.ReadInt32(); + break; case IconAppearanceProperty.Invisibility: Invisibility = buffer.ReadSByte(); break; @@ -507,6 +514,7 @@ public MutableAppearance ToMutable() { result.RenderTarget = RenderTarget; result.BlendMode = BlendMode; result.AppearanceFlags = AppearanceFlags; + result.VisFlags = VisFlags; result.Invisibility = Invisibility; result.Opacity = Opacity; result.MouseOpacity = MouseOpacity; @@ -627,6 +635,11 @@ public void WriteToBuffer(NetBuffer buffer, IRobustSerializer serializer) { buffer.Write((int)AppearanceFlags); } + if (VisFlags != MutableAppearance.Default.VisFlags) { + buffer.Write((byte)IconAppearanceProperty.VisFlags); + buffer.Write((int)VisFlags); + } + if (Invisibility != MutableAppearance.Default.Invisibility) { buffer.Write((byte)IconAppearanceProperty.Invisibility); buffer.Write(Invisibility); diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 0a897d64bf..7355ddabe7 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -40,6 +40,7 @@ public sealed class MutableAppearance : IEquatable, IDisposab [ViewVariables] public int Plane = -32767; [ViewVariables] public BlendMode BlendMode = BlendMode.Default; [ViewVariables] public AppearanceFlags AppearanceFlags = AppearanceFlags.None; + [ViewVariables] public VisFlags VisFlags = VisFlags.None; [ViewVariables] public sbyte Invisibility; [ViewVariables] public bool Opacity; [ViewVariables] public bool Override; @@ -134,6 +135,7 @@ public void CopyFrom(MutableAppearance appearance) { RenderTarget = appearance.RenderTarget; BlendMode = appearance.BlendMode; AppearanceFlags = appearance.AppearanceFlags; + VisFlags = appearance.VisFlags; Invisibility = appearance.Invisibility; Opacity = appearance.Opacity; MouseOpacity = appearance.MouseOpacity; @@ -183,6 +185,7 @@ public bool Equals(MutableAppearance? appearance) { if (appearance.RenderTarget != RenderTarget) return false; if (appearance.BlendMode != BlendMode) return false; if (appearance.AppearanceFlags != AppearanceFlags) return false; + if (appearance.VisFlags != VisFlags) return false; if (appearance.Invisibility != Invisibility) return false; if (appearance.Opacity != Opacity) return false; if (appearance.MouseOpacity != MouseOpacity) return false; @@ -285,6 +288,7 @@ public override int GetHashCode() { hashCode.Add(RenderTarget); hashCode.Add(BlendMode); hashCode.Add(AppearanceFlags); + hashCode.Add(VisFlags); hashCode.Add(Maptext); hashCode.Add(MaptextOffset); hashCode.Add(MaptextSize); @@ -376,6 +380,19 @@ public enum AppearanceFlags { TileMover = 2048 } +[Flags] +public enum VisFlags { + None = 0, + InheritIcon = 1, + InheritIconState = 2, + InheritDir = 4, + InheritLayer = 8, + InheritPlane = 16, + InheritId = 32, + Underlay = 64, + Hide = 128, +} + [Flags] //kinda, but only EASE_IN and EASE_OUT are used as bitflags, everything else is an enum public enum AnimationEasing { Linear = 0, @@ -431,6 +448,7 @@ public enum IconAppearanceProperty : byte { Plane, BlendMode, AppearanceFlags, + VisFlags, Invisibility, Opacity, Override, From ccf64477038d03e16d1668b7d3d0c7dbb1712913 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:11:25 -0400 Subject: [PATCH 02/10] isVisContent bool --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index ee5e8a8d7a..abf67c72e5 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -197,7 +197,20 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) } //handles underlays, overlays, appearance flags, images. Adds them to the result list, so they can be sorted and drawn with DrawIcon() - private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid uid, bool isScreen, ref int tieBreaker, List result, sbyte seeVis, RendererMetaData? parentIcon = null, bool keepTogether = false, Vector3? turfCoords = null, ClientAppearanceSystem.Flick? flick = null) { + private void ProcessIconComponents( + DreamIcon icon, + Vector2 position, + EntityUid uid, + bool isScreen, + bool isVisContent, + ref int tieBreaker, + List result, + sbyte seeVis, + RendererMetaData? parentIcon = null, + bool keepTogether = false, + Vector3? turfCoords = null, + ClientAppearanceSystem.Flick? flick = null + ) { if (icon.Appearance is null) //in the event that appearance hasn't loaded yet return; @@ -309,10 +322,10 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u (underlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(underlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -331,10 +344,10 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u (overlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(overlay, current.Position, uid, isScreen, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -352,7 +365,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u current.MainIcon = sprite.Icon; current.Position += (sprite.Icon.Appearance.TotalPixelOffset / (float)IconSize); } else - ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); } } @@ -363,7 +376,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) continue; - ProcessIconComponents(sprite.Icon, position, visContentEntity, false, ref tieBreaker, result, seeVis, current, keepTogether); + ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); // TODO: click uid should be set to current.uid again // TODO: vis_flags @@ -649,7 +662,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU tValue = 0; //pass the turf coords for client.images lookup Vector3 turfCoords = new Vector3(tileRef.X, tileRef.Y, (int) worldPos.MapId); - ProcessIconComponents(_appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, ref tValue, _spriteContainer, seeVis, turfCoords: turfCoords, flick: flick); + ProcessIconComponents(_appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, false, ref tValue, _spriteContainer, seeVis, turfCoords: turfCoords, flick: flick); } // Visible entities @@ -680,7 +693,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU var flick = _appearanceSystem.GetMovableFlick(entity); tValue = 0; - ProcessIconComponents(sprite.Icon, worldPos - new Vector2(0.5f), entity, false, ref tValue, _spriteContainer, seeVis, flick: flick); + ProcessIconComponents(sprite.Icon, worldPos - new Vector2(0.5f), entity, false, false, ref tValue, _spriteContainer, seeVis, flick: flick); } } @@ -702,7 +715,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU for (int x = 0; x < sprite.ScreenLocation.RepeatX; x++) { for (int y = 0; y < sprite.ScreenLocation.RepeatY; y++) { tValue = 0; - ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, ref tValue, _spriteContainer, seeVis); + ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, false, ref tValue, _spriteContainer, seeVis); } } } From c5c8ec50b1c28c99c1296444fb1cff747b74278e Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:21:17 -0400 Subject: [PATCH 03/10] Implement the other vis flags maybe --- OpenDreamClient/Rendering/DreamIcon.cs | 19 +++++------ OpenDreamClient/Rendering/DreamViewOverlay.cs | 34 +++++++++++++++---- OpenDreamShared/Dream/MutableAppearance.cs | 2 +- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index 322a42212c..e2ca41872b 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -29,10 +29,7 @@ private set { private DMIResource? _dmi; public int AnimationFrame { - get { - UpdateAnimation(); - return _animationFrame; - } + get => GetAnimationFrame(_iconState, _direction); } [ViewVariables] @@ -221,16 +218,16 @@ public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { } } - private void UpdateAnimation() { + public int GetAnimationFrame(string? iconState, AtomDirection dir) { if(DMI == null || Appearance == null || _animationComplete) - return; + return 0; - DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(_iconState); + DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(iconState); if(dmiState == null) - return; - DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(_direction); + return 0; + DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(dir); - if (frames.Length <= 1) return; + if (frames.Length <= 1) return 0; var oldFrame = _animationFrame; var currentGameTicks = gameTiming.CurTime.Ticks; @@ -253,6 +250,8 @@ private void UpdateAnimation() { if (oldFrame != _animationFrame) DirtyTexture(); + + return _animationFrame; } private ImmutableAppearance? CalculateAnimatedAppearance() { diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index abf67c72e5..e01d5c9342 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -237,8 +237,23 @@ private void ProcessIconComponents( ); if (parentIcon != null) { - current.ClickUid = parentIcon.ClickUid; - current.MouseOpacity = parentIcon.MouseOpacity; + if(isVisContent) { + var visFlags = current.VisFlags; + current.ClickUid = (visFlags & VisFlags.InheritId) != 0 ? parentIcon.ClickUid : current.ClickUid; + current.MouseOpacity = (visFlags & VisFlags.InheritId) != 0 ? parentIcon.MouseOpacity : icon.Appearance.MouseOpacity; + + if((visFlags & (VisFlags.InheritIcon | VisFlags.InheritIconState | VisFlags.InheritDir)) != 0) { + var usedIcon = (visFlags & VisFlags.InheritIcon) != 0 ? parentIcon.MainIcon! : current.MainIcon; + var usedIconState = (visFlags & VisFlags.InheritIconState) != 0 ? parentIcon.MainIcon!.Appearance!.IconState : current.MainIcon.Appearance.IconState; + var usedDir = (visFlags & VisFlags.InheritDir) != 0 ? parentIcon.MainIcon!.Appearance!.Direction : current.MainIcon.Appearance.Direction; + + current.TextureOverride = usedIcon.DMI?.GetState(usedIconState)?.GetFrames(usedDir).ElementAtOrDefault(usedIcon.GetAnimationFrame(usedIconState, usedDir)); + } + } else { + current.ClickUid = parentIcon.ClickUid; + current.MouseOpacity = parentIcon.MouseOpacity; + } + if ((icon.Appearance.AppearanceFlags & AppearanceFlags.ResetColor) != 0 || keepTogether) { //RESET_COLOR current.ColorToApply = icon.Appearance.Color; current.ColorMatrixToApply = icon.Appearance.ColorMatrix; @@ -257,13 +272,16 @@ private void ProcessIconComponents( else current.TransformToApply = iconAppearanceTransformMatrix * parentIcon.TransformToApply; - if ((icon.Appearance.Plane < -10000)) //FLOAT_PLANE - Note: yes, this really is how it works. Yes it's dumb as shit. - current.Plane = parentIcon.Plane + (icon.Appearance.Plane + 32767); + var effectivePlane = (isVisContent && (current.VisFlags & VisFlags.InheritPlane) != 0) ? parentIcon.Plane : icon.Appearance.Plane; + var effectiveLayer = (isVisContent && (current.VisFlags & VisFlags.InheritLayer) != 0) ? parentIcon.Layer : icon.Appearance.Layer; + + if ((effectivePlane < -10000)) //FLOAT_PLANE - Note: yes, this really is how it works. Yes it's dumb as shit. + current.Plane = parentIcon.Plane + (effectivePlane + 32767); else - current.Plane = icon.Appearance.Plane; + current.Plane = effectivePlane; //FLOAT_LAYER - if this icon's layer is negative, it's a float layer so set it's layer equal to the parent object and sort through the float_layer shit later - current.Layer = (icon.Appearance.Layer < 0) ? parentIcon.Layer : icon.Appearance.Layer; + current.Layer = (effectiveLayer < 0) ? parentIcon.Layer : effectiveLayer; if (current.BlendMode == BlendMode.Default) current.BlendMode = parentIcon.BlendMode; @@ -376,6 +394,10 @@ private void ProcessIconComponents( if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) continue; + var spriteIcon = sprite.Icon; + if(spriteIcon.Appearance is null || (spriteIcon.Appearance.VisFlags & VisFlags.Hide) != 0) + continue; + ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); // TODO: click uid should be set to current.uid again diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 7355ddabe7..3c0e755c83 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -389,7 +389,7 @@ public enum VisFlags { InheritLayer = 8, InheritPlane = 16, InheritId = 32, - Underlay = 64, + Underlay = 64, // unimplemented Hide = 128, } From 51f393093c3d4fd267e652edf53f3dc101dceb12 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:25:46 -0400 Subject: [PATCH 04/10] move Hide check so that it catches overlays/underlays that have it --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index e01d5c9342..26e48d6b02 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -213,6 +213,8 @@ private void ProcessIconComponents( ) { if (icon.Appearance is null) //in the event that appearance hasn't loaded yet return; + if (isVisContent && (icon.Appearance.VisFlags & VisFlags.Hide) != 0) + return; result.EnsureCapacity(result.Count + icon.Underlays.Count + icon.Overlays.Count + 1); RendererMetaData current = RentRendererMetaData(); @@ -340,10 +342,10 @@ private void ProcessIconComponents( (underlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(underlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(underlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -362,10 +364,10 @@ private void ProcessIconComponents( (overlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(overlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(overlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -394,10 +396,6 @@ private void ProcessIconComponents( if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) continue; - var spriteIcon = sprite.Icon; - if(spriteIcon.Appearance is null || (spriteIcon.Appearance.VisFlags & VisFlags.Hide) != 0) - continue; - ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); // TODO: click uid should be set to current.uid again From 82e45fd8b9190767a56f0e235fc9fef1fcf0a9f3 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:13:02 -0400 Subject: [PATCH 05/10] VIS_HIDE is implemented correctly --- OpenDreamClient/Rendering/DreamIcon.cs | 10 ++++---- OpenDreamClient/Rendering/DreamViewOverlay.cs | 23 +++++++++---------- OpenDreamClient/Rendering/RendererMetaData.cs | 2 ++ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index e2ca41872b..4d8bcfcf77 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -28,10 +28,6 @@ private set { private DMIResource? _dmi; - public int AnimationFrame { - get => GetAnimationFrame(_iconState, _direction); - } - [ViewVariables] public ImmutableAppearance? Appearance { get => CalculateAnimatedAppearance(); @@ -92,9 +88,9 @@ public void Dispose() { var dmi = flick?.Icon ?? DMI; var iconState = flick?.IconState ?? _iconState; - var animationFrame = flick?.GetAnimationFrame(gameTiming) ?? AnimationFrame; + var animationFrame = flick?.GetAnimationFrame(gameTiming) ?? GetAnimationFrame(); if (animationFrame == -1) // A flick returns -1 for a finished animation - animationFrame = AnimationFrame; + animationFrame = GetAnimationFrame(); if (CachedTexture != null && !_textureDirty && flick == null) return CachedTexture.Texture; @@ -218,6 +214,8 @@ public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { } } + public int GetAnimationFrame() => GetAnimationFrame(_iconState, _direction); + public int GetAnimationFrame(string? iconState, AtomDirection dir) { if(DMI == null || Appearance == null || _animationComplete) return 0; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 26e48d6b02..99e9941014 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -202,7 +202,7 @@ private void ProcessIconComponents( Vector2 position, EntityUid uid, bool isScreen, - bool isVisContent, + bool isVisChild, // DIRECT vis_contents child ref int tieBreaker, List result, sbyte seeVis, @@ -213,7 +213,7 @@ private void ProcessIconComponents( ) { if (icon.Appearance is null) //in the event that appearance hasn't loaded yet return; - if (isVisContent && (icon.Appearance.VisFlags & VisFlags.Hide) != 0) + if ((isVisChild || (parentIcon?.IsVisContent ?? false)) && (icon.Appearance.VisFlags & VisFlags.Hide) != 0) return; result.EnsureCapacity(result.Count + icon.Underlays.Count + icon.Overlays.Count + 1); @@ -227,6 +227,7 @@ private void ProcessIconComponents( current.RenderSource = icon.Appearance.RenderSource; current.RenderTarget = icon.Appearance.RenderTarget; current.VisFlags = icon.Appearance.VisFlags; + current.IsVisContent = parentIcon?.IsVisContent ?? false; current.AppearanceFlags = icon.Appearance.AppearanceFlags; current.BlendMode = icon.Appearance.BlendMode; current.Flick = flick; @@ -239,7 +240,8 @@ private void ProcessIconComponents( ); if (parentIcon != null) { - if(isVisContent) { + if(isVisChild) { + current.IsVisContent = true; var visFlags = current.VisFlags; current.ClickUid = (visFlags & VisFlags.InheritId) != 0 ? parentIcon.ClickUid : current.ClickUid; current.MouseOpacity = (visFlags & VisFlags.InheritId) != 0 ? parentIcon.MouseOpacity : icon.Appearance.MouseOpacity; @@ -274,8 +276,8 @@ private void ProcessIconComponents( else current.TransformToApply = iconAppearanceTransformMatrix * parentIcon.TransformToApply; - var effectivePlane = (isVisContent && (current.VisFlags & VisFlags.InheritPlane) != 0) ? parentIcon.Plane : icon.Appearance.Plane; - var effectiveLayer = (isVisContent && (current.VisFlags & VisFlags.InheritLayer) != 0) ? parentIcon.Layer : icon.Appearance.Layer; + var effectivePlane = (isVisChild && (current.VisFlags & VisFlags.InheritPlane) != 0) ? parentIcon.Plane : icon.Appearance.Plane; + var effectiveLayer = (isVisChild && (current.VisFlags & VisFlags.InheritLayer) != 0) ? parentIcon.Layer : icon.Appearance.Layer; if ((effectivePlane < -10000)) //FLOAT_PLANE - Note: yes, this really is how it works. Yes it's dumb as shit. current.Plane = parentIcon.Plane + (effectivePlane + 32767); @@ -342,10 +344,10 @@ private void ProcessIconComponents( (underlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(underlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(underlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(underlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -364,10 +366,10 @@ private void ProcessIconComponents( (overlay.Appearance.AppearanceFlags & AppearanceFlags.KeepApart) != 0; if (!keepTogether || keepApart) { //KEEP_TOGETHER wasn't set on our parent, or KEEP_APART - ProcessIconComponents(overlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, result, seeVis, current); + ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, result, seeVis, current); } else { current.KeepTogetherGroup ??= new(); - ProcessIconComponents(overlay, current.Position, uid, isScreen, isVisContent, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); + ProcessIconComponents(overlay, current.Position, uid, isScreen, false, ref tieBreaker, current.KeepTogetherGroup, seeVis, current, keepTogether); } } @@ -397,9 +399,6 @@ private void ProcessIconComponents( continue; ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); - - // TODO: click uid should be set to current.uid again - // TODO: vis_flags } //maptext is basically just an image of rendered text added as an overlay diff --git a/OpenDreamClient/Rendering/RendererMetaData.cs b/OpenDreamClient/Rendering/RendererMetaData.cs index ce09b272dd..adcb0b263a 100644 --- a/OpenDreamClient/Rendering/RendererMetaData.cs +++ b/OpenDreamClient/Rendering/RendererMetaData.cs @@ -21,6 +21,7 @@ internal sealed class RendererMetaData : IComparable { public string? RenderTarget; public List? KeepTogetherGroup; public VisFlags VisFlags; + public bool IsVisContent; public AppearanceFlags AppearanceFlags; public BlendMode BlendMode; public MouseOpacity MouseOpacity; @@ -55,6 +56,7 @@ public void Reset() { RenderTarget = ""; KeepTogetherGroup = null; //don't actually need to allocate this 90% of the time VisFlags = VisFlags.None; + IsVisContent = false; AppearanceFlags = AppearanceFlags.None; BlendMode = BlendMode.Default; MouseOpacity = MouseOpacity.Transparent; From 392af159e0ff97f9755a0f7ab696609fb91a007d Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:13:53 -0400 Subject: [PATCH 06/10] fixes KEEP_TOGETHER bug (unatomic?) --- OpenDreamClient/Rendering/DreamPlane.cs | 4 +++- OpenDreamClient/Rendering/DreamViewOverlay.cs | 4 ++-- OpenDreamClient/Rendering/RendererMetaData.cs | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index 3d6fc05fe3..c6ce329fda 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -102,7 +102,9 @@ public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Ve texture = Texture.White; } - var pos = (sprite.Position - worldAABB.BottomLeft) * overlay.IconSize; + var posOffset = -worldAABB.BottomLeft + sprite.RenderPosOffset; + + var pos = (sprite.Position + posOffset) * overlay.IconSize; if (sprite.MainIcon != null) pos += sprite.MainIcon.TextureRenderOffset; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 99e9941014..173f75cb4e 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -486,7 +486,7 @@ public void DrawIcon(DrawingHandleWorld handle, Vector2i renderTargetSize, Rende // For now, just generate a buffer of 128 pixels around the main icon... Vector2i ktSize = (256, 256) + iconMetaData.MainIcon?.DMI?.IconSize ?? (0,0); iconMetaData.TextureOverride = ProcessKeepTogether(handle, iconMetaData, ktSize); - positionOffset -= ((ktSize/IconSize) - Vector2.One) * new Vector2(0.5f); //correct for KT group texture offset + iconMetaData.RenderPosOffset -= ((ktSize/IconSize) - Vector2.One) * new Vector2(0.5f); //correct for KT group texture offset } //Maptext @@ -505,7 +505,7 @@ public void DrawIcon(DrawingHandleWorld handle, Vector2i renderTargetSize, Rende } var frame = iconMetaData.GetTexture(this, handle); - var pixelPosition = (iconMetaData.Position + positionOffset) * IconSize; + var pixelPosition = (iconMetaData.Position + (positionOffset + iconMetaData.RenderPosOffset)) * IconSize; //if frame is null, this doesn't require a draw, so return NOP if (frame == null) diff --git a/OpenDreamClient/Rendering/RendererMetaData.cs b/OpenDreamClient/Rendering/RendererMetaData.cs index adcb0b263a..25a54d98d9 100644 --- a/OpenDreamClient/Rendering/RendererMetaData.cs +++ b/OpenDreamClient/Rendering/RendererMetaData.cs @@ -17,6 +17,7 @@ internal sealed class RendererMetaData : IComparable { public ColorMatrix ColorMatrixToApply; public float AlphaToApply; public Matrix3x2 TransformToApply; + public Vector2 RenderPosOffset; public string? RenderSource; public string? RenderTarget; public List? KeepTogetherGroup; @@ -52,6 +53,7 @@ public void Reset() { ColorMatrixToApply = ColorMatrix.Identity; AlphaToApply = 1.0f; TransformToApply = Matrix3x2.Identity; + RenderPosOffset = Vector2.Zero; RenderSource = ""; RenderTarget = ""; KeepTogetherGroup = null; //don't actually need to allocate this 90% of the time From 0805e200f8c5dadc37c266a008241a019b5c84c3 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:27:47 -0400 Subject: [PATCH 07/10] Correct regression in GetAnimationFrame --- OpenDreamClient/Rendering/DreamIcon.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index 4d8bcfcf77..fb0fb48cef 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -218,14 +218,15 @@ public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { public int GetAnimationFrame(string? iconState, AtomDirection dir) { if(DMI == null || Appearance == null || _animationComplete) - return 0; + return _animationFrame; DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(iconState); if(dmiState == null) - return 0; + return _animationFrame; DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(dir); - if (frames.Length <= 1) return 0; + if (frames.Length <= 1) + return _animationFrame; var oldFrame = _animationFrame; var currentGameTicks = gameTiming.CurTime.Ticks; From 532af30bd790b3626e6d0c9d7f17e55de62d790b Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:40:08 -0400 Subject: [PATCH 08/10] This part should probably return 0 actually --- OpenDreamClient/Rendering/DreamIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index fb0fb48cef..26e4498cce 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -226,7 +226,7 @@ public int GetAnimationFrame(string? iconState, AtomDirection dir) { DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(dir); if (frames.Length <= 1) - return _animationFrame; + return 0; var oldFrame = _animationFrame; var currentGameTicks = gameTiming.CurTime.Ticks; From 8fef5204bd3b8e0e73ffe703a7641e249204b1e2 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:48:51 -0400 Subject: [PATCH 09/10] Implement underlay --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 43 +++++++++++++++---- OpenDreamShared/Dream/MutableAppearance.cs | 2 +- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 173f75cb4e..c8f142e11f 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -330,6 +330,35 @@ private void ProcessIconComponents( result.Add(renderTargetPlaceholder); } + // vis_contents are split with VIS_UNDERLAY, so we need to do this early + var visContents = icon.Appearance.VisContents; + List? underContents = null; + List? overContents = null; + foreach(var visContent in visContents) { + EntityUid visContentEntity = _entityManager.GetEntity(visContent); + if (!_spriteQuery.TryGetComponent(visContentEntity, out var sprite)) + continue; + + var appearance = sprite.Icon.Appearance; + if(appearance is null || (appearance.VisFlags & VisFlags.Hide) != 0) // don't waste our time + continue; + if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) + continue; + + if((appearance.VisFlags & VisFlags.Underlay) == 0) + (overContents ??= new(visContents.Length)).Add(visContentEntity); + else + (underContents ??= new(visContents.Length)).Add(visContentEntity); + } + + //underlay vis_contents are rendered first, before the underlays even + if(underContents is not null) { + foreach(var visContentEntity in underContents) { + var sprite = _spriteQuery.GetComponent(visContentEntity); + ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); + } + } + //underlays - colour, alpha, and transform are inherited, but filters aren't //underlays are sorted in reverse order to overlays for(int underlayIndex = icon.Underlays.Count-1; underlayIndex >= 0; underlayIndex--) { @@ -391,14 +420,12 @@ private void ProcessIconComponents( } } - foreach (var visContent in icon.Appearance.VisContents) { - EntityUid visContentEntity = _entityManager.GetEntity(visContent); - if (!_spriteQuery.TryGetComponent(visContentEntity, out var sprite)) - continue; - if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) - continue; - - ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); + // overlay vis_contents are rendered last + if(overContents is not null) { + foreach(var visContentEntity in overContents) { + var sprite = _spriteQuery.GetComponent(visContentEntity); + ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); + } } //maptext is basically just an image of rendered text added as an overlay diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 30a6559486..7aded10a13 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -395,7 +395,7 @@ public enum VisFlags { InheritLayer = 8, InheritPlane = 16, InheritId = 32, - Underlay = 64, // unimplemented + Underlay = 64, Hide = 128, } From 3943e363b9bd25c84564e5c726259e160717a623 Mon Sep 17 00:00:00 2001 From: tontyGH <39193182+tontyGH@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:16:00 -0400 Subject: [PATCH 10/10] bru --- OpenDreamClient/Rendering/DreamViewOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index c8f142e11f..05530b8c70 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -422,7 +422,7 @@ private void ProcessIconComponents( // overlay vis_contents are rendered last if(overContents is not null) { - foreach(var visContentEntity in overContents) { + foreach(var visContentEntity in overContents) { var sprite = _spriteQuery.GetComponent(visContentEntity); ProcessIconComponents(sprite.Icon, position, visContentEntity, false, true, ref tieBreaker, result, seeVis, current, keepTogether); }