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 ebb99dd087..b398e04b0e 100644 --- a/Content.IntegrationTests/DMProject/code.dm +++ b/Content.IntegrationTests/DMProject/code.dm @@ -30,4 +30,6 @@ test_color_matrix() test_range() test_verb_duplicate() + test_nonlocal_var() + test_images() 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 a4abe97328..824fd23de1 100644 --- a/Content.IntegrationTests/DMProject/environment.dme +++ b/Content.IntegrationTests/DMProject/environment.dme @@ -3,5 +3,7 @@ #include "Tests/color_matrix.dm" #include "Tests/range.dm" #include "Tests/verb_duplicate.dm" +#include "Tests/nonlocal_var.dm" +#include "Tests/image.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..3963b2a4ed 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, MutableIconAppearance appearance) { } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { } + public void SetAreaAppearance(DreamObjectArea area, MutableIconAppearance appearance) { } public void SetArea(DreamObjectTurf turf, DreamObjectArea area) { } diff --git a/DMCompiler/DMStandard/Types/Image.dm b/DMCompiler/DMStandard/Types/Image.dm index 2b4a00575e..b09b2cb916 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 MutableIconAppearance.cs var/alpha = 255 var/appearance var/appearance_flags = 0 diff --git a/OpenDreamClient/EntryPoint.cs b/OpenDreamClient/EntryPoint.cs index b9f70af63c..dc82f7669e 100644 --- a/OpenDreamClient/EntryPoint.cs +++ b/OpenDreamClient/EntryPoint.cs @@ -86,6 +86,7 @@ public override void PostInit() { IoCManager.Resolve().Initialize(); _netManager.RegisterNetMessage(RxAllAppearances); + _netManager.RegisterNetMessage(RxNewAppearance); if (_configurationManager.GetCVar(CVars.DisplayCompat)) _dreamInterface.OpenAlert( @@ -113,6 +114,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/Interface/DebugWindows/IconDebugWindow.xaml.cs b/OpenDreamClient/Interface/DebugWindows/IconDebugWindow.xaml.cs index 6d8d2d273c..e106313e35 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, MutableIconAppearance.Default.Name); + AddPropertyIfNotDefault("Icon State", appearance.IconState, MutableIconAppearance.Default.IconState); + AddPropertyIfNotDefault("Direction", appearance.Direction, MutableIconAppearance.Default.Direction); + AddPropertyIfNotDefault("Inherits Direction", appearance.InheritsDirection, MutableIconAppearance.Default.InheritsDirection); + AddPropertyIfNotDefault("Pixel Offset X/Y", appearance.PixelOffset, MutableIconAppearance.Default.PixelOffset); + AddPropertyIfNotDefault("Pixel Offset W/Z", appearance.PixelOffset2, MutableIconAppearance.Default.PixelOffset2); + AddPropertyIfNotDefault("Color", appearance.Color, MutableIconAppearance.Default.Color); + AddPropertyIfNotDefault("Alpha", appearance.Alpha, MutableIconAppearance.Default.Alpha); + AddPropertyIfNotDefault("Glide Size", appearance.GlideSize, MutableIconAppearance.Default.GlideSize); + AddPropertyIfNotDefault("Layer", appearance.Layer, MutableIconAppearance.Default.Layer); + AddPropertyIfNotDefault("Plane", appearance.Plane, MutableIconAppearance.Default.Plane); + AddPropertyIfNotDefault("Blend Mode", appearance.BlendMode, MutableIconAppearance.Default.BlendMode); + AddPropertyIfNotDefault("Appearance Flags", appearance.AppearanceFlags, MutableIconAppearance.Default.AppearanceFlags); + AddPropertyIfNotDefault("Invisibility", appearance.Invisibility, MutableIconAppearance.Default.Invisibility); + AddPropertyIfNotDefault("Opacity", appearance.Opacity, MutableIconAppearance.Default.Opacity); + AddPropertyIfNotDefault("Override", appearance.Override, MutableIconAppearance.Default.Override); + AddPropertyIfNotDefault("Render Source", appearance.RenderSource, MutableIconAppearance.Default.RenderSource); + AddPropertyIfNotDefault("Render Target", appearance.RenderTarget, MutableIconAppearance.Default.RenderTarget); + AddPropertyIfNotDefault("Mouse Opacity", appearance.MouseOpacity, MutableIconAppearance.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..e08e594c20 100644 --- a/OpenDreamClient/Rendering/ClientAppearanceSystem.cs +++ b/OpenDreamClient/Rendering/ClientAppearanceSystem.cs @@ -6,12 +6,13 @@ 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 Dictionary _appearances = new(); + private readonly Dictionary>> _appearanceLoadCallbacks = new(); private readonly Dictionary _turfIcons = new(); private readonly Dictionary _filterShaders = new(); @@ -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(int appearanceId, Action loadCallback) { if (_appearances.TryGetValue(appearanceId, out var appearance)) { loadCallback(appearance); + return; } if (!_appearanceLoadCallbacks.ContainsKey(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) { + int appearanceId = e.Appearance.GetHashCode(); + _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.ToMutable(), 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.ToMutable(), 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,8 @@ private set { UpdateIcon(); } } - private IconAppearance? _appearance; + + private MutableIconAppearance? _appearance; // TODO: We could cache these per-appearance instead of per-atom public IRenderTexture? CachedTexture { @@ -121,18 +122,17 @@ public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) { } appearanceSystem.LoadAppearance(appearanceId.Value, appearance => { + var mutableAppearance = appearance.ToMutable(); if (parentDir != null && appearance.InheritsDirection) { - appearance = new IconAppearance(appearance) { - Direction = parentDir.Value - }; + mutableAppearance.Direction = parentDir.Value; } - Appearance = appearance; + Appearance = mutableAppearance; }); } //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(MutableIconAppearance 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 +156,7 @@ public void StartAppearanceAnimation(IconAppearance endingAppearance, TimeSpan d _appearanceAnimations[i] = lastAnim; break; } - + _appearanceAnimations.Add(new AppearanceAnimation(start, duration, endingAppearance, easing, flags, delay, true)); } @@ -232,12 +232,12 @@ private void UpdateAnimation() { DirtyTexture(); } - private IconAppearance? CalculateAnimatedAppearance() { + private MutableIconAppearance? CalculateAnimatedAppearance() { if (_appearanceAnimations == null || _appearance == null) return _appearance; _textureDirty = true; //if we have animations, we need to recalculate the texture - IconAppearance appearance = new IconAppearance(_appearance); + MutableIconAppearance appearance = new MutableIconAppearance(_appearance); List? toRemove = null; List? toReAdd = null; for(int i = 0; i < _appearanceAnimations.Count; i++) { @@ -295,7 +295,7 @@ private void UpdateAnimation() { break; } - IconAppearance endAppearance = animation.EndAppearance; + MutableIconAppearance endAppearance = animation.EndAppearance; //non-smooth animations /* @@ -475,16 +475,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.GetHashCode(), Appearance.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.GetHashCode(), Appearance.Direction); underlay.SizeChanged += CheckSizeChange; Underlays.Add(underlay); @@ -552,10 +552,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, MutableIconAppearance 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 MutableIconAppearance EndAppearance = endAppearance; public readonly AnimationEasing Easing = easing; public readonly AnimationFlags Flags = flags; public readonly int Delay = delay; diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index 46b119799b..e942eec318 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(MutableIconAppearance appearance, string varName, DreamValue value) { switch (varName) { case "name": value.TryGetValueAsString(out var name); @@ -377,7 +385,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(MutableIconAppearance appearance, string varName) { + return GetAppearanceVar(new ImmutableIconAppearance(appearance, AppearanceSystem), varName); + } + + public DreamValue GetAppearanceVar(ImmutableIconAppearance appearance, string varName) { switch (varName) { case "name": return new(appearance.Name); @@ -450,7 +463,7 @@ public DreamValue GetAppearanceVar(IconAppearance appearance, string varName) { return new(matrix); case "appearance": - IconAppearance appearanceCopy = new IconAppearance(appearance); // Return a copy + MutableIconAppearance appearanceCopy = appearance.ToMutable(); // Return a copy return new(appearanceCopy); // TODO: overlays, underlays, filters // Those are handled separately by whatever is calling GetAppearanceVar currently @@ -460,15 +473,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 ImmutableIconAppearance 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!, registerApearance: false) : image.SpriteComponent!.Appearance!, _ => throw new Exception($"Cannot get appearance of {atom}") }; } @@ -476,65 +489,107 @@ 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 ImmutableIconAppearance? 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!, registerApearance: 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) { + ImmutableIconAppearance immutableAppearance = MustGetAppearance(atom); + MutableIconAppearance appearance = immutableAppearance.ToMutable(); // Clone the appearance update(appearance); SetAtomAppearance(atom, appearance); } - public void SetAtomAppearance(DreamObject atom, IconAppearance appearance) { + public void SetAtomAppearance(DreamObject atom, MutableIconAppearance 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); + } + + public void SetSpriteAppearance(Entity ent, MutableIconAppearance appearance) { + DMISpriteSystem.SetSpriteAppearance(ent, appearance); + } - IconAppearance appearance = new IconAppearance(movable.SpriteComponent.Appearance); + public void AnimateAppearance(DreamObject atom, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, Action animate) { + MutableIconAppearance appearance; + EntityUid targetEntity; + DMISpriteComponent? targetComponent = null; + NetEntity ent = NetEntity.Invalid; + int? 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}"); animate(appearance); - // Don't send the updated appearance to clients, they will animate it - movable.SpriteComponent.SetAppearance(appearance, dirty: false); - - 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 = appearance.GetHashCode()+1; + } 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 MutableIconAppearance? 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; } @@ -544,12 +599,12 @@ public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out Ic } 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 MutableIconAppearance() { Icon = iconResource.Id }; @@ -560,7 +615,7 @@ public bool TryCreateAppearanceFrom(DreamValue value, [NotNullWhen(true)] out Ic return false; } - public IconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { + public MutableIconAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) { if (_definitionAppearanceCache.TryGetValue(def, out var appearance)) return appearance; @@ -583,7 +638,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 MutableIconAppearance(); 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 c3c6929016..b8bc9cee8d 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 f7b7d7c751..a9b14b663b 100644 --- a/OpenDreamRuntime/DreamManager.cs +++ b/OpenDreamRuntime/DreamManager.cs @@ -242,7 +242,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 = _appearanceSystem.AddAppearance(appearance).GetHashCode(); } else if (value.TryGetValueAsDreamResource(out var refRsc)) { refType = RefType.DreamResource; idx = refRsc.Id; @@ -325,8 +325,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(refId, out ImmutableIconAppearance? 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 b7585478b4..0f765e2091 100644 --- a/OpenDreamRuntime/DreamMapManager.cs +++ b/OpenDreamRuntime/DreamMapManager.cs @@ -173,7 +173,7 @@ private DreamObject SetTurf(Vector2i pos, int z, DreamObjectDefinition type, Dre cell.Area.Contents.AddValue(new(cell.Turf)); } - IconAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); + MutableIconAppearance turfAppearance = _atomManager.GetAppearanceFromDefinition(cell.Turf.ObjectDefinition); SetTurfAppearance(cell.Turf, turfAppearance); cell.Turf.InitSpawn(creationArguments); @@ -188,40 +188,55 @@ 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, MutableIconAppearance> _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, MutableIconAppearance appearance) { + if(turf.Cell.Area.Appearance != _appearanceSystem.DefaultAppearance) + if(!appearance.Overlays.Contains(turf.Cell.Area.Appearance)) { + if(!_turfAreaLookup.TryGetValue((appearance, turf.Cell.Area.Appearance.GetHashCode()), 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.GetHashCode()), 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 + int turfId = immutableAppearance.GetHashCode() + 1; // +1 because 0 is used for empty turfs level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile(turfId); - turf.AppearanceId = appearanceId; + turf.Appearance = immutableAppearance; } - public void SetAreaAppearance(DreamObjectArea area, IconAppearance appearance) { + public void SetAreaAppearance(DreamObjectArea area, MutableIconAppearance 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.Contents.GetTurfs()) { - var turfAppearance = _atomManager.MustGetAppearance(turf); + var oldAppearance = area.Appearance; + 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.Contents.GetTurfs()) { + if(oldToNewAppearance.TryGetValue(turf.Appearance, out var newAppearance)) + turf.Appearance = newAppearance; + else { + MutableIconAppearance 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]; + int turfId = newAppearance.GetHashCode() + 1; // +1 because 0 is used for empty turfs + level.QueuedTileUpdates[(turf.X, turf.Y)] = new Tile(turfId); } } @@ -480,8 +495,8 @@ public Cell(DreamObjectArea area) { 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, MutableIconAppearance appearance); + public void SetAreaAppearance(DreamObjectArea area, MutableIconAppearance 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 d173114d3a..a53f6a8852 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(MutableIconAppearance 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 MutableIconAppearance? 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 MutableIconAppearance 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 31d66392a5..2c4789822d 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -579,7 +579,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])); @@ -590,7 +590,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); @@ -629,11 +629,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"); @@ -646,7 +646,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; @@ -663,13 +662,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; @@ -677,10 +674,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); }); } @@ -689,17 +685,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}"); + ImmutableIconAppearance 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); + ImmutableIconAppearance overlayAppearance = overlaysList[overlayIndex - 1]; + return new DreamValue(overlayAppearance.ToMutable()); } public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { @@ -710,11 +705,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(); + MutableIconAppearance? overlayAppearance = CreateOverlayAppearance(_atomManager, value, _atomManager.MustGetAppearance(_owner).Icon); + overlayAppearance ??= new MutableIconAppearance(); + ImmutableIconAppearance 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); }); } @@ -722,36 +720,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; + MutableIconAppearance? 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, registerApearance: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(MutableIconAppearance 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 ImmutableIconAppearance[] GetOverlaysArray(ImmutableIconAppearance appearance) => + _isUnderlays ? appearance.Underlays : appearance.Overlays; - public static IconAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { - IconAppearance overlay; + public static MutableIconAppearance? CreateOverlayAppearance(AtomManager atomManager, DreamValue value, int? defaultIcon) { + MutableIconAppearance overlay; if (value.TryGetValueAsString(out var iconState)) { - overlay = new IconAppearance() { + overlay = new MutableIconAppearance() { IconState = iconState }; overlay.Icon ??= defaultIcon; @@ -881,9 +875,15 @@ public override void Cut(int start = 1, int end = 0) { } public int GetIndexOfFilter(DreamFilter filter) { - IconAppearance appearance = GetAppearance(); + ImmutableIconAppearance 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) { @@ -908,9 +908,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}"); + ImmutableIconAppearance 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); @@ -919,8 +919,8 @@ public override DreamValue GetValue(DreamValue key) { } public override List GetValues() { - IconAppearance appearance = GetAppearance(); - List filterList = new List(appearance.Filters.Count); + ImmutableIconAppearance appearance = GetAppearance(); + List filterList = new List(appearance.Filters.Length); foreach (var filter in appearance.Filters) { DreamObjectFilter filterObject = ObjectTree.CreateObject(ObjectTree.Filter); @@ -958,11 +958,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 ImmutableIconAppearance GetAppearance() { + ImmutableIconAppearance? 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 f36b227d37..bdaa3dfc39 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 { @@ -23,13 +25,14 @@ public int Z { } public readonly AreaContentsList Contents; - public int AppearanceId; + public ImmutableIconAppearance Appearance; // Iterating all our turfs to find the one with the lowest coordinates is slow business private int? _cachedX, _cachedY, _cachedZ; public DreamObjectArea(DreamObjectDefinition objectDefinition) : base(objectDefinition) { Contents = new(ObjectTree.List.ObjectDefinition, this); + Appearance = AppearanceSystem!.DefaultAppearance; AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index df8754eb48..1c88badfa3 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,16 +178,11 @@ 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(); - + MutableIconAppearance appearance = AtomManager.MustGetAppearance(this).ToMutable(); AtomManager.SetAppearanceVar(appearance, varName, value); AtomManager.SetAtomAppearance(this, appearance); break; - } + } base.SetVar(varName, value); break; diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs b/OpenDreamRuntime/Objects/Types/DreamObjectImage.cs index 97cfae5a57..56c3e8c693 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 MutableIconAppearance? 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 mutableIconAppearance)) { // Use a default appearance, but log a warning about it if icon wasn't null - Appearance = new(AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); + mutableIconAppearance = 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(mutableIconAppearance, 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; + mutableIconAppearance.InheritsDirection = false; } } + + AtomManager.SetAtomAppearance(this, mutableIconAppearance); } 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(); + MutableIconAppearance mutableIconAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + mutableIconAppearance.Override = value.IsTruthy(); + AtomManager.SetAtomAppearance(this, mutableIconAppearance); break; } default: if (AtomManager.IsValidAppearanceVar(varName)) { - AtomManager.SetAppearanceVar(Appearance!, varName, value); - if(_entity != EntityUid.Invalid) { - DMISpriteComponent sprite = EntityManager.GetComponent(_entity); - sprite.SetAppearance(Appearance!); - } - + MutableIconAppearance mutableIconAppearance = IsMutableAppearance ? MutableAppearance! : AtomManager.MustGetAppearance(this).ToMutable(); + AtomManager.SetAppearanceVar(mutableIconAppearance, varName, value); + AtomManager.SetAtomAppearance(this, mutableIconAppearance); 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..d65ad2b315 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 3ceb6e995a..729d3fa884 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectTurf.cs @@ -1,10 +1,12 @@ -namespace OpenDreamRuntime.Objects.Types; +using OpenDreamShared.Dream; + +namespace OpenDreamRuntime.Objects.Types; public sealed class DreamObjectTurf : DreamObjectAtom { public readonly int X, Y, Z; public readonly IDreamMapManager.Cell Cell; public readonly TurfContentsList Contents; - public int AppearanceId; + public ImmutableIconAppearance Appearance; public DreamObjectTurf(DreamObjectDefinition objectDefinition, int x, int y, int z, IDreamMapManager.Cell cell) : base(objectDefinition) { X = x; @@ -12,6 +14,7 @@ public DreamObjectTurf(DreamObjectDefinition objectDefinition, int x, int y, int Z = z; Cell = cell; Contents = new TurfContentsList(ObjectTree.List.ObjectDefinition, Cell); + AtomManager.SetAtomAppearance(this, AtomManager.GetAppearanceFromDefinition(ObjectDefinition)); } public void SetTurfType(DreamObjectDefinition objectDefinition) { diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 1d1e856773..a4337f5ea9 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -2616,7 +2616,7 @@ private static bool IsEqual(DreamValue first, DreamValue second) { if (!second.TryGetValueAsAppearance(out var secondValue)) return false; - IconAppearance firstValue = first.MustGetValueAsAppearance(); + MutableIconAppearance firstValue = first.MustGetValueAsAppearance(); return firstValue.Equals(secondValue); } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeHelpers.cs index fb85fda761..2f6d830e42 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..68ec90b618 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 ImmutableIconAppearance? Appearance; } + diff --git a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs index 7d7d7c865c..5da77d0156 100644 --- a/OpenDreamRuntime/Rendering/DMISpriteSystem.cs +++ b/OpenDreamRuntime/Rendering/DMISpriteSystem.cs @@ -1,21 +1,32 @@ -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); - } + args.State = new SharedDMISpriteComponent.DMISpriteComponentState(component.Appearance?.GetHashCode(), component.ScreenLocation); + } + + public void SetSpriteAppearance(Entity ent, MutableIconAppearance appearance, bool dirty = true) { + DMISpriteComponent component = ent.Comp; + component.Appearance = _appearance?.AddAppearance(appearance); + if(dirty) + Dirty(ent, component); + } - args.State = new SharedDMISpriteComponent.DMISpriteComponentState(appearanceId, component.ScreenLocation); + 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..a8c4686585 100644 --- a/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs +++ b/OpenDreamRuntime/Rendering/ServerAppearanceSystem.cs @@ -5,13 +5,22 @@ using System.Diagnostics.CodeAnalysis; using OpenDreamShared.Network.Messages; using Robust.Shared.Player; +using Robust.Shared.Network; +using System.Diagnostics; +using Robust.Shared.Utility; namespace OpenDreamRuntime.Rendering; public sealed class ServerAppearanceSystem : SharedAppearanceSystem { - private readonly Dictionary _appearanceToId = new(); - private readonly Dictionary _idToAppearance = new(); - private int _appearanceIdCounter; + /// + /// Each appearance's HashCode is used as its ID. Here we store these as weakrefs, so each object which holds an appearance MUST + /// hold that ImmutableIconAppearance until it is no longer needed. Overlays & underlays are stored as hard refs on the ImmutableIconAppearance + /// so you only need to hold the main appearance. + /// + private readonly Dictionary> _idToAppearance = new(); + + public readonly ImmutableIconAppearance DefaultAppearance; + [Dependency] private readonly IServerNetManager _networkManager = default!; /// /// This system is used by the PVS thread, we need to be thread-safe @@ -20,62 +29,90 @@ public sealed class ServerAppearanceSystem : SharedAppearanceSystem { [Dependency] private readonly IPlayerManager _playerManager = default!; + public ServerAppearanceSystem() { + DefaultAppearance = new ImmutableIconAppearance(MutableIconAppearance.Default, this); + DefaultAppearance.MarkRegistered(); + Debug.Assert(DefaultAppearance.GetHashCode() == MutableIconAppearance.Default.GetHashCode()); + } + public override void Initialize() { - //register empty appearance as ID 0 - _appearanceToId.Add(IconAppearance.Default, 0); - _idToAppearance.Add(0, IconAppearance.Default); - _appearanceIdCounter = 1; + _idToAppearance.Add(DefaultAppearance.GetHashCode(), new(DefaultAppearance)); _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } public override void Shutdown() { lock (_lock) { - _appearanceToId.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(_idToAppearance.Count); + + foreach(int key in _idToAppearance.Keys){ + if(_idToAppearance[key].TryGetTarget(out var immutable)) + sendData.Add(key, immutable); + } + + e.Session.Channel.SendMessage(new MsgAllAppearances(sendData)); + } + } } - public int AddAppearance(IconAppearance appearance) { + public ImmutableIconAppearance AddAppearance(MutableIconAppearance appearance, bool registerApearance = true) { + ImmutableIconAppearance immutableAppearance = new(appearance, this); + //if this debug assert fails, you've probably changed an icon appearance var and not updated its counterpart + //this debug MUST pass. A number of things rely on these hashcodes being equivalent *on the server*. + DebugTools.Assert(appearance.GetHashCode() == immutableAppearance.GetHashCode()); 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(_idToAppearance.TryGetValue(immutableAppearance.GetHashCode(), out var weakReference) && weakReference.TryGetTarget(out var originalImmutable)) { + return originalImmutable; + } else if (registerApearance) { + immutableAppearance.MarkRegistered(); //lets this appearance know it needs to do GC finaliser + _idToAppearance[immutableAppearance.GetHashCode()] = new(immutableAppearance); + _networkManager.ServerSendToAll(new MsgNewAppearance(immutableAppearance)); + return immutableAppearance; + } else { + return immutableAppearance; } - - return appearanceId; } } - public IconAppearance MustGetAppearance(int appearanceId) { + //this should only be called by the ImmutableIconAppearance's finalizer + public override void RemoveAppearance(ImmutableIconAppearance appearance) { lock (_lock) { - return _idToAppearance[appearanceId]; + if(_idToAppearance.TryGetValue(appearance.GetHashCode(), 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; + _idToAppearance.Remove(appearance.GetHashCode()); + RaiseNetworkEvent(new RemoveAppearanceEvent(appearance.GetHashCode())); + } } } - public bool TryGetAppearance(int appearanceId, [NotNullWhen(true)] out IconAppearance? appearance) { + public override ImmutableIconAppearance MustGetAppearanceById(int 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(int appearanceId, [NotNullWhen(true)] out ImmutableIconAppearance? 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, MutableIconAppearance targetAppearance, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, int? turfId) { + int appearanceId = AddAppearance(targetAppearance).GetHashCode(); - RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim)); + RaiseNetworkEvent(new AnimationEvent(entity, appearanceId, duration, easing, loop, flags, delay, chainAnim, turfId)); } } 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..60b39a48b6 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. /// diff --git a/OpenDreamShared/Dream/ImmutableIconAppearance.cs b/OpenDreamShared/Dream/ImmutableIconAppearance.cs new file mode 100644 index 0000000000..a685f3fd1a --- /dev/null +++ b/OpenDreamShared/Dream/ImmutableIconAppearance.cs @@ -0,0 +1,630 @@ +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; + +namespace OpenDreamShared.Dream; + +/* + * Woe, weary traveler, modifying this class is not for the faint of heart. + * If you modify MutableIconAppearance, be sure to update the following places: + * - All of the methods on ImmutableIconAppearance itself + * - MutableIconAppearance + * - MutableIconAppearance 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 ImmutableIconAppearance : IEquatable{ + private bool _registered; + private int? _storedHashCode; + private readonly SharedAppearanceSystem? _appearanceSystem; + [ViewVariables] public readonly string Name = MutableIconAppearance.Default.Name; + [ViewVariables] public readonly int? Icon = MutableIconAppearance.Default.Icon; + [ViewVariables] public readonly string? IconState = MutableIconAppearance.Default.IconState; + [ViewVariables] public readonly AtomDirection Direction = MutableIconAppearance.Default.Direction; + [ViewVariables] public readonly bool InheritsDirection = MutableIconAppearance.Default.InheritsDirection; // Inherits direction when used as an overlay + [ViewVariables] public readonly Vector2i PixelOffset = MutableIconAppearance.Default.PixelOffset; // pixel_x and pixel_y + [ViewVariables] public readonly Vector2i PixelOffset2 = MutableIconAppearance.Default.PixelOffset2; // pixel_w and pixel_z + [ViewVariables] public readonly Color Color = MutableIconAppearance.Default.Color; + [ViewVariables] public readonly byte Alpha = MutableIconAppearance.Default.Alpha; + [ViewVariables] public readonly float GlideSize = MutableIconAppearance.Default.GlideSize; + [ViewVariables] public readonly float Layer = MutableIconAppearance.Default.Layer; + [ViewVariables] public readonly int Plane = MutableIconAppearance.Default.Plane; + [ViewVariables] public readonly BlendMode BlendMode = MutableIconAppearance.Default.BlendMode; + [ViewVariables] public readonly AppearanceFlags AppearanceFlags = MutableIconAppearance.Default.AppearanceFlags; + [ViewVariables] public readonly sbyte Invisibility = MutableIconAppearance.Default.Invisibility; + [ViewVariables] public readonly bool Opacity = MutableIconAppearance.Default.Opacity; + [ViewVariables] public readonly bool Override = MutableIconAppearance.Default.Override; + [ViewVariables] public readonly string? RenderSource = MutableIconAppearance.Default.RenderSource; + [ViewVariables] public readonly string? RenderTarget = MutableIconAppearance.Default.RenderTarget; + [ViewVariables] public readonly MouseOpacity MouseOpacity = MutableIconAppearance.Default.MouseOpacity; + [ViewVariables] public readonly ImmutableIconAppearance[] Overlays; + [ViewVariables] public readonly ImmutableIconAppearance[] 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(){ + _registered = 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 ImmutableIconAppearance(MutableIconAppearance 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 ImmutableIconAppearance immutable && Equals(immutable); + + public bool Equals(ImmutableIconAppearance? immutableIconAppearance) { + if (immutableIconAppearance == null) return false; + + if (immutableIconAppearance.Name != Name) return false; + if (immutableIconAppearance.Icon != Icon) return false; + if (immutableIconAppearance.IconState != IconState) return false; + if (immutableIconAppearance.Direction != Direction) return false; + if (immutableIconAppearance.InheritsDirection != InheritsDirection) return false; + if (immutableIconAppearance.PixelOffset != PixelOffset) return false; + if (immutableIconAppearance.PixelOffset2 != PixelOffset2) return false; + if (immutableIconAppearance.Color != Color) return false; + if (immutableIconAppearance.Alpha != Alpha) return false; + if (immutableIconAppearance.GlideSize != GlideSize) return false; + if (!immutableIconAppearance.ColorMatrix.Equals(ColorMatrix)) return false; + if (immutableIconAppearance.Layer != Layer) return false; + if (immutableIconAppearance.Plane != Plane) return false; + if (immutableIconAppearance.RenderSource != RenderSource) return false; + if (immutableIconAppearance.RenderTarget != RenderTarget) return false; + if (immutableIconAppearance.BlendMode != BlendMode) return false; + if (immutableIconAppearance.AppearanceFlags != AppearanceFlags) return false; + if (immutableIconAppearance.Invisibility != Invisibility) return false; + if (immutableIconAppearance.Opacity != Opacity) return false; + if (immutableIconAppearance.MouseOpacity != MouseOpacity) return false; + if (immutableIconAppearance.Overlays.Length != Overlays.Length) return false; + if (immutableIconAppearance.Underlays.Length != Underlays.Length) return false; + if (immutableIconAppearance.VisContents.Length != VisContents.Length) return false; + if (immutableIconAppearance.Filters.Length != Filters.Length) return false; + if (immutableIconAppearance.Verbs.Length != Verbs.Length) return false; + if (immutableIconAppearance.Override != Override) return false; + + for (int i = 0; i < Filters.Length; i++) { + if (immutableIconAppearance.Filters[i] != Filters[i]) return false; + } + + for (int i = 0; i < Overlays.Length; i++) { + if (!immutableIconAppearance.Overlays[i].Equals(Overlays[i])) return false; + } + + for (int i = 0; i < Underlays.Length; i++) { + if (!immutableIconAppearance.Underlays[i].Equals(Underlays[i])) return false; + } + + for (int i = 0; i < VisContents.Length; i++) { + if (immutableIconAppearance.VisContents[i] != VisContents[i]) return false; + } + + for (int i = 0; i < Verbs.Length; i++) { + if (immutableIconAppearance.Verbs[i] != Verbs[i]) return false; + } + + for (int i = 0; i < 6; i++) { + if (!immutableIconAppearance.Transform[i].Equals(Transform[i])) return false; + } + + return true; + } + + 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(MouseOpacity); + hashCode.Add(Alpha); + hashCode.Add(GlideSize); + hashCode.Add(Plane); + hashCode.Add(RenderSource); + hashCode.Add(RenderTarget); + hashCode.Add(BlendMode); + hashCode.Add(AppearanceFlags); + + foreach (ImmutableIconAppearance overlay in Overlays) { + hashCode.Add(overlay.GetHashCode()); + } + + foreach (ImmutableIconAppearance 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 ImmutableIconAppearance(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: + _storedHashCode = buffer.ReadVariableInt32(); + 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 ImmutableIconAppearance[overlaysCount]; + _overlayIDs = new(overlaysCount); + for (int overlaysI = 0; overlaysI < overlaysCount; overlaysI++) { + _overlayIDs.Add(buffer.ReadVariableInt32()); + } + + break; + } + case IconAppearanceProperty.Underlays: { + var underlaysCount = buffer.ReadVariableInt32(); + + Underlays = new ImmutableIconAppearance[underlaysCount]; + _underlayIDs = new(underlaysCount); + for (int underlaysI = 0; underlaysI < underlaysCount; underlaysI++) { + _underlayIDs.Add(buffer.ReadVariableInt32()); + } + + 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(_storedHashCode 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 MutableIconAppearance ToMutable() { + MutableIconAppearance result = new MutableIconAppearance() { + 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(ImmutableIconAppearance overlay in Overlays) + result.Overlays.Add(overlay); + + foreach(ImmutableIconAppearance 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.WriteVariableInt32(GetHashCode()); + + if (Name != MutableIconAppearance.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 != MutableIconAppearance.Default.Direction) { + buffer.Write((byte)IconAppearanceProperty.Direction); + buffer.Write((byte)Direction); + } + + if (InheritsDirection != true) { + buffer.Write((byte)IconAppearanceProperty.DoesntInheritDirection); + } + + if (PixelOffset != MutableIconAppearance.Default.PixelOffset) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset); + buffer.WriteVariableInt32(PixelOffset.X); + buffer.WriteVariableInt32(PixelOffset.Y); + } + + if (PixelOffset2 != MutableIconAppearance.Default.PixelOffset2) { + buffer.Write((byte)IconAppearanceProperty.PixelOffset2); + buffer.WriteVariableInt32(PixelOffset2.X); + buffer.WriteVariableInt32(PixelOffset2.Y); + } + + if (Color != MutableIconAppearance.Default.Color) { + buffer.Write((byte)IconAppearanceProperty.Color); + buffer.Write(Color); + } + + if (Alpha != MutableIconAppearance.Default.Alpha) { + buffer.Write((byte)IconAppearanceProperty.Alpha); + buffer.Write(Alpha); + } + + if (!GlideSize.Equals(MutableIconAppearance.Default.GlideSize)) { + buffer.Write((byte)IconAppearanceProperty.GlideSize); + buffer.Write(GlideSize); + } + + if (!ColorMatrix.Equals(MutableIconAppearance.Default.ColorMatrix)) { + buffer.Write((byte)IconAppearanceProperty.ColorMatrix); + + foreach (var value in ColorMatrix.GetValues()) + buffer.Write(value); + } + + if (!Layer.Equals(MutableIconAppearance.Default.Layer)) { + buffer.Write((byte)IconAppearanceProperty.Layer); + buffer.Write(Layer); + } + + if (Plane != MutableIconAppearance.Default.Plane) { + buffer.Write((byte)IconAppearanceProperty.Plane); + buffer.WriteVariableInt32(Plane); + } + + if (BlendMode != MutableIconAppearance.Default.BlendMode) { + buffer.Write((byte)IconAppearanceProperty.BlendMode); + buffer.Write((byte)BlendMode); + } + + if (AppearanceFlags != MutableIconAppearance.Default.AppearanceFlags) { + buffer.Write((byte)IconAppearanceProperty.AppearanceFlags); + buffer.Write((int)AppearanceFlags); + } + + if (Invisibility != MutableIconAppearance.Default.Invisibility) { + buffer.Write((byte)IconAppearanceProperty.Invisibility); + buffer.Write(Invisibility); + } + + if (Opacity != MutableIconAppearance.Default.Opacity) { + buffer.Write((byte)IconAppearanceProperty.Opacity); + buffer.Write(Opacity); + } + + if (Override != MutableIconAppearance.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 != MutableIconAppearance.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.WriteVariableInt32(overlay.GetHashCode()); + } + } + + if (Underlays.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Underlays); + + buffer.WriteVariableInt32(Underlays.Length); + foreach (var underlay in Underlays) { + buffer.WriteVariableInt32(underlay.GetHashCode()); + } + } + + 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(MutableIconAppearance.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(); + } + + ~ImmutableIconAppearance() { + if(_registered) + _appearanceSystem!.RemoveAppearance(this); + } +} + diff --git a/OpenDreamShared/Dream/IconAppearance.cs b/OpenDreamShared/Dream/MutableIconAppearance.cs similarity index 86% rename from OpenDreamShared/Dream/IconAppearance.cs rename to OpenDreamShared/Dream/MutableIconAppearance.cs index 78e38af359..cf83026ad8 100644 --- a/OpenDreamShared/Dream/IconAppearance.cs +++ b/OpenDreamShared/Dream/MutableIconAppearance.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 MutableIconAppearance, be sure to update the following places: + * - All of the methods on MutableIconAppearance itself + * - ImmutableIconAppearance * - 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 MutableIconAppearance : IEquatable{ + public static readonly MutableIconAppearance 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 MutableIconAppearance() { Overlays = new(); Underlays = new(); VisContents = new(); @@ -80,7 +80,7 @@ public IconAppearance() { Verbs = new(); } - public IconAppearance(IconAppearance appearance) { + public MutableIconAppearance(MutableIconAppearance 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 MutableIconAppearance appearance && Equals(appearance); - public bool Equals(IconAppearance? appearance) { + public bool Equals(MutableIconAppearance? 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 ImmutableIconAppearance. There's a debug assert and everything. public override int GetHashCode() { HashCode hashCode = new HashCode(); @@ -227,12 +228,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 +334,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..f20cdd228a 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 ImmutableIconAppearance(buffer, serializer); + AllAppearances.Add(appearance.GetHashCode(), 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..e08f7fce13 --- /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 ImmutableIconAppearance(MutableIconAppearance.Default, null)) {} + public MsgNewAppearance(ImmutableIconAppearance appearance) => Appearance = appearance; + public ImmutableIconAppearance Appearance; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + Appearance = new ImmutableIconAppearance(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..3d732d8669 100644 --- a/OpenDreamShared/Rendering/SharedAppearanceSystem.cs +++ b/OpenDreamShared/Rendering/SharedAppearanceSystem.cs @@ -6,14 +6,22 @@ namespace OpenDreamShared.Rendering; public abstract class SharedAppearanceSystem : EntitySystem { + public abstract ImmutableIconAppearance MustGetAppearanceById(int appearanceId); + public abstract void RemoveAppearance(ImmutableIconAppearance appearance); + + [Serializable, NetSerializable] + public sealed class NewAppearanceEvent(int appearanceId, MutableIconAppearance appearance) : EntityEventArgs { + public int AppearanceId { get; } = appearanceId; + public MutableIconAppearance Appearance { get; } = appearance; + } + [Serializable, NetSerializable] - public sealed class NewAppearanceEvent(int appearanceId, IconAppearance appearance) : EntityEventArgs { + public sealed class RemoveAppearanceEvent(int appearanceId) : EntityEventArgs { public int AppearanceId { get; } = appearanceId; - public IconAppearance Appearance { get; } = appearance; } [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, int targetAppearanceId, TimeSpan duration, AnimationEasing easing, int loop, AnimationFlags flags, int delay, bool chainAnim, int? turfId) : EntityEventArgs { public NetEntity Entity = entity; public int TargetAppearanceId = targetAppearanceId; @@ -23,5 +31,6 @@ public sealed class AnimationEvent(NetEntity entity, int targetAppearanceId, Tim public AnimationFlags Flags = flags; public int Delay = delay; public bool ChainAnim = chainAnim; + public int? TurfId = turfId; } } diff --git a/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs b/OpenDreamShared/Rendering/SharedDMISpriteComponent.cs index 004ffe2534..47d3bb60c6 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 int? AppearanceId; + public readonly ScreenLocation ScreenLocation; + + public DMISpriteComponentState(int? appearanceId, ScreenLocation screenLocation) { + AppearanceId = appearanceId; + ScreenLocation = screenLocation; } } } diff --git a/TestGame/map_z1.dmm b/TestGame/map_z1.dmm index 899ae1e80e..6ba6898417 100644 --- a/TestGame/map_z1.dmm +++ b/TestGame/map_z1.dmm @@ -36,8 +36,10 @@ "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) "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) @@ -45,8 +47,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'