diff --git a/Content.IntegrationTests/DMProject/Tests/icons.dmi b/Content.IntegrationTests/DMProject/Tests/icons.dmi new file mode 100644 index 0000000000..401b72d431 Binary files /dev/null and b/Content.IntegrationTests/DMProject/Tests/icons.dmi differ diff --git a/Content.Tests/DMProject/Tests/Image/subclass.dm b/Content.IntegrationTests/DMProject/Tests/image.dm similarity index 85% rename from Content.Tests/DMProject/Tests/Image/subclass.dm rename to Content.IntegrationTests/DMProject/Tests/image.dm index 1a8010a04a..e436c22df3 100644 --- a/Content.Tests/DMProject/Tests/Image/subclass.dm +++ b/Content.IntegrationTests/DMProject/Tests/image.dm @@ -2,7 +2,9 @@ plane = 123 icon_state = "subclass" -/proc/RunTest() +/proc/test_images() + ASSERT(image('icons.dmi', "mob") != null) + var/image/test = new /image/subclass ASSERT(test.plane == 123) ASSERT(test.icon_state == "subclass") diff --git a/Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm b/Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm similarity index 86% rename from Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm rename to Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm index 8f01871b2c..e0a8432b69 100644 --- a/Content.Tests/DMProject/Tests/Statements/For/nonlocal_var.dm +++ b/Content.IntegrationTests/DMProject/Tests/nonlocal_var.dm @@ -10,6 +10,6 @@ out += dir ASSERT(out == 14) -/proc/RunTest() +/proc/test_nonlocal_var() var/mob/m = new m.dodir() diff --git a/Content.IntegrationTests/DMProject/code.dm b/Content.IntegrationTests/DMProject/code.dm index 22379ac075..894fc35758 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -30,5 +30,7 @@ test_color_matrix() test_range() test_verb_duplicate() + test_nonlocal_var() + test_images() test_filter_init() 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 f58687b514..aa33082512 100644 --- a/Content.IntegrationTests/DMProject/environment.dme +++ b/Content.IntegrationTests/DMProject/environment.dme @@ -3,6 +3,8 @@ #include "Tests/color_matrix.dm" #include "Tests/range.dm" #include "Tests/verb_duplicate.dm" +#include "Tests/nonlocal_var.dm" +#include "Tests/image.dm" #include "Tests/filter_initial.dm" #include "map.dmm" #include "interface.dmf" \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Image/Image.dm b/Content.Tests/DMProject/Tests/Image/Image.dm deleted file mode 100644 index b50648f3e6..0000000000 --- a/Content.Tests/DMProject/Tests/Image/Image.dm +++ /dev/null @@ -1,2 +0,0 @@ -/proc/RunTest() - ASSERT(image('icons.dmi', "mob") != null) \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Savefile/ExportText.dm b/Content.Tests/DMProject/Tests/Savefile/ExportText.dm index b99c840a7c..7ecb792a14 100644 --- a/Content.Tests/DMProject/Tests/Savefile/ExportText.dm +++ b/Content.Tests/DMProject/Tests/Savefile/ExportText.dm @@ -1,8 +1,9 @@ -/obj/savetest - var/obj/savetest/recurse = null +/datum/savetest + var/name + var/datum/savetest/recurse = null /proc/RunTest() - var/obj/savetest/O = new() //create a test object + var/datum/savetest/O = new() //create a test object O.name = "test" //O.recurse = O //TODO diff --git a/Content.Tests/DummyDreamMapManager.cs b/Content.Tests/DummyDreamMapManager.cs index 12bece3217..7a5de816d4 100644 --- a/Content.Tests/DummyDreamMapManager.cs +++ b/Content.Tests/DummyDreamMapManager.cs @@ -26,9 +26,9 @@ public void InitializeAtoms(List? maps) { } public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcArguments creationArguments) { } - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance) { } + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance) { } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { } + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance) { } public void SetArea(DreamObjectTurf turf, DreamObjectArea area) { } diff --git a/DMCompiler/DMStandard/Types/Image.dm b/DMCompiler/DMStandard/Types/Image.dm index 2b4a00575e..b4dfc5546a 100644 --- a/DMCompiler/DMStandard/Types/Image.dm +++ b/DMCompiler/DMStandard/Types/Image.dm @@ -1,7 +1,7 @@ /image parent_type = /datum - //note these values also need to be set in IconAppearance.cs + //note these values also need to be set in MutableAppearance.cs var/alpha = 255 var/appearance var/appearance_flags = 0 diff --git a/OpenDreamClient/EntryPoint.cs b/OpenDreamClient/EntryPoint.cs index 00e2b13b6c..5860becfaa 100644 --- a/OpenDreamClient/EntryPoint.cs +++ b/OpenDreamClient/EntryPoint.cs @@ -85,6 +85,7 @@ public override void PostInit() { IoCManager.Resolve().Initialize(); _netManager.RegisterNetMessage(RxAllAppearances); + _netManager.RegisterNetMessage(RxNewAppearance); if (_configurationManager.GetCVar(CVars.DisplayCompat)) _dreamInterface.OpenAlert( @@ -112,6 +113,15 @@ 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/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index 42ce42f878..f760189b27 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -43,7 +43,7 @@ public ContextMenuPopup() { _metadataQuery = _entityManager.GetEntityQuery(); } - public void RepopulateEntities(ClientObjectReference[] entities, int? turfId) { + public void RepopulateEntities(ClientObjectReference[] entities, uint? turfId) { ContextMenu.RemoveAllChildren(); if (_transformSystem == null) diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 0c90fa271b..160b7aa761 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -106,14 +106,14 @@ public void HandleStatClick(string atomRef, bool isRight, bool isMiddle) { } } - private (ClientObjectReference Atom, Vector2i IconPosition)? GetTurfUnderMouse(MapCoordinates mapCoords, out int? turfId) { + private (ClientObjectReference Atom, Vector2i IconPosition)? GetTurfUnderMouse(MapCoordinates mapCoords, out uint? turfId) { // Grid coordinates are half a meter off from entity coordinates mapCoords = new MapCoordinates(mapCoords.Position + new Vector2(0.5f), mapCoords.MapId); if (_mapManager.TryFindGridAt(mapCoords, out var gridEntity, out var grid)) { Vector2i position = _mapSystem.CoordinatesToTile(gridEntity, grid, _mapSystem.MapToGrid(gridEntity, mapCoords)); _mapSystem.TryGetTile(grid, position, out Tile tile); - turfId = tile.TypeId; + turfId = (uint)tile.TypeId; Vector2i turfIconPosition = (Vector2i) ((mapCoords.Position - position) * EyeManager.PixelsPerMeter); MapCoordinates worldPosition = _mapSystem.GridTileToWorld(gridEntity, grid, position); diff --git a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index 6d8d2d273c..dc0c97c588 100644 --- a/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs +++ b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs @@ -44,25 +44,25 @@ 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, IconAppearance.Default.Name); - AddPropertyIfNotDefault("Icon State", appearance.IconState, IconAppearance.Default.IconState); - AddPropertyIfNotDefault("Direction", appearance.Direction, IconAppearance.Default.Direction); - AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, IconAppearance.Default.InheritsDirection); - AddPropertyIfNotDefault("Pixel Offset X/Y", appearance.PixelOffset, IconAppearance.Default.PixelOffset); - AddPropertyIfNotDefault("Pixel Offset W/Z", appearance.PixelOffset2, IconAppearance.Default.PixelOffset2); - AddPropertyIfNotDefault("Color", appearance.Color, IconAppearance.Default.Color); - AddPropertyIfNotDefault("Alpha", appearance.Alpha, IconAppearance.Default.Alpha); - AddPropertyIfNotDefault("Glide Size", appearance.GlideSize, IconAppearance.Default.GlideSize); - AddPropertyIfNotDefault("Layer", appearance.Layer, IconAppearance.Default.Layer); - AddPropertyIfNotDefault("Plane", appearance.Plane, IconAppearance.Default.Plane); - AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, IconAppearance.Default.BlendMode); - AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, IconAppearance.Default.AppearanceFlags); - AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, IconAppearance.Default.Invisibility); - AddPropertyIfNotDefault("Opacity", appearance.Opacity, IconAppearance.Default.Opacity); - AddPropertyIfNotDefault("Override", appearance.Override, IconAppearance.Default.Override); - AddPropertyIfNotDefault("Render Source", appearance.RenderSource, IconAppearance.Default.RenderSource); - AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, IconAppearance.Default.RenderTarget); - AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, IconAppearance.Default.MouseOpacity); + AddPropertyIfNotDefault("Name", appearance.Name, MutableAppearance.Default.Name); + AddPropertyIfNotDefault("Icon State", appearance.IconState, MutableAppearance.Default.IconState); + AddPropertyIfNotDefault("Direction", appearance.Direction, MutableAppearance.Default.Direction); + AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, MutableAppearance.Default.InheritsDirection); + AddPropertyIfNotDefault("Pixel Offset X/Y", appearance.PixelOffset, MutableAppearance.Default.PixelOffset); + AddPropertyIfNotDefault("Pixel Offset W/Z", appearance.PixelOffset2, MutableAppearance.Default.PixelOffset2); + AddPropertyIfNotDefault("Color", appearance.Color, MutableAppearance.Default.Color); + AddPropertyIfNotDefault("Alpha", appearance.Alpha, MutableAppearance.Default.Alpha); + AddPropertyIfNotDefault("Glide Size", appearance.GlideSize, MutableAppearance.Default.GlideSize); + AddPropertyIfNotDefault("Layer", appearance.Layer, MutableAppearance.Default.Layer); + AddPropertyIfNotDefault("Plane", appearance.Plane, MutableAppearance.Default.Plane); + AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, MutableAppearance.Default.BlendMode); + AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, MutableAppearance.Default.AppearanceFlags); + AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, MutableAppearance.Default.Invisibility); + AddPropertyIfNotDefault("Opacity", appearance.Opacity, MutableAppearance.Default.Opacity); + AddPropertyIfNotDefault("Override", appearance.Override, MutableAppearance.Default.Override); + AddPropertyIfNotDefault("Render Source", appearance.RenderSource, MutableAppearance.Default.RenderSource); + AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, MutableAppearance.Default.RenderTarget); + AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, MutableAppearance.Default.MouseOpacity); foreach (var overlay in _icon.Overlays) { AddDreamIconButton(OverlaysGrid, overlay); diff --git a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs index c9c7cdf28a..b47ec0f558 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -6,13 +6,14 @@ using OpenDreamClient.Resources; using OpenDreamClient.Resources.ResourceTypes; using Robust.Shared.Timing; +using OpenDreamShared.Network.Messages; namespace OpenDreamClient.Rendering; internal sealed class ClientAppearanceSystem : SharedAppearanceSystem { - private Dictionary _appearances = new(); - private readonly Dictionary>> _appearanceLoadCallbacks = new(); - private readonly Dictionary _turfIcons = new(); + private Dictionary _appearances = new(); + private readonly Dictionary>> _appearanceLoadCallbacks = new(); + private readonly Dictionary _turfIcons = new(); private readonly Dictionary _filterShaders = new(); [Dependency] private readonly IEntityManager _entityManager = default!; @@ -23,7 +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); } @@ -34,19 +35,21 @@ public override void Shutdown() { _turfIcons.Clear(); } - public void SetAllAppearances(Dictionary appearances) { + public void SetAllAppearances(Dictionary appearances) { _appearances = appearances; - - foreach (KeyValuePair pair in _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); } } } - public void LoadAppearance(int appearanceId, Action loadCallback) { + public void LoadAppearance(uint appearanceId, Action loadCallback) { if (_appearances.TryGetValue(appearanceId, out var appearance)) { loadCallback(appearance); + return; } if (!_appearanceLoadCallbacks.ContainsKey(appearanceId)) { @@ -56,8 +59,8 @@ public void LoadAppearance(int appearanceId, Action loadCallback _appearanceLoadCallbacks[appearanceId].Add(loadCallback); } - public DreamIcon GetTurfIcon(int turfId) { - int appearanceId = turfId - 1; + public DreamIcon GetTurfIcon(uint turfId) { + uint appearanceId = turfId; if (!_turfIcons.TryGetValue(appearanceId, out var icon)) { icon = new DreamIcon(_spriteSystem.RenderTargetPool, _gameTiming, _clyde, this, appearanceId); @@ -67,22 +70,31 @@ public DreamIcon GetTurfIcon(int turfId) { return icon; } - private void OnNewAppearance(NewAppearanceEvent e) { - _appearances[e.AppearanceId] = e.Appearance; + public void OnNewAppearance(MsgNewAppearance e) { + uint appearanceId = e.Appearance.MustGetID(); + _appearances[appearanceId] = e.Appearance; + _appearances[appearanceId].ResolveOverlays(this); - if (_appearanceLoadCallbacks.TryGetValue(e.AppearanceId, out var callbacks)) { - foreach (var callback in callbacks) callback(e.Appearance); + if (_appearanceLoadCallbacks.TryGetValue(appearanceId, out var callbacks)) { + foreach (var callback in callbacks) callback(_appearances[appearanceId]); } } private void OnAnimation(AnimationEvent e) { - EntityUid ent = _entityManager.GetEntity(e.Entity); - if (!_entityManager.TryGetComponent(ent, out var sprite)) - return; - - LoadAppearance(e.TargetAppearanceId, targetAppearance => { - sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); - }); + if(e.Entity == NetEntity.Invalid && e.TurfId is not null) { //it's a turf or area + if(_turfIcons.TryGetValue(e.TurfId.Value-1, out var turfIcon)) + LoadAppearance(e.TargetAppearanceId, targetAppearance => { + turfIcon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); + }); + } else { //image or movable + EntityUid ent = _entityManager.GetEntity(e.Entity); + if (!_entityManager.TryGetComponent(ent, out var sprite)) + return; + + LoadAppearance(e.TargetAppearanceId, targetAppearance => { + sprite.Icon.StartAppearanceAnimation(targetAppearance, e.Duration, e.Easing, e.Loop, e.Flags, e.Delay, e.ChainAnim); + }); + } } private void OnWorldAABB(EntityUid uid, DMISpriteComponent comp, ref WorldAABBEvent e) { @@ -197,4 +209,12 @@ public ShaderInstance GetFilterShader(DreamFilter filter, Dictionary CalculateAnimatedAppearance(); private set { if (_appearance?.Equals(value) is true) @@ -42,7 +42,12 @@ private set { UpdateIcon(); } } - private IconAppearance? _appearance; + + private ImmutableAppearance? _appearance; + + //acts as a cache for the mutable appearance, so we don't have to ToMutable() every frame + private MutableAppearance? _animatedAppearance; + private AtomDirection _direction; // TODO: We could cache these per-appearance instead of per-atom public IRenderTexture? CachedTexture { @@ -65,7 +70,7 @@ private set { private bool _animationComplete; private IRenderTexture? _cachedTexture; - public DreamIcon(RenderTargetPool renderTargetPool, IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem, int appearanceId, + public DreamIcon(RenderTargetPool renderTargetPool, IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem, uint appearanceId, AtomDirection? parentDir = null) : this(renderTargetPool, gameTiming, clyde, appearanceSystem) { SetAppearance(appearanceId, parentDir); } @@ -88,12 +93,12 @@ public void Dispose() { return CachedTexture.Texture; _textureDirty = false; - frame = DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[animationFrame]; + frame = DMI.GetState(Appearance.IconState)?.GetFrames(_direction)[animationFrame]; } else { frame = textureOverride; } - var canSkipFullRender = Appearance?.Filters.Count is 0 or null && + var canSkipFullRender = Appearance?.Filters.Length is 0 or null && iconMetaData.ColorToApply == Color.White && iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity) && iconMetaData.AlphaToApply.Equals(1.0f); @@ -110,7 +115,7 @@ public void Dispose() { return CachedTexture?.Texture; } - public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { + public void SetAppearance(uint? appearanceId, AtomDirection? parentDir = null) { // End any animations that are currently happening // Note that this isn't faithful to the original behavior EndAppearanceAnimation(null); @@ -122,9 +127,9 @@ public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { appearanceSystem.LoadAppearance(appearanceId.Value, appearance => { if (parentDir != null && appearance.InheritsDirection) { - appearance = new IconAppearance(appearance) { - Direction = parentDir.Value - }; + _direction = parentDir.Value; + } else { + _direction = appearance.Direction; } Appearance = appearance; @@ -132,7 +137,7 @@ public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { } //three things to do here, chained animations, loops and parallel animations - public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan duration, AnimationEasing easing, int loops, AnimationFlags flags, int delay, bool chainAnim) { + public void StartAppearanceAnimation(ImmutableAppearance endingAppearance, TimeSpan duration, AnimationEasing easing, int loops, AnimationFlags flags, int delay, bool chainAnim) { _appearance = CalculateAnimatedAppearance(); //Animation starts from the current animated appearance DateTime start = DateTime.Now; if(!chainAnim) @@ -156,7 +161,7 @@ public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan d _appearanceAnimations[i] = lastAnim; break; } - + _appearanceAnimations.Add(new AppearanceAnimation(start, duration, endingAppearance, easing, flags, delay, true)); } @@ -205,7 +210,7 @@ private void UpdateAnimation() { DMIParser.ParsedDMIState? dmiState = DMI.Description.GetStateOrDefault(Appearance.IconState); if(dmiState == null) return; - DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(Appearance.Direction); + DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(_direction); if (frames.Length <= 1) return; @@ -232,12 +237,14 @@ private void UpdateAnimation() { DirtyTexture(); } - private IconAppearance? CalculateAnimatedAppearance() { - if (_appearanceAnimations == null || _appearance == null) + private ImmutableAppearance? CalculateAnimatedAppearance() { + if (_appearanceAnimations == null || _appearanceAnimations.Count == 0 || _appearance == null) { + _animatedAppearance = null; //null it if _appearanceAnimations is empty return _appearance; + } _textureDirty = true; //if we have animations, we need to recalculate the texture - IconAppearance appearance = new IconAppearance(_appearance); + _animatedAppearance = _appearance.ToMutable(); List? toRemove = null; List? toReAdd = null; for(int i = 0; i < _appearanceAnimations.Count; i++) { @@ -295,7 +302,7 @@ private void UpdateAnimation() { break; } - IconAppearance endAppearance = animation.EndAppearance; + var endAppearance = animation.EndAppearance; //non-smooth animations /* @@ -308,16 +315,16 @@ private void UpdateAnimation() { */ if (endAppearance.Direction != _appearance.Direction) { - appearance.Direction = endAppearance.Direction; + _animatedAppearance.Direction = endAppearance.Direction; } if (endAppearance.Icon != _appearance.Icon) { - appearance.Icon = endAppearance.Icon; + _animatedAppearance.Icon = endAppearance.Icon; } if (endAppearance.IconState != _appearance.IconState) { - appearance.IconState = endAppearance.IconState; + _animatedAppearance.IconState = endAppearance.IconState; } if (endAppearance.Invisibility != _appearance.Invisibility) { - appearance.Invisibility = endAppearance.Invisibility; + _animatedAppearance.Invisibility = endAppearance.Invisibility; } /* TODO maptext if (endAppearance.MapText != _appearance.MapText) { @@ -344,11 +351,11 @@ private void UpdateAnimation() { */ if (endAppearance.Alpha != _appearance.Alpha) { - appearance.Alpha = (byte)Math.Clamp(((1-factor) * _appearance.Alpha) + (factor * endAppearance.Alpha), 0, 255); + _animatedAppearance.Alpha = (byte)Math.Clamp(((1-factor) * _appearance.Alpha) + (factor * endAppearance.Alpha), 0, 255); } if (endAppearance.Color != _appearance.Color) { - appearance.Color = Color.FromSrgb(new Color( + _animatedAppearance.Color = Color.FromSrgb(new Color( Math.Clamp(((1-factor) * _appearance.Color.R) + (factor * endAppearance.Color.R), 0, 1), Math.Clamp(((1-factor) * _appearance.Color.G) + (factor * endAppearance.Color.G), 0, 1), Math.Clamp(((1-factor) * _appearance.Color.B) + (factor * endAppearance.Color.B), 0, 1), @@ -357,12 +364,12 @@ private void UpdateAnimation() { } if (!endAppearance.ColorMatrix.Equals(_appearance.ColorMatrix)){ - ColorMatrix.Interpolate(ref _appearance.ColorMatrix, ref endAppearance.ColorMatrix, factor, out appearance.ColorMatrix); + ColorMatrix.Interpolate(in _appearance.ColorMatrix, in endAppearance.ColorMatrix, factor, out _animatedAppearance.ColorMatrix); } if (endAppearance.GlideSize != _appearance.GlideSize) { - appearance.GlideSize = ((1-factor) * _appearance.GlideSize) + (factor * endAppearance.GlideSize); + _animatedAppearance.GlideSize = ((1-factor) * _appearance.GlideSize) + (factor * endAppearance.GlideSize); } /* TODO infraluminosity @@ -372,7 +379,7 @@ private void UpdateAnimation() { */ if (endAppearance.Layer != _appearance.Layer) { - appearance.Layer = ((1-factor) * _appearance.Layer) + (factor * endAppearance.Layer); + _animatedAppearance.Layer = ((1-factor) * _appearance.Layer) + (factor * endAppearance.Layer); } /* TODO luminosity @@ -400,26 +407,26 @@ private void UpdateAnimation() { */ if (endAppearance.PixelOffset != _appearance.PixelOffset) { - Vector2 startingOffset = appearance.PixelOffset; + Vector2 startingOffset = _appearance.PixelOffset; Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset, 1.0f-factor); - appearance.PixelOffset = (Vector2i)newPixelOffset; + _animatedAppearance.PixelOffset = (Vector2i)newPixelOffset; } if (endAppearance.PixelOffset2 != _appearance.PixelOffset2) { - Vector2 startingOffset = appearance.PixelOffset2; + Vector2 startingOffset = _appearance.PixelOffset2; Vector2 newPixelOffset = Vector2.Lerp(startingOffset, endAppearance.PixelOffset2, 1.0f-factor); - appearance.PixelOffset2 = (Vector2i)newPixelOffset; + _animatedAppearance.PixelOffset2 = (Vector2i)newPixelOffset; } if (!endAppearance.Transform.SequenceEqual(_appearance.Transform)) { - appearance.Transform[0] = (1.0f-factor)*_appearance.Transform[0] + (factor * endAppearance.Transform[0]); - appearance.Transform[1] = (1.0f-factor)*_appearance.Transform[1] + (factor * endAppearance.Transform[1]); - appearance.Transform[2] = (1.0f-factor)*_appearance.Transform[2] + (factor * endAppearance.Transform[2]); - appearance.Transform[3] = (1.0f-factor)*_appearance.Transform[3] + (factor * endAppearance.Transform[3]); - appearance.Transform[4] = (1.0f-factor)*_appearance.Transform[4] + (factor * endAppearance.Transform[4]); - appearance.Transform[5] = (1.0f-factor)*_appearance.Transform[5] + (factor * endAppearance.Transform[5]); + _animatedAppearance.Transform[0] = (1.0f-factor)*_appearance.Transform[0] + (factor * endAppearance.Transform[0]); + _animatedAppearance.Transform[1] = (1.0f-factor)*_appearance.Transform[1] + (factor * endAppearance.Transform[1]); + _animatedAppearance.Transform[2] = (1.0f-factor)*_appearance.Transform[2] + (factor * endAppearance.Transform[2]); + _animatedAppearance.Transform[3] = (1.0f-factor)*_appearance.Transform[3] + (factor * endAppearance.Transform[3]); + _animatedAppearance.Transform[4] = (1.0f-factor)*_appearance.Transform[4] + (factor * endAppearance.Transform[4]); + _animatedAppearance.Transform[5] = (1.0f-factor)*_appearance.Transform[5] + (factor * endAppearance.Transform[5]); } if (timeFactor >= 1f) { @@ -451,7 +458,7 @@ private void UpdateAnimation() { _appearanceAnimations.Add(animation); } - return appearance; + return new(_animatedAppearance, null); //one of the very few times it's okay to do this. } private void UpdateIcon() { @@ -475,16 +482,16 @@ private void UpdateIcon() { } Overlays.Clear(); - foreach (int overlayId in Appearance.Overlays) { - DreamIcon overlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, overlayId, Appearance.Direction); + foreach (var overlayAppearance in Appearance.Overlays) { + DreamIcon overlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, overlayAppearance.MustGetID(), _direction); overlay.SizeChanged += CheckSizeChange; Overlays.Add(overlay); } Underlays.Clear(); - foreach (int underlayId in Appearance.Underlays) { - DreamIcon underlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, underlayId, Appearance.Direction); + foreach (var underlayAppearance in Appearance.Underlays) { + DreamIcon underlay = new DreamIcon(renderTargetPool, gameTiming, clyde, appearanceSystem, underlayAppearance.MustGetID(), _direction); underlay.SizeChanged += CheckSizeChange; Underlays.Add(underlay); @@ -552,10 +559,10 @@ private void DirtyTexture() { CachedTexture = null; } - private struct AppearanceAnimation(DateTime start, TimeSpan duration, IconAppearance endAppearance, AnimationEasing easing, AnimationFlags flags, int delay, bool lastInSequence) { + private struct AppearanceAnimation(DateTime start, TimeSpan duration, ImmutableAppearance endAppearance, AnimationEasing easing, AnimationFlags flags, int delay, bool lastInSequence) { public readonly DateTime Start = start; public readonly TimeSpan Duration = duration; - public readonly IconAppearance EndAppearance = endAppearance; + public readonly ImmutableAppearance EndAppearance = endAppearance; public readonly AnimationEasing Easing = easing; public readonly AnimationFlags Flags = flags; public readonly int Delay = delay; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index 9fa9171912..d189af04b3 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -201,7 +201,7 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u current.ColorMatrixToApply = icon.Appearance.ColorMatrix; } else { current.ColorToApply = parentIcon.ColorToApply * icon.Appearance.Color; - ColorMatrix.Multiply(ref parentIcon.ColorMatrixToApply, ref icon.Appearance.ColorMatrix, out current.ColorMatrixToApply); + ColorMatrix.Multiply(in parentIcon.ColorMatrixToApply, in icon.Appearance.ColorMatrix, out current.ColorMatrixToApply); } if ((icon.Appearance.AppearanceFlags & AppearanceFlags.ResetAlpha) != 0 || keepTogether) //RESET_ALPHA @@ -316,9 +316,10 @@ private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid u continue; if(sprite.Icon.Appearance == null) continue; - if(sprite.Icon.Appearance.Override) + if(sprite.Icon.Appearance.Override) { current.MainIcon = sprite.Icon; - else + current.Position = current.Position + (sprite.Icon.Appearance.TotalPixelOffset / (float)EyeManager.PixelsPerMeter); + } else ProcessIconComponents(sprite.Icon, current.Position, uid, isScreen, ref tieBreaker, result, current); } } @@ -561,7 +562,7 @@ private void DrawPlanes(DrawingHandleWorld handle, Box2 worldAABB) { // 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(tileRef.Tile.TypeId).Appearance; + var appearance = _appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId).Appearance; if (appearance == null) continue; @@ -627,7 +628,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU tValue = 0; //pass the turf coords for client.images lookup Vector3 turfCoords = new Vector3(tileRef.X, tileRef.Y, (int) worldPos.MapId); - ProcessIconComponents(_appearanceSystem.GetTurfIcon(tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, ref tValue, _spriteContainer, turfCoords: turfCoords); + ProcessIconComponents(_appearanceSystem.GetTurfIcon((uint)tileRef.Tile.TypeId), worldPos.Position - Vector2.One, EntityUid.Invalid, false, ref tValue, _spriteContainer, turfCoords: turfCoords); } // Visible entities diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 32d2bb5ba9..4932b052e3 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -30,12 +30,20 @@ public sealed class AtomManager { private int _nextEmptyTurfSlot; private readonly Dictionary _entityToAtom = new(); - private readonly Dictionary _definitionAppearanceCache = new(); - - private ServerAppearanceSystem AppearanceSystem => _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); + private readonly Dictionary _definitionAppearanceCache = new(); + + private ServerAppearanceSystem? AppearanceSystem{ + get { + if(_appearanceSystem is null) + _entitySystemManager.TryGetEntitySystem(out _appearanceSystem); + return _appearanceSystem; + } + } private ServerVerbSystem VerbSystem => _verbSystem ??= _entitySystemManager.GetEntitySystem(); private ServerAppearanceSystem? _appearanceSystem; private ServerVerbSystem? _verbSystem; + private DMISpriteSystem DMISpriteSystem => _dmiSpriteSystem ??= _entitySystemManager.GetEntitySystem(); + private DMISpriteSystem? _dmiSpriteSystem; // ReSharper disable ForCanBeConvertedToForeach (the collections could be added to) public IEnumerable EnumerateAtoms(TreeEntry? filterType = null) { @@ -191,7 +199,7 @@ public EntityUid CreateMovableEntity(DreamObjectMovable movable) { var entity = _entityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); DMISpriteComponent sprite = _entityManager.AddComponent(entity); - sprite.SetAppearance(GetAppearanceFromDefinition(movable.ObjectDefinition)); + DMISpriteSystem.SetSpriteAppearance(new(entity, sprite), GetAppearanceFromDefinition(movable.ObjectDefinition)); _entityToAtom.Add(entity, movable); return entity; @@ -242,7 +250,7 @@ public bool IsValidAppearanceVar(string name) { } } - public void SetAppearanceVar(IconAppearance appearance, string varName, DreamValue value) { + public void SetAppearanceVar(MutableAppearance appearance, string varName, DreamValue value) { switch (varName) { case "name": value.TryGetValueAsString(out var name); @@ -383,7 +391,12 @@ public void SetAppearanceVar(IconAppearance appearance, string varName, DreamVal } } - public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { + //TODO THIS IS A SUPER NASTY HACK + public DreamValue GetAppearanceVar(MutableAppearance appearance, string varName) { + return GetAppearanceVar(new ImmutableAppearance(appearance, null), varName); + } + + public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varName) { switch (varName) { case "name": return new(appearance.Name); @@ -456,7 +469,7 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { return new(matrix); case "appearance": - IconAppearance appearanceCopy = new IconAppearance(appearance); // Return a copy + MutableAppearance appearanceCopy = appearance.ToMutable(); // Return a copy return new(appearanceCopy); // These should be handled by an atom if referenced through one @@ -464,13 +477,11 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { case "underlays": // In BYOND this just creates a new normal list var lays = varName == "overlays" ? appearance.Overlays : appearance.Underlays; - var list = _objectTree.CreateList(lays.Count); + var list = _objectTree.CreateList(lays.Length); if (_appearanceSystem != null) { - foreach (var layId in lays) { - var lay = _appearanceSystem.MustGetAppearance(layId); - - list.AddValue(new(lay)); + foreach (var lay in lays) { + list.AddValue(new(lay.ToMutable())); } } @@ -484,15 +495,15 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { } /// - /// Gets an atom's appearance. + /// Gets an atom's appearance. Will throw if the appearance system is not available. /// /// The atom to find the appearance of. - public IconAppearance? MustGetAppearance(DreamObject atom) { + public ImmutableAppearance MustGetAppearance(DreamObject atom) { return atom switch { - DreamObjectTurf turf => AppearanceSystem.MustGetAppearance(turf.AppearanceId), - DreamObjectMovable movable => movable.SpriteComponent.Appearance, - DreamObjectArea area => AppearanceSystem.MustGetAppearance(area.AppearanceId), - DreamObjectImage image => image.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}") }; } @@ -500,80 +511,122 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { /// /// 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 IconAppearance? appearance) { + public bool TryGetAppearance(DreamObject atom, [NotNullWhen(true)] out ImmutableAppearance? appearance) { if (atom is DreamObjectTurf turf) - appearance = AppearanceSystem.MustGetAppearance(turf.AppearanceId); - else if (atom is DreamObjectMovable movable) + appearance = turf.Appearance; + else if (atom is DreamObjectMovable movable && movable.SpriteComponent.Appearance is not null) appearance = movable.SpriteComponent.Appearance; else if (atom is DreamObjectImage image) - appearance = image.Appearance; + appearance = image.IsMutableAppearance ? AppearanceSystem!.AddAppearance(image.MutableAppearance!, registerAppearance: false) : image.SpriteComponent?.Appearance; else if (atom is DreamObjectArea area) - appearance = AppearanceSystem.MustGetAppearance(area.AppearanceId); + appearance = area.Appearance; else appearance = null; return appearance is not null; } - public void UpdateAppearance(DreamObject atom, Action update) { - var appearance = MustGetAppearance(atom); - appearance = (appearance != null) ? new(appearance) : new(); // Clone the appearance - + public void UpdateAppearance(DreamObject atom, Action update) { + ImmutableAppearance immutableAppearance = MustGetAppearance(atom); + MutableAppearance appearance = immutableAppearance.ToMutable(); // Clone the appearance update(appearance); SetAtomAppearance(atom, appearance); } - public void SetAtomAppearance(DreamObject atom, IconAppearance appearance) { + public void SetAtomAppearance(DreamObject atom, MutableAppearance appearance) { if (atom is DreamObjectTurf turf) { _dreamMapManager.SetTurfAppearance(turf, appearance); } else if (atom is DreamObjectMovable movable) { - movable.SpriteComponent.SetAppearance(appearance); + DMISpriteSystem.SetSpriteAppearance(new(movable.Entity, movable.SpriteComponent), appearance); } else if (atom is DreamObjectImage image) { - image.Appearance = appearance; + if(image.IsMutableAppearance) + image.MutableAppearance = new(appearance); //this needs to be a copy + else + DMISpriteSystem.SetSpriteAppearance(new(image.Entity, image.SpriteComponent!), appearance); } else if (atom is DreamObjectArea area) { _dreamMapManager.SetAreaAppearance(area, appearance); } } - public void AnimateAppearance(DreamObject atom, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, Action animate) { - if (atom is not DreamObjectMovable movable) - return; //Animating non-movables is unimplemented TODO: should handle images and maybe filters + public void SetMovableScreenLoc(DreamObjectMovable movable, ScreenLocation screenLocation) { + DMISpriteSystem.SetSpriteScreenLocation(new(movable.Entity, movable.SpriteComponent), screenLocation); + } - IconAppearance appearance = new IconAppearance(movable.SpriteComponent.Appearance); + public void SetSpriteAppearance(Entity ent, MutableAppearance appearance) { + DMISpriteSystem.SetSpriteAppearance(ent, appearance); + } - animate(appearance); + public void AnimateAppearance(DreamObject atom, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, Action animate) { + MutableAppearance appearance; + EntityUid targetEntity; + DMISpriteComponent? targetComponent = null; + NetEntity ent = NetEntity.Invalid; + uint? turfId = null; + + if (atom is DreamObjectMovable movable) { + targetEntity = movable.Entity; + targetComponent = movable.SpriteComponent; + appearance = MustGetAppearance(atom).ToMutable(); + } else if (atom is DreamObjectImage image && !image.IsMutableAppearance){ + targetEntity = image.Entity; + targetComponent = image.SpriteComponent; + appearance = MustGetAppearance(atom).ToMutable(); + } else if (atom is DreamObjectTurf turf) { + targetEntity = EntityUid.Invalid; + appearance = turf.Appearance.ToMutable(); + } else if (atom is DreamObjectArea area) { + return; + //TODO: animate area appearance + //area appearance should be an overlay on turfs, so could maybe get away with animating that? + } else if (atom is DreamObjectClient client) { + return; + //TODO: animate client appearance + } else if (atom is DreamObjectFilter filter) { + return; + //TODO: animate filters + } else + throw new ArgumentException($"Cannot animate appearance of {atom}"); - // Don't send the updated appearance to clients, they will animate it - movable.SpriteComponent.SetAppearance(appearance, dirty: false); + animate(appearance); - NetEntity ent = _entityManager.GetNetEntity(movable.Entity); + if(targetComponent is not null) { + ent = _entityManager.GetNetEntity(targetEntity); + // Don't send the updated appearance to clients, they will animate it + DMISpriteSystem.SetSpriteAppearance(new(targetEntity, targetComponent), appearance, dirty: false); + } else if (atom is DreamObjectTurf turf) { + //TODO: turf appearances are just set to the end appearance, they do not get properly animated + _dreamMapManager.SetTurfAppearance(turf, appearance); + turfId = turf.Appearance.MustGetID(); + } else if (atom is DreamObjectArea area) { + //fuck knows, this will trigger a bunch of turf updates to? idek + } - AppearanceSystem.Animate(ent, appearance, duration, easing, loop, flags, delay, chainAnim); + AppearanceSystem?.Animate(ent, appearance, duration, easing, loop, flags, delay, chainAnim, turfId); } - public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out IconAppearance? appearance) { + public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out MutableAppearance? appearance) { if (value.TryGetValueAsAppearance(out var copyFromAppearance)) { appearance = new(copyFromAppearance); return true; } if (value.TryGetValueAsDreamObject(out var copyFromImage)) { - appearance = new(copyFromImage.Appearance!); + appearance = MustGetAppearance(copyFromImage).ToMutable(); return true; } if (value.TryGetValueAsType(out var copyFromType)) { - appearance = GetAppearanceFromDefinition(copyFromType.ObjectDefinition); + appearance = new(GetAppearanceFromDefinition(copyFromType.ObjectDefinition)); return true; } if (value.TryGetValueAsDreamObject(out var copyFromAtom)) { - appearance = new(MustGetAppearance(copyFromAtom)); + appearance = MustGetAppearance(copyFromAtom).ToMutable(); return true; } if (_resourceManager.TryLoadIcon(value, out var iconResource)) { - appearance = new IconAppearance() { + appearance = new MutableAppearance() { Icon = iconResource.Id }; @@ -584,7 +637,7 @@ public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out Ic return false; } - public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { + public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { if (_definitionAppearanceCache.TryGetValue(def, out var appearance)) return appearance; @@ -607,7 +660,7 @@ public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { def.TryGetVariable("blend_mode", out var blendModeVar); def.TryGetVariable("appearance_flags", out var appearanceFlagsVar); - appearance = new IconAppearance(); + appearance = new MutableAppearance(); SetAppearanceVar(appearance, "name", nameVar); SetAppearanceVar(appearance, "icon", iconVar); SetAppearanceVar(appearance, "icon_state", stateVar); diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 0c79b2389b..935518968d 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -71,6 +71,7 @@ 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/DreamManager.cs b/OpenDreamRuntime/DreamManager.cs index 20aacd3428..5c85902e51 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -240,7 +240,7 @@ public string CreateRef(DreamValue value) { } else if (value.TryGetValueAsAppearance(out var appearance)) { refType = RefType.DreamAppearance; _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); - idx = (int)_appearanceSystem.AddAppearance(appearance); + idx = (int)_appearanceSystem.AddAppearance(appearance).MustGetID(); } else if (value.TryGetValueAsDreamResource(out var refRsc)) { refType = RefType.DreamResource; idx = refRsc.Id; @@ -323,8 +323,8 @@ public DreamValue LocateRef(string refString) { return new DreamValue(resource); case RefType.DreamAppearance: _appearanceSystem ??= _entitySystemManager.GetEntitySystem(); - return _appearanceSystem.TryGetAppearance(refId, out IconAppearance? appearance) - ? new DreamValue(appearance) + return _appearanceSystem.TryGetAppearanceById((uint) refId, out ImmutableAppearance? appearance) + ? new DreamValue(appearance.ToMutable()) : DreamValue.Null; case RefType.Proc: return new(_objectTree.Procs[refId]); diff --git a/OpenDreamRuntime/DreamMapManager.cs b/OpenDreamRuntime/DreamMapManager.cs index 89864ab959..d5f0c0683d 100644 --- a/OpenDreamRuntime/DreamMapManager.cs +++ b/OpenDreamRuntime/DreamMapManager.cs @@ -162,7 +162,7 @@ private void SetTurf(Vector2i pos, int z, DreamObjectDefinition type, DreamProcA cell.Turf.SetTurfType(type); - IconAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); + MutableAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); SetTurfAppearance(cell.Turf, turfAppearance); cell.Turf.InitSpawn(creationArguments); @@ -176,40 +176,56 @@ public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcA /// Caches the turf/area appearance pair instead of recreating and re-registering it for every turf in the game. /// This is cleared out when an area appearance changes /// - private readonly Dictionary, IconAppearance> _turfAreaLookup = new(); + private readonly Dictionary, MutableAppearance> _turfAreaLookup = new(); - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance) { - if(turf.Cell.Area.AppearanceId != 0) - if(!appearance.Overlays.Contains(turf.Cell.Area.AppearanceId)) { - if(!_turfAreaLookup.TryGetValue((appearance, turf.Cell.Area.AppearanceId), out var newAppearance)) { + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance) { + if(turf.Cell.Area.Appearance != _appearanceSystem.DefaultAppearance) + if(!appearance.Overlays.Contains(turf.Cell.Area.Appearance)) { + if(!_turfAreaLookup.TryGetValue((appearance, turf.Cell.Area.Appearance.MustGetID()), out var newAppearance)) { newAppearance = new(appearance); - newAppearance.Overlays.Add(turf.Cell.Area.AppearanceId); - _turfAreaLookup.Add((appearance, turf.Cell.Area.AppearanceId), newAppearance); + newAppearance.Overlays.Add(turf.Cell.Area.Appearance); + _turfAreaLookup.Add((appearance, turf.Cell.Area.Appearance.MustGetID()), newAppearance); } appearance = newAppearance; } - int appearanceId = _appearanceSystem.AddAppearance(appearance); + var immutableAppearance = _appearanceSystem.AddAppearance(appearance); var level = _levels[turf.Z - 1]; - int turfId = (appearanceId + 1); // +1 because 0 is used for empty turfs - level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile(turfId); - turf.AppearanceId = appearanceId; + uint turfId = immutableAppearance.MustGetID(); + level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile((int)turfId); + turf.Appearance = immutableAppearance; } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance) { //if an area changes appearance, invalidate the lookup _turfAreaLookup.Clear(); - int oldAppearance = area.AppearanceId; - area.AppearanceId = _appearanceSystem.AddAppearance(appearance); - foreach (var turf in area.Turfs) { - var turfAppearance = _atomManager.MustGetAppearance(turf); + var oldAppearance = area.Appearance; + appearance.AppearanceFlags |= AppearanceFlags.ResetColor | AppearanceFlags.ResetAlpha | AppearanceFlags.ResetTransform; + area.Appearance = _appearanceSystem.AddAppearance(appearance); + + //get all unique turf appearances + //create the new version of each of those appearances + //for each turf, update the appropriate ID - if(turfAppearance is null) continue; + Dictionary oldToNewAppearance = new(); + foreach (var turf in area.Turfs) { + if(oldToNewAppearance.TryGetValue(turf.Appearance, out var newAppearance)) + turf.Appearance = newAppearance; + else { + MutableAppearance turfAppearance = _atomManager.MustGetAppearance(turf).ToMutable(); + + turfAppearance.Overlays.Remove(oldAppearance); + turfAppearance.Overlays.Add(area.Appearance); + newAppearance = _appearanceSystem.AddAppearance(turfAppearance); + oldToNewAppearance.Add(turf.Appearance, newAppearance); + turf.Appearance = newAppearance; + } - turfAppearance.Overlays.Remove(oldAppearance); - SetTurfAppearance(turf, turfAppearance); + var level = _levels[turf.Z - 1]; + uint turfId = newAppearance.MustGetID(); + level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile((int)turfId); } } @@ -471,8 +487,8 @@ public Cell(DreamObjectArea area, DreamObjectTurf turf) { public void UpdateTiles(); public void SetTurf(DreamObjectTurf turf, DreamObjectDefinition type, DreamProcArguments creationArguments); - public void SetTurfAppearance(DreamObjectTurf turf, IconAppearance appearance); - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance); + public void SetTurfAppearance(DreamObjectTurf turf, MutableAppearance appearance); + public void SetAreaAppearance(DreamObjectArea area, MutableAppearance appearance); public bool TryGetCellAt(Vector2i pos, int z, [NotNullWhen(true)] out Cell? cell); public bool TryGetTurfAt(Vector2i pos, int z, [NotNullWhen(true)] out DreamObjectTurf? turf); public void SetZLevels(int levels); diff --git a/OpenDreamRuntime/DreamValue.cs b/OpenDreamRuntime/DreamValue.cs index 99102d0b6c..5990080892 100644 --- a/OpenDreamRuntime/DreamValue.cs +++ b/OpenDreamRuntime/DreamValue.cs @@ -102,7 +102,7 @@ public DreamValue(DreamProc value) { _refValue = value; } - public DreamValue(IconAppearance appearance) { + public DreamValue(MutableAppearance appearance) { Type = DreamValueType.Appearance; _refValue = appearance; } @@ -315,9 +315,9 @@ public DreamProc MustGetValueAsProc() { throw new InvalidCastException("Value " + this + " was not the expected type of DreamProc"); } - public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out IconAppearance? args) { + public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out MutableAppearance? args) { if (Type == DreamValueType.Appearance) { - args = Unsafe.As(_refValue)!; + args = Unsafe.As(_refValue)!; return true; } @@ -326,9 +326,9 @@ public readonly bool TryGetValueAsAppearance([NotNullWhen(true)] out IconAppeara return false; } - public IconAppearance MustGetValueAsAppearance() { + public MutableAppearance MustGetValueAsAppearance() { if (Type == DreamValueType.Appearance) { - return Unsafe.As(_refValue)!; + return Unsafe.As(_refValue)!; } throw new InvalidCastException("Value " + this + " was not the expected type of Appearance"); diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 793886360b..6b088cfdcf 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -581,7 +581,7 @@ public override DreamValue GetValue(DreamValue key) { throw new Exception($"Invalid index into verbs list: {key}"); var verbs = GetVerbs(); - if (index < 1 || index > verbs.Count) + if (index < 1 || index > verbs.Length) throw new Exception($"Out of bounds index on verbs list: {index}"); return new DreamValue(verbSystem.GetVerb(verbs[index - 1])); @@ -592,7 +592,7 @@ public override List GetValues() { if (appearance == null || verbSystem == null) return new List(); - var values = new List(appearance.Verbs.Count); + var values = new List(appearance.Verbs.Length); foreach (var verbId in appearance.Verbs) { var verb = verbSystem.GetVerb(verbId); @@ -631,11 +631,11 @@ public override void Cut(int start = 1, int end = 0) { } public override int GetLength() { - return GetVerbs().Count; + return GetVerbs().Length; } - private List GetVerbs() { - IconAppearance? appearance = atomManager.MustGetAppearance(atom); + private int[] GetVerbs() { + var appearance = atomManager.MustGetAppearance(atom); if (appearance == null) throw new Exception("Atom has no appearance"); @@ -648,7 +648,6 @@ private List GetVerbs() { public sealed class DreamOverlaysList : DreamList { [Dependency] private readonly AtomManager _atomManager = default!; private readonly ServerAppearanceSystem? _appearanceSystem; - private readonly DreamObject _owner; private readonly bool _isUnderlays; @@ -665,13 +664,11 @@ public override List GetValues() { if (appearance == null || _appearanceSystem == null) return new List(); - var overlays = GetOverlaysList(appearance); - var values = new List(overlays.Count); + var overlays = GetOverlaysArray(appearance); + var values = new List(overlays.Length); foreach (var overlay in overlays) { - var overlayAppearance = _appearanceSystem.MustGetAppearance(overlay); - - values.Add(new(overlayAppearance)); + values.Add(new(overlay.ToMutable())); } return values; @@ -679,10 +676,9 @@ public override List GetValues() { public override void Cut(int start = 1, int end = 0) { _atomManager.UpdateAppearance(_owner, appearance => { - List overlaysList = GetOverlaysList(appearance); + var overlaysList = GetOverlaysList(appearance); int count = overlaysList.Count + 1; if (end == 0 || end > count) end = count; - overlaysList.RemoveRange(start - 1, end - start); }); } @@ -691,17 +687,16 @@ public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var overlayIndex) || overlayIndex < 1) throw new Exception($"Invalid index into {(_isUnderlays ? "underlays" : "overlays")} list: {key}"); - IconAppearance appearance = GetAppearance(); - List overlaysList = GetOverlaysList(appearance); - if (overlayIndex > overlaysList.Count) - throw new Exception($"Atom only has {overlaysList.Count} {(_isUnderlays ? "underlay" : "overlay")}(s), cannot index {overlayIndex}"); + ImmutableAppearance appearance = _atomManager.MustGetAppearance(_owner); + var overlaysList = GetOverlaysArray(appearance); + if (overlayIndex > overlaysList.Length) + throw new Exception($"Atom only has {overlaysList.Length} {(_isUnderlays ? "underlay" : "overlay")}(s), cannot index {overlayIndex}"); if (_appearanceSystem == null) return DreamValue.Null; - int overlayId = GetOverlaysList(appearance)[overlayIndex - 1]; - IconAppearance overlayAppearance = _appearanceSystem.MustGetAppearance(overlayId); - return new DreamValue(overlayAppearance); + ImmutableAppearance overlayAppearance = overlaysList[overlayIndex - 1]; + return new DreamValue(overlayAppearance.ToMutable()); } public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { @@ -712,11 +707,14 @@ public override void AddValue(DreamValue value) { if (_appearanceSystem == null) return; - _atomManager.UpdateAppearance(_owner, appearance => { - IconAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, appearance.Icon); - overlayAppearance ??= new IconAppearance(); + MutableAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, _atomManager.MustGetAppearance(_owner).Icon); + overlayAppearance ??= new MutableAppearance(); + ImmutableAppearance immutableOverlay = _appearanceSystem.AddAppearance(overlayAppearance); - GetOverlaysList(appearance).Add(_appearanceSystem.AddAppearance(overlayAppearance)); + //after UpdateApparance is done, the atom is set with a new immutable appearance containing a hard ref to the overlay + //only /mutable_appearance handles it differently, and that's done in DreamObjectImage + _atomManager.UpdateAppearance(_owner, appearance => { + GetOverlaysList(appearance).Add(immutableOverlay); }); } @@ -724,36 +722,32 @@ public override void RemoveValue(DreamValue value) { if (_appearanceSystem == null) return; - _atomManager.UpdateAppearance(_owner, appearance => { - IconAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, appearance.Icon); - if (overlayAppearance == null || !_appearanceSystem.TryGetAppearanceId(overlayAppearance, out var id)) - return; + MutableAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, _atomManager.MustGetAppearance(_owner).Icon); + if (overlayAppearance == null) + return; - GetOverlaysList(appearance).Remove(id); + _atomManager.UpdateAppearance(_owner, appearance => { + GetOverlaysList(appearance).Remove(_appearanceSystem.AddAppearance(overlayAppearance, registerAppearance:false)); }); } public override int GetLength() { - return GetOverlaysList(GetAppearance()).Count; + return GetOverlaysArray(_atomManager.MustGetAppearance(_owner)).Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private List GetOverlaysList(IconAppearance appearance) => + private List GetOverlaysList(MutableAppearance appearance) => _isUnderlays ? appearance.Underlays : appearance.Overlays; - private IconAppearance GetAppearance() { - IconAppearance? appearance = _atomManager.MustGetAppearance(_owner); - if (appearance == null) - throw new Exception("Atom has no appearance"); - - return appearance; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ImmutableAppearance[] GetOverlaysArray(ImmutableAppearance appearance) => + _isUnderlays ? appearance.Underlays : appearance.Overlays; - public static IconAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { - IconAppearance overlay; + public static MutableAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { + MutableAppearance overlay; if (value.TryGetValueAsString(out var iconState)) { - overlay = new IconAppearance() { + overlay = new MutableAppearance() { IconState = iconState }; overlay.Icon ??= defaultIcon; @@ -883,9 +877,15 @@ public override void Cut(int start = 1, int end = 0) { } public int GetIndexOfFilter(DreamFilter filter) { - IconAppearance appearance = GetAppearance(); + ImmutableAppearance appearance = GetAppearance(); + int i = 0; + while(i < appearance.Filters.Length) { + if(appearance.Filters[i] == filter) + return i; + i++; + } - return appearance.Filters.IndexOf(filter) + 1; + return -1; } public void SetFilter(int index, DreamFilter? filter) { @@ -910,9 +910,9 @@ public override DreamValue GetValue(DreamValue key) { if (!key.TryGetValueAsInteger(out var filterIndex) || filterIndex < 1) throw new Exception($"Invalid index into filter list: {key}"); - IconAppearance appearance = GetAppearance(); - if (filterIndex > appearance.Filters.Count) - throw new Exception($"Atom only has {appearance.Filters.Count} filter(s), cannot index {filterIndex}"); + ImmutableAppearance appearance = GetAppearance(); + if (filterIndex > appearance.Filters.Length) + throw new Exception($"Atom only has {appearance.Filters.Length} filter(s), cannot index {filterIndex}"); DreamFilter filter = appearance.Filters[filterIndex - 1]; DreamObjectFilter filterObject = ObjectTree.CreateObject(ObjectTree.Filter); @@ -921,8 +921,8 @@ public override DreamValue GetValue(DreamValue key) { } public override List GetValues() { - IconAppearance appearance = GetAppearance(); - List filterList = new List(appearance.Filters.Count); + ImmutableAppearance appearance = GetAppearance(); + List filterList = new List(appearance.Filters.Length); foreach (var filter in appearance.Filters) { DreamObjectFilter filterObject = ObjectTree.CreateObject(ObjectTree.Filter); @@ -960,11 +960,11 @@ public override void AddValue(DreamValue value) { } public override int GetLength() { - return GetAppearance().Filters.Count; + return GetAppearance().Filters.Length; } - private IconAppearance GetAppearance() { - IconAppearance? appearance = _atomManager.MustGetAppearance(_owner); + private ImmutableAppearance GetAppearance() { + ImmutableAppearance? appearance = _atomManager.MustGetAppearance(_owner); if (appearance == null) throw new Exception("Atom has no appearance"); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs b/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs index e94f2c82d7..2a5d56f77f 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectArea.cs @@ -1,4 +1,6 @@ -namespace OpenDreamRuntime.Objects.Types; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectArea : DreamObjectAtom { public int X { @@ -22,8 +24,8 @@ public int Z { } } + public ImmutableAppearance Appearance; public readonly HashSet Turfs; - public int AppearanceId; private readonly AreaContentsList _contents; @@ -31,9 +33,10 @@ public int Z { private int? _cachedX, _cachedY, _cachedZ; public DreamObjectArea(DreamObjectDefinition objectDefinition) : base(objectDefinition) { + Appearance = AppearanceSystem!.DefaultAppearance; Turfs = new(); - AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); _contents = new(ObjectTree.List.ObjectDefinition, this); + AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } /// diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index df8754eb48..401d298832 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -58,7 +58,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { value = (Desc != null) ? new(Desc) : DreamValue.Null; return true; case "appearance": - var appearanceCopy = new IconAppearance(AtomManager.MustGetAppearance(this)!); + var appearanceCopy = AtomManager.MustGetAppearance(this).ToMutable(); value = new(appearanceCopy); return true; @@ -84,7 +84,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { default: if (AtomManager.IsValidAppearanceVar(varName)) { - var appearance = AtomManager.MustGetAppearance(this)!; + var appearance = AtomManager.MustGetAppearance(this); value = AtomManager.GetAppearanceVar(appearance, varName); return true; @@ -114,7 +114,7 @@ protected override void SetVar(string varName, DreamValue value) { return; // Ignore attempts to set an invalid appearance // The dir does not get changed - newAppearance.Direction = AtomManager.MustGetAppearance(this)!.Direction; + newAppearance.Direction = AtomManager.MustGetAppearance(this).Direction; AtomManager.SetAtomAppearance(this, newAppearance); break; @@ -178,12 +178,7 @@ protected override void SetVar(string varName, DreamValue value) { default: if (AtomManager.IsValidAppearanceVar(varName)) { // Basically AtomManager.UpdateAppearance() but without the performance impact of using actions - var appearance = AtomManager.MustGetAppearance(this); - - // Clone the appearance - // TODO: We can probably avoid cloning while the DMISpriteComponent is dirty - appearance = (appearance != null) ? new(appearance) : new(); - + MutableAppearance appearance = AtomManager.MustGetAppearance(this).ToMutable(); AtomManager.SetAppearanceVar(appearance, varName, value); AtomManager.SetAtomAppearance(this, appearance); break; diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs index 97cfae5a57..b27aed6332 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs @@ -6,13 +6,14 @@ namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectImage : DreamObject { - public IconAppearance? Appearance; - + public EntityUid Entity = EntityUid.Invalid; + public readonly DMISpriteComponent? SpriteComponent; private DreamObject? _loc; private DreamList _overlays; private DreamList _underlays; private readonly DreamList _filters; - private EntityUid _entity = EntityUid.Invalid; + public readonly bool IsMutableAppearance; + public MutableAppearance? MutableAppearance; /// /// All the args in /image/New() after "icon" and "loc", in their correct order @@ -31,20 +32,26 @@ public DreamObjectImage(DreamObjectDefinition objectDefinition) : base(objectDef _overlays = ObjectTree.CreateList(); _underlays = ObjectTree.CreateList(); _filters = ObjectTree.CreateList(); + IsMutableAppearance = true; } else { _overlays = new DreamOverlaysList(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, false); _underlays = new DreamOverlaysList(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, true); _filters = new DreamFilterList(ObjectTree.List.ObjectDefinition, this); + IsMutableAppearance = false; + Entity = EntityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); //spawning an entity in nullspace means it never actually gets sent to any clients until it's placed on the map, or it gets a PVS override + SpriteComponent = EntityManager.AddComponent(Entity); } + + AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } public override void Initialize(DreamProcArguments args) { base.Initialize(args); DreamValue icon = args.GetArgument(0); - if (icon.IsNull || !AtomManager.TryCreateAppearanceFrom(icon, out Appearance)) { + if (icon.IsNull || !AtomManager.TryCreateAppearanceFrom(icon, out var mutableAppearance)) { // Use a default appearance, but log a warning about it if icon wasn't null - Appearance = new(AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); + mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); //object def appearance is created in the constructor if (!icon.IsNull) Logger.GetSawmill("opendream.image") .Warning($"Attempted to create an /image from {icon}. This is invalid and a default image was created instead."); @@ -61,14 +68,16 @@ public override void Initialize(DreamProcArguments args) { if (arg.IsNull) continue; - AtomManager.SetAppearanceVar(Appearance, argName, arg); + AtomManager.SetAppearanceVar(mutableAppearance, argName, arg); if (argName == "dir" && arg.TryGetValueAsInteger(out var argDir) && argDir > 0) { // If a dir is explicitly given in the constructor then overlays using this won't use their owner's dir // Setting dir after construction does not affect this // This is undocumented and I hate it - Appearance.InheritsDirection = false; + mutableAppearance.InheritsDirection = false; } } + + AtomManager.SetAtomAppearance(this, mutableAppearance); } protected override bool TryGetVar(string varName, out DreamValue value) { @@ -89,7 +98,7 @@ protected override bool TryGetVar(string varName, out DreamValue value) { return true; default: { if (AtomManager.IsValidAppearanceVar(varName)) { - value = AtomManager.GetAppearanceVar(Appearance!, varName); + value = IsMutableAppearance ? AtomManager.GetAppearanceVar(MutableAppearance!, varName) : AtomManager.GetAppearanceVar(AtomManager.MustGetAppearance(this), varName); return true; } else { return base.TryGetVar(varName, out value); @@ -105,14 +114,9 @@ protected override void SetVar(string varName, DreamValue value) { return; // Ignore attempts to set an invalid appearance // The dir does not get changed - newAppearance.Direction = Appearance!.Direction; - - Appearance = newAppearance; - if(_entity != EntityUid.Invalid) { - DMISpriteComponent sprite = EntityManager.GetComponent(_entity); - sprite.SetAppearance(Appearance!); - } - + var originalAppearance = AtomManager.MustGetAppearance(this); + newAppearance.Direction = originalAppearance.Direction; + AtomManager.SetAtomAppearance(this, newAppearance); break; case "loc": value.TryGetValueAsDreamObject(out _loc); @@ -128,7 +132,7 @@ protected override void SetVar(string varName, DreamValue value) { if (valueList != null) { _overlays = valueList.CreateCopy(); } else { - var overlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, Appearance?.Icon); + var overlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, AtomManager.MustGetAppearance(this).Icon); if (overlay == null) return; @@ -160,7 +164,7 @@ protected override void SetVar(string varName, DreamValue value) { if (valueList != null) { _underlays = valueList.CreateCopy(); } else { - var underlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, Appearance?.Icon); + var underlay = DreamOverlaysList.CreateOverlayAppearance(AtomManager, value, AtomManager.MustGetAppearance(this).Icon); if (underlay == null) return; @@ -202,17 +206,16 @@ protected override void SetVar(string varName, DreamValue value) { break; } case "override": { - Appearance!.Override = value.IsTruthy(); + MutableAppearance mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + mutableAppearance.Override = value.IsTruthy(); + AtomManager.SetAtomAppearance(this, mutableAppearance); break; } default: if (AtomManager.IsValidAppearanceVar(varName)) { - AtomManager.SetAppearanceVar(Appearance!, varName, value); - if(_entity != EntityUid.Invalid) { - DMISpriteComponent sprite = EntityManager.GetComponent(_entity); - sprite.SetAppearance(Appearance!); - } - + MutableAppearance mutableAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + AtomManager.SetAppearanceVar(mutableAppearance, varName, value); + AtomManager.SetAtomAppearance(this, mutableAppearance); break; } @@ -225,20 +228,6 @@ protected override void SetVar(string varName, DreamValue value) { return this._loc; } - /// - /// Get or create the entity associated with this image. Used for putting this image in the world ie, with vis_contents - /// The associated entity is deleted when the image is. - /// - public EntityUid GetEntity() { - if(_entity == EntityUid.Invalid) { - _entity = EntityManager.SpawnEntity(null, new MapCoordinates(0, 0, MapId.Nullspace)); - DMISpriteComponent sprite = EntityManager.AddComponent(_entity); - sprite.SetAppearance(Appearance!); - } - - return _entity; - } - protected override void HandleDeletion(bool possiblyThreaded) { // SAFETY: Deleting entities is not threadsafe. if (possiblyThreaded) { @@ -246,8 +235,8 @@ protected override void HandleDeletion(bool possiblyThreaded) { return; } - if(_entity != EntityUid.Invalid) { - EntityManager.DeleteEntity(_entity); + if(Entity != EntityUid.Invalid) { + EntityManager.DeleteEntity(Entity); } base.HandleDeletion(possiblyThreaded); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs index ed1a311ddb..a4b3e3d7c4 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMatrix.cs @@ -259,7 +259,8 @@ public override DreamValue OperatorRemove(DreamValue b) { #endregion Operators #region Helpers - /// Used to create a float array understandable by to be a transform. + + /// Used to create a float array understandable by to be a transform. /// The matrix's values in an array, in [a,d,b,e,c,f] order. /// This will not verify that this is a /matrix public static float[] MatrixToTransformFloatArray(DreamObjectMatrix matrix) { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs index 9b38720942..53688032b6 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectMovable.cs @@ -22,15 +22,7 @@ public class DreamObjectMovable : DreamObjectAtom { private string? ScreenLoc { get => _screenLoc; - set { - _screenLoc = value; - if (!EntityManager.TryGetComponent(Entity, out var sprite)) - return; - - sprite.ScreenLocation = !string.IsNullOrEmpty(value) ? - new ScreenLocation(value) : - new ScreenLocation(0, 0, 0, 0); - } + set => SetScreenLoc(value); } private string? _screenLoc; @@ -38,6 +30,7 @@ private string? ScreenLoc { public DreamObjectMovable(DreamObjectDefinition objectDefinition) : base(objectDefinition) { Entity = AtomManager.CreateMovableEntity(this); SpriteComponent = EntityManager.GetComponent(Entity); + AtomManager.SetSpriteAppearance((Entity, SpriteComponent), AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); _transformComponent = EntityManager.GetComponent(Entity); } @@ -204,4 +197,9 @@ private void SetLoc(DreamObjectAtom? loc) { throw new ArgumentException($"Invalid loc {loc}"); } } + + private void SetScreenLoc(string? screenLoc) { + _screenLoc = screenLoc; + AtomManager.SetMovableScreenLoc(this, !string.IsNullOrEmpty(screenLoc) ? new ScreenLocation(screenLoc) : new ScreenLocation(0, 0, 0, 0)); + } } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs index 7248f524f6..57290cc5ba 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs @@ -1,17 +1,21 @@ -namespace OpenDreamRuntime.Objects.Types; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectTurf : DreamObjectAtom { public readonly int X, Y, Z; public readonly TurfContentsList Contents; + public ImmutableAppearance Appearance; public IDreamMapManager.Cell Cell; - public int AppearanceId; public DreamObjectTurf(DreamObjectDefinition objectDefinition, int x, int y, int z) : base(objectDefinition) { X = x; Y = y; Z = z; + Cell = default!; // NEEDS to be set by DreamMapManager after creation Contents = new TurfContentsList(ObjectTree.List.ObjectDefinition, this); + Appearance = AppearanceSystem!.AddAppearance(AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } public void SetTurfType(DreamObjectDefinition objectDefinition) { diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 51a05c774d..742d675ba0 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -2660,7 +2660,7 @@ private static bool IsEqual(DreamValue first, DreamValue second) { if (!second.TryGetValueAsAppearance(out var secondValue)) return false; - IconAppearance firstValue = first.MustGetValueAsAppearance(); + MutableAppearance firstValue = first.MustGetValueAsAppearance(); return firstValue.Equals(secondValue); } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs index fb85fda761..f94f3de8a0 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs @@ -177,7 +177,7 @@ public static (DreamObjectAtom?, ViewRange) ResolveViewArguments(DreamManager dr if (!mapManager.TryGetCellAt((eyePos.X + deltaX, eyePos.Y + deltaY), eyePos.Z, out var cell)) continue; - var appearance = atomManager.MustGetAppearance(cell.Turf!)!; + var appearance = atomManager.MustGetAppearance(cell.Turf); var tile = new ViewAlgorithm.Tile() { Opaque = appearance.Opacity, Luminosity = 0, @@ -186,7 +186,7 @@ public static (DreamObjectAtom?, ViewRange) ResolveViewArguments(DreamManager dr }; foreach (var movable in cell.Movables) { - appearance = atomManager.MustGetAppearance(movable)!; + appearance = atomManager.MustGetAppearance(movable); tile.Opaque |= appearance.Opacity; } diff --git a/OpenDreamRuntime/Rendering/DMISpriteComponent.cs b/OpenDreamRuntime/Rendering/DMISpriteComponent.cs index f89e0b9047..b1ed8c3ebe 100644 --- a/OpenDreamRuntime/Rendering/DMISpriteComponent.cs +++ b/OpenDreamRuntime/Rendering/DMISpriteComponent.cs @@ -1,27 +1,15 @@ using OpenDreamShared.Dream; using OpenDreamShared.Rendering; -namespace OpenDreamRuntime.Rendering { - [RegisterComponent] - public sealed partial class DMISpriteComponent : SharedDMISpriteComponent { - [ViewVariables] - public ScreenLocation? ScreenLocation { - get => _screenLocation; - set { - _screenLocation = value; - Dirty(); - } - } - private ScreenLocation? _screenLocation; +namespace OpenDreamRuntime.Rendering; - [ViewVariables] public IconAppearance? Appearance { get; private set; } +[RegisterComponent] +public sealed partial class DMISpriteComponent : SharedDMISpriteComponent { + [ViewVariables] + [Access(typeof(DMISpriteSystem))] + public ScreenLocation ScreenLocation; - public void SetAppearance(IconAppearance? appearance, bool dirty = true) { - Appearance = appearance; - - if (dirty) { - Dirty(); - } - } - } + [Access(typeof(DMISpriteSystem))] + [ViewVariables] public ImmutableAppearance? Appearance; } + diff --git a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs index 7d7d7c865c..5bafbabe4f 100644 --- a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs +++ b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs @@ -1,21 +1,36 @@ -using OpenDreamShared.Rendering; +using OpenDreamShared.Dream; +using OpenDreamShared.Rendering; using Robust.Shared.GameStates; namespace OpenDreamRuntime.Rendering; public sealed class DMISpriteSystem : EntitySystem { - [Dependency] private readonly ServerAppearanceSystem _appearance = default!; + private ServerAppearanceSystem? _appearance; + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; public override void Initialize() { SubscribeLocalEvent(GetComponentState); + _entitySystemManager.TryGetEntitySystem(out _appearance); } private void GetComponentState(EntityUid uid, DMISpriteComponent component, ref ComponentGetState args) { - int? appearanceId = null; - if (component.Appearance != null) { - appearanceId = _appearance.AddAppearance(component.Appearance); - } + uint? appearanceId = (component.Appearance != null) + ? _appearance?.AddAppearance(component.Appearance).MustGetID() + : null; args.State = new SharedDMISpriteComponent.DMISpriteComponentState(appearanceId, component.ScreenLocation); } + + public void SetSpriteAppearance(Entity ent, MutableAppearance appearance, bool dirty = true) { + DMISpriteComponent component = ent.Comp; + component.Appearance = new ImmutableAppearance(appearance, _appearance); + if(dirty) + Dirty(ent, component); + } + + public void SetSpriteScreenLocation(Entity ent, ScreenLocation screenLocation) { + DMISpriteComponent component = ent.Comp; + component.ScreenLocation = screenLocation; + Dirty(ent, component); + } } diff --git a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs index 8f05b3b686..6bc1a78c2c 100644 --- a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs @@ -5,13 +5,23 @@ using System.Diagnostics.CodeAnalysis; using OpenDreamShared.Network.Messages; using Robust.Shared.Player; +using Robust.Shared.Network; +using System.Diagnostics; +using Robust.Shared.Console; namespace OpenDreamRuntime.Rendering; public sealed class ServerAppearanceSystem : SharedAppearanceSystem { - private readonly Dictionary _appearanceToId = new(); - private readonly Dictionary _idToAppearance = new(); - private int _appearanceIdCounter; + /// + /// 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 + /// as a hard ref but allows quick lookup. Each object which holds an appearance MUST hold that ImmutableAppearance until it is no longer + /// needed or it will be GC'd. Overlays & underlays are stored as hard refs on the ImmutableAppearance so you only need to hold the main appearance. + /// + private readonly HashSet _appearanceLookup = new(); + private readonly Dictionary _idToAppearance = new(); + private uint _counter; + public readonly ImmutableAppearance DefaultAppearance; + [Dependency] private readonly IServerNetManager _networkManager = default!; /// /// This system is used by the PVS thread, we need to be thread-safe @@ -20,62 +30,136 @@ public sealed class ServerAppearanceSystem : SharedAppearanceSystem { [Dependency] private readonly IPlayerManager _playerManager = default!; + public ServerAppearanceSystem() { + 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); + _appearanceLookup.Add(proxyWeakRef); + _idToAppearance.Add(DefaultAppearance.MustGetID(), proxyWeakRef); + //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() { - //register empty appearance as ID 0 - _appearanceToId.Add(IconAppearance.Default, 0); - _idToAppearance.Add(0, IconAppearance.Default); - _appearanceIdCounter = 1; _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } public override void Shutdown() { lock (_lock) { - _appearanceToId.Clear(); + _appearanceLookup.Clear(); _idToAppearance.Clear(); - _appearanceIdCounter = 0; } } private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.NewStatus == SessionStatus.InGame) { - e.Session.Channel.SendMessage(new MsgAllAppearances(_idToAppearance)); + //todo this is probably stupid slow + lock (_lock) { + Dictionary sendData = new(_appearanceLookup.Count); + + foreach(ProxyWeakRef proxyWeakRef in _appearanceLookup){ + if(proxyWeakRef.TryGetTarget(out var immutable)) + sendData.Add(immutable.MustGetID(), immutable); + } + + Logger.GetSawmill("appearance").Debug($"Sending {sendData.Count} appearances to new player {e.Session.Name}"); + e.Session.Channel.SendMessage(new MsgAllAppearances(sendData)); + } } } - public int AddAppearance(IconAppearance appearance) { + private void RegisterAppearance(ImmutableAppearance immutableAppearance) { + immutableAppearance.MarkRegistered(_counter++); //lets this appearance know it needs to do GC finaliser & get an ID + ProxyWeakRef proxyWeakRef = new(immutableAppearance); + _appearanceLookup.Add(proxyWeakRef); + _idToAppearance.Add(immutableAppearance.MustGetID(), proxyWeakRef); + _networkManager.ServerSendToAll(new MsgNewAppearance(immutableAppearance)); + } + + public ImmutableAppearance AddAppearance(MutableAppearance appearance, bool registerAppearance = true) { + ImmutableAppearance immutableAppearance = new(appearance, this); + + return AddAppearance(immutableAppearance, registerAppearance); + } + + public ImmutableAppearance AddAppearance(ImmutableAppearance appearance, bool registerAppearance = true) { lock (_lock) { - if (!_appearanceToId.TryGetValue(appearance, out int appearanceId)) { - appearanceId = _appearanceIdCounter++; - _appearanceToId.Add(appearance, appearanceId); - _idToAppearance.Add(appearanceId, appearance); - RaiseNetworkEvent(new NewAppearanceEvent(appearanceId, appearance)); + if(_appearanceLookup.TryGetValue(new(appearance), out var weakReference) && weakReference.TryGetTarget(out var originalImmutable)) { + return originalImmutable; + } else if (registerAppearance) { + RegisterAppearance(appearance); + return appearance; + } else { + return appearance; } - - return appearanceId; } } - public IconAppearance MustGetAppearance(int appearanceId) { + //this should only be called by the ImmutableAppearance's finalizer + public override void RemoveAppearance(ImmutableAppearance appearance) { lock (_lock) { - return _idToAppearance[appearanceId]; + ProxyWeakRef proxyWeakRef = new(appearance); + if(_appearanceLookup.TryGetValue(proxyWeakRef, out var weakRef)) { + //it is possible that a new appearance was created with the same hash before the GC got around to cleaning up the old one + if(weakRef.TryGetTarget(out var target) && !ReferenceEquals(target,appearance)) + return; + _appearanceLookup.Remove(proxyWeakRef); + _idToAppearance.Remove(appearance.MustGetID()); + RaiseNetworkEvent(new RemoveAppearanceEvent(appearance.MustGetID())); + } } } - public bool TryGetAppearance(int appearanceId, [NotNullWhen(true)] out IconAppearance? appearance) { + public override ImmutableAppearance MustGetAppearanceById(uint appearanceId) { lock (_lock) { - return _idToAppearance.TryGetValue(appearanceId, out appearance); + if(!_idToAppearance[appearanceId].TryGetTarget(out var result)) + throw new Exception($"Attempted to access deleted appearance ID ${appearanceId} in MustGetAppearanceByID()"); + return result; } } - public bool TryGetAppearanceId(IconAppearance appearance, out int appearanceId) { + public bool TryGetAppearanceById(uint appearanceId, [NotNullWhen(true)] out ImmutableAppearance? appearance) { lock (_lock) { - return _appearanceToId.TryGetValue(appearance, out appearanceId); + appearance = null; + return _idToAppearance.TryGetValue(appearanceId, out var appearanceRef) && appearanceRef.TryGetTarget(out appearance); } } - public void Animate(NetEntity entity, IconAppearance targetAppearance, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim) { - int appearanceId = AddAppearance(targetAppearance); + public void Animate(NetEntity entity, MutableAppearance targetAppearance, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, uint? turfId) { + uint appearanceId = AddAppearance(targetAppearance).MustGetID(); + + RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim, turfId)); + } +} + +//this class lets us hold a weakref and also do quick lookups in hash tables +internal sealed class ProxyWeakRef: IEquatable{ + public WeakReference WeakRef; + private readonly uint? _registeredId; + private readonly int _hashCode; + public bool IsAlive => WeakRef.TryGetTarget(out var _); + public bool TryGetTarget([NotNullWhen(true)] out ImmutableAppearance? target) => WeakRef.TryGetTarget(out target); + + public ProxyWeakRef(ImmutableAppearance appearance) { + appearance.TryGetID(out _registeredId); + WeakRef = new(appearance); + _hashCode = appearance.GetHashCode(); + } + + public override int GetHashCode() { + return _hashCode; + } + + public override bool Equals(object? obj) => obj is ProxyWeakRef proxy && Equals(proxy); - RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim)); + public bool Equals(ProxyWeakRef? proxy) { + if(proxy is null) + return false; + if(_registeredId is not null && _registeredId == proxy._registeredId) + return true; + if(WeakRef.TryGetTarget(out ImmutableAppearance? thisRef) && proxy.WeakRef.TryGetTarget(out ImmutableAppearance? thatRef)) + return thisRef.Equals(thatRef); + return false; } } diff --git a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs index 5faef9180d..8e088b7bb2 100644 --- a/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerClientImagesSystem.cs @@ -22,7 +22,7 @@ public void AddImageObject(DreamConnection connection, DreamObjectImage imageObj turfCoords = new Vector3(turf.X, turf.Y, turf.Z); NetEntity ent = GetNetEntity(locEntity); - EntityUid imageObjectEntity = imageObject.GetEntity(); + EntityUid imageObjectEntity = imageObject.Entity; NetEntity imageObjectNetEntity = GetNetEntity(imageObjectEntity); if (imageObjectEntity != EntityUid.Invalid) _pvsOverrideSystem.AddSessionOverride(imageObjectEntity, connection.Session!); @@ -44,10 +44,10 @@ public void RemoveImageObject(DreamConnection connection, DreamObjectImage image NetEntity ent = GetNetEntity(locEntity); - EntityUid imageObjectEntity = imageObject.GetEntity(); + EntityUid imageObjectEntity = imageObject.Entity; if (imageObjectEntity != EntityUid.Invalid) _pvsOverrideSystem.RemoveSessionOverride(imageObjectEntity, connection.Session!); - NetEntity imageObjectNetEntity = GetNetEntity(imageObject.GetEntity()); + NetEntity imageObjectNetEntity = GetNetEntity(imageObject.Entity); RaiseNetworkEvent(new RemoveClientImageEvent(ent, turfCoords, imageObjectNetEntity), connection.Session!.Channel); } } diff --git a/OpenDreamRuntime/ServerContentIoC.cs b/OpenDreamRuntime/ServerContentIoC.cs index 77ec1706bf..2673ade59f 100644 --- a/OpenDreamRuntime/ServerContentIoC.cs +++ b/OpenDreamRuntime/ServerContentIoC.cs @@ -1,6 +1,7 @@ using OpenDreamRuntime.Objects; using OpenDreamRuntime.Procs; using OpenDreamRuntime.Procs.DebugAdapter; +using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; namespace OpenDreamRuntime { @@ -14,6 +15,7 @@ public static void Register(bool unitTests = false) { IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); #if DEBUG IoCManager.Register(); diff --git a/OpenDreamRuntime/ServerVerbSystem.cs b/OpenDreamRuntime/ServerVerbSystem.cs index a618d35ba1..6da5de48e6 100644 --- a/OpenDreamRuntime/ServerVerbSystem.cs +++ b/OpenDreamRuntime/ServerVerbSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using DMCompiler.DM; using OpenDreamRuntime.Objects; using OpenDreamRuntime.Objects.Types; @@ -180,8 +181,7 @@ private bool CanExecute(DreamConnection connection, DreamObject src, DreamProc v return true; } else if (src is DreamObjectAtom atom) { var appearance = _atomManager.MustGetAppearance(atom); - - if (appearance?.Verbs.Contains(verb.VerbId.Value) is not true) // Inside atom.verbs? + if (appearance.Verbs.Contains(verb.VerbId.Value) is not true) // Inside atom.verbs? return false; } diff --git a/OpenDreamShared/Dream/ColorMatrix.cs b/OpenDreamShared/Dream/ColorMatrix.cs index cc9d5c255f..00414e8a53 100644 --- a/OpenDreamShared/Dream/ColorMatrix.cs +++ b/OpenDreamShared/Dream/ColorMatrix.cs @@ -263,6 +263,36 @@ public bool Equals(in ColorMatrix other) { c54 == other.c54; } + public override int GetHashCode() { + HashCode hashCode = new HashCode(); + hashCode.Add(c11); + hashCode.Add(c12); + hashCode.Add(c13); + hashCode.Add(c14); + + hashCode.Add(c21); + hashCode.Add(c22); + hashCode.Add(c23); + hashCode.Add(c24); + + hashCode.Add(c31); + hashCode.Add(c32); + hashCode.Add(c33); + hashCode.Add(c34); + + hashCode.Add(c41); + hashCode.Add(c42); + hashCode.Add(c43); + hashCode.Add(c44); + + hashCode.Add(c51); + hashCode.Add(c52); + hashCode.Add(c53); + hashCode.Add(c54); + + return hashCode.ToHashCode(); + } + /// /// Multiplies two instances. /// @@ -270,7 +300,7 @@ public bool Equals(in ColorMatrix other) { /// The right operand of the multiplication. /// A new instance that is the result of the multiplication [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Multiply(ref ColorMatrix left, ref ColorMatrix right, out ColorMatrix result) { + public static void Multiply(ref readonly ColorMatrix left, ref readonly ColorMatrix right, out ColorMatrix result) { float lM11 = left.c11, lM12 = left.c12, lM13 = left.c13, @@ -331,7 +361,7 @@ public static void Multiply(ref ColorMatrix left, ref ColorMatrix right, out Col /// The right operand of the interpolation. /// The amount to interpolate between them. 0..1 is equivalent to left..right. /// A new instance that is the result of the interpolation - public static void Interpolate(ref ColorMatrix left, ref ColorMatrix right, float factor, out ColorMatrix result) { + public static void Interpolate(ref readonly ColorMatrix left, ref readonly ColorMatrix right, float factor, out ColorMatrix result) { result = new ColorMatrix( ((1-factor) * left.c11) + (factor * right.c11), ((1-factor) * left.c12) + (factor * right.c12), diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs new file mode 100644 index 0000000000..8272a838df --- /dev/null +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -0,0 +1,645 @@ +using System.Diagnostics.Contracts; +using System.IO; +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using System.Linq; +using Robust.Shared.ViewVariables; +using Robust.Shared.Maths; +using System; +using OpenDreamShared.Rendering; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OpenDreamShared.Dream; + +/* + * Woe, weary traveler, modifying this class is not for the faint of heart. + * If you modify MutableAppearance, be sure to update the following places: + * - All of the methods on ImmutableAppearance itself + * - MutableAppearance + * - MutableAppearance methods in AtomManager + * - There may be others + */ + +// TODO: Wow this is huge! Probably look into splitting this by most used/least used to reduce the size of these +[Serializable] +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 int? Icon = MutableAppearance.Default.Icon; + [ViewVariables] public readonly string? IconState = MutableAppearance.Default.IconState; + [ViewVariables] public readonly AtomDirection Direction = MutableAppearance.Default.Direction; + [ViewVariables] public readonly bool InheritsDirection = MutableAppearance.Default.InheritsDirection; // Inherits direction when used as an overlay + [ViewVariables] public readonly Vector2i PixelOffset = MutableAppearance.Default.PixelOffset; // pixel_x and pixel_y + [ViewVariables] public readonly Vector2i PixelOffset2 = MutableAppearance.Default.PixelOffset2; // pixel_w and pixel_z + [ViewVariables] public readonly Color Color = MutableAppearance.Default.Color; + [ViewVariables] public readonly byte Alpha = MutableAppearance.Default.Alpha; + [ViewVariables] public readonly float GlideSize = MutableAppearance.Default.GlideSize; + [ViewVariables] public readonly float Layer = MutableAppearance.Default.Layer; + [ViewVariables] public readonly int Plane = MutableAppearance.Default.Plane; + [ViewVariables] public readonly BlendMode BlendMode = MutableAppearance.Default.BlendMode; + [ViewVariables] public readonly AppearanceFlags AppearanceFlags = MutableAppearance.Default.AppearanceFlags; + [ViewVariables] public readonly sbyte Invisibility = MutableAppearance.Default.Invisibility; + [ViewVariables] public readonly bool Opacity = MutableAppearance.Default.Opacity; + [ViewVariables] public readonly bool Override = MutableAppearance.Default.Override; + [ViewVariables] public readonly string? RenderSource = MutableAppearance.Default.RenderSource; + [ViewVariables] public readonly string? RenderTarget = MutableAppearance.Default.RenderTarget; + [ViewVariables] public readonly MouseOpacity MouseOpacity = MutableAppearance.Default.MouseOpacity; + [ViewVariables] public readonly ImmutableAppearance[] Overlays; + [ViewVariables] public readonly ImmutableAppearance[] Underlays; + + [NonSerialized] + private List? _overlayIDs; + + [NonSerialized] + private List? _underlayIDs; + + [ViewVariables] public readonly Robust.Shared.GameObjects.NetEntity[] VisContents; + [ViewVariables] public readonly DreamFilter[] Filters; + [ViewVariables] public readonly int[] Verbs; + [ViewVariables] public readonly ColorMatrix ColorMatrix = ColorMatrix.Identity; + + /// The Transform property of this appearance, in [a,d,b,e,c,f] order + [ViewVariables] public readonly float[] Transform = [ + 1, 0, // a d + 0, 1, // b e + 0, 0 // c f + ]; + + // PixelOffset2 behaves the same as PixelOffset in top-down mode, so this is used + public Vector2i TotalPixelOffset => PixelOffset + PixelOffset2; + + public void MarkRegistered(uint registeredId){ + _registeredId = registeredId; + _needsFinalizer = true; + } + + //this should only be called client-side, after network transfer + public void ResolveOverlays(SharedAppearanceSystem appearanceSystem) { + if(_overlayIDs is not null) + for (int i = 0; i < _overlayIDs.Count; i++) + Overlays[i] = appearanceSystem.MustGetAppearanceById(_overlayIDs[i]); + + if(_underlayIDs is not null) + for (int i = 0; i < _underlayIDs.Count; i++) + Underlays[i] = appearanceSystem.MustGetAppearanceById(_underlayIDs[i]); + + _overlayIDs = null; + _underlayIDs = null; + } + + public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? serverAppearanceSystem) { + _appearanceSystem = serverAppearanceSystem; + + Name = appearance.Name; + Icon = appearance.Icon; + IconState = appearance.IconState; + Direction = appearance.Direction; + InheritsDirection = appearance.InheritsDirection; + PixelOffset = appearance.PixelOffset; + PixelOffset2 = appearance.PixelOffset2; + Color = appearance.Color; + Alpha = appearance.Alpha; + GlideSize = appearance.GlideSize; + ColorMatrix = appearance.ColorMatrix; + Layer = appearance.Layer; + Plane = appearance.Plane; + RenderSource = appearance.RenderSource; + RenderTarget = appearance.RenderTarget; + BlendMode = appearance.BlendMode; + AppearanceFlags = appearance.AppearanceFlags; + Invisibility = appearance.Invisibility; + Opacity = appearance.Opacity; + MouseOpacity = appearance.MouseOpacity; + + Overlays = appearance.Overlays.ToArray(); + Underlays = appearance.Underlays.ToArray(); + + VisContents = appearance.VisContents.ToArray(); + Filters = appearance.Filters.ToArray(); + Verbs = appearance.Verbs.ToArray(); + Override = appearance.Override; + + for (int i = 0; i < 6; i++) { + Transform[i] = appearance.Transform[i]; + } + } + + public override bool Equals(object? obj) => obj is ImmutableAppearance immutable && Equals(immutable); + + public bool Equals(ImmutableAppearance? immutableAppearance) { + if (immutableAppearance == null) return false; + + if (immutableAppearance.Name != Name) return false; + if (immutableAppearance.Icon != Icon) return false; + if (immutableAppearance.IconState != IconState) return false; + if (immutableAppearance.Direction != Direction) return false; + if (immutableAppearance.InheritsDirection != InheritsDirection) return false; + if (immutableAppearance.PixelOffset != PixelOffset) return false; + 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.ColorMatrix.Equals(ColorMatrix)) return false; + if (immutableAppearance.Layer != Layer) return false; + if (immutableAppearance.Plane != Plane) return false; + if (immutableAppearance.RenderSource != RenderSource) return false; + if (immutableAppearance.RenderTarget != RenderTarget) return false; + if (immutableAppearance.BlendMode != BlendMode) return false; + if (immutableAppearance.AppearanceFlags != AppearanceFlags) return false; + if (immutableAppearance.Invisibility != Invisibility) return false; + if (immutableAppearance.Opacity != Opacity) return false; + if (immutableAppearance.MouseOpacity != MouseOpacity) return false; + if (immutableAppearance.Overlays.Length != Overlays.Length) return false; + if (immutableAppearance.Underlays.Length != Underlays.Length) return false; + if (immutableAppearance.VisContents.Length != VisContents.Length) return false; + if (immutableAppearance.Filters.Length != Filters.Length) return false; + if (immutableAppearance.Verbs.Length != Verbs.Length) return false; + if (immutableAppearance.Override != Override) return false; + + for (int i = 0; i < Filters.Length; i++) { + if (immutableAppearance.Filters[i] != Filters[i]) return false; + } + + for (int i = 0; i < Overlays.Length; i++) { + if (!immutableAppearance.Overlays[i].Equals(Overlays[i])) return false; + } + + for (int i = 0; i < Underlays.Length; i++) { + if (!immutableAppearance.Underlays[i].Equals(Underlays[i])) return false; + } + + for (int i = 0; i < VisContents.Length; i++) { + if (immutableAppearance.VisContents[i] != VisContents[i]) return false; + } + + for (int i = 0; i < Verbs.Length; i++) { + if (immutableAppearance.Verbs[i] != Verbs[i]) return false; + } + + for (int i = 0; i < 6; i++) { + if (!immutableAppearance.Transform[i].Equals(Transform[i])) return false; + } + + return true; + } + + public uint MustGetID() { + if(_registeredId is null) + throw new InvalidDataException("GetID() was called on an appearance without an ID"); + return (uint)_registeredId; + } + + public bool TryGetID([NotNullWhen(true)] out uint? id) { + id = _registeredId; + return _registeredId is not null; + } + + public override int GetHashCode() { + if(_storedHashCode is not null) //because everything is readonly, this only needs to be done once + return (int)_storedHashCode; + + HashCode hashCode = new HashCode(); + + hashCode.Add(Name); + hashCode.Add(Icon); + hashCode.Add(IconState); + hashCode.Add(Direction); + hashCode.Add(InheritsDirection); + hashCode.Add(PixelOffset); + hashCode.Add(PixelOffset2); + hashCode.Add(Color); + hashCode.Add(ColorMatrix); + hashCode.Add(Layer); + hashCode.Add(Invisibility); + hashCode.Add(Opacity); + hashCode.Add(Override); + hashCode.Add(MouseOpacity); + hashCode.Add(Alpha); + hashCode.Add(GlideSize); + hashCode.Add(Plane); + hashCode.Add(RenderSource); + hashCode.Add(RenderTarget); + hashCode.Add(BlendMode); + hashCode.Add(AppearanceFlags); + + foreach (ImmutableAppearance overlay in Overlays) { + hashCode.Add(overlay.GetHashCode()); + } + + foreach (ImmutableAppearance underlay in Underlays) { + hashCode.Add(underlay.GetHashCode()); + } + + foreach (int visContent in VisContents) { + hashCode.Add(visContent); + } + + foreach (DreamFilter filter in Filters) { + hashCode.Add(filter); + } + + foreach (int verb in Verbs) { + hashCode.Add(verb); + } + + for (int i = 0; i < 6; i++) { + hashCode.Add(Transform[i]); + } + + _storedHashCode = hashCode.ToHashCode(); + return (int)_storedHashCode; + } + + public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializer) { + Overlays = []; + Underlays = []; + VisContents = []; + Filters = []; + Verbs =[]; + + var property = (IconAppearanceProperty)buffer.ReadByte(); + while (property != IconAppearanceProperty.End) { + switch (property) { + case IconAppearanceProperty.Name: + Name = buffer.ReadString(); + break; + case IconAppearanceProperty.Id: + _registeredId = buffer.ReadVariableUInt32(); + break; + case IconAppearanceProperty.Icon: + Icon = buffer.ReadVariableInt32(); + break; + case IconAppearanceProperty.IconState: + IconState = buffer.ReadString(); + break; + case IconAppearanceProperty.Direction: + Direction = (AtomDirection)buffer.ReadByte(); + break; + case IconAppearanceProperty.DoesntInheritDirection: + InheritsDirection = false; + break; + case IconAppearanceProperty.PixelOffset: + PixelOffset = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + case IconAppearanceProperty.PixelOffset2: + PixelOffset2 = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); + break; + case IconAppearanceProperty.Color: + Color = buffer.ReadColor(); + break; + case IconAppearanceProperty.Alpha: + Alpha = buffer.ReadByte(); + break; + case IconAppearanceProperty.GlideSize: + GlideSize = buffer.ReadFloat(); + break; + case IconAppearanceProperty.Layer: + Layer = buffer.ReadFloat(); + break; + case IconAppearanceProperty.Plane: + Plane = buffer.ReadVariableInt32(); + break; + case IconAppearanceProperty.BlendMode: + BlendMode = (BlendMode)buffer.ReadByte(); + break; + case IconAppearanceProperty.AppearanceFlags: + AppearanceFlags = (AppearanceFlags)buffer.ReadInt32(); + break; + case IconAppearanceProperty.Invisibility: + Invisibility = buffer.ReadSByte(); + break; + case IconAppearanceProperty.Opacity: + Opacity = buffer.ReadBoolean(); + break; + case IconAppearanceProperty.Override: + Override = buffer.ReadBoolean(); + break; + case IconAppearanceProperty.RenderSource: + RenderSource = buffer.ReadString(); + break; + case IconAppearanceProperty.RenderTarget: + RenderTarget = buffer.ReadString(); + break; + case IconAppearanceProperty.MouseOpacity: + MouseOpacity = (MouseOpacity)buffer.ReadByte(); + break; + case IconAppearanceProperty.ColorMatrix: + ColorMatrix = new( + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle() + ); + + break; + case IconAppearanceProperty.Overlays: { + var overlaysCount = buffer.ReadVariableInt32(); + + Overlays = new ImmutableAppearance[overlaysCount]; + _overlayIDs = new(overlaysCount); + for (int overlaysI = 0; overlaysI < overlaysCount; overlaysI++) { + _overlayIDs.Add(buffer.ReadVariableUInt32()); + } + + break; + } + case IconAppearanceProperty.Underlays: { + var underlaysCount = buffer.ReadVariableInt32(); + + Underlays = new ImmutableAppearance[underlaysCount]; + _underlayIDs = new(underlaysCount); + for (int underlaysI = 0; underlaysI < underlaysCount; underlaysI++) { + _underlayIDs.Add(buffer.ReadVariableUInt32()); + } + + break; + } + case IconAppearanceProperty.VisContents: { + var visContentsCount = buffer.ReadVariableInt32(); + + VisContents = new Robust.Shared.GameObjects.NetEntity[visContentsCount]; + for (int visContentsI = 0; visContentsI < visContentsCount; visContentsI++) { + VisContents[visContentsI] = buffer.ReadNetEntity(); + } + + break; + } + case IconAppearanceProperty.Filters: { + var filtersCount = buffer.ReadInt32(); + + Filters = new DreamFilter[filtersCount]; + for (int filtersI = 0; filtersI < filtersCount; filtersI++) { + var filterLength = buffer.ReadVariableInt32(); + var filterData = buffer.ReadBytes(filterLength); + using var filterStream = new MemoryStream(filterData); + var filter = serializer.Deserialize(filterStream); + + Filters[filtersI] = filter; + } + + break; + } + case IconAppearanceProperty.Verbs: { + var verbsCount = buffer.ReadVariableInt32(); + + Verbs = new int[verbsCount]; + for (int verbsI = 0; verbsI < verbsCount; verbsI++) { + Verbs[verbsI] = buffer.ReadVariableInt32(); + } + + break; + } + case IconAppearanceProperty.Transform: { + Transform = [ + buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle(), + buffer.ReadSingle(), buffer.ReadSingle() + ]; + + break; + } + default: + throw new Exception($"Invalid property {property}"); + } + + property = (IconAppearanceProperty)buffer.ReadByte(); + } + + if(_registeredId is null) + throw new Exception("No appearance ID found in buffer"); + } + + //Creates an editable *copy* of this appearance, which must be added to the ServerAppearanceSystem to be used. + [Pure] + public MutableAppearance ToMutable() { + MutableAppearance result = new MutableAppearance() { + Name = Name, + Icon = Icon, + IconState = IconState, + Direction = Direction, + InheritsDirection = InheritsDirection, + PixelOffset = PixelOffset, + PixelOffset2 = PixelOffset2, + Color = Color, + Alpha = Alpha, + GlideSize = GlideSize, + ColorMatrix = ColorMatrix, + Layer = Layer, + Plane = Plane, + RenderSource = RenderSource, + RenderTarget = RenderTarget, + BlendMode = BlendMode, + AppearanceFlags = AppearanceFlags, + Invisibility = Invisibility, + Opacity = Opacity, + MouseOpacity = MouseOpacity, + Overlays = new(Overlays.Length), + Underlays = new(Underlays.Length), + VisContents = new(VisContents), + Filters = new(Filters), + Verbs = new(Verbs), + Override = Override, + }; + + foreach(ImmutableAppearance overlay in Overlays) + result.Overlays.Add(overlay); + + foreach(ImmutableAppearance underlay in Underlays) + result.Underlays.Add(underlay); + + for (int i = 0; i < 6; i++) { + result.Transform[i] = Transform[i]; + } + + return result; + } + + public void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.Write((byte)IconAppearanceProperty.Id); + buffer.WriteVariableUInt32(MustGetID()); + + if (Name != MutableAppearance.Default.Name) { + buffer.Write((byte)IconAppearanceProperty.Name); + buffer.Write(Name); + } + + if (Icon != null) { + buffer.Write((byte)IconAppearanceProperty.Icon); + buffer.WriteVariableInt32(Icon.Value); + } + + if (IconState != null) { + buffer.Write((byte)IconAppearanceProperty.IconState); + buffer.Write(IconState); + } + + if (Direction != MutableAppearance.Default.Direction) { + buffer.Write((byte)IconAppearanceProperty.Direction); + buffer.Write((byte)Direction); + } + + if (InheritsDirection != true) { + buffer.Write((byte)IconAppearanceProperty.DoesntInheritDirection); + } + + if (PixelOffset != MutableAppearance.Default.PixelOffset) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset); + buffer.WriteVariableInt32(PixelOffset.X); + buffer.WriteVariableInt32(PixelOffset.Y); + } + + if (PixelOffset2 != MutableAppearance.Default.PixelOffset2) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset2); + buffer.WriteVariableInt32(PixelOffset2.X); + buffer.WriteVariableInt32(PixelOffset2.Y); + } + + if (Color != MutableAppearance.Default.Color) { + buffer.Write((byte)IconAppearanceProperty.Color); + buffer.Write(Color); + } + + if (Alpha != MutableAppearance.Default.Alpha) { + buffer.Write((byte)IconAppearanceProperty.Alpha); + buffer.Write(Alpha); + } + + if (!GlideSize.Equals(MutableAppearance.Default.GlideSize)) { + buffer.Write((byte)IconAppearanceProperty.GlideSize); + buffer.Write(GlideSize); + } + + if (!ColorMatrix.Equals(MutableAppearance.Default.ColorMatrix)) { + buffer.Write((byte)IconAppearanceProperty.ColorMatrix); + + foreach (var value in ColorMatrix.GetValues()) + buffer.Write(value); + } + + if (!Layer.Equals(MutableAppearance.Default.Layer)) { + buffer.Write((byte)IconAppearanceProperty.Layer); + buffer.Write(Layer); + } + + if (Plane != MutableAppearance.Default.Plane) { + buffer.Write((byte)IconAppearanceProperty.Plane); + buffer.WriteVariableInt32(Plane); + } + + if (BlendMode != MutableAppearance.Default.BlendMode) { + buffer.Write((byte)IconAppearanceProperty.BlendMode); + buffer.Write((byte)BlendMode); + } + + if (AppearanceFlags != MutableAppearance.Default.AppearanceFlags) { + buffer.Write((byte)IconAppearanceProperty.AppearanceFlags); + buffer.Write((int)AppearanceFlags); + } + + if (Invisibility != MutableAppearance.Default.Invisibility) { + buffer.Write((byte)IconAppearanceProperty.Invisibility); + buffer.Write(Invisibility); + } + + if (Opacity != MutableAppearance.Default.Opacity) { + buffer.Write((byte)IconAppearanceProperty.Opacity); + buffer.Write(Opacity); + } + + if (Override != MutableAppearance.Default.Override) { + buffer.Write((byte)IconAppearanceProperty.Override); + buffer.Write(Override); + } + + if (!string.IsNullOrWhiteSpace(RenderSource)) { + buffer.Write((byte)IconAppearanceProperty.RenderSource); + buffer.Write(RenderSource); + } + + if (!string.IsNullOrWhiteSpace(RenderTarget)) { + buffer.Write((byte)IconAppearanceProperty.RenderTarget); + buffer.Write(RenderTarget); + } + + if (MouseOpacity != MutableAppearance.Default.MouseOpacity) { + buffer.Write((byte)IconAppearanceProperty.MouseOpacity); + buffer.Write((byte)MouseOpacity); + } + + if (Overlays.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Overlays); + + buffer.WriteVariableInt32(Overlays.Length); + foreach (var overlay in Overlays) { + buffer.WriteVariableUInt32(overlay.MustGetID()); + } + } + + if (Underlays.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Underlays); + + buffer.WriteVariableInt32(Underlays.Length); + foreach (var underlay in Underlays) { + buffer.WriteVariableUInt32(underlay.MustGetID()); + } + } + + if (VisContents.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.VisContents); + + buffer.WriteVariableInt32(VisContents.Length); + foreach (var item in VisContents) { + buffer.Write(item); + } + } + + if (Filters.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Filters); + + buffer.Write(Filters.Length); + foreach (var filter in Filters) { + using var filterStream = new MemoryStream(); + + serializer.Serialize(filterStream, filter); + buffer.WriteVariableInt32((int)filterStream.Length); + filterStream.TryGetBuffer(out var filterBuffer); + buffer.Write(filterBuffer); + } + } + + if (Verbs.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Verbs); + + buffer.WriteVariableInt32(Verbs.Length); + foreach (var verb in Verbs) { + buffer.WriteVariableInt32(verb); + } + } + + if (!Transform.SequenceEqual(MutableAppearance.Default.Transform)) { + buffer.Write((byte)IconAppearanceProperty.Transform); + + for (int i = 0; i < 6; i++) { + buffer.Write(Transform[i]); + } + } + + buffer.Write((byte)IconAppearanceProperty.End); + } + + public int ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + throw new NotImplementedException(); + } + + ~ImmutableAppearance() { + if(_needsFinalizer && _registeredId is not null) + _appearanceSystem!.RemoveAppearance(this); + } +} + diff --git a/OpenDreamShared/Dream/IconAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs similarity index 86% rename from OpenDreamShared/Dream/IconAppearance.cs rename to OpenDreamShared/Dream/MutableAppearance.cs index 78e38af359..94b8f64ff0 100644 --- a/OpenDreamShared/Dream/IconAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -1,27 +1,27 @@ using Robust.Shared.Maths; -using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Robust.Shared.GameObjects; namespace OpenDreamShared.Dream; /* * Woe, weary traveler, modifying this class is not for the faint of heart. - * If you modify IconAppearance, be sure to update the following places: - * - All of the methods on IconAppearance itself + * If you modify MutableAppearance, be sure to update the following places: + * - All of the methods on MutableAppearance itself + * - ImmutableAppearance * - IconAppearance methods in AtomManager * - MsgAllAppearances * - IconDebugWindow + * - IconAppearanceProperty enum * - There may be others */ // TODO: Wow this is huge! Probably look into splitting this by most used/least used to reduce the size of these -[Serializable, NetSerializable] -public sealed class IconAppearance : IEquatable { - public static readonly IconAppearance Default = new(); +[Serializable] +public sealed class MutableAppearance : IEquatable{ + public static readonly MutableAppearance Default = new(); [ViewVariables] public string Name = string.Empty; [ViewVariables] public int? Icon; @@ -43,9 +43,9 @@ public sealed class IconAppearance : IEquatable { [ViewVariables] public string? RenderSource; [ViewVariables] public string? RenderTarget; [ViewVariables] public MouseOpacity MouseOpacity = MouseOpacity.PixelOpaque; - [ViewVariables] public List Overlays; - [ViewVariables] public List Underlays; - [ViewVariables] public List VisContents; + [ViewVariables] public List Overlays; + [ViewVariables] public List Underlays; + [ViewVariables] public List VisContents; [ViewVariables] public List Filters; [ViewVariables] public List Verbs; @@ -72,7 +72,7 @@ public sealed class IconAppearance : IEquatable { // PixelOffset2 behaves the same as PixelOffset in top-down mode, so this is used public Vector2i TotalPixelOffset => PixelOffset + PixelOffset2; - public IconAppearance() { + public MutableAppearance() { Overlays = new(); Underlays = new(); VisContents = new(); @@ -80,7 +80,7 @@ public IconAppearance() { Verbs = new(); } - public IconAppearance(IconAppearance appearance) { + public MutableAppearance(MutableAppearance appearance) { Name = appearance.Name; Icon = appearance.Icon; IconState = appearance.IconState; @@ -113,9 +113,9 @@ public IconAppearance(IconAppearance appearance) { } } - public override bool Equals(object? obj) => obj is IconAppearance appearance && Equals(appearance); + public override bool Equals(object? obj) => obj is MutableAppearance appearance && Equals(appearance); - public bool Equals(IconAppearance? appearance) { + public bool Equals(MutableAppearance? appearance) { if (appearance == null) return false; if (appearance.Name != Name) return false; @@ -203,6 +203,7 @@ private static bool TryRepresentMatrixAsRgbaColor(in ColorMatrix matrix, [NotNul return maybeColor is not null; } + //it is *ESSENTIAL* that this matches the hashcode of the equivelant ImmutableAppearance. There's a debug assert and everything. public override int GetHashCode() { HashCode hashCode = new HashCode(); @@ -218,6 +219,7 @@ public override int GetHashCode() { hashCode.Add(Layer); hashCode.Add(Invisibility); hashCode.Add(Opacity); + hashCode.Add(Override); hashCode.Add(MouseOpacity); hashCode.Add(Alpha); hashCode.Add(GlideSize); @@ -227,12 +229,12 @@ public override int GetHashCode() { hashCode.Add(BlendMode); hashCode.Add(AppearanceFlags); - foreach (int overlay in Overlays) { - hashCode.Add(overlay); + foreach (var overlay in Overlays) { + hashCode.Add(overlay.GetHashCode()); } - foreach (int underlay in Underlays) { - hashCode.Add(underlay); + foreach (var underlay in Underlays) { + hashCode.Add(underlay.GetHashCode()); } foreach (int visContent in VisContents) { @@ -333,3 +335,36 @@ public enum AnimationFlags { AnimationRelative = 256, AnimationContinue = 512 } + +//used for encoding for netmessages +public enum IconAppearanceProperty : byte { + Name, + Icon, + IconState, + Direction, + DoesntInheritDirection, + PixelOffset, + PixelOffset2, + Color, + Alpha, + GlideSize, + ColorMatrix, + Layer, + Plane, + BlendMode, + AppearanceFlags, + Invisibility, + Opacity, + Override, + RenderSource, + RenderTarget, + MouseOpacity, + Overlays, + Underlays, + VisContents, + Filters, + Verbs, + Transform, + Id, + End + } diff --git a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs index 4377d00aa6..433b166904 100644 --- a/OpenDreamShared/Network/Messages/MsgAllAppearances.cs +++ b/OpenDreamShared/Network/Messages/MsgAllAppearances.cs @@ -9,389 +9,25 @@ namespace OpenDreamShared.Network.Messages; -public sealed class MsgAllAppearances(Dictionary allAppearances) : NetMessage { +public sealed class MsgAllAppearances(Dictionary allAppearances) : NetMessage { public override MsgGroups MsgGroup => MsgGroups.EntityEvent; - - private enum Property : byte { - Name, - Icon, - IconState, - Direction, - DoesntInheritDirection, - PixelOffset, - PixelOffset2, - Color, - Alpha, - GlideSize, - ColorMatrix, - Layer, - Plane, - BlendMode, - AppearanceFlags, - Invisibility, - Opacity, - Override, - RenderSource, - RenderTarget, - MouseOpacity, - Overlays, - Underlays, - VisContents, - Filters, - Verbs, - Transform, - - Id, - End - } - - public Dictionary AllAppearances = allAppearances; - + public Dictionary AllAppearances = allAppearances; public MsgAllAppearances() : this(new()) { } public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { var count = buffer.ReadInt32(); - var appearanceId = -1; - AllAppearances = new(count); for (int i = 0; i < count; i++) { - var appearance = new IconAppearance(); - var property = (Property)buffer.ReadByte(); - - appearanceId++; - - while (property != Property.End) { - switch (property) { - case Property.Name: - appearance.Name = buffer.ReadString(); - break; - case Property.Id: - appearanceId = buffer.ReadVariableInt32(); - break; - case Property.Icon: - appearance.Icon = buffer.ReadVariableInt32(); - break; - case Property.IconState: - appearance.IconState = buffer.ReadString(); - break; - case Property.Direction: - appearance.Direction = (AtomDirection)buffer.ReadByte(); - break; - case Property.DoesntInheritDirection: - appearance.InheritsDirection = false; - break; - case Property.PixelOffset: - appearance.PixelOffset = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); - break; - case Property.PixelOffset2: - appearance.PixelOffset2 = (buffer.ReadVariableInt32(), buffer.ReadVariableInt32()); - break; - case Property.Color: - appearance.Color = buffer.ReadColor(); - break; - case Property.Alpha: - appearance.Alpha = buffer.ReadByte(); - break; - case Property.GlideSize: - appearance.GlideSize = buffer.ReadFloat(); - break; - case Property.Layer: - appearance.Layer = buffer.ReadFloat(); - break; - case Property.Plane: - appearance.Plane = buffer.ReadVariableInt32(); - break; - case Property.BlendMode: - appearance.BlendMode = (BlendMode)buffer.ReadByte(); - break; - case Property.AppearanceFlags: - appearance.AppearanceFlags = (AppearanceFlags)buffer.ReadInt32(); - break; - case Property.Invisibility: - appearance.Invisibility = buffer.ReadSByte(); - break; - case Property.Opacity: - appearance.Opacity = buffer.ReadBoolean(); - break; - case Property.Override: - appearance.Override = buffer.ReadBoolean(); - break; - case Property.RenderSource: - appearance.RenderSource = buffer.ReadString(); - break; - case Property.RenderTarget: - appearance.RenderTarget = buffer.ReadString(); - break; - case Property.MouseOpacity: - appearance.MouseOpacity = (MouseOpacity)buffer.ReadByte(); - break; - case Property.ColorMatrix: - appearance.ColorMatrix = new( - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle(), buffer.ReadSingle() - ); - - break; - case Property.Overlays: { - var overlaysCount = buffer.ReadVariableInt32(); - - appearance.Overlays.EnsureCapacity(overlaysCount); - for (int overlaysI = 0; overlaysI < overlaysCount; overlaysI++) { - appearance.Overlays.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.Underlays: { - var underlaysCount = buffer.ReadVariableInt32(); - - appearance.Underlays.EnsureCapacity(underlaysCount); - for (int underlaysI = 0; underlaysI < underlaysCount; underlaysI++) { - appearance.Underlays.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.VisContents: { - var visContentsCount = buffer.ReadVariableInt32(); - - appearance.VisContents.EnsureCapacity(visContentsCount); - for (int visContentsI = 0; visContentsI < visContentsCount; visContentsI++) { - appearance.VisContents.Add(buffer.ReadNetEntity()); - } - - break; - } - case Property.Filters: { - var filtersCount = buffer.ReadInt32(); - - appearance.Filters.EnsureCapacity(filtersCount); - for (int filtersI = 0; filtersI < filtersCount; filtersI++) { - var filterLength = buffer.ReadVariableInt32(); - var filterData = buffer.ReadBytes(filterLength); - using var filterStream = new MemoryStream(filterData); - var filter = serializer.Deserialize(filterStream); - - appearance.Filters.Add(filter); - } - - break; - } - case Property.Verbs: { - var verbsCount = buffer.ReadVariableInt32(); - - appearance.Verbs.EnsureCapacity(verbsCount); - for (int verbsI = 0; verbsI < verbsCount; verbsI++) { - appearance.Verbs.Add(buffer.ReadVariableInt32()); - } - - break; - } - case Property.Transform: { - appearance.Transform = [ - buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle(), - buffer.ReadSingle(), buffer.ReadSingle() - ]; - - break; - } - default: - throw new Exception($"Invalid property {property}"); - } - - property = (Property)buffer.ReadByte(); - } - - AllAppearances.Add(appearanceId, appearance); + var appearance = new ImmutableAppearance(buffer, serializer); + AllAppearances.Add(appearance.MustGetID(), appearance); } } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { - int lastId = -1; - buffer.Write(AllAppearances.Count); foreach (var pair in AllAppearances) { - var appearance = pair.Value; - - if (pair.Key != lastId + 1) { - buffer.Write((byte)Property.Id); - buffer.WriteVariableInt32(pair.Key); - } - - lastId = pair.Key; - - if (appearance.Name != IconAppearance.Default.Name) { - buffer.Write((byte)Property.Name); - buffer.Write(appearance.Name); - } - - if (appearance.Icon != null) { - buffer.Write((byte)Property.Icon); - buffer.WriteVariableInt32(appearance.Icon.Value); - } - - if (appearance.IconState != null) { - buffer.Write((byte)Property.IconState); - buffer.Write(appearance.IconState); - } - - if (appearance.Direction != IconAppearance.Default.Direction) { - buffer.Write((byte)Property.Direction); - buffer.Write((byte)appearance.Direction); - } - - if (appearance.InheritsDirection != true) { - buffer.Write((byte)Property.DoesntInheritDirection); - } - - if (appearance.PixelOffset != IconAppearance.Default.PixelOffset) { - buffer.Write((byte)Property.PixelOffset); - buffer.WriteVariableInt32(appearance.PixelOffset.X); - buffer.WriteVariableInt32(appearance.PixelOffset.Y); - } - - if (appearance.PixelOffset2 != IconAppearance.Default.PixelOffset2) { - buffer.Write((byte)Property.PixelOffset2); - buffer.WriteVariableInt32(appearance.PixelOffset2.X); - buffer.WriteVariableInt32(appearance.PixelOffset2.Y); - } - - if (appearance.Color != IconAppearance.Default.Color) { - buffer.Write((byte)Property.Color); - buffer.Write(appearance.Color); - } - - if (appearance.Alpha != IconAppearance.Default.Alpha) { - buffer.Write((byte)Property.Alpha); - buffer.Write(appearance.Alpha); - } - - if (!appearance.GlideSize.Equals(IconAppearance.Default.GlideSize)) { - buffer.Write((byte)Property.GlideSize); - buffer.Write(appearance.GlideSize); - } - - if (!appearance.ColorMatrix.Equals(IconAppearance.Default.ColorMatrix)) { - buffer.Write((byte)Property.ColorMatrix); - - foreach (var value in appearance.ColorMatrix.GetValues()) - buffer.Write(value); - } - - if (!appearance.Layer.Equals(IconAppearance.Default.Layer)) { - buffer.Write((byte)Property.Layer); - buffer.Write(appearance.Layer); - } - - if (appearance.Plane != IconAppearance.Default.Plane) { - buffer.Write((byte)Property.Plane); - buffer.WriteVariableInt32(appearance.Plane); - } - - if (appearance.BlendMode != IconAppearance.Default.BlendMode) { - buffer.Write((byte)Property.BlendMode); - buffer.Write((byte)appearance.BlendMode); - } - - if (appearance.AppearanceFlags != IconAppearance.Default.AppearanceFlags) { - buffer.Write((byte)Property.AppearanceFlags); - buffer.Write((int)appearance.AppearanceFlags); - } - - if (appearance.Invisibility != IconAppearance.Default.Invisibility) { - buffer.Write((byte)Property.Invisibility); - buffer.Write(appearance.Invisibility); - } - - if (appearance.Opacity != IconAppearance.Default.Opacity) { - buffer.Write((byte)Property.Opacity); - buffer.Write(appearance.Opacity); - } - - if (appearance.Override != IconAppearance.Default.Override) { - buffer.Write((byte)Property.Override); - buffer.Write(appearance.Override); - } - - if (!string.IsNullOrWhiteSpace(appearance.RenderSource)) { - buffer.Write((byte)Property.RenderSource); - buffer.Write(appearance.RenderSource); - } - - if (!string.IsNullOrWhiteSpace(appearance.RenderTarget)) { - buffer.Write((byte)Property.RenderTarget); - buffer.Write(appearance.RenderTarget); - } - - if (appearance.MouseOpacity != IconAppearance.Default.MouseOpacity) { - buffer.Write((byte)Property.MouseOpacity); - buffer.Write((byte)appearance.MouseOpacity); - } - - if (appearance.Overlays.Count != 0) { - buffer.Write((byte)Property.Overlays); - - buffer.WriteVariableInt32(appearance.Overlays.Count); - foreach (var overlay in appearance.Overlays) { - buffer.WriteVariableInt32(overlay); - } - } - - if (appearance.Underlays.Count != 0) { - buffer.Write((byte)Property.Underlays); - - buffer.WriteVariableInt32(appearance.Underlays.Count); - foreach (var underlay in appearance.Underlays) { - buffer.WriteVariableInt32(underlay); - } - } - - if (appearance.VisContents.Count != 0) { - buffer.Write((byte)Property.VisContents); - - buffer.WriteVariableInt32(appearance.VisContents.Count); - foreach (var item in appearance.VisContents) { - buffer.Write(item); - } - } - - if (appearance.Filters.Count != 0) { - buffer.Write((byte)Property.Filters); - - buffer.Write(appearance.Filters.Count); - foreach (var filter in appearance.Filters) { - using var filterStream = new MemoryStream(); - - serializer.Serialize(filterStream, filter); - buffer.WriteVariableInt32((int)filterStream.Length); - filterStream.TryGetBuffer(out var filterBuffer); - buffer.Write(filterBuffer); - } - } - - if (appearance.Verbs.Count != 0) { - buffer.Write((byte)Property.Verbs); - - buffer.WriteVariableInt32(appearance.Verbs.Count); - foreach (var verb in appearance.Verbs) { - buffer.WriteVariableInt32(verb); - } - } - - if (!appearance.Transform.SequenceEqual(IconAppearance.Default.Transform)) { - buffer.Write((byte)Property.Transform); - - for (int i = 0; i < 6; i++) { - buffer.Write(appearance.Transform[i]); - } - } - - buffer.Write((byte)Property.End); + pair.Value.WriteToBuffer(buffer,serializer); } } } diff --git a/OpenDreamShared/Network/Messages/MsgNewAppearance.cs b/OpenDreamShared/Network/Messages/MsgNewAppearance.cs new file mode 100644 index 0000000000..e40973a781 --- /dev/null +++ b/OpenDreamShared/Network/Messages/MsgNewAppearance.cs @@ -0,0 +1,22 @@ +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 d2d1ed5d5a..5f6d0101fb 100644 --- a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs +++ b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs @@ -6,22 +6,31 @@ namespace OpenDreamShared.Rendering; public abstract class SharedAppearanceSystem : EntitySystem { + public abstract ImmutableAppearance MustGetAppearanceById(uint appearanceId); + 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; + } + [Serializable, NetSerializable] - public sealed class NewAppearanceEvent(int appearanceId, IconAppearance appearance) : EntityEventArgs { - public int AppearanceId { get; } = appearanceId; - public IconAppearance Appearance { get; } = appearance; + public sealed class RemoveAppearanceEvent(uint appearanceId) : EntityEventArgs { + public uint AppearanceId { get; } = appearanceId; } [Serializable, NetSerializable] - public sealed class AnimationEvent(NetEntity entity, int targetAppearanceId, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim) + public sealed class AnimationEvent(NetEntity entity, uint targetAppearanceId, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, uint? turfId) : EntityEventArgs { public NetEntity Entity = entity; - public int TargetAppearanceId = targetAppearanceId; + public uint TargetAppearanceId = targetAppearanceId; public TimeSpan Duration = duration; public AnimationEasing Easing = easing; public int Loop = loop; public AnimationFlags Flags = flags; public int Delay = delay; public bool ChainAnim = chainAnim; + public uint? TurfId = turfId; } } diff --git a/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs b/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs index 004ffe2534..8434d22623 100644 --- a/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs +++ b/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs @@ -4,18 +4,18 @@ using Robust.Shared.GameStates; using OpenDreamShared.Dream; -namespace OpenDreamShared.Rendering { - [NetworkedComponent] - public abstract partial class SharedDMISpriteComponent : Component { - [Serializable, NetSerializable] - public sealed class DMISpriteComponentState : ComponentState { - public readonly int? AppearanceId; - public readonly ScreenLocation ScreenLocation; +namespace OpenDreamShared.Rendering; - public DMISpriteComponentState(int? appearanceId, ScreenLocation screenLocation) { - AppearanceId = appearanceId; - ScreenLocation = screenLocation; - } +[NetworkedComponent] +public abstract partial class SharedDMISpriteComponent : Component { + [Serializable, NetSerializable] + public sealed class DMISpriteComponentState : ComponentState { + public readonly uint? AppearanceId; + public readonly ScreenLocation ScreenLocation; + + public DMISpriteComponentState(uint? appearanceId, ScreenLocation screenLocation) { + AppearanceId = appearanceId; + ScreenLocation = screenLocation; } } } diff --git a/TestGame/map_z1.dmm b/TestGame/map_z1.dmm index 92d34c551c..1854213037 100644 --- a/TestGame/map_z1.dmm +++ b/TestGame/map_z1.dmm @@ -36,9 +36,11 @@ "J" = (/obj/order_test_target,/turf,/area) "K" = (/obj/complex_overlay_test,/turf,/area) "L" = (/obj/float_layer_test,/turf,/area) +"N" = (/obj/plaque/animation_turf_test,/turf,/area) "M" = (/mob,/turf,/area) "O" = (/obj/plaque/animation_test,/turf,/area) "R" = (/turf/blue,/area/withicon) +"S" = (/obj/button/animation_turf_test,/turf,/area) "X" = (/turf,/area/withicon) "Z" = (/obj/button/animation_test,/turf,/area) @@ -46,8 +48,8 @@ bbbbbbbbbbbbbbbbbbbbbb bedciklwopsuaaaaaaaaab bfghjmnxqrtvaaaaaaaaab -byADFGZaaaaaaaaaaaaaab -bzBEHIOaaaaaaaaaaaaaab +byADFGZSaaaaaaaaaaaaab +bzBEHIONaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab diff --git a/TestGame/renderer_tests.dm b/TestGame/renderer_tests.dm index 45c414786a..4e806e75c0 100644 --- a/TestGame/renderer_tests.dm +++ b/TestGame/renderer_tests.dm @@ -350,9 +350,93 @@ i++; if(i>8) i = 0 + /obj/plaque/animation_test data = "

Animation Test

Click the button to apply a series of animations to your mob

" + +/obj/button/animation_turf_test + name = "Animation Turf Test" + desc = "Click me to animate the turfs around you!" + var/i = 0 + + push() + if(i==0) + //grow and fade + usr << "grow and fade" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, alpha = 0, time = 5) + animate(transform = matrix(), alpha = 255, time = 5) + sleep(5) + if(i==1) + //spin + usr << "spin" + for(var/turf/T in range(src, 2)) + animate(T, transform = turn(matrix(), 120), time = 2, loop = 5) + animate(transform = turn(matrix(), 240), time = 2) + animate(transform = null, time = 2) + sleep(14) + if(i==2) + //colour + usr << "colour" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=5) + animate(color="#00ff00", time=5) + animate(color="#0000ff", time=5) + animate(color="#ffffff", time=5) + sleep(20) + if(i==3) + //colour matrix + usr << "colour matrix" + for(var/turf/T in range(src, 2)) + animate(T, color=list(0,0,1,0, 1,0,0,0, 0,1,0,0, 0,0,0,1, 0,0,0,0), time=5) + animate(color=list(0,1,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,1, 0,0,0,0), time=5) + animate(color=list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0), time=5) + sleep(15) + if(i==4) + //parallel + usr << "parallel" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=4) + animate(T, transform = turn(matrix(), 120), time = 2, flags=ANIMATION_PARALLEL) + animate(transform = turn(matrix(), 240), time = 2) + animate(color="#ffffff", transform = null, time = 2) + sleep(6) + if(i==5) + //easings + usr << "easings" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, time = 5, easing=BACK_EASING) + animate(transform = matrix(), time = 5, easing=BOUNCE_EASING) + animate(transform = matrix()*2, time = 5, easing=ELASTIC_EASING) + animate(transform = matrix(), time = 5, easing=QUAD_EASING) + animate(transform = matrix()*2, time = 5, easing=CUBIC_EASING) + animate(transform = matrix(), time = 5, easing=SINE_EASING) + animate(transform = matrix()*2, time = 5, easing=CIRCULAR_EASING) + animate(transform = matrix(), time = 5, easing=JUMP_EASING) + if(i==6) + usr << "relative color" + for(var/turf/T in range(src, 2)) + animate(T, color="#ff0000", time=5, flags=ANIMATION_RELATIVE) + animate(color="#00ff00", time=5, flags=ANIMATION_RELATIVE) + animate(color="#0000ff", time=5, flags=ANIMATION_RELATIVE) + if(i==7) + usr << "relative transform" + for(var/turf/T in range(src, 2)) + animate(T, transform = matrix()*2, time = 5, flags=ANIMATION_RELATIVE) + animate(transform = matrix()*0.5, time = 5, flags=ANIMATION_RELATIVE) + if(i==8) + usr << "more relative tests" + for(var/turf/T in range(src, 2)) + animate(T, alpha=-125, pixel_x=16, time = 5, flags=ANIMATION_RELATIVE) + animate(alpha=125, pixel_x=-16, time = 5, flags=ANIMATION_RELATIVE) + i++; + if(i>8) + i = 0 + +/obj/plaque/animation_turf_test + data = "

Animation Turf Test

Click the button to apply a series of animations to the turfs your mob

" + //render order sanity checks /obj/order_test icon = 'icons/hanoi.dmi'