diff --git a/DMCompiler/DMStandard/Types/Atoms/Movable.dm b/DMCompiler/DMStandard/Types/Atoms/Movable.dm index 185e87280f..ff0a4b589e 100644 --- a/DMCompiler/DMStandard/Types/Atoms/Movable.dm +++ b/DMCompiler/DMStandard/Types/Atoms/Movable.dm @@ -13,7 +13,7 @@ //Undocumented var. "[x],[y]" or "[x],[y] to [x2],[y2]" based on bound_* vars var/bounds as opendream_unimplemented - var/particles/particles as opendream_unimplemented + var/particles/particles proc/Bump(atom/Obstacle) diff --git a/DMCompiler/DMStandard/Types/Particles.dm b/DMCompiler/DMStandard/Types/Particles.dm index 3b7a64ca4d..d57e4b2103 100644 --- a/DMCompiler/DMStandard/Types/Particles.dm +++ b/DMCompiler/DMStandard/Types/Particles.dm @@ -3,34 +3,34 @@ /particles parent_type = /datum //Particle vars that affect the entire set (generators are not allowed for these) - var/width = 100 as opendream_unimplemented //null defaults to 0. width is the size of the particle "image" ie particles within this width image will be rendered, if they are partially in they get partially cut. if they reenter this area after leaving it they reapper. image is centered on particle owner. - var/height = 100 as opendream_unimplemented //ditto - var/count = 100 as opendream_unimplemented // if null, uses the last set value. is checked BEFORE lifespan so (count 10, lifespan 10, spawning 1) will skip a pixel every 10 pixels - var/spawning = 1 as opendream_unimplemented // null is treated as 0 - var/bound1 = -1000 as opendream_unimplemented // Usually list but if a number treated as list(bound1, bound1, bound1). if particles go above/below bound they will get immediately deleted regardless of lifespan. null is treated as the default value (-1000 and 1000)(this could be treated as infinity as well but 1000 is so large its hard to tell) - var/bound2 = 1000 as opendream_unimplemented // Ditto! - var/gravity as opendream_unimplemented // Usually list but if a number treated as list(gravity, gravity, gravity). - var/list/gradient = null as opendream_unimplemented // not cast as a list on byond as of 514.1580 despite only being able to be a list - var/transform as opendream_unimplemented // matrix or list. list can be simple matrix, complex matrix or projection matrix. thus: list(a, b, c, d, e, f) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz, cx,cy,cz) OR list(xx,xy,xz,xw, yx,yy,yz,yw, zx,zy,zz,zw, wx,wy,wz,ww) + var/width = 100 //null defaults to 0. width is the size of the particle "image" ie particles within this width image will be rendered, if they are partially in they get partially cut. if they reenter this area after leaving it they reapper. image is centered on particle owner. + var/height = 100 //ditto + var/count = 100 // if null, uses the last set value. is checked BEFORE lifespan so (count 10, lifespan 10, spawning 1) will skip a pixel every 10 pixels + var/spawning = 1 // null is treated as 0 + var/bound1 = -1000 // Usually list but if a number treated as list(bound1, bound1, bound1). if particles go above/below bound they will get immediately deleted regardless of lifespan. null is treated as the default value (-1000 and 1000)(this could be treated as infinity as well but 1000 is so large its hard to tell) + var/bound2 = 1000 // Ditto! + var/gravity // Usually list but if a number treated as list(gravity, gravity, gravity). + var/list/gradient = null // not cast as a list on byond as of 514.1580 despite only being able to be a list + var/transform // matrix or list. list can be simple matrix, complex matrix or projection matrix. thus: list(a, b, c, d, e, f) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz) OR list(xx,xy,xz, yx,yy,yz, zx,zy,zz, cx,cy,cz) OR list(xx,xy,xz,xw, yx,yy,yz,yw, zx,zy,zz,zw, wx,wy,wz,ww) //Vars that apply when a particle spawns - var/lifespan as opendream_unimplemented // actual time a particle exists is fadein + lifespan + fade. thus this just the time it spends fully faded in. null is treated as - var/fade as opendream_unimplemented // null treated as 0 - var/fadein as opendream_unimplemented // null treated as 0 - var/icon as opendream_unimplemented // either icon or list(icon = weightofthisicon, icon = weightofthisicon) if null defaults to a 1x1 white pixel - var/icon_state as opendream_unimplemented // either string or list(string = weightofthisiconstate, string = weightofthisiconstate) if null defaults to a 1x1 white pixel - var/color as opendream_unimplemented // null treated as 0 - var/color_change as opendream_unimplemented // null treated as 0 - var/position as opendream_unimplemented // Usually list but if a number treated as list(position, position, position). null is treated as 0 - var/velocity as opendream_unimplemented // Usually list but if a number treated as list(velocity, velocity, velocity). null is treated as 0 - var/scale as opendream_unimplemented // if null defaults to 1, if number treated as list(scale, scale) - var/grow as opendream_unimplemented // if null defaults to 0, if number treated as list(grow, grow) - var/rotation as opendream_unimplemented // null treated as 0 - var/spin as opendream_unimplemented // null treated as 0 - var/friction as opendream_unimplemented // null treated as 0, numbers below 0 treated as 0 + var/lifespan // actual time a particle exists is fadein + lifespan + fade. thus this just the time it spends fully faded in. null is treated as + var/fade // null treated as 0 + var/fadein // null treated as 0 + var/icon // either icon or list(icon = weightofthisicon, icon = weightofthisicon) if null defaults to a 1x1 white pixel + var/icon_state // either string or list(string = weightofthisiconstate, string = weightofthisiconstate) if null defaults to a 1x1 white pixel + var/color // null treated as 0 + var/color_change // null treated as 0 + var/position // Usually list but if a number treated as list(position, position, position). null is treated as 0 + var/velocity // Usually list but if a number treated as list(velocity, velocity, velocity). null is treated as 0 + var/scale // if null defaults to 1, if number treated as list(scale, scale) + var/grow // if null defaults to 0, if number treated as list(grow, grow) + var/rotation // null treated as 0 + var/spin // null treated as 0 + var/friction // null treated as 0, numbers below 0 treated as 0 //Vars that are evaluated every tick - var/drift as opendream_unimplemented // Usually list but if a number treated as list(drift, drift, drift) + var/drift // Usually list but if a number treated as list(drift, drift, drift) //misc notes // particle image height/width is not considered for TILE_BOUND-less atoms diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 410b1cd950..e36c673aa6 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -29,6 +29,7 @@ proc/flist(Path) as /list proc/floor(A) as num proc/fract(n) as num proc/ftime(File, IsCreationTime = 0) as num +proc/generator(type, A, B, rand) as /generator proc/get_step_to(Ref, Trg, Min=0) as num proc/get_steps_to(Ref, Trg, Min=0) as /list proc/gradient(A, index) diff --git a/OpenDreamRuntime/Objects/Types/DreamList.cs b/OpenDreamRuntime/Objects/Types/DreamList.cs index 0689448131..c4bb8500c6 100644 --- a/OpenDreamRuntime/Objects/Types/DreamList.cs +++ b/OpenDreamRuntime/Objects/Types/DreamList.cs @@ -858,6 +858,95 @@ public override int GetLength() { } } +// atom.particles list +// Operates on an atom's appearance +public sealed class DreamParticlesList : DreamList { + [Dependency] private readonly AtomManager _atomManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + private readonly PvsOverrideSystem? _pvsOverrideSystem; + + private readonly List _particles = new(); + private readonly DreamObject _atom; + + public DreamParticlesList(DreamObjectDefinition listDef, PvsOverrideSystem? pvsOverrideSystem, DreamObject atom) : base(listDef, 0) { + IoCManager.InjectDependencies(this); + + _pvsOverrideSystem = pvsOverrideSystem; + _atom = atom; + } + + public override List GetValues() { + var values = new List(_particles.Count); + + foreach (var particlesObject in _particles) { + values.Add(new(particlesObject)); + } + + return values; + } + + public override void Cut(int start = 1, int end = 0) { + int count = _particles.Count + 1; + if (end == 0 || end > count) end = count; + + _particles.RemoveRange(start - 1, end - start); + _atomManager.UpdateAppearance(_atom, appearance => { + appearance.Particles.RemoveRange(start - 1, end - start); + }); + } + + public override DreamValue GetValue(DreamValue key) { + if (!key.TryGetValueAsInteger(out var particlesIndex) || particlesIndex < 1) + throw new Exception($"Invalid index into particles list: {key}"); + if (particlesIndex > _particles.Count) + throw new Exception($"Atom only has {_particles.Count} particles element(s), cannot index {particlesIndex}"); + + return new DreamValue(_particles[particlesIndex - 1]); + } + + public override void SetValue(DreamValue key, DreamValue value, bool allowGrowth = false) { + throw new Exception("Cannot write to an index of a particles list"); + } + + public override void AddValue(DreamValue value) { + EntityUid entity; + if (value.TryGetValueAsDreamObject(out var particles)) { + if (_particles.Contains(particles)) + return; // particles cannot contain duplicates + _particles.Add(particles); + entity = particles.Entity; + } else if (value == DreamValue.Null) { + return; // particles cannot contain nulls + } else { + throw new Exception($"Cannot add {value} to a particles list"); + } + + // TODO: Only override the entity's visibility if its parent atom is visible + if (entity != EntityUid.Invalid) + _pvsOverrideSystem?.AddGlobalOverride(entity); + + _atomManager.UpdateAppearance(_atom, appearance => { + // Add even an invalid UID to keep this and _visContents in sync + appearance.Particles.Add(_entityManager.GetNetEntity(entity)); + }); + } + + public override void RemoveValue(DreamValue value) { + if (!value.TryGetValueAsDreamObject(out var particles)) + return; + + _particles.Remove(particles); + _atomManager.UpdateAppearance(_atom, appearance => { + appearance.Particles.Remove(_entityManager.GetNetEntity(particles.Entity)); + }); + } + + public override int GetLength() { + return _particles.Count; + } +} + + // atom.filters list // Operates on an object's appearance public sealed class DreamFilterList : DreamList { diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs index 20cba60f33..1d9f5b3abf 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs @@ -7,6 +7,7 @@ public class DreamObjectAtom : DreamObject { public readonly DreamOverlaysList Overlays; public readonly DreamOverlaysList Underlays; public readonly DreamVisContentsList VisContents; + public readonly DreamParticlesList Particles; public readonly DreamFilterList Filters; public DreamList? VisLocs; // TODO: Implement @@ -14,6 +15,7 @@ public DreamObjectAtom(DreamObjectDefinition objectDefinition) : base(objectDefi Overlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, false); Underlays = new(ObjectTree.List.ObjectDefinition, this, AppearanceSystem, true); VisContents = new(ObjectTree.List.ObjectDefinition, PvsOverrideSystem, this); + Particles = new(ObjectTree.List.ObjectDefinition, PvsOverrideSystem, this); Filters = new(ObjectTree.List.ObjectDefinition, this); AtomManager.AddAtom(this); @@ -73,6 +75,9 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "vis_contents": value = new(VisContents); return true; + case "particles": + value = new(Particles); + return true; default: if (AtomManager.IsValidAppearanceVar(varName)) { @@ -146,6 +151,20 @@ protected override void SetVar(string varName, DreamValue value) { break; } + case "particles": { + Particles.Cut(); + + if (value.TryGetValueAsDreamList(out var valueList)) { + // TODO: This should postpone UpdateAppearance until after everything is added + foreach (DreamValue particlesValue in valueList.GetValues()) { + Particles.AddValue(particlesValue); + } + } else if (!value.IsNull) { + Particles.AddValue(value); + } + + break; + } case "filters": { Filters.Cut(); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectParticles.cs b/OpenDreamRuntime/Objects/Types/DreamObjectParticles.cs index d57f031743..d9b1fc5d62 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectParticles.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectParticles.cs @@ -20,11 +20,8 @@ public DreamObjectParticles(DreamObjectDefinition objectDefinition) : base(objec 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 ParticlesComponent = EntityManager.AddComponent(Entity); //populate component with settings from type - //do set/get var to grab those also //check if I need to manually send update events to the component? - //add entity array to appearance objects //collect entities client-side for the rendermetadata - //set up a special list type on /atom for /particles } protected override void SetVar(string varName, DreamValue value) { @@ -167,8 +164,7 @@ protected override void SetVar(string varName, DreamValue value) { ParticlesComponent.SpawnPositionHigh = new Vector3(dreamValues[0].MustGetValueAsFloat(), dreamValues[1].MustGetValueAsFloat(), dreamValues[2].MustGetValueAsFloat()); dreamValues = dreamObjectGenerator.A.MustGetValueAsDreamList().GetValues(); ParticlesComponent.SpawnPositionLow = new Vector3(dreamValues[0].MustGetValueAsFloat(), dreamValues[1].MustGetValueAsFloat(), dreamValues[2].MustGetValueAsFloat()); - ParticlesComponent.RotationLow = dreamObjectGenerator.A.MustGetValueAsFloat(); - ParticlesComponent.RotationType = ParticlePropertyType.RandomUniform; //TODO all the other distributions + ParticlesComponent.SpawnPositionType = ParticlePropertyType.RandomUniform; //TODO all the other distributions } break; case "velocity": //num, list, vector, or generator @@ -288,9 +284,7 @@ protected override void SetVar(string varName, DreamValue value) { ParticlesComponent.DriftType = ParticlePropertyType.RandomUniform; //TODO all the other distributions } break; - default: - base.SetVar(varName, value); - break; } + base.SetVar(varName, value); //all calls should set the internal vars, so GetVar() can just be default also } } diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index acaaac09e2..53c9d38603 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -33,6 +33,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_floor); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_fract); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_ftime); + objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_generator); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_get_step_to); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_get_steps_to); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_hascall); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 436a9c6d95..d2af41d246 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -1097,7 +1097,7 @@ public static DreamValue NativeProc_get_steps_to(NativeProc.Bundle bundle, Dream [DreamProcParameter("type", Type = DreamValueTypeFlag.String)] [DreamProcParameter("A", Type = DreamValueTypeFlag.DreamObject)] [DreamProcParameter("B", Type = DreamValueTypeFlag.DreamObject)] - [DreamProcParameter("rand", Type = DreamValueTypeFlag.Float)] + [DreamProcParameter("rand", Type = DreamValueTypeFlag.Float, DefaultValue = 0)] public static DreamValue NativeProc_generator(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) { string outputTypeString = bundle.GetArgument(0, "type").MustGetValueAsString(); var A = bundle.GetArgument(1, "A"); diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs index 97bffc6d38..a8929c2505 100644 --- a/OpenDreamShared/Dream/ImmutableAppearance.cs +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -51,6 +51,7 @@ public sealed class ImmutableAppearance : IEquatable { [ViewVariables] public readonly ImmutableAppearance[] Overlays; [ViewVariables] public readonly ImmutableAppearance[] Underlays; [ViewVariables] public readonly Robust.Shared.GameObjects.NetEntity[] VisContents; + [ViewVariables] public readonly Robust.Shared.GameObjects.NetEntity[] Particles; [ViewVariables] public readonly DreamFilter[] Filters; [ViewVariables] public readonly int[] Verbs; [ViewVariables] public readonly ColorMatrix ColorMatrix = ColorMatrix.Identity; @@ -106,6 +107,7 @@ public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? Underlays = appearance.Underlays.ToArray(); VisContents = appearance.VisContents.ToArray(); + Particles = appearance.Particles.ToArray(); Filters = appearance.Filters.ToArray(); Verbs = appearance.Verbs.ToArray(); Override = appearance.Override; @@ -168,6 +170,7 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.Overlays.Length != Overlays.Length) return false; if (immutableAppearance.Underlays.Length != Underlays.Length) return false; if (immutableAppearance.VisContents.Length != VisContents.Length) return false; + if (immutableAppearance.Particles.Length != Particles.Length) return false; if (immutableAppearance.Filters.Length != Filters.Length) return false; if (immutableAppearance.Verbs.Length != Verbs.Length) return false; if (immutableAppearance.Override != Override) return false; @@ -195,6 +198,10 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.Verbs[i] != Verbs[i]) return false; } + for (int i = 0; i < Particles.Length; i++) { + if (immutableAppearance.Particles[i] != Particles[i]) return false; + } + for (int i = 0; i < 6; i++) { if (!immutableAppearance.Transform[i].Equals(Transform[i])) return false; } @@ -258,6 +265,10 @@ public override int GetHashCode() { hashCode.Add(visContent); } + foreach (int particlesObject in Particles) { + hashCode.Add(particlesObject); + } + foreach (DreamFilter filter in Filters) { hashCode.Add(filter); } @@ -279,7 +290,8 @@ public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializ Underlays = []; VisContents = []; Filters = []; - Verbs =[]; + Verbs = []; + Particles = []; var property = (IconAppearanceProperty)buffer.ReadByte(); while (property != IconAppearanceProperty.End) { @@ -392,6 +404,16 @@ public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializ break; } + case IconAppearanceProperty.Particles: { + var particlesCount = buffer.ReadVariableInt32(); + + Particles = new Robust.Shared.GameObjects.NetEntity[particlesCount]; + for (int particlesI = 0; particlesI < particlesCount; particlesI++) { + Particles[particlesI] = buffer.ReadNetEntity(); + } + + break; + } case IconAppearanceProperty.Filters: { var filtersCount = buffer.ReadInt32(); @@ -485,11 +507,13 @@ public MutableAppearance ToMutable() { result.VisContents.EnsureCapacity(VisContents.Length); result.Filters.EnsureCapacity(Filters.Length); result.Verbs.EnsureCapacity(Verbs.Length); + result.Particles.EnsureCapacity(Particles.Length); result.Overlays.AddRange(Overlays); result.Underlays.AddRange(Underlays); result.VisContents.AddRange(VisContents); result.Filters.AddRange(Filters); result.Verbs.AddRange(Verbs); + result.Particles.AddRange(Particles); Array.Copy(Transform, result.Transform, 6); return result; @@ -639,6 +663,15 @@ public void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serialize } } + if (Particles.Length != 0) { + buffer.Write((byte)IconAppearanceProperty.Particles); + + buffer.WriteVariableInt32(Particles.Length); + foreach (var item in Particles) { + buffer.Write(item); + } + } + if (Filters.Length != 0) { buffer.Write((byte)IconAppearanceProperty.Filters); diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 76c853cb03..805819eaa9 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -49,6 +49,7 @@ public sealed class MutableAppearance : IEquatable, IDisposab [ViewVariables] public List Overlays; [ViewVariables] public List Underlays; [ViewVariables] public List VisContents; + [ViewVariables] public List Particles; [ViewVariables] public List Filters; [ViewVariables] public List Verbs; [ViewVariables] public Vector2i MaptextSize = new(32,32); @@ -84,6 +85,7 @@ private MutableAppearance() { VisContents = []; Filters = []; Verbs = []; + Particles = []; } public void Dispose() { @@ -142,6 +144,7 @@ public void CopyFrom(MutableAppearance appearance) { VisContents.AddRange(appearance.VisContents); Filters.AddRange(appearance.Filters); Verbs.AddRange(appearance.Verbs); + Particles.AddRange(appearance.Particles); Array.Copy(appearance.Transform, Transform, 6); } @@ -176,6 +179,7 @@ public bool Equals(MutableAppearance? appearance) { if (appearance.VisContents.Count != VisContents.Count) return false; if (appearance.Filters.Count != Filters.Count) return false; if (appearance.Verbs.Count != Verbs.Count) return false; + if (appearance.Particles.Count != Particles.Count) return false; if (appearance.Override != Override) return false; if (appearance.Maptext != Maptext) return false; if (appearance.MaptextSize != MaptextSize) return false; @@ -201,6 +205,10 @@ public bool Equals(MutableAppearance? appearance) { if (appearance.Verbs[i] != Verbs[i]) return false; } + for (int i = 0; i < Particles.Count; i++) { + if (appearance.Particles[i] != Particles[i]) return false; + } + for (int i = 0; i < 6; i++) { if (!appearance.Transform[i].Equals(Transform[i])) return false; } @@ -282,6 +290,10 @@ public override int GetHashCode() { hashCode.Add(visContent); } + foreach (int particlesObject in Particles) { + hashCode.Add(particlesObject); + } + foreach (DreamFilter filter in Filters) { hashCode.Add(filter); } @@ -404,6 +416,7 @@ public enum IconAppearanceProperty : byte { Overlays, Underlays, VisContents, + Particles, Filters, Verbs, Transform,