From 3bc0f0760859ee13060b8b779a21cc6dc33c58a8 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 22 Jan 2025 20:43:18 -0500 Subject: [PATCH 01/14] Fix a missed RenderTargetPool return (#2180) --- OpenDreamClient/Rendering/DreamIcon.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index a7072990a8..fc3be2d189 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -109,8 +109,13 @@ public void Dispose() { TextureRenderOffset = Vector2.Zero; return frame; } else { - if(textureOverride is not null) - return FullRenderTexture(viewOverlay, handle, iconMetaData, frame).Texture; //no caching in the presence of overrides + if (textureOverride is not null) { //no caching in the presence of overrides + var texture = FullRenderTexture(viewOverlay, handle, iconMetaData, frame); + + renderTargetPool.ReturnAtEndOfFrame(texture); + return texture.Texture; + } + CachedTexture = FullRenderTexture(viewOverlay, handle, iconMetaData, frame); } From c6720681058d18dd1926458ec5af482886e8f283 Mon Sep 17 00:00:00 2001 From: wixoa Date: Wed, 22 Jan 2025 22:01:26 -0500 Subject: [PATCH 02/14] Rich text rendering and HTML parsing for maptext (#2179) --- .../DebugWindows/IconDebugWindow.xaml.cs | 3 + OpenDreamClient/Interface/Html/HtmlParser.cs | 10 +- OpenDreamClient/Rendering/DreamViewOverlay.cs | 48 ++- OpenDreamClient/Rendering/MapTextRenderer.cs | 287 ++++++++++++++++++ .../Objects/Types/DreamObjectMovable.cs | 4 + 5 files changed, 320 insertions(+), 32 deletions(-) create mode 100644 OpenDreamClient/Rendering/MapTextRenderer.cs diff --git a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index dc0c97c588..bbdec962a0 100644 --- a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs +++ b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs @@ -63,6 +63,9 @@ private void Update() { AddPropertyIfNotDefault("Render Source", appearance.RenderSource, MutableAppearance.Default.RenderSource); AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, MutableAppearance.Default.RenderTarget); AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, MutableAppearance.Default.MouseOpacity); + AddPropertyIfNotDefault("Map Text Offset", appearance.MaptextOffset, MutableAppearance.Default.MaptextOffset); + AddPropertyIfNotDefault("Map Text Size", appearance.MaptextSize, MutableAppearance.Default.MaptextSize); + AddPropertyIfNotDefault("Map Text", appearance.Maptext, MutableAppearance.Default.Maptext); foreach (var overlay in _icon.Overlays) { AddDreamIconButton(OverlaysGrid, overlay); diff --git a/OpenDreamClient/Interface/Html/HtmlParser.cs b/OpenDreamClient/Interface/Html/HtmlParser.cs index c2394d8af9..476fd4f928 100644 --- a/OpenDreamClient/Interface/Html/HtmlParser.cs +++ b/OpenDreamClient/Interface/Html/HtmlParser.cs @@ -24,6 +24,9 @@ void SkipWhitespace() { } void PushCurrentText() { + if (currentText.Length == 0) + return; + appendTo.AddText(currentText.ToString()); currentText.Clear(); } @@ -83,9 +86,6 @@ void PushCurrentText() { appendTo.PushTag(new MarkupNode(tagType, null, ParseAttributes(attributes)), selfClosing: attributes[^1] == "/"); } - break; - case '\n': - appendTo.PushNewline(); break; case '&': // HTML named/numbered entity @@ -122,6 +122,10 @@ void PushCurrentText() { } } + break; + case '\n': + PushCurrentText(); + appendTo.PushNewline(); break; default: currentText.Append(c); diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index b3075f7f6a..343a6e6166 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -13,6 +13,7 @@ using Vector3 = Robust.Shared.Maths.Vector3; using Matrix3x2 = System.Numerics.Matrix3x2; using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.RichText; using Robust.Shared.Enums; namespace OpenDreamClient.Rendering; @@ -21,6 +22,8 @@ namespace OpenDreamClient.Rendering; /// Overlay for rendering world atoms /// internal sealed class DreamViewOverlay : Overlay { + public static ShaderInstance ColorInstance = default!; + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; public bool ScreenOverlayEnabled = true; @@ -42,8 +45,7 @@ internal sealed class DreamViewOverlay : Overlay { [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly ProfManager _prof = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; - - private readonly Font _defaultMaptextFont; + [Dependency] private readonly MarkupTagManager _tagManager = default!; private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.view"); @@ -62,13 +64,13 @@ internal sealed class DreamViewOverlay : Overlay { private readonly List _spriteContainer = new(); private readonly Dictionary _blendModeInstances; - public static ShaderInstance ColorInstance = default!; private IRenderTexture? _mouseMapRenderTarget; private IRenderTexture? _baseRenderTarget; private readonly RenderTargetPool _renderTargetPool; private readonly Stack _rendererMetaDataRental = new(); private readonly Stack _rendererMetaDataToReturn = new(); + private readonly MapTextRenderer _mapTextRenderer; private static readonly Matrix3x2 FlipMatrix = Matrix3x2.Identity with { M22 = -1 @@ -87,7 +89,6 @@ public DreamViewOverlay(RenderTargetPool renderTargetPool, TransformSystem trans _appearanceSystem = appearanceSystem; _screenOverlaySystem = screenOverlaySystem; _clientImagesSystem = clientImagesSystem; - _defaultMaptextFont = new VectorFont(_resourceCache.GetResource("/Fonts/NotoSans-Regular.ttf"),8); _spriteQuery = _entityManager.GetEntityQuery(); _xformQuery = _entityManager.GetEntityQuery(); @@ -104,6 +105,8 @@ public DreamViewOverlay(RenderTargetPool renderTargetPool, TransformSystem trans {BlendMode.Multiply, _protoManager.Index("blend_multiply").InstanceUnique()}, //BLEND_MULTIPLY {BlendMode.InsertOverlay, _protoManager.Index("blend_inset_overlay").InstanceUnique()} //BLEND_INSET_OVERLAY //TODO }; + + _mapTextRenderer = new(_resourceCache, _tagManager); } protected override void Draw(in OverlayDrawArgs args) { @@ -434,8 +437,18 @@ public void DrawIcon(DrawingHandleWorld handle, Vector2i renderTargetSize, Rende } //Maptext - if(iconMetaData.Maptext != null){ - iconMetaData.TextureOverride = GetTextureFromMaptext(iconMetaData.Maptext, iconMetaData.MaptextSize!.Value.X, iconMetaData.MaptextSize!.Value.Y, handle); + if(iconMetaData.Maptext != null) { + var maptextSize = iconMetaData.MaptextSize!.Value; + if (maptextSize.X == 0) + maptextSize.X = 32; + if (maptextSize.Y == 0) + maptextSize.Y = 32; + + var renderTarget = _renderTargetPool.Rent(maptextSize); + + _mapTextRenderer.RenderToTarget(handle, renderTarget, iconMetaData.Maptext); + _renderTargetPool.ReturnAtEndOfFrame(renderTarget); + iconMetaData.TextureOverride = renderTarget.Texture; } var frame = iconMetaData.GetTexture(this, handle); @@ -794,28 +807,6 @@ private Texture ProcessKeepTogether(DrawingHandleWorld handle, RendererMetaData return ktTexture.Texture; } - public Texture GetTextureFromMaptext(string maptext, int width, int height, DrawingHandleWorld handle) { - if(width == 0) width = 32; - if(height == 0) height = 32; - IRenderTexture tempTexture = _renderTargetPool.Rent(new Vector2i(width, height)); - handle.RenderInRenderTarget(tempTexture, () => { - handle.SetTransform(CreateRenderTargetFlipMatrix(tempTexture.Size, Vector2.Zero)); - float scale = 1; - var font = _defaultMaptextFont; - var baseLine = new Vector2(0, 0); - foreach (var rune in maptext.EnumerateRunes()){ - var metric = font.GetCharMetrics(rune, scale); - Vector2 mod = new Vector2(0); - if(metric.HasValue) - mod.Y += metric.Value.BearingY - (metric.Value.Height - metric.Value.BearingY); - - baseLine.X += font.DrawChar(handle, rune, baseLine+mod, scale, Color.White); - } - }, Color.Transparent); - _renderTargetPool.ReturnAtEndOfFrame(tempTexture); - return tempTexture.Texture; - } - /// /// Creates a transformation matrix that counteracts RT's /// quirks @@ -848,7 +839,6 @@ public static Matrix3x2 CalculateDrawingMatrix(Matrix3x2 transform, Vector2 pixe * Matrix3x2.CreateTranslation(frameSize/2) //translate back to original position * Matrix3x2.CreateScale(scaleFactors) //scale * CreateRenderTargetFlipMatrix(renderTargetSize, pixelPosition-((scaleFactors-Vector2.One)*frameSize/2)); //flip and apply scale-corrected translation - } } diff --git a/OpenDreamClient/Rendering/MapTextRenderer.cs b/OpenDreamClient/Rendering/MapTextRenderer.cs new file mode 100644 index 0000000000..d1b4aa6fd0 --- /dev/null +++ b/OpenDreamClient/Rendering/MapTextRenderer.cs @@ -0,0 +1,287 @@ +using System.Diagnostics.Contracts; +using System.Text; +using OpenDreamClient.Interface.Html; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.RichText; +using Robust.Shared.Utility; + +namespace OpenDreamClient.Rendering; + +/// +/// Helper for rendering maptext to a render target. +/// Adapted from RobustToolbox's RichTextEntry. +/// +public sealed class MapTextRenderer(IResourceCache resourceCache, MarkupTagManager tagManager) { + private const float Scale = 1f; + + private readonly VectorFont _defaultFont = + new(resourceCache.GetResource("/Fonts/NotoSans-Regular.ttf"), 8); + + private readonly Color _defaultColor = Color.White; + + // TODO: This is probably unoptimal and could cache a lot of things between frames + public void RenderToTarget(DrawingHandleWorld handle, IRenderTexture texture, string maptext) { + handle.RenderInRenderTarget(texture, () => { + handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(texture.Size, Vector2.Zero)); + + var message = new FormattedMessage(); + HtmlParser.Parse(maptext, message); + + var (height, lineBreaks) = ProcessWordWrap(message, texture.Size.X); + var lineHeight = _defaultFont.GetLineHeight(Scale); + var context = new MarkupDrawingContext(); + context.Color.Push(_defaultColor); + context.Font.Push(_defaultFont); + + var baseLine = new Vector2(0, height - lineHeight); + var lineBreakIndex = 0; + var globalBreakCounter = 0; + + foreach (var node in message) { + var text = ProcessNode(node, context); + if (!context.Color.TryPeek(out var color)) + color = _defaultColor; + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + foreach (var rune in text.EnumerateRunes()) { + if (lineBreakIndex < lineBreaks.Count && lineBreaks[lineBreakIndex] == globalBreakCounter) { + baseLine = new(0, baseLine.Y - lineHeight); + lineBreakIndex += 1; + } + + var metric = font.GetCharMetrics(rune, Scale); + Vector2 mod = new Vector2(0); + if (metric.HasValue) + mod.Y += metric.Value.BearingY - (metric.Value.Height - metric.Value.BearingY); + + var advance = font.DrawChar(handle, rune, baseLine + mod, Scale, color); + baseLine.X += advance; + + globalBreakCounter += 1; + } + } + }, Color.Transparent); + } + + private string ProcessNode(MarkupNode node, MarkupDrawingContext context) { + // If a nodes name is null it's a text node. + if (node.Name == null) + return node.Value.StringValue ?? ""; + + //Skip the node if there is no markup tag for it. + if (!tagManager.TryGetMarkupTag(node.Name, null, out var tag)) + return ""; + + if (!node.Closing) { + tag.PushDrawContext(node, context); + return tag.TextBefore(node); + } + + tag.PopDrawContext(node, context); + return tag.TextAfter(node); + } + + private (int, List) ProcessWordWrap(FormattedMessage message, float maxSizeX) { + // This method is gonna suck due to complexity. + // Bear with me here. + // I am so deeply sorry for the person adding stuff to this in the future. + + var lineBreaks = new List(); + var height = _defaultFont.GetLineHeight(Scale); + + int? breakLine; + var wordWrap = new WordWrap(maxSizeX); + var context = new MarkupDrawingContext(); + context.Font.Push(_defaultFont); + context.Color.Push(_defaultColor); + + // Go over every node. + // Nodes can change the markup drawing context and return additional text. + // It's also possible for nodes to return inline controls. They get treated as one large rune. + foreach (var node in message) { + var text = ProcessNode(node, context); + + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + // And go over every character. + foreach (var rune in text.EnumerateRunes()) { + if (ProcessRune(rune, out breakLine)) + continue; + + // Uh just skip unknown characters I guess. + if (!font.TryGetCharMetrics(rune, Scale, out var metrics)) + continue; + + if (ProcessMetric(metrics, out breakLine)) + return (height, lineBreaks); + } + } + + breakLine = wordWrap.FinalizeText(); + CheckLineBreak(breakLine); + return (height, lineBreaks); + + bool ProcessRune(Rune rune, out int? outBreakLine) { + wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip); + CheckLineBreak(breakLine); + CheckLineBreak(breakNewLine); + outBreakLine = breakLine; + return skip; + } + + bool ProcessMetric(CharMetrics metrics, out int? outBreakLine) { + wordWrap.NextMetrics(metrics, out breakLine, out var abort); + CheckLineBreak(breakLine); + outBreakLine = breakLine; + return abort; + } + + void CheckLineBreak(int? line) { + if (line is { } l) { + lineBreaks.Add(l); + if (!context.Font.TryPeek(out var font)) + font = _defaultFont; + + height += font.GetLineHeight(Scale); + } + } + } + + /// + /// Helper utility struct for word-wrapping calculations. + /// + private struct WordWrap { + private readonly float _maxSizeX; + + private float _maxUsedWidth; + private Rune _lastRune; + + // Index we put into the LineBreaks list when a line break should occur. + private int _breakIndexCounter; + + private int _nextBreakIndexCounter; + + // If the CURRENT processing word ends up too long, this is the index to put a line break. + private (int index, float lineSize)? _wordStartBreakIndex; + + // Word size in pixels. + private int _wordSizePixels; + + // The horizontal position of the text cursor. + private int _posX; + + // If a word is larger than maxSizeX, we split it. + // We need to keep track of some data to split it into two words. + private (int breakIndex, int wordSizePixels)? _forceSplitData = null; + + public WordWrap(float maxSizeX) { + this = default; + _maxSizeX = maxSizeX; + _lastRune = new Rune('A'); + } + + public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip) { + _breakIndexCounter = _nextBreakIndexCounter; + _nextBreakIndexCounter += rune.Utf16SequenceLength; + + breakLine = null; + breakNewLine = null; + skip = false; + + if (IsWordBoundary(_lastRune, rune) || rune == new Rune('\n')) { + // Word boundary means we know where the word ends. + if (_posX > _maxSizeX && _lastRune != new Rune(' ')) { + DebugTools.Assert(_wordStartBreakIndex.HasValue, + "wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line."); + //Ensure the assert had a chance to run and then just return + if (!_wordStartBreakIndex.HasValue) + return; + + // We ran into a word boundary and the word is too big to fit the previous line. + // So we insert the line break BEFORE the last word. + breakLine = _wordStartBreakIndex!.Value.index; + _maxUsedWidth = Math.Max(_maxUsedWidth, _wordStartBreakIndex.Value.lineSize); + _posX = _wordSizePixels; + } + + // Start a new word since we hit a word boundary. + //wordSize = 0; + _wordSizePixels = 0; + _wordStartBreakIndex = (_breakIndexCounter, _posX); + _forceSplitData = null; + + // Just manually handle newlines. + if (rune == new Rune('\n')) { + _maxUsedWidth = Math.Max(_maxUsedWidth, _posX); + _posX = 0; + _wordStartBreakIndex = null; + skip = true; + breakNewLine = _breakIndexCounter; + } + } + + _lastRune = rune; + } + + public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abort) { + abort = false; + breakLine = null; + + // Increase word size and such with the current character. + var oldWordSizePixels = _wordSizePixels; + _wordSizePixels += metrics.Advance; + // TODO: Theoretically, does it make sense to break after the glyph's width instead of its advance? + // It might result in some more tight packing but I doubt it'd be noticeable. + // Also definitely even more complex to implement. + _posX += metrics.Advance; + + if (_posX <= _maxSizeX) + return; + + _forceSplitData ??= (_breakIndexCounter, oldWordSizePixels); + + // Oh hey we get to break a word that doesn't fit on a single line. + if (_wordSizePixels > _maxSizeX) { + var (breakIndex, splitWordSize) = _forceSplitData.Value; + if (splitWordSize == 0) { + // Happens if there's literally not enough space for a single character so uh... + // Yeah just don't. + abort = true; + return; + } + + // Reset forceSplitData so that we can split again if necessary. + _forceSplitData = null; + breakLine = breakIndex; + _wordSizePixels -= splitWordSize; + _wordStartBreakIndex = null; + _maxUsedWidth = Math.Max(_maxUsedWidth, _maxSizeX); + _posX = _wordSizePixels; + } + } + + public int? FinalizeText() { + // This needs to happen because word wrapping doesn't get checked for the last word. + if (_posX > _maxSizeX) { + if (!_wordStartBreakIndex.HasValue) { + throw new Exception( + "wordStartBreakIndex can only be null if the word begins at a new line," + + "in which case this branch shouldn't be reached as" + + "the word would be split due to being longer than a single line."); + } + + return _wordStartBreakIndex.Value.index; + } else { + return null; + } + } + + [Pure] + private static bool IsWordBoundary(Rune a, Rune b) { + return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-'); + } + } +} diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index b012437857..ae565f51ae 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -77,6 +77,10 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "loc": value = new(Loc); return true; + case "bound_width": + case "bound_height": + value = new(32); // TODO: Custom bounds support + return true; case "screen_loc": value = (ScreenLoc != null) ? new(ScreenLoc) : DreamValue.Null; return true; From d6d86da2851a39f92fa891b7043b5645e1aa1974 Mon Sep 17 00:00:00 2001 From: ike709 Date: Thu, 23 Jan 2025 07:17:10 -0600 Subject: [PATCH 03/14] Fix parsing global var decls with whitespace (#2177) Co-authored-by: ike709 --- .../Tests/Statements/VarDecl/global_whitespace.dm | 9 +++++++++ DMCompiler/Compiler/DM/DMParser.cs | 8 ++++++++ 2 files changed, 17 insertions(+) create mode 100644 Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm diff --git a/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm new file mode 100644 index 0000000000..8edf10558f --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/VarDecl/global_whitespace.dm @@ -0,0 +1,9 @@ + +//# issue 2139 + +var/foo = 2 +var bar = 3 + +/proc/RunTest() + ASSERT(foo == 2) + ASSERT(bar == 3) diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 7c89b63fef..c0ecacbba4 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -293,6 +293,14 @@ public DMASTFile File() { } else { ReuseToken(possibleNewline); } + } else if (Current().Type == TokenType.DM_Identifier) { // "var foo" instead of "var/foo" + DMASTPath? newVarPath = Path(); + if (newVarPath == null) { + Emit(WarningCode.InvalidVarDefinition, "Expected a var definition"); + return new DMASTInvalidStatement(CurrentLoc); + } + + varPath = CurrentPath.AddToPath(newVarPath.Path.PathString); } while (true) { From b8e919daa24521b0d2acef701a32f9ab35d7a0b5 Mon Sep 17 00:00:00 2001 From: TobleroneSwordfish <20713227+TobleroneSwordfish@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:22:04 +0000 Subject: [PATCH 04/14] Fix atom name/desc not copying on appearance set (#2145) Co-authored-by: wixoaGit --- .../DMProject/Tests/atom_appearance.dm | 10 +++ .../DMProject/Tests/string_interpolation.dm | 13 ++++ Content.IntegrationTests/DMProject/code.dm | 2 + .../DMProject/environment.dme | 2 + .../Tests/Preprocessor/macro_newline_var.dm | 14 +++-- .../Tests/Preprocessor/macro_numeric_path.dm | 12 ++-- .../Tests/Text/StringInterpolation2.dm | 6 +- .../Tests/Text/StringInterpolation3.dm | 31 ++++++---- .../Tests/Text/StringInterpolation4.dm | 32 ++++------ .../Tests/Text/StringInterpolation5.dm | 35 ++++++++--- .../Tests/Text/StringInterpolation6.dm | 26 ++++---- .../Tests/Text/StringInterpolation7.dm | 61 +++++++++++-------- .../Tests/Text/StringInterpolation8.dm | 11 ---- .../Tests/Text/StringInterpolation9.dm | 39 ------------ OpenDream.sln.DotSettings | 1 + .../DebugWindows/IconDebugWindow.xaml.cs | 1 + OpenDreamClient/Rendering/DMISpriteSystem.cs | 4 +- OpenDreamRuntime/AtomManager.cs | 32 ++++++---- OpenDreamRuntime/Objects/DreamObject.cs | 11 +++- .../Objects/Types/DreamObjectAtom.cs | 28 +++------ .../Objects/Types/DreamObjectMovable.cs | 2 +- OpenDreamShared/Dream/ImmutableAppearance.cs | 19 +++++- OpenDreamShared/Dream/MutableAppearance.cs | 5 ++ .../Network/Messages/MsgAllAppearances.cs | 6 +- 24 files changed, 216 insertions(+), 187 deletions(-) create mode 100644 Content.IntegrationTests/DMProject/Tests/atom_appearance.dm create mode 100644 Content.IntegrationTests/DMProject/Tests/string_interpolation.dm delete mode 100644 Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm delete mode 100644 Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm diff --git a/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm b/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm new file mode 100644 index 0000000000..4b07dda7e3 --- /dev/null +++ b/Content.IntegrationTests/DMProject/Tests/atom_appearance.dm @@ -0,0 +1,10 @@ +/obj/thingtocopy + name = "hello" + desc = "this is a thing" + +/proc/test_appearance() + var/obj/thingtocopy/T = new() + var/obj/otherthing = new() + otherthing.appearance = T.appearance + ASSERT(otherthing.name == T.name) + ASSERT(otherthing.desc == T.desc) \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm b/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm new file mode 100644 index 0000000000..f64128b0c1 --- /dev/null +++ b/Content.IntegrationTests/DMProject/Tests/string_interpolation.dm @@ -0,0 +1,13 @@ +/obj/blombo + name = "Blombo" + gender = FEMALE + +/obj/blorpo + name = "Blorpo" + gender = MALE + +/proc/test_string_interpolation() + var/obj/blombo/b = new + var/obj/blorpo/b2 = new + var/result_text = "[b]? Nobody likes \him. \He is awful! Unlike [b2]. \He is pretty cool!" + ASSERT(result_text == "Blombo? Nobody likes her. She is awful! Unlike Blorpo. He is pretty cool!") \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/code.dm b/Content.IntegrationTests/DMProject/code.dm index 894fc35758..6e6827200f 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -30,7 +30,9 @@ test_color_matrix() test_range() test_verb_duplicate() + test_appearance() test_nonlocal_var() test_images() test_filter_init() + test_string_interpolation() world.log << "IntegrationTests successful, /world/New() exiting..." \ No newline at end of file diff --git a/Content.IntegrationTests/DMProject/environment.dme b/Content.IntegrationTests/DMProject/environment.dme index aa33082512..80f5c14baf 100644 --- a/Content.IntegrationTests/DMProject/environment.dme +++ b/Content.IntegrationTests/DMProject/environment.dme @@ -1,4 +1,5 @@ #include "code.dm" +#include "Tests/atom_appearance.dm" #include "Tests/block.dm" #include "Tests/color_matrix.dm" #include "Tests/range.dm" @@ -6,5 +7,6 @@ #include "Tests/nonlocal_var.dm" #include "Tests/image.dm" #include "Tests/filter_initial.dm" +#include "Tests/string_interpolation.dm" #include "map.dmm" #include "interface.dmf" \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm b/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm index 2d2d609cc8..4aa1d76373 100644 --- a/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm +++ b/Content.Tests/DMProject/Tests/Preprocessor/macro_newline_var.dm @@ -1,8 +1,10 @@ +/datum/var/name + #define DEFINE_FLOORS(_PATH, _VARS) \ - /obj/simulated/floor/_PATH{_VARS};\ - /obj/unsimulated/floor/_PATH{_VARS};\ - /obj/simulated/floor/airless/_PATH{_VARS};\ - /obj/unsimulated/floor/airless/_PATH{_VARS}; + /datum/simulated/floor/_PATH{_VARS};\ + /datum/unsimulated/floor/_PATH{_VARS};\ + /datum/simulated/floor/airless/_PATH{_VARS};\ + /datum/unsimulated/floor/airless/_PATH{_VARS}; var/list/gvars_datum_init_order = list() @@ -42,8 +44,8 @@ proc/RunTest() ASSERT(test.len == 4) InitGlobaltest2() ASSERT(test2.len == 2) - var/obj/simulated/floor/carpet/regalcarpet/C1 = new() - var/obj/simulated/floor/carpet/regalcarpet/border/C2 = new() + var/datum/simulated/floor/carpet/regalcarpet/C1 = new() + var/datum/simulated/floor/carpet/regalcarpet/border/C2 = new() ASSERT(C1.name == "regal carpet") ASSERT(C2.name == "regal carpet border") diff --git a/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm b/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm index a3d4448a56..2adb74318d 100644 --- a/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm +++ b/Content.Tests/DMProject/Tests/Preprocessor/macro_numeric_path.dm @@ -1,15 +1,17 @@ -/obj/thing_1/dodaa +/datum/var/name + +/datum/thing_1/dodaa name = "underscore 1 test" -#define NUMPATH_OBJDEF(num) /obj/thing_##num/name = #num +#define NUMPATH_OBJDEF(num) /datum/thing_##num/name = #num NUMPATH_OBJDEF(4) NUMPATH_OBJDEF(stuff) /proc/RunTest() - var/obj/thing_1/dodaa/D = new + var/datum/thing_1/dodaa/D = new ASSERT(D.name == "underscore 1 test") - var/obj/thing_4/T = new + var/datum/thing_4/T = new ASSERT(T.name == "4") - var/obj/thing_stuff/Y = new + var/datum/thing_stuff/Y = new ASSERT(Y.name == "stuff") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm index 385d9c0c88..1322bff03a 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation2.dm @@ -1,8 +1,8 @@ -/obj/blombo - name = "Blombo" +/datum/blombo + var/name = "Blombo" /proc/RunTest() - var/obj/blombo/b = new + var/datum/blombo/b = new var/result_text = "Nobody likes [b]!" ASSERT(result_text == "Nobody likes Blombo!") diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm index 54cb329034..20bea3f509 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation3.dm @@ -1,14 +1,23 @@ +/datum/test/var/name -/obj/blombo - name = "Blombo" - gender = FEMALE - -/obj/blorpo - name = "Blorpo" - gender = MALE +/datum/test/test1 + name = "" +/datum/test/test2 + name = " " // 3 spaces +/datum/test/test3 + name = "\t" /proc/RunTest() - var/obj/blombo/b = new - var/obj/blorpo/b2 = new - var/result_text = "[b]? Nobody likes \him. \He is awful! Unlike [b2]. \He is pretty cool!" - ASSERT(result_text == "Blombo? Nobody likes her. She is awful! Unlike Blorpo. He is pretty cool!") + var/list/correct = list( + "/datum/test/test1: ", + "/datum/test/test2: ", + "/datum/test/test3: \t" + ) + var/i = 1 + for (var/T in typesof(/datum/test)) + if(T == /datum/test) + continue + var/datum/test/D = new T() + var/true_text = correct[i] + ASSERT(true_text == "[T]: \the [D]") + ++i diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm index 5688dd1030..e960741bb0 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation4.dm @@ -1,21 +1,15 @@ -/obj/test1 - name = "" -/obj/test2 - name = " " // 3 spaces -/obj/test3 - name = "\t" + /proc/RunTest() - var/list/correct = list( - "/obj/test1: ", - "/obj/test2: ", - "/obj/test3: \t" - ) - var/i = 1 - for (var/T in typesof(/obj)) - if(T == /obj) - continue - var/obj/O = new T() - var/true_text = correct[i] - ASSERT(true_text == "[T]: \the [O]") - ++i + var/text = "["1"]\s" + ASSERT(text == "1s") + text = "[0]\s" + ASSERT(text == "0s") + text = "[null]\s" + ASSERT(text == "s") + text = "[1]\s" + ASSERT(text == "1") + text = "[1.00000001]\s" + ASSERT(text == "1") + text = "[1.0001]\s" + ASSERT(text == "1.0001s") diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm index fc768b0371..d2b0eeb0cd 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation5.dm @@ -1,11 +1,30 @@ /proc/RunTest() - var/atom/O = new() - O.name = "foo" - O.gender = FEMALE - var/atom/O2 = new - O2.name = "foob" - O2.gender = MALE - var/text = "[O2], \ref[O], \his" - ASSERT(findtextEx(text,", his") != 0) + var/text = "[0]\th" + ASSERT(text == "0th") + text = "[1]\th" + ASSERT(text == "1st") + text = "[2]\th" + ASSERT(text == "2nd") + text = "[3]\th" + ASSERT(text == "3rd") + text = "[4]\th" + ASSERT(text == "4th") + text = "[-1]\th" + ASSERT(text == "-1th") + text = "[4.52]\th" + ASSERT(text == "4th") + text = "the fitness [1.7]\th is a" + ASSERT(text == "the fitness 1st is a") + text = "the fitness [99999999]\th is a" + ASSERT(text == "the fitness 100000000th is a") + text = "[null]\th" + ASSERT(text == "0th") + var/datum/D = new + text = "[D]\th" + ASSERT(text == "0th") + var/foo = "bar" + text = "[foo]\th" + ASSERT(text == "0th") + diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm index e960741bb0..4d9b638105 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation6.dm @@ -1,15 +1,11 @@ - - -/proc/RunTest() - var/text = "["1"]\s" - ASSERT(text == "1s") - text = "[0]\s" - ASSERT(text == "0s") - text = "[null]\s" - ASSERT(text == "s") - text = "[1]\s" - ASSERT(text == "1") - text = "[1.00000001]\s" - ASSERT(text == "1") - text = "[1.0001]\s" - ASSERT(text == "1.0001s") +/proc/RunTest() + ASSERT("\roman[1.5]" == "i") + ASSERT("\roman [1.5]" == " i") + ASSERT("\Roman[1.5]" == "I") + ASSERT("\Roman [1.5]" == " I") + ASSERT("\roman shitposts [1]" == " shitposts i") + ASSERT("\roman shitposts [1] \the [2] [3]\s" == " shitposts i 3s") + ASSERT("\roman[1.#INF]" == "∞") + ASSERT("\roman[-1.#INF]" == "-∞") + ASSERT("\roman [-1.#INF]" == " -∞") + ASSERT("\roman[1.#IND]" == "�") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm index d2b0eeb0cd..9ff619a52a 100644 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm +++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation7.dm @@ -1,30 +1,39 @@ +/datum/thing + var/name = "thing" +/datum/Thing + var/name = "Thing" + +/datum/proper_thing + var/name = "\proper thing" + +/datum/plural_things + var/name = "things" + var/gender = PLURAL /proc/RunTest() - var/text = "[0]\th" - ASSERT(text == "0th") - text = "[1]\th" - ASSERT(text == "1st") - text = "[2]\th" - ASSERT(text == "2nd") - text = "[3]\th" - ASSERT(text == "3rd") - text = "[4]\th" - ASSERT(text == "4th") - text = "[-1]\th" - ASSERT(text == "-1th") - text = "[4.52]\th" - ASSERT(text == "4th") - text = "the fitness [1.7]\th is a" - ASSERT(text == "the fitness 1st is a") - text = "the fitness [99999999]\th is a" - ASSERT(text == "the fitness 100000000th is a") - text = "[null]\th" - ASSERT(text == "0th") - var/datum/D = new - text = "[D]\th" - ASSERT(text == "0th") - var/foo = "bar" - text = "[foo]\th" - ASSERT(text == "0th") + // Lowercase \a on datums + ASSERT("\a [new /datum/thing]" == "a thing") + ASSERT("\a [new /datum/Thing]" == "Thing") + ASSERT("\a [new /datum/proper_thing]" == "thing") + ASSERT("\a [new /datum/plural_things]" == "some things") + + // Uppercase \A on datums + ASSERT("\A [new /datum/thing]" == "A thing") + ASSERT("\A [new /datum/Thing]" == "Thing") + ASSERT("\A [new /datum/proper_thing]" == "thing") + ASSERT("\A [new /datum/plural_things]" == "Some things") + + // Lowercase \a on strings + ASSERT("\a ["thing"]" == "a thing") + ASSERT("\a ["Thing"]" == "Thing") + ASSERT("\a ["\proper thing"]" == "thing") + + // Uppercase \A on strings + ASSERT("\A ["thing"]" == "A thing") + ASSERT("\A ["Thing"]" == "Thing") + ASSERT("\A ["\proper thing"]" == "thing") + // Invalid \a + ASSERT("\a [123]" == "") + ASSERT("\A [123]" == "") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm deleted file mode 100644 index 4d9b638105..0000000000 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation8.dm +++ /dev/null @@ -1,11 +0,0 @@ -/proc/RunTest() - ASSERT("\roman[1.5]" == "i") - ASSERT("\roman [1.5]" == " i") - ASSERT("\Roman[1.5]" == "I") - ASSERT("\Roman [1.5]" == " I") - ASSERT("\roman shitposts [1]" == " shitposts i") - ASSERT("\roman shitposts [1] \the [2] [3]\s" == " shitposts i 3s") - ASSERT("\roman[1.#INF]" == "∞") - ASSERT("\roman[-1.#INF]" == "-∞") - ASSERT("\roman [-1.#INF]" == " -∞") - ASSERT("\roman[1.#IND]" == "�") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm deleted file mode 100644 index 9ff619a52a..0000000000 --- a/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm +++ /dev/null @@ -1,39 +0,0 @@ -/datum/thing - var/name = "thing" - -/datum/Thing - var/name = "Thing" - -/datum/proper_thing - var/name = "\proper thing" - -/datum/plural_things - var/name = "things" - var/gender = PLURAL - -/proc/RunTest() - // Lowercase \a on datums - ASSERT("\a [new /datum/thing]" == "a thing") - ASSERT("\a [new /datum/Thing]" == "Thing") - ASSERT("\a [new /datum/proper_thing]" == "thing") - ASSERT("\a [new /datum/plural_things]" == "some things") - - // Uppercase \A on datums - ASSERT("\A [new /datum/thing]" == "A thing") - ASSERT("\A [new /datum/Thing]" == "Thing") - ASSERT("\A [new /datum/proper_thing]" == "thing") - ASSERT("\A [new /datum/plural_things]" == "Some things") - - // Lowercase \a on strings - ASSERT("\a ["thing"]" == "a thing") - ASSERT("\a ["Thing"]" == "Thing") - ASSERT("\a ["\proper thing"]" == "thing") - - // Uppercase \A on strings - ASSERT("\A ["thing"]" == "A thing") - ASSERT("\A ["Thing"]" == "Thing") - ASSERT("\A ["\proper thing"]" == "thing") - - // Invalid \a - ASSERT("\a [123]" == "") - ASSERT("\A [123]" == "") \ No newline at end of file diff --git a/OpenDream.sln.DotSettings b/OpenDream.sln.DotSettings index 3c4c83094e..9303eadec4 100644 --- a/OpenDream.sln.DotSettings +++ b/OpenDream.sln.DotSettings @@ -10,6 +10,7 @@ 1 1 AABB + RT DM DMF DMI diff --git a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index bbdec962a0..338956b9f4 100644 --- a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs +++ b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs @@ -45,6 +45,7 @@ private void Update() { // Would be nice if we could use ViewVariables instead, but I couldn't find a nice way to do that // Would be especially nice if we could use VV to make these editable AddPropertyIfNotDefault("Name", appearance.Name, MutableAppearance.Default.Name); + AddPropertyIfNotDefault("Desc", appearance.Desc, MutableAppearance.Default.Desc); AddPropertyIfNotDefault("Icon State", appearance.IconState, MutableAppearance.Default.IconState); AddPropertyIfNotDefault("Direction", appearance.Direction, MutableAppearance.Default.Direction); AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, MutableAppearance.Default.InheritsDirection); diff --git a/OpenDreamClient/Rendering/DMISpriteSystem.cs b/OpenDreamClient/Rendering/DMISpriteSystem.cs index 46f9e10069..562f27a7be 100644 --- a/OpenDreamClient/Rendering/DMISpriteSystem.cs +++ b/OpenDreamClient/Rendering/DMISpriteSystem.cs @@ -39,7 +39,9 @@ public override void Shutdown() { } private void OnIconSizeChanged(EntityUid uid) { - _entityManager.TryGetComponent(uid, out var transform); + if (!_entityManager.TryGetComponent(uid, out var transform)) + return; + _lookupSystem.FindAndAddToEntityTree(uid, xform: transform); } diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index eef1f98338..0a376bce57 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -233,6 +233,7 @@ public void DeleteMovableEntity(DreamObjectMovable movable) { public bool IsValidAppearanceVar(string name) { switch (name) { case "name": + case "desc": case "icon": case "icon_state": case "dir": @@ -277,6 +278,10 @@ public void SetAppearanceVar(MutableAppearance appearance, string varName, Dream value.TryGetValueAsString(out var name); appearance.Name = name ?? string.Empty; break; + case "desc": + value.TryGetValueAsString(out var desc); + appearance.Desc = desc; + break; case "icon": if (_resourceManager.TryLoadIcon(value, out var icon)) { appearance.Icon = icon.Id; @@ -439,6 +444,10 @@ public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varNam switch (varName) { case "name": return new(appearance.Name); + case "desc": + if (appearance.Desc == null) + return DreamValue.Null; + return new(appearance.Desc); case "icon": if (appearance.Icon == null) return DreamValue.Null; @@ -551,9 +560,9 @@ public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varNam /// The atom to find the appearance of. public ImmutableAppearance MustGetAppearance(DreamObject atom) { return atom switch { + DreamObjectArea area => area.Appearance, DreamObjectTurf turf => turf.Appearance, DreamObjectMovable movable => movable.SpriteComponent.Appearance!, - DreamObjectArea area => area.Appearance, DreamObjectImage image => image.IsMutableAppearance ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) : image.SpriteComponent!.Appearance!, _ => throw new Exception($"Cannot get appearance of {atom}") }; @@ -563,16 +572,15 @@ public ImmutableAppearance MustGetAppearance(DreamObject atom) { /// Optionally looks up for an appearance. Does not try to create a new one when one is not found for this atom. /// public bool TryGetAppearance(DreamObject atom, [NotNullWhen(true)] out ImmutableAppearance? appearance) { - if (atom is DreamObjectTurf turf) - appearance = turf.Appearance; - else if (atom is DreamObjectMovable { SpriteComponent.Appearance: not null } movable) - appearance = movable.SpriteComponent.Appearance; - else if (atom is DreamObjectImage image) - appearance = image.IsMutableAppearance ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) : image.SpriteComponent?.Appearance; - else if (atom is DreamObjectArea area) - appearance = area.Appearance; - else - appearance = null; + appearance = atom switch { + DreamObjectArea area => area.Appearance, + DreamObjectTurf turf => turf.Appearance, + DreamObjectMovable { SpriteComponent.Appearance: { } movableAppearance } => movableAppearance, + DreamObjectImage image => image.IsMutableAppearance + ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) + : image.SpriteComponent?.Appearance, + _ => null + }; return appearance is not null; } @@ -692,6 +700,7 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) return appearance; def.TryGetVariable("name", out var nameVar); + def.TryGetVariable("desc", out var descVar); def.TryGetVariable("icon", out var iconVar); def.TryGetVariable("icon_state", out var stateVar); def.TryGetVariable("color", out var colorVar); @@ -712,6 +721,7 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) appearance = MutableAppearance.Get(); SetAppearanceVar(appearance, "name", nameVar); + SetAppearanceVar(appearance, "desc", descVar); SetAppearanceVar(appearance, "icon", iconVar); SetAppearanceVar(appearance, "icon_state", stateVar); SetAppearanceVar(appearance, "color", colorVar); diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs index 19794ab9b0..e3e4389248 100644 --- a/OpenDreamRuntime/Objects/DreamObject.cs +++ b/OpenDreamRuntime/Objects/DreamObject.cs @@ -376,10 +376,15 @@ public string GetNameUnformatted() { /// /// Returns the name of this object with no formatting evaluated /// - /// public string GetRawName() { - if (!TryGetVariable("name", out DreamValue nameVar) || !nameVar.TryGetValueAsString(out string? name)) - return ObjectDefinition.Type.ToString(); + string name = ObjectDefinition.Type; + + if (this is DreamObjectAtom) { + if (AtomManager.TryGetAppearance(this, out var appearance)) + name = appearance.Name; + } else if (TryGetVariable("name", out DreamValue nameVar) && nameVar.TryGetValueAsString(out var nameVarStr)) { + name = nameVarStr; + } return name; } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index 63f7a47e29..20cba60f33 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -4,8 +4,6 @@ namespace OpenDreamRuntime.Objects.Types; [Virtual] public class DreamObjectAtom : DreamObject { - public string? Name; - public string? Desc; public readonly DreamOverlaysList Overlays; public readonly DreamOverlaysList Underlays; public readonly DreamVisContentsList VisContents; @@ -21,11 +19,6 @@ public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefi AtomManager.AddAtom(this); } - public override void Initialize(DreamProcArguments args) { - ObjectDefinition.Variables["name"].TryGetValueAsString(out Name); - ObjectDefinition.Variables["desc"].TryGetValueAsString(out Desc); - } - protected override void HandleDeletion(bool possiblyThreaded) { // SAFETY: RemoveAtom is not threadsafe. if (possiblyThreaded) { @@ -38,6 +31,13 @@ protected override void HandleDeletion(bool possiblyThreaded) { base.HandleDeletion(possiblyThreaded); } + public string GetRTEntityDesc() { + if (AtomManager.TryGetAppearance(this, out var appearance) && appearance.Desc != null) + return appearance.Desc; + + return ObjectDefinition.Type; + } + protected override bool TryGetVar(string varName, out DreamValue value) { switch (varName) { // x/y/z/loc should be overriden by subtypes @@ -49,13 +49,6 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "loc": value = DreamValue.Null; return true; - - case "name": - value = (Name != null) ? new(Name) : DreamValue.Null; - return true; - case "desc": - value = (Desc != null) ? new(Desc) : DreamValue.Null; - return true; case "appearance": var appearanceCopy = AtomManager.MustGetAppearance(this).ToMutable(); @@ -101,13 +94,6 @@ protected override void SetVar(string varName, DreamValue value) { case "z": case "loc": break; - - case "name": - value.TryGetValueAsString(out Name); - break; - case "desc": - value.TryGetValueAsString(out Desc); - break; case "appearance": if (!AtomManager.TryCreateAppearanceFrom(value, out var newAppearance)) return; // Ignore attempts to set an invalid appearance diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index ae565f51ae..685aeced7f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -43,7 +43,7 @@ public override void Initialize(DreamProcArguments args) { if (EntityManager.TryGetComponent(Entity, out MetaDataComponent? metaData)) { MetaDataSystem?.SetEntityName(Entity, GetDisplayName(), metaData); - MetaDataSystem?.SetEntityDescription(Entity, Desc ?? string.Empty, metaData); + MetaDataSystem?.SetEntityDescription(Entity, GetRTEntityDesc(), metaData); } args.GetArgument(0).TryGetValueAsDreamObject(out var loc); diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs index 8dc97ecc2a..5688237d6d 100644 --- a/OpenDreamShared/Dream/ImmutableAppearance.cs +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -29,7 +29,9 @@ public sealed class ImmutableAppearance : IEquatable { private bool _needsFinalizer; private int? _storedHashCode; private readonly SharedAppearanceSystem? _appearanceSystem; + [ViewVariables] public readonly string Name = MutableAppearance.Default.Name; + [ViewVariables] public readonly string? Desc = MutableAppearance.Default.Desc; [ViewVariables] public readonly int? Icon = MutableAppearance.Default.Icon; [ViewVariables] public readonly string? IconState = MutableAppearance.Default.IconState; [ViewVariables] public readonly AtomDirection Direction = MutableAppearance.Default.Direction; @@ -76,6 +78,7 @@ public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? _appearanceSystem = serverAppearanceSystem; Name = appearance.Name; + Desc = appearance.Desc; Icon = appearance.Icon; IconState = appearance.IconState; Direction = appearance.Direction; @@ -142,6 +145,7 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance == null) return false; if (immutableAppearance.Name != Name) return false; + if (immutableAppearance.Desc != Desc) return false; if (immutableAppearance.Icon != Icon) return false; if (immutableAppearance.IconState != IconState) return false; if (immutableAppearance.Direction != Direction) return false; @@ -150,9 +154,9 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.PixelOffset2 != PixelOffset2) return false; if (immutableAppearance.Color != Color) return false; if (immutableAppearance.Alpha != Alpha) return false; - if (immutableAppearance.GlideSize != GlideSize) return false; + if (!immutableAppearance.GlideSize.Equals(GlideSize)) return false; if (!immutableAppearance.ColorMatrix.Equals(ColorMatrix)) return false; - if (immutableAppearance.Layer != Layer) return false; + if (!immutableAppearance.Layer.Equals(Layer)) return false; if (immutableAppearance.Plane != Plane) return false; if (immutableAppearance.RenderSource != RenderSource) return false; if (immutableAppearance.RenderTarget != RenderTarget) return false; @@ -171,7 +175,6 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.MaptextSize != MaptextSize) return false; if (immutableAppearance.MaptextOffset != MaptextOffset) return false; - for (int i = 0; i < Filters.Length; i++) { if (immutableAppearance.Filters[i] != Filters[i]) return false; } @@ -218,6 +221,7 @@ public override int GetHashCode() { HashCode hashCode = new HashCode(); hashCode.Add(Name); + hashCode.Add(Desc); hashCode.Add(Icon); hashCode.Add(IconState); hashCode.Add(Direction); @@ -283,6 +287,9 @@ public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializ case IconAppearanceProperty.Name: Name = buffer.ReadString(); break; + case IconAppearanceProperty.Desc: + Desc = buffer.ReadString(); + break; case IconAppearanceProperty.Id: _registeredId = buffer.ReadVariableUInt32(); break; @@ -448,6 +455,7 @@ public MutableAppearance ToMutable() { MutableAppearance result = MutableAppearance.Get(); result.Name = Name; + result.Desc = Desc; result.Icon = Icon; result.IconState = IconState; result.Direction = Direction; @@ -496,6 +504,11 @@ public void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serialize buffer.Write(Name); } + if (Desc != MutableAppearance.Default.Desc) { + buffer.Write((byte)IconAppearanceProperty.Desc); + buffer.Write(Desc); + } + if (Icon != null) { buffer.Write((byte)IconAppearanceProperty.Icon); buffer.WriteVariableInt32(Icon.Value); diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 6750f2381b..76c853cb03 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -26,6 +26,7 @@ public sealed class MutableAppearance : IEquatable, IDisposab private static Stack _mutableAppearancePool = new(); [ViewVariables] public string Name = string.Empty; + [ViewVariables] public string? Desc = string.Empty; [ViewVariables] public int? Icon; [ViewVariables] public string? IconState; [ViewVariables] public AtomDirection Direction = AtomDirection.South; @@ -106,6 +107,7 @@ public static MutableAppearance GetCopy(MutableAppearance appearance) { public void CopyFrom(MutableAppearance appearance) { Name = appearance.Name; + Desc = appearance.Desc; Icon = appearance.Icon; IconState = appearance.IconState; Direction = appearance.Direction; @@ -149,6 +151,7 @@ public bool Equals(MutableAppearance? appearance) { if (appearance == null) return false; if (appearance.Name != Name) return false; + if (appearance.Desc != Desc) return false; if (appearance.Icon != Icon) return false; if (appearance.IconState != IconState) return false; if (appearance.Direction != Direction) return false; @@ -242,6 +245,7 @@ public override int GetHashCode() { HashCode hashCode = new HashCode(); hashCode.Add(Name); + hashCode.Add(Desc); hashCode.Add(Icon); hashCode.Add(IconState); hashCode.Add(Direction); @@ -376,6 +380,7 @@ public enum AnimationFlags { //used for encoding for netmessages public enum IconAppearanceProperty : byte { Name, + Desc, Icon, IconState, Direction, diff --git a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs index 6f7a2bc0f4..2d4b1afd91 100644 --- a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs +++ b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.Collections.Generic; using Lidgren.Network; using OpenDreamShared.Dream; using Robust.Shared.Network; @@ -12,6 +9,7 @@ namespace OpenDreamShared.Network.Messages; public sealed class MsgAllAppearances(Dictionary allAppearances) : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; public Dictionary AllAppearances = allAppearances; + public MsgAllAppearances() : this(new()) { } public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { From b90c210d1e3d05c80108ceef52ec490d8603c363 Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 23 Jan 2025 21:07:13 -0500 Subject: [PATCH 05/14] Fix initial maptext vars (#2181) --- OpenDreamRuntime/AtomManager.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 0a376bce57..3ee582911a 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -718,6 +718,11 @@ 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("maptext", out var maptextVar); + def.TryGetVariable("maptext_width", out var maptextWidthVar); + def.TryGetVariable("maptext_height", out var maptextHeightVar); + def.TryGetVariable("maptext_x", out var maptextXVar); + def.TryGetVariable("maptext_y", out var maptextYVar); appearance = MutableAppearance.Get(); SetAppearanceVar(appearance, "name", nameVar); @@ -739,6 +744,11 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) SetAppearanceVar(appearance, "render_target", renderTargetVar); SetAppearanceVar(appearance, "blend_mode", blendModeVar); SetAppearanceVar(appearance, "appearance_flags", appearanceFlagsVar); + SetAppearanceVar(appearance, "maptext", maptextVar); + SetAppearanceVar(appearance, "maptext_width", maptextWidthVar); + SetAppearanceVar(appearance, "maptext_height", maptextHeightVar); + SetAppearanceVar(appearance, "maptext_x", maptextXVar); + SetAppearanceVar(appearance, "maptext_y", maptextYVar); if (def.TryGetVariable("transform", out var transformVar) && transformVar.TryGetValueAsDreamObject(out var transformMatrix)) { appearance.Transform = DreamObjectMatrix.MatrixToTransformFloatArray(transformMatrix); From 114e76ef7d440aa3672f5287f53c05d07c2bb898 Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 23 Jan 2025 21:25:48 -0500 Subject: [PATCH 06/14] Fix an error in `OnNewAppearance()` (#2182) --- OpenDreamClient/Interface/Html/HtmlParser.cs | 4 +++- .../Rendering/ClientAppearanceSystem.cs | 15 ++++++++++++--- OpenDreamClient/Rendering/DreamViewOverlay.cs | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/OpenDreamClient/Interface/Html/HtmlParser.cs b/OpenDreamClient/Interface/Html/HtmlParser.cs index 476fd4f928..ef434e83c2 100644 --- a/OpenDreamClient/Interface/Html/HtmlParser.cs +++ b/OpenDreamClient/Interface/Html/HtmlParser.cs @@ -8,6 +8,7 @@ public static class HtmlParser { private const string TagNotClosedError = "HTML tag was not closed"; private static readonly ISawmill Sawmill; + private static readonly HashSet WarnedAttributes = new(); static HtmlParser() { Sawmill = IoCManager.Resolve().GetSawmill("opendream.html_parser"); @@ -169,7 +170,8 @@ private static Dictionary ParseAttributes(string[] attr parameter = new(color); break; default: - Sawmill.Debug($"Unimplemented HTML attribute \"{attributeName}\""); + if (WarnedAttributes.Add(attributeName)) + Sawmill.Debug($"Unimplemented HTML attribute \"{attributeName}\""); continue; } diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index 684730c40f..00caf24338 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -15,6 +15,7 @@ internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { private readonly Dictionary>> _appearanceLoadCallbacks = new(); private readonly Dictionary _turfIcons = new(); private readonly Dictionary _filterShaders = new(); + private bool _receivedAllAppearancesMsg; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IDreamResourceManager _dreamResourceManager = default!; @@ -30,13 +31,17 @@ public override void Initialize() { } public override void Shutdown() { + _receivedAllAppearancesMsg = false; _appearances.Clear(); _appearanceLoadCallbacks.Clear(); _turfIcons.Clear(); + _filterShaders.Clear(); } public void SetAllAppearances(Dictionary appearances) { _appearances = appearances; + _receivedAllAppearancesMsg = true; + //need to do this because all overlays can't be resolved until the whole appearance table is populated foreach(KeyValuePair pair in _appearances) { pair.Value.ResolveOverlays(this); @@ -73,10 +78,14 @@ public DreamIcon GetTurfIcon(uint turfId) { public void OnNewAppearance(MsgNewAppearance e) { uint appearanceId = e.Appearance.MustGetId(); _appearances[appearanceId] = e.Appearance; - _appearances[appearanceId].ResolveOverlays(this); - if (_appearanceLoadCallbacks.TryGetValue(appearanceId, out var callbacks)) { - foreach (var callback in callbacks) callback(_appearances[appearanceId]); + // If we haven't received the MsgAllAppearances yet, leave this initialization for later + if (_receivedAllAppearancesMsg) { + _appearances[appearanceId].ResolveOverlays(this); + + if (_appearanceLoadCallbacks.TryGetValue(appearanceId, out var callbacks)) { + foreach (var callback in callbacks) callback(_appearances[appearanceId]); + } } } diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 343a6e6166..526276bf1e 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -513,7 +513,7 @@ private DreamPlane GetPlane(int planeIndex, Vector2i viewportSize) { plane = new(renderTarget); _planes.Add(planeIndex, plane); - _sawmill.Info($"Created plane {planeIndex}"); + _sawmill.Verbose($"Created plane {planeIndex}"); return plane; } From 83ee0f1850b225ec36bc1c4ebc877be04ae915e0 Mon Sep 17 00:00:00 2001 From: wixoa Date: Thu, 23 Jan 2025 22:54:09 -0500 Subject: [PATCH 07/14] Fix appearance-load callbacks being called before the appearance is initialized (#2183) --- OpenDreamClient/Rendering/ClientAppearanceSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index 00caf24338..35df689a9e 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -52,7 +52,7 @@ public void SetAllAppearances(Dictionary appearances) } public void LoadAppearance(uint appearanceId, Action loadCallback) { - if (_appearances.TryGetValue(appearanceId, out var appearance)) { + if (_appearances.TryGetValue(appearanceId, out var appearance) && _receivedAllAppearancesMsg) { loadCallback(appearance); return; } From 2b2cd64cf1d650b4c4c3f4c9a1ddc8ce5a3a0c98 Mon Sep 17 00:00:00 2001 From: wixoa Date: Fri, 24 Jan 2025 15:33:38 -0500 Subject: [PATCH 08/14] Only send `MsgNewAppearance` to InGame players (#2184) --- OpenDreamClient/EntryPoint.cs | 10 --------- .../Interface/DummyDreamInterfaceManager.cs | 2 +- .../Rendering/ClientAppearanceSystem.cs | 4 ++-- OpenDreamRuntime/DreamManager.Connections.cs | 1 - .../Rendering/ServerAppearanceSystem.cs | 14 +++++------- OpenDreamShared/Dream/ImmutableAppearance.cs | 8 +++---- .../Network/Messages/MsgNewAppearance.cs | 22 ------------------- .../Rendering/SharedAppearanceSystem.cs | 5 ++--- 8 files changed, 15 insertions(+), 51 deletions(-) delete mode 100644 OpenDreamShared/Network/Messages/MsgNewAppearance.cs diff --git a/OpenDreamClient/EntryPoint.cs b/OpenDreamClient/EntryPoint.cs index 5860becfaa..00e2b13b6c 100644 --- a/OpenDreamClient/EntryPoint.cs +++ b/OpenDreamClient/EntryPoint.cs @@ -85,7 +85,6 @@ public override void PostInit() { IoCManager.Resolve().Initialize(); _netManager.RegisterNetMessage(RxAllAppearances); - _netManager.RegisterNetMessage(RxNewAppearance); if (_configurationManager.GetCVar(CVars.DisplayCompat)) _dreamInterface.OpenAlert( @@ -113,15 +112,6 @@ private void RxAllAppearances(MsgAllAppearances message) { clientAppearanceSystem.SetAllAppearances(message.AllAppearances); } - private void RxNewAppearance(MsgNewAppearance message) { - if (!_entitySystemManager.TryGetEntitySystem(out var clientAppearanceSystem)) { - Logger.GetSawmill("opendream").Error("Received MsgNewAppearance before initializing entity systems"); - return; - } - - clientAppearanceSystem.OnNewAppearance(message); - } - // As of RobustToolbox v0.90.0.0 there's a TileEdgeOverlay that breaks our rendering // because we don't have an ITileDefinition for each tile. // This removes that overlay immediately after MapSystem adds it. diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 61d4c06dff..ebba9c9ccd 100644 --- a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs @@ -1,5 +1,4 @@ using OpenDreamClient.Interface.Controls; -using OpenDreamClient.Interface.Descriptors; using OpenDreamShared.Dream; using OpenDreamShared.Network.Messages; using Robust.Shared.Network; @@ -20,6 +19,7 @@ public sealed class DummyDreamInterfaceManager : IDreamInterfaceManager { public ControlMap? DefaultMap => null; public ViewRange View => new(5); public bool ShowPopupMenus => true; + [Dependency] private readonly IClientNetManager _netManager = default!; public void Initialize() { diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index 35df689a9e..fd02bc198a 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -6,7 +6,6 @@ using OpenDreamClient.Resources; using OpenDreamClient.Resources.ResourceTypes; using Robust.Shared.Timing; -using OpenDreamShared.Network.Messages; namespace OpenDreamClient.Rendering; @@ -25,6 +24,7 @@ internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { [Dependency] private readonly DMISpriteSystem _spriteSystem = default!; public override void Initialize() { + SubscribeNetworkEvent(OnNewAppearance); SubscribeNetworkEvent(e => _appearances.Remove(e.AppearanceId)); SubscribeNetworkEvent(OnAnimation); SubscribeLocalEvent(OnWorldAABB); @@ -75,7 +75,7 @@ public DreamIcon GetTurfIcon(uint turfId) { return icon; } - public void OnNewAppearance(MsgNewAppearance e) { + public void OnNewAppearance(NewAppearanceEvent e) { uint appearanceId = e.Appearance.MustGetId(); _appearances[appearanceId] = e.Appearance; diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index b7f8524ed0..e8373517c8 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -72,7 +72,6 @@ private void InitializeConnectionManager() { _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); - _netManager.RegisterNetMessage(); var topicPort = _config.GetCVar(OpenDreamCVars.TopicPort); var worldTopicAddress = new IPEndPoint(IPAddress.Loopback, topicPort); diff --git a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs index 0091f4f64b..c2afa5d9f1 100644 --- a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs @@ -1,17 +1,16 @@ using OpenDreamShared.Dream; using Robust.Server.Player; using Robust.Shared.Enums; -using SharedAppearanceSystem = OpenDreamShared.Rendering.SharedAppearanceSystem; using System.Diagnostics.CodeAnalysis; using OpenDreamShared.Network.Messages; using Robust.Shared.Player; -using Robust.Shared.Network; using System.Diagnostics; +using SharedAppearanceSystem = OpenDreamShared.Rendering.SharedAppearanceSystem; namespace OpenDreamRuntime.Rendering; public sealed class ServerAppearanceSystem : SharedAppearanceSystem { - public readonly ImmutableAppearance DefaultAppearance; + public ImmutableAppearance DefaultAppearance = default!; /// /// Each appearance gets a unique ID when marked as registered. Here we store these as a key -> weakref in a weaktable, which does not count @@ -28,10 +27,9 @@ public sealed class ServerAppearanceSystem : SharedAppearanceSystem { private readonly Dictionary _idToAppearance = new(); private uint _counter; - [Dependency] private readonly IServerNetManager _networkManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - public ServerAppearanceSystem() { + public override void Initialize() { DefaultAppearance = new ImmutableAppearance(MutableAppearance.Default, this); DefaultAppearance.MarkRegistered(_counter++); //first appearance registered gets id 0, this is the blank default appearance ProxyWeakRef proxyWeakRef = new(DefaultAppearance); @@ -40,9 +38,7 @@ public ServerAppearanceSystem() { //leaving this in as a sanity check for mutable and immutable appearance hashcodes covering all the same vars //if this debug assert fails, you've probably changed appearance var and not updated its counterpart Debug.Assert(DefaultAppearance.GetHashCode() == MutableAppearance.Default.GetHashCode()); - } - public override void Initialize() { _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } @@ -75,7 +71,8 @@ private void RegisterAppearance(ImmutableAppearance immutableAppearance) { ProxyWeakRef proxyWeakRef = new(immutableAppearance); _appearanceLookup.Add(proxyWeakRef); _idToAppearance.Add(immutableAppearance.MustGetId(), proxyWeakRef); - _networkManager.ServerSendToAll(new MsgNewAppearance(immutableAppearance)); + + RaiseNetworkEvent(new NewAppearanceEvent(immutableAppearance)); } public ImmutableAppearance AddAppearance(MutableAppearance appearance, bool registerAppearance = true) { @@ -98,6 +95,7 @@ public ImmutableAppearance AddAppearance(ImmutableAppearance appearance, bool re } //this should only be called by the ImmutableAppearance's finalizer + [Access(typeof(ImmutableAppearance))] public override void RemoveAppearance(ImmutableAppearance appearance) { lock (_lock) { ProxyWeakRef proxyWeakRef = new(appearance); diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs index 5688237d6d..97bffc6d38 100644 --- a/OpenDreamShared/Dream/ImmutableAppearance.cs +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -23,12 +23,9 @@ namespace OpenDreamShared.Dream; */ // TODO: Wow this is huge! Probably look into splitting this by most used/least used to reduce the size of these -[Serializable] +[Serializable, NetSerializable] public sealed class ImmutableAppearance : IEquatable { private uint? _registeredId; - private bool _needsFinalizer; - private int? _storedHashCode; - private readonly SharedAppearanceSystem? _appearanceSystem; [ViewVariables] public readonly string Name = MutableAppearance.Default.Name; [ViewVariables] public readonly string? Desc = MutableAppearance.Default.Desc; @@ -71,6 +68,9 @@ public sealed class ImmutableAppearance : IEquatable { // PixelOffset2 behaves the same as PixelOffset in top-down mode, so this is used public Vector2i TotalPixelOffset => PixelOffset + PixelOffset2; + [NonSerialized] private readonly SharedAppearanceSystem? _appearanceSystem; + [NonSerialized] private bool _needsFinalizer; + [NonSerialized] private int? _storedHashCode; [NonSerialized] private List? _overlayIDs; [NonSerialized] private List? _underlayIDs; diff --git a/OpenDreamShared/Network/Messages/MsgNewAppearance.cs b/OpenDreamShared/Network/Messages/MsgNewAppearance.cs deleted file mode 100644 index e40973a781..0000000000 --- a/OpenDreamShared/Network/Messages/MsgNewAppearance.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Lidgren.Network; -using OpenDreamShared.Dream; -using Robust.Shared.Network; -using Robust.Shared.Serialization; - -namespace OpenDreamShared.Network.Messages; - -public sealed class MsgNewAppearance: NetMessage { - public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - - public MsgNewAppearance() : this(new ImmutableAppearance(MutableAppearance.Default, null)) {} - public MsgNewAppearance(ImmutableAppearance appearance) => Appearance = appearance; - public ImmutableAppearance Appearance; - - public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { - Appearance = new ImmutableAppearance(buffer, serializer); - } - - public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - Appearance.WriteToBuffer(buffer,serializer); - } -} diff --git a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs index 5f6d0101fb..dfae4c5ebf 100644 --- a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs +++ b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs @@ -10,9 +10,8 @@ public abstract class SharedAppearanceSystem : EntitySystem { public abstract void RemoveAppearance(ImmutableAppearance appearance); [Serializable, NetSerializable] - public sealed class NewAppearanceEvent(uint appearanceId, MutableAppearance appearance) : EntityEventArgs { - public uint AppearanceId { get; } = appearanceId; - public MutableAppearance Appearance { get; } = appearance; + public sealed class NewAppearanceEvent(ImmutableAppearance appearance) : EntityEventArgs { + public ImmutableAppearance Appearance { get; } = appearance; } [Serializable, NetSerializable] From 471411176ed06de2f803fa37d54bd113fbab719a Mon Sep 17 00:00:00 2001 From: wixoa Date: Sat, 25 Jan 2025 02:41:02 -0500 Subject: [PATCH 09/14] Make overlay-resolves and load-callbacks separate passes (#2185) --- OpenDreamClient/Rendering/ClientAppearanceSystem.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index fd02bc198a..0786c1c4e8 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -45,9 +45,15 @@ public void SetAllAppearances(Dictionary appearances) //need to do this because all overlays can't be resolved until the whole appearance table is populated foreach(KeyValuePair pair in _appearances) { pair.Value.ResolveOverlays(this); - if (_appearanceLoadCallbacks.TryGetValue(pair.Key, out var callbacks)) { - foreach (var callback in callbacks) callback(pair.Value); - } + } + + // Callbacks called in another pass to ensure all appearances are initialized first + foreach (var callbackPair in _appearanceLoadCallbacks) { + if (!_appearances.TryGetValue(callbackPair.Key, out var appearance)) + continue; + + foreach (var callback in callbackPair.Value) + callback(appearance); } } From d12f5ef40f9e73f0fb6b0465c8995c6f34128405 Mon Sep 17 00:00:00 2001 From: ike709 Date: Sun, 26 Jan 2025 23:40:49 -0600 Subject: [PATCH 10/14] Support `@(XYZ)` raw strings (#2110) Co-authored-by: ike709 Co-authored-by: wixoa --- .../DMProject/Tests/Expression/String/raw2.dm | 7 ++ .../DMProject/Tests/Expression/String/raw3.dm | 7 ++ .../DMPreprocessor/DMPreprocessorLexer.cs | 72 ++++++++++++++++++- 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Expression/String/raw2.dm create mode 100644 Content.Tests/DMProject/Tests/Expression/String/raw3.dm diff --git a/Content.Tests/DMProject/Tests/Expression/String/raw2.dm b/Content.Tests/DMProject/Tests/Expression/String/raw2.dm new file mode 100644 index 0000000000..01f9e97343 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/String/raw2.dm @@ -0,0 +1,7 @@ + +//# issue 380 + +/proc/RunTest() + var/a = @(ZZZ) +asdfZZZ + ASSERT(a == "asdf") \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Expression/String/raw3.dm b/Content.Tests/DMProject/Tests/Expression/String/raw3.dm new file mode 100644 index 0000000000..b750f3610b --- /dev/null +++ b/Content.Tests/DMProject/Tests/Expression/String/raw3.dm @@ -0,0 +1,7 @@ + +//# issue 380 + +/proc/RunTest() + var/a = @(ZZQ) +asdfZZQ + ASSERT(a == "asdf") \ No newline at end of file diff --git a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs index 83a3a3a403..659cc3c03b 100644 --- a/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs +++ b/DMCompiler/Compiler/DMPreprocessor/DMPreprocessorLexer.cs @@ -301,12 +301,31 @@ public Token NextToken(bool ignoreWhitespace = false) { } case '@': { //Raw string char delimiter = Advance(); + var startLoc = CurrentLocation(); + + // @(XYZ) where XYZ is the delimiter + string complexDelimiter = string.Empty; + if (delimiter == '(') { + Advance(); + while (GetCurrent() != ')') { + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + break; + } + + complexDelimiter += GetCurrent(); + Advance(); + } + } TokenTextBuilder.Clear(); TokenTextBuilder.Append('@'); TokenTextBuilder.Append(delimiter); + bool isComplex = complexDelimiter != string.Empty; bool isLong = false; + c = Advance(); if (delimiter == '{') { TokenTextBuilder.Append(c); @@ -314,7 +333,33 @@ public Token NextToken(bool ignoreWhitespace = false) { if (c == '"') isLong = true; } - if (isLong) { + if (isComplex) { + TokenTextBuilder.Append(complexDelimiter); + TokenTextBuilder.Append(')'); + + // Ignore a newline immediately after @(complexDelimiter) + if (c == '\r') c = Advance(); + if (c == '\n') c = Advance(); + + var delimIdx = 0; + do { + TokenTextBuilder.Append(c); + + if (GetCurrent() == complexDelimiter[delimIdx]) delimIdx++; + else delimIdx = 0; + + if (delimIdx == complexDelimiter.Length && c == complexDelimiter[^1]) { // latter check ensures a 1-char delimiter actually matches + break; + } + + c = Advance(); + } while (!AtEndOfSource()); + + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + } + } else if (isLong) { bool nextCharCanTerm = false; Advance(); @@ -335,6 +380,11 @@ public Token NextToken(bool ignoreWhitespace = false) { if (c == '"') nextCharCanTerm = true; } while (!AtEndOfSource()); + + if (AtEndOfSource()) { + _compiler.Emit(WarningCode.BadExpression, startLoc, + "Unterminated string delimiter"); + } } else { while (c != delimiter && !AtLineEnd() && !AtEndOfSource()) { TokenTextBuilder.Append(c); @@ -342,19 +392,31 @@ public Token NextToken(bool ignoreWhitespace = false) { } } - TokenTextBuilder.Append(c); + if (!isComplex) TokenTextBuilder.Append(c); + if (!HandleLineEnd()) Advance(); string text = TokenTextBuilder.ToString(); string value; - if (isLong) { + if (isComplex) { + // Complex strings need to strip @(complexDelimiter) and a potential final newline. Newline after @(complexDelimiter) is already handled + var trimEnd = complexDelimiter.Length; + if (TokenTextBuilder[^(complexDelimiter.Length + 1)] == '\n') trimEnd += 1; + if (TokenTextBuilder[^(complexDelimiter.Length + 2)] == '\r') trimEnd += 1; + var trimStart = 3 + complexDelimiter.Length; // 3 is from these chars: @() + value = TokenTextBuilder.ToString(trimStart, TokenTextBuilder.Length - (trimStart + trimEnd)); + } else if (isLong) { // Long strings ignore a newline immediately after the @{" and before the "} + if (TokenTextBuilder[3] == '\r') + TokenTextBuilder.Remove(3, 1); if (TokenTextBuilder[3] == '\n') TokenTextBuilder.Remove(3, 1); if (TokenTextBuilder[^3] == '\n') TokenTextBuilder.Remove(TokenTextBuilder.Length - 3, 1); + if (TokenTextBuilder[^3] == '\r') + TokenTextBuilder.Remove(TokenTextBuilder.Length - 3, 1); value = TokenTextBuilder.ToString(3, TokenTextBuilder.Length - 5); } else { @@ -639,6 +701,10 @@ private char GetCurrent() { return _current; } + private Location CurrentLocation() { + return new Location(File, _previousLine, _previousColumn, _isDMStandard); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private char Advance() { int value = _source.Read(); From 863f60d58d5a079d2716c23a35f1e44b4245a12e Mon Sep 17 00:00:00 2001 From: ike709 Date: Tue, 28 Jan 2025 08:59:43 -0600 Subject: [PATCH 11/14] Fix `AssignAndPushReferenceValue` ignoring var value coercion (#2190) Co-authored-by: ike709 --- DMCompiler/Optimizer/PeepholeOptimizations.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DMCompiler/Optimizer/PeepholeOptimizations.cs b/DMCompiler/Optimizer/PeepholeOptimizations.cs index 6b2b9ed3e1..9341de9fa4 100644 --- a/DMCompiler/Optimizer/PeepholeOptimizations.cs +++ b/DMCompiler/Optimizer/PeepholeOptimizations.cs @@ -638,6 +638,11 @@ public bool CheckPreconditions(List input, int index) { AnnotatedBytecodeReference assignTarget = firstInstruction.GetArg(0); AnnotatedBytecodeReference pushTarget = secondInstruction.GetArg(0); + // Assigning certain values to certain vars (e.g. setting a SrcField like "dir" to null) + // actually causes the value to be coerced to something different than the value on the stack. + // We don't have a good way to identify those vars at this point, so just restrict the opt to locals and args since those shouldn't have side effects + if (pushTarget.RefType != DMReference.Type.Local && pushTarget.RefType != DMReference.Type.Argument) return false; + return assignTarget.Equals(pushTarget); } From 8434b8e7fe79388ccb9d576d4895d72dbf0c57e7 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 28 Jan 2025 13:16:14 -0500 Subject: [PATCH 12/14] Avoid full-rendering sprites a second time in the mouse map (#2189) --- OpenDreamClient/Rendering/DreamPlane.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index cd54400a4b..b6e351c8d9 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -72,11 +72,13 @@ public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle, Box2 world public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Vector2i renderTargetSize, Box2 worldAABB) { if (Master?.MouseOpacity == MouseOpacity.Transparent) return; + + handle.UseShader(overlay.BlockColorInstance); foreach (var sprite in Sprites) { if (sprite.MouseOpacity == MouseOpacity.Transparent || sprite.ShouldPassMouse) continue; - var texture = sprite.GetTexture(overlay, handle); + var texture = sprite.MainIcon?.LastRenderedTexture; if (texture == null) continue; @@ -90,7 +92,6 @@ public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Ve var colorB = (byte)((hash >> 16) & 0xFF); Color targetColor = new Color(colorR, colorG, colorB); //TODO - this could result in mis-clicks due to hash-collision since we ditch a whole byte. overlay.MouseMapLookup[targetColor] = sprite; - handle.UseShader(overlay.BlockColorInstance); //it seems like you could put this outside the loop, but you can't without breakng the mousemap handle.SetTransform(DreamViewOverlay.CalculateDrawingMatrix(sprite.TransformToApply, pos, texture.Size, renderTargetSize)); handle.DrawTextureRect(texture, new Box2(Vector2.Zero, texture.Size), targetColor); } From fdc07d9091ac53c20105274b753b4377652386bf Mon Sep 17 00:00:00 2001 From: ike709 Date: Tue, 28 Jan 2025 12:18:19 -0600 Subject: [PATCH 13/14] Add some type commands to the disassembler (#2151) Co-authored-by: ike709 --- DMDisassembler/Program.cs | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/DMDisassembler/Program.cs b/DMDisassembler/Program.cs index b5791d7d95..58be41ff93 100644 --- a/DMDisassembler/Program.cs +++ b/DMDisassembler/Program.cs @@ -86,6 +86,7 @@ static void Main(string[] args) { case "d": case "decompile": Decompile(split); break; case "stats": Stats(GetArg()); break; + case "dump-types": DumpTypes(); break; case "test-all": TestAll(); break; case "dump-all": DumpAll(); break; case "help": { @@ -119,6 +120,7 @@ private static void PrintHelp([CanBeNull] string command) { Console.WriteLine("Prints various statistics. Usage: stats [type]"); Console.WriteLine("Options for [type]:"); Console.WriteLine("procs-by-type : Prints the number of proc declarations (not overrides) on each type in descending order"); + Console.WriteLine("subtypes-by-type : Prints the number of direct-descendant subtypes on each type in descending order"); Console.WriteLine("opcode-count : Prints the number of occurrences for each opcode in descending order"); break; } @@ -139,6 +141,7 @@ void AllCommands() { Console.WriteLine("list procs|globals : List all globals, or all procs on a selected type"); Console.WriteLine("decompile|d [name] : Decompiles the proc on the selected type"); Console.WriteLine("stats [type] : Prints various stats about the game. Use \"help stats\" for more info"); + Console.WriteLine("dump-types : Writes a list of every type to a file"); Console.WriteLine("dump-all : Decompiles every proc and writes the output to a file"); Console.WriteLine("test-all : Tries to decompile every single proc to check for issues with this disassembler; not for production use"); } @@ -155,6 +158,10 @@ private static void Stats([CanBeNull] string statType) { ProcsByType(); return; } + case "subtypes-by-type": { + SubtypesByType(); + return; + } case "opcode-count": { OpcodeCount(); return; @@ -189,6 +196,40 @@ void ProcsByType() { } } + void SubtypesByType() { + Console.WriteLine("Counting all subtypes by type. This may take a moment."); + Dictionary typeIdToSubtypeCount = new Dictionary(TypesById.Count); + + foreach (DMType type in TypesById) { + var parent = type.Json.Parent; + if (parent is null) continue; + + if (typeIdToSubtypeCount.TryGetValue(parent.Value, out var count)) { + typeIdToSubtypeCount[parent.Value] = count + 1; + } else { + typeIdToSubtypeCount[parent.Value] = 1; + } + } + + var outputFile = Path.ChangeExtension(JsonFile, ".txt")!; + var name = Path.GetFileName(outputFile); + var path = Path.GetDirectoryName(outputFile)!; + outputFile = Path.Combine(path, $"__od_subtypes-by-type_{name}"); + using StreamWriter writer = new StreamWriter(outputFile, append: false, encoding: Encoding.UTF8, bufferSize: 65536); + + writer.WriteLine("Type: Subtype Count"); + foreach (var pair in typeIdToSubtypeCount.OrderByDescending(kvp => kvp.Value)) { + var type = TypesById[pair.Key]; + if (pair.Key == 0) { + writer.WriteLine($": {pair.Value:n0}"); + } else { + writer.WriteLine($"{type.Path}: {pair.Value:n0}"); + } + } + + Console.WriteLine($"Successfully dumped subtypes-by-type to {outputFile}"); + } + void OpcodeCount() { Console.WriteLine("Counting all opcode occurrences. This may take a moment."); Dictionary opcodeToCount = new Dictionary(); @@ -374,6 +415,22 @@ private static int TestAll() { return errored; } + private static void DumpTypes() { + Console.WriteLine("Dumping all types. This may take a moment."); + + var outputFile = Path.ChangeExtension(JsonFile, ".txt")!; + var name = Path.GetFileName(outputFile); + var path = Path.GetDirectoryName(outputFile)!; + outputFile = Path.Combine(path, $"__od_types_{name}"); + using StreamWriter writer = new StreamWriter(outputFile, append: false, encoding: Encoding.UTF8, bufferSize: 65536); + + foreach (DMType type in TypesById) { + writer.WriteLine(type.Path); + } + + Console.WriteLine($"Successfully dumped {TypesById.Count:n0} types to {outputFile}"); + } + private static void DumpAll() { Console.WriteLine("Dumping all procs. This may take a moment."); int errored = 0, all = 0; @@ -394,7 +451,7 @@ private static void DumpAll() { ++all; } - var procCount = errored > 0 ? $"{all - errored}/{all} ({errored} failed procs)" : $"all {all}"; + var procCount = errored > 0 ? $"{(all - errored):n0}/{all:n0} ({errored:n0} failed procs)" : $"all {all:n0}"; Console.WriteLine($"Successfully dumped {procCount} procs to {outputFile}"); } From 02ee3e18fc4277d57f128776d5cdebdadc29d5e9 Mon Sep 17 00:00:00 2001 From: wixoa Date: Tue, 28 Jan 2025 15:48:59 -0500 Subject: [PATCH 14/14] Avoid recalculating tile visibility when nothing is changing (#2191) --- OpenDreamClient/Rendering/DMISpriteSystem.cs | 23 ++++- .../DreamViewOverlay.TileVisibility.cs | 83 +++++++++++++++++++ OpenDreamClient/Rendering/DreamViewOverlay.cs | 67 +-------------- 3 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 OpenDreamClient/Rendering/DreamViewOverlay.TileVisibility.cs diff --git a/OpenDreamClient/Rendering/DMISpriteSystem.cs b/OpenDreamClient/Rendering/DMISpriteSystem.cs index 562f27a7be..807e04817c 100644 --- a/OpenDreamClient/Rendering/DMISpriteSystem.cs +++ b/OpenDreamClient/Rendering/DMISpriteSystem.cs @@ -1,6 +1,7 @@ using OpenDreamShared.Rendering; using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.Player; using Robust.Shared.GameStates; using Robust.Shared.Timing; @@ -9,10 +10,11 @@ namespace OpenDreamClient.Rendering; public sealed class DMISpriteSystem : EntitySystem { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly MapSystem _mapSystem = default!; [Dependency] private readonly ClientScreenOverlaySystem _screenOverlaySystem = default!; @@ -20,14 +22,18 @@ public sealed class DMISpriteSystem : EntitySystem { public RenderTargetPool RenderTargetPool = default!; + private EntityQuery _spriteQuery; private DreamViewOverlay _mapOverlay = default!; public override void Initialize() { SubscribeLocalEvent(HandleComponentAdd); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(HandleComponentRemove); + SubscribeLocalEvent(HandleTransformMove); + SubscribeLocalEvent(HandleTileChanged); RenderTargetPool = new(_clyde); + _spriteQuery = _entityManager.GetEntityQuery(); _mapOverlay = new DreamViewOverlay(RenderTargetPool, _transformSystem, _mapSystem, _lookupSystem, _appearanceSystem, _screenOverlaySystem, _clientImagesSystem); _overlayManager.AddOverlay(_mapOverlay); } @@ -50,15 +56,28 @@ private void HandleComponentAdd(EntityUid uid, DMISpriteComponent component, ref component.Icon.SizeChanged += () => OnIconSizeChanged(uid); } - private static void HandleComponentState(EntityUid uid, DMISpriteComponent component, ref ComponentHandleState args) { + private void HandleComponentState(EntityUid uid, DMISpriteComponent component, ref ComponentHandleState args) { SharedDMISpriteComponent.DMISpriteComponentState? state = (SharedDMISpriteComponent.DMISpriteComponentState?)args.Current; if (state == null) return; + _mapOverlay.DirtyTileVisibility(); // Our icon's opacity may have changed component.ScreenLocation = state.ScreenLocation; component.Icon.SetAppearance(state.AppearanceId); } + private void HandleTransformMove(EntityUid uid, TransformComponent component, ref MoveEvent args) { + if (!_spriteQuery.TryGetComponent(uid, out var sprite)) + return; + + if (sprite.Icon.Appearance?.Opacity is true || uid == _playerManager.LocalSession?.AttachedEntity) + _mapOverlay.DirtyTileVisibility(); // A movable with opacity=TRUE, or our eye, has moved + } + + private void HandleTileChanged(ref TileChangedEvent ev) { + _mapOverlay.DirtyTileVisibility(); + } + private static void HandleComponentRemove(EntityUid uid, DMISpriteComponent component, ref ComponentRemove args) { component.Icon.Dispose(); } diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.TileVisibility.cs b/OpenDreamClient/Rendering/DreamViewOverlay.TileVisibility.cs new file mode 100644 index 0000000000..eb10285ee8 --- /dev/null +++ b/OpenDreamClient/Rendering/DreamViewOverlay.TileVisibility.cs @@ -0,0 +1,83 @@ +using OpenDreamShared.Dream; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace OpenDreamClient.Rendering; + +internal partial class DreamViewOverlay { + private ViewAlgorithm.Tile?[,]? _tileInfo; + private bool _tileInfoDirty; + + public void DirtyTileVisibility() { + _tileInfoDirty = true; + } + + private ViewAlgorithm.Tile?[,] CalculateTileVisibility(EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, int seeVis) { + using var _ = _prof.Group("visible turfs"); + + var viewRange = _interfaceManager.View; + if (_tileInfo == null || _tileInfo.GetLength(0) != viewRange.Width + 2 || + _tileInfo.GetLength(1) != viewRange.Height + 2) { + // _tileInfo hasn't been created yet or view range has changed, so create a new array. + // Leave a 1 tile buffer on each side + _tileInfo = new ViewAlgorithm.Tile[viewRange.Width + 2, viewRange.Height + 2]; + _tileInfoDirty = true; + } + + if (!_tileInfoDirty) + return _tileInfo; + + var eyeWorldPos = _mapSystem.GridTileToWorld(gridUid, grid, eyeTile.GridIndices); + var tileRefs = _mapSystem.GetTilesEnumerator(gridUid, grid, + Box2.CenteredAround(eyeWorldPos.Position, new Vector2(_tileInfo.GetLength(0), _tileInfo.GetLength(1)))); + + // Gather up all the data the view algorithm needs + while (tileRefs.MoveNext(out var tileRef)) { + var delta = tileRef.GridIndices - eyeTile.GridIndices; + var appearance = _appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId).Appearance; + if (appearance == null) + continue; + + int xIndex = delta.X + viewRange.CenterX; + int yIndex = delta.Y + viewRange.CenterY; + if (xIndex < 0 || yIndex < 0 || xIndex >= _tileInfo.GetLength(0) || yIndex >= _tileInfo.GetLength(1)) + continue; + + var tile = new ViewAlgorithm.Tile { + Opaque = appearance.Opacity, + Luminosity = 0, + DeltaX = delta.X, + DeltaY = delta.Y + }; + + _tileInfo[xIndex, yIndex] = tile; + } + + // Apply entities' opacity + foreach (EntityUid entity in EntitiesInView) { + // TODO use a sprite tree. + if (!_spriteQuery.TryGetComponent(entity, out var sprite)) + continue; + + var transform = _xformQuery.GetComponent(entity); + if (!sprite.IsVisible(transform, seeVis)) + continue; + if (sprite.Icon.Appearance == null) //appearance hasn't loaded yet + continue; + + var worldPos = _transformSystem.GetWorldPosition(transform); + var tilePos = _mapSystem.WorldToTile(gridUid, grid, worldPos) - eyeTile.GridIndices + viewRange.Center; + if (tilePos.X < 0 || tilePos.Y < 0 || tilePos.X >= _tileInfo.GetLength(0) || + tilePos.Y >= _tileInfo.GetLength(1)) + continue; + + var tile = _tileInfo[tilePos.X, tilePos.Y]; + if (tile != null) + tile.Opaque |= sprite.Icon.Appearance.Opacity; + } + + ViewAlgorithm.CalculateVisibility(_tileInfo); + _tileInfoDirty = false; + return _tileInfo; + } +} diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 526276bf1e..ead729a933 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -21,7 +21,7 @@ namespace OpenDreamClient.Rendering; /// /// Overlay for rendering world atoms /// -internal sealed class DreamViewOverlay : Overlay { +internal sealed partial class DreamViewOverlay : Overlay { public static ShaderInstance ColorInstance = default!; public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld; @@ -76,9 +76,6 @@ internal sealed class DreamViewOverlay : Overlay { M22 = -1 }; - // Defined here so it isn't recreated every frame - private ViewAlgorithm.Tile?[,]? _tileInfo; - public DreamViewOverlay(RenderTargetPool renderTargetPool, TransformSystem transformSystem, MapSystem mapSystem, EntityLookupSystem lookupSystem, ClientAppearanceSystem appearanceSystem, ClientScreenOverlaySystem screenOverlaySystem, ClientImagesSystem clientImagesSystem) { IoCManager.InjectDependencies(this); @@ -599,68 +596,6 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { } } - private ViewAlgorithm.Tile?[,] CalculateTileVisibility(EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, int seeVis) { - using var _ = _prof.Group("visible turfs"); - - var viewRange = _interfaceManager.View; - if (_tileInfo == null || _tileInfo.GetLength(0) != viewRange.Width + 2 || _tileInfo.GetLength(1) != viewRange.Height + 2) { - // _tileInfo hasn't been created yet or view range has changed, so create a new array. - // Leave a 1 tile buffer on each side - _tileInfo = new ViewAlgorithm.Tile[viewRange.Width + 2, viewRange.Height + 2]; - } - - var eyeWorldPos = _mapSystem.GridTileToWorld(gridUid, grid, eyeTile.GridIndices); - var tileRefs = _mapSystem.GetTilesEnumerator(gridUid, grid, - Box2.CenteredAround(eyeWorldPos.Position, new Vector2(_tileInfo.GetLength(0), _tileInfo.GetLength(1)))); - - // Gather up all the data the view algorithm needs - while (tileRefs.MoveNext(out var tileRef)) { - var delta = tileRef.GridIndices - eyeTile.GridIndices; - var appearance = _appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId).Appearance; - if (appearance == null) - continue; - - int xIndex = delta.X + viewRange.CenterX; - int yIndex = delta.Y + viewRange.CenterY; - if (xIndex < 0 || yIndex < 0 || xIndex >= _tileInfo.GetLength(0) || yIndex >= _tileInfo.GetLength(1)) - continue; - - var tile = new ViewAlgorithm.Tile { - Opaque = appearance.Opacity, - Luminosity = 0, - DeltaX = delta.X, - DeltaY = delta.Y - }; - - _tileInfo[xIndex, yIndex] = tile; - } - - // Apply entities' opacity - foreach (EntityUid entity in EntitiesInView) { - // TODO use a sprite tree. - if (!_spriteQuery.TryGetComponent(entity, out var sprite)) - continue; - - var transform = _xformQuery.GetComponent(entity); - if (!sprite.IsVisible(transform, seeVis)) - continue; - if (sprite.Icon.Appearance == null) //appearance hasn't loaded yet - continue; - - var worldPos = _transformSystem.GetWorldPosition(transform); - var tilePos = _mapSystem.WorldToTile(gridUid, grid, worldPos) - eyeTile.GridIndices + viewRange.Center; - if (tilePos.X < 0 || tilePos.Y < 0 || tilePos.X >= _tileInfo.GetLength(0) || tilePos.Y >= _tileInfo.GetLength(1)) - continue; - - var tile = _tileInfo[tilePos.X, tilePos.Y]; - if (tile != null) - tile.Opaque |= sprite.Icon.Appearance.Opacity; - } - - ViewAlgorithm.CalculateVisibility(_tileInfo); - return _tileInfo; - } - private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridUid, MapGridComponent grid, TileRef eyeTile, sbyte seeVis, SightFlags sight, Box2 worldAABB) { _spriteContainer.Clear();