// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Xml; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using MonoGame.Extended.Graphics; using MonoGame.Extended.Particles.Data; using MonoGame.Extended.Particles.Modifiers; using MonoGame.Extended.Particles.Modifiers.Containers; using MonoGame.Extended.Particles.Modifiers.Interpolators; using MonoGame.Extended.Particles.Profiles; using MonoGame.Extended.Serialization.Xml; namespace MonoGame.Extended.Particles; /// /// Represents a reader that deserializes a from an XML configuration. /// public sealed class ParticleEffectReader : IDisposable { private readonly XmlReader _reader; private readonly ContentManager _content; /// /// Gets a value that indicates whether this has been disposed of. /// public bool IsDisposed { get; private set; } /// /// Initializes a new instance of the class that reads from a file. /// /// The file path to read the XMl from. /// The to use for loading textures. /// is /// is or empty. public ParticleEffectReader(string fileName, ContentManager content) { ArgumentNullException.ThrowIfNull(content); ArgumentException.ThrowIfNullOrEmpty(fileName); XmlReaderSettings settings = new XmlReaderSettings(); settings.CloseInput = true; settings.IgnoreComments = true; settings.IgnoreWhitespace = true; _content = content; _reader = XmlReader.Create(fileName, settings); } /// /// Initializes a new instance of the class that reads from a stream. /// /// The stream to read from. /// The to use for loading textures. /// or is public ParticleEffectReader(Stream stream, ContentManager content) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(content); XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreComments = true; settings.IgnoreWhitespace = true; settings.CloseInput = false; _content = content; _reader = XmlReader.Create(stream, settings); } /// ~ParticleEffectReader() { Dispose(); } /// /// Reads a from the XML input. /// /// The deserialized . /// The Xml format is invalid. public ParticleEffect ReadParticleEffect() { _reader.MoveToContent(); if (_reader.NodeType != XmlNodeType.Element || _reader.LocalName != nameof(ParticleEffect)) { throw new XmlException($"Expected {nameof(ParticleEffect)} root element"); } string name = _reader.GetAttribute(nameof(ParticleEffect.Name)) ?? "Unnamed"; ParticleEffect effect = new ParticleEffect(name); effect.Position = _reader.GetAttributeVector2(nameof(ParticleEffect.Position)); effect.Rotation = _reader.GetAttributeFloat(nameof(ParticleEffect.Rotation)); effect.Scale = _reader.GetAttributeVector2(nameof(ParticleEffect.Scale)); effect.AutoTrigger = _reader.GetAttributeBool(nameof(ParticleEffect.AutoTrigger)); effect.AutoTriggerFrequency = _reader.GetAttributeFloat(nameof(ParticleEffect.AutoTriggerFrequency)); if (_reader.ReadToDescendant(nameof(ParticleEffect.Emitters))) { if (_reader.ReadToDescendant(nameof(ParticleEmitter))) { do { ParticleEmitter emitter = ReadParticleEmitter(); effect.Emitters.Add(emitter); } while (_reader.ReadToNextSibling(nameof(ParticleEmitter))); } } return effect; } private ParticleEmitter ReadParticleEmitter() { int capacity = _reader.GetAttributeInt(nameof(ParticleEmitter.Capacity)); ParticleEmitter emitter = new ParticleEmitter(capacity); emitter.Name = _reader.GetAttribute(nameof(ParticleEmitter.Name)) ?? nameof(ParticleEmitter.Name); emitter.LifeSpan = _reader.GetAttributeFloat(nameof(ParticleEmitter.LifeSpan)); emitter.Offset = _reader.GetAttributeVector2(nameof(ParticleEmitter.Offset)); emitter.LayerDepth = _reader.GetAttributeFloat(nameof(ParticleEmitter.LayerDepth)); emitter.ReclaimFrequency = _reader.GetAttributeFloat(nameof(ParticleEmitter.ReclaimFrequency)); string strategy = _reader.GetAttribute(nameof(ParticleEmitter.ModifierExecutionStrategy)); if (strategy.Equals(nameof(ModifierExecutionStrategy.Serial))) { emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Serial; } else if (strategy.Equals(nameof(ModifierExecutionStrategy.Parallel))) { emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Parallel; } else { emitter.ModifierExecutionStrategy = ModifierExecutionStrategy.Serial; } emitter.RenderingOrder = _reader.GetAttributeEnum(nameof(ParticleEmitter.RenderingOrder)); using XmlReader subtree = _reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element) { switch (subtree.LocalName) { case nameof(ParticleEmitter.TextureRegion): emitter.TextureRegion = ReadTexture2DRegion(subtree); break; case nameof(ParticleEmitter.Parameters): emitter.Parameters = ReadParticleReleaseParameters(subtree); break; case nameof(ParticleEmitter.Profile): emitter.Profile = ReadProfile(subtree); break; case nameof(ParticleEmitter.Modifiers): ReadModifiers(subtree, emitter.Modifiers); break; } } } return emitter; } private Texture2DRegion ReadTexture2DRegion(XmlReader reader) { string name = reader.GetAttribute(nameof(Texture2DRegion.Texture.Name)); if (string.IsNullOrEmpty(name)) { return null; } Rectangle bounds = _reader.GetAttributeRectangle(nameof(Texture2DRegion.Bounds)); try { // Try loading directly though the content manager. This should work, but there is a bug // for relative paths which has been documented at // https://github.com/MonoGame/MonoGame/issues/8786 // And a propsed fix at // https://github.com/MonoGame/MonoGame/pull/8787 // But until that is merged and released, we'll attempt to load here, then fall back to direct load in // the catch Texture2D texture = _content.Load(name); return new Texture2DRegion(texture, bounds); } catch (ContentLoadException) { return TryLoadTextureDirectly(name, bounds); } } private Texture2DRegion TryLoadTextureDirectly(string name, Rectangle bounds) { if(_content?.ServiceProvider == null) { return null; } IGraphicsDeviceService graphicsDeviceService = _content.ServiceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService; if(graphicsDeviceService?.GraphicsDevice == null) { return null; } // Try common image extensions string[] extensions = { ".png", ".jpg", ".jpeg", ".bmp" }; string baseDirectory = _content.RootDirectory; foreach(string extension in extensions) { string filePath = Path.Combine(baseDirectory, name + extension); if(File.Exists(filePath)) { try { Texture2D texture = Texture2D.FromFile(graphicsDeviceService.GraphicsDevice, filePath); texture.Name = name; return new Texture2DRegion(texture, bounds); } catch { // Continue to next extension continue; } } } return null; } private ParticleReleaseParameters ReadParticleReleaseParameters(XmlReader reader) { ParticleReleaseParameters parameters = new ParticleReleaseParameters(); using XmlReader subtree = reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element) { switch (subtree.LocalName) { case nameof(ParticleReleaseParameters.Quantity): parameters.Quantity = ReadParticleInt32Parameter(subtree); break; case nameof(ParticleReleaseParameters.Speed): parameters.Speed = ReadParticleFloatParameter(subtree); break; case nameof(ParticleReleaseParameters.Color): parameters.Color = ReadParticleColorParameter(subtree); break; case nameof(ParticleReleaseParameters.Opacity): parameters.Opacity = ReadParticleFloatParameter(subtree); break; case nameof(ParticleReleaseParameters.Scale): parameters.Scale = ReadParticleVector2Parameter(subtree); break; case nameof(ParticleReleaseParameters.Rotation): parameters.Rotation = ReadParticleFloatParameter(subtree); break; case nameof(ParticleReleaseParameters.Mass): parameters.Mass = ReadParticleFloatParameter(subtree); break; } } } return parameters; } private ParticleInt32Parameter ReadParticleInt32Parameter(XmlReader reader) { ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleInt32Parameter.Kind)); if (kind == ParticleValueKind.Constant) { int value = reader.GetAttributeInt(nameof(ParticleInt32Parameter.Constant)); return new ParticleInt32Parameter(value); } else if (kind == ParticleValueKind.Random) { int min = reader.GetAttributeInt(nameof(ParticleInt32Parameter.RandomMin)); int max = reader.GetAttributeInt(nameof(ParticleInt32Parameter.RandomMax)); return new ParticleInt32Parameter(min, max); } return new ParticleInt32Parameter(0); } private ParticleFloatParameter ReadParticleFloatParameter(XmlReader reader) { ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleFloatParameter.Kind)); if (kind == ParticleValueKind.Constant) { float value = reader.GetAttributeFloat(nameof(ParticleFloatParameter.Constant)); return new ParticleFloatParameter(value); } else if (kind == ParticleValueKind.Random) { float min = reader.GetAttributeFloat(nameof(ParticleFloatParameter.RandomMin)); float max = reader.GetAttributeFloat(nameof(ParticleFloatParameter.RandomMax)); return new ParticleFloatParameter(min, max); } return new ParticleFloatParameter(0); } private ParticleVector2Parameter ReadParticleVector2Parameter(XmlReader reader) { ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleVector2Parameter.Kind)); if(kind == ParticleValueKind.Constant) { Vector2 value = reader.GetAttributeVector2(nameof(ParticleVector2Parameter.Constant)); return new ParticleVector2Parameter(value); } else if(kind == ParticleValueKind.Random) { Vector2 min = reader.GetAttributeVector2(nameof(ParticleVector2Parameter.RandomMin)); Vector2 max = reader.GetAttributeVector2(nameof(ParticleVector2Parameter.RandomMax)); return new ParticleVector2Parameter(min, max); } return new ParticleVector2Parameter(Vector2.Zero); } private ParticleColorParameter ReadParticleColorParameter(XmlReader reader) { ParticleValueKind kind = reader.GetAttributeEnum(nameof(ParticleColorParameter.Kind)); if (kind == ParticleValueKind.Constant) { Vector3 value = reader.GetAttributeVector3(nameof(ParticleColorParameter.Constant)); return new ParticleColorParameter(value); } else if (kind == ParticleValueKind.Random) { Vector3 min = reader.GetAttributeVector3(nameof(ParticleColorParameter.RandomMin)); Vector3 max = reader.GetAttributeVector3(nameof(ParticleColorParameter.RandomMax)); return new ParticleColorParameter(min, max); } return new ParticleColorParameter(Vector3.Zero); } private Profile ReadProfile(XmlReader reader) { string type = reader.GetAttribute(nameof(Type)); switch (type) { case nameof(BoxProfile): BoxProfile boxProfile = new BoxProfile(); boxProfile.Width = reader.GetAttributeFloat(nameof(BoxProfile.Width)); boxProfile.Height = reader.GetAttributeFloat(nameof(BoxProfile.Height)); return boxProfile; case nameof(BoxFillProfile): BoxFillProfile boxFillProfile = new BoxFillProfile(); boxFillProfile.Width = reader.GetAttributeFloat(nameof(BoxFillProfile.Width)); boxFillProfile.Height = reader.GetAttributeFloat(nameof(BoxFillProfile.Height)); return boxFillProfile; case nameof(BoxUniformProfile): BoxUniformProfile boxUniformProfile = new BoxUniformProfile(); boxUniformProfile.Width = reader.GetAttributeFloat(nameof(BoxUniformProfile.Width)); boxUniformProfile.Height = reader.GetAttributeFloat(nameof(BoxUniformProfile.Height)); return boxUniformProfile; case nameof(CircleProfile): CircleProfile circleProfile = new CircleProfile(); circleProfile.Radius = reader.GetAttributeFloat(nameof(CircleProfile.Radius)); circleProfile.Radiate = reader.GetAttributeEnum(nameof(CircleProfile.Radiate)); return circleProfile; case nameof(LineProfile): LineProfile lineProfile = new LineProfile(); lineProfile.Axis = reader.GetAttributeVector2(nameof(LineProfile.Axis)); lineProfile.Length = reader.GetAttributeFloat(nameof(LineProfile.Length)); return lineProfile; case nameof(PointProfile): return Profile.Point(); case nameof(RingProfile): RingProfile ringProfile = new RingProfile(); ringProfile.Radius = reader.GetAttributeFloat(nameof(RingProfile.Radius)); ringProfile.Radiate = reader.GetAttributeEnum(nameof(RingProfile.Radiate)); return ringProfile; case nameof(SprayProfile): SprayProfile sprayProfile = new SprayProfile(); sprayProfile.Direction = reader.GetAttributeVector2(nameof(SprayProfile.Direction)); sprayProfile.Spread = reader.GetAttributeFloat(nameof(SprayProfile.Spread)); return sprayProfile; default: return new PointProfile(); } } private void ReadModifiers(XmlReader reader, List modifiers) { using XmlReader subtree = reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(Modifier)) { Modifier modifier = ReadModifier(subtree); if (modifier != null) { modifiers.Add(modifier); } } } } private Modifier ReadModifier(XmlReader reader) { string type = reader.GetAttribute(nameof(Type)); string name = reader.GetAttribute(nameof(Modifier.Name)); float frequency = reader.GetAttributeFloat(nameof(Modifier.Frequency)); Modifier modifier = type switch { nameof(AgeModifier) => ReadAgeModifier(reader), nameof(DragModifier) => ReadDragModifier(reader), nameof(LinearGravityModifier) => ReadLinearGravityModifier(reader), nameof(OpacityFastFadeModifier) => new OpacityFastFadeModifier(), nameof(RotationModifier) => ReadRotationModifier(reader), nameof(VelocityColorModifier) => ReadVelocityColorModifier(reader), nameof(VelocityModifier) => ReadVelocityModifier(reader), nameof(VortexModifier) => ReadVortexModifier(reader), nameof(CircleContainerModifier) => ReadCircleContainerModifier(reader), nameof(RectangleContainerModifier) => ReadRectangleContainerModifier(reader), nameof(RectangleLoopContainerModifier) => ReadRectangleLoopContainerModifier(reader), _ => null }; if (modifier != null) { modifier.Name = name; modifier.Frequency = frequency; } return modifier; } private Modifier ReadAgeModifier(XmlReader reader) { AgeModifier modifier = new AgeModifier(); using XmlReader subtree = reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(AgeModifier.Interpolators)) { ReadInterpolators(subtree, modifier.Interpolators); } } return modifier; } private Modifier ReadDragModifier(XmlReader reader) { DragModifier modifier = new DragModifier(); modifier.DragCoefficient = reader.GetAttributeFloat(nameof(DragModifier.DragCoefficient)); modifier.Density = reader.GetAttributeFloat(nameof(DragModifier.Density)); return modifier; } private LinearGravityModifier ReadLinearGravityModifier(XmlReader reader) { LinearGravityModifier modifier = new LinearGravityModifier(); modifier.Direction = reader.GetAttributeVector2(nameof(LinearGravityModifier.Direction)); modifier.Strength = reader.GetAttributeFloat(nameof(LinearGravityModifier.Strength)); return modifier; } private Modifier ReadRotationModifier(XmlReader reader) { RotationModifier modifier = new RotationModifier(); modifier.RotationRate = reader.GetAttributeFloat(nameof(RotationModifier.RotationRate)); return modifier; } private Modifier ReadVelocityColorModifier(XmlReader reader) { VelocityColorModifier modifier = new VelocityColorModifier(); modifier.StationaryColor = reader.GetAttributeVector3(nameof(VelocityColorModifier.StationaryColor)); modifier.VelocityColor = reader.GetAttributeVector3(nameof(VelocityColorModifier.VelocityColor)); modifier.VelocityThreshold = reader.GetAttributeFloat(nameof(VelocityColorModifier.VelocityThreshold)); return modifier; } private Modifier ReadVelocityModifier(XmlReader reader) { VelocityModifier modifier = new VelocityModifier(); modifier.VelocityThreshold = reader.GetAttributeFloat(nameof(VelocityModifier.VelocityThreshold)); using XmlReader subtree = reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(VelocityModifier.Interpolators)) { ReadInterpolators(subtree, modifier.Interpolators); } } return modifier; } private Modifier ReadVortexModifier(XmlReader reader) { VortexModifier modifier = new VortexModifier(); modifier.Mass = reader.GetAttributeFloat(nameof(VortexModifier.Mass)); modifier.MaxSpeed = reader.GetAttributeFloat(nameof(VortexModifier.MaxSpeed)); return modifier; } private CircleContainerModifier ReadCircleContainerModifier(XmlReader reader) { CircleContainerModifier modifier = new CircleContainerModifier(); modifier.Radius = reader.GetAttributeFloat(nameof(CircleContainerModifier.Radius)); modifier.Inside = reader.GetAttributeBool(nameof(CircleContainerModifier.Inside)); modifier.RestitutionCoefficient = reader.GetAttributeFloat(nameof(CircleContainerModifier.RestitutionCoefficient)); return modifier; } private Modifier ReadRectangleContainerModifier(XmlReader reader) { RectangleContainerModifier modifier = new RectangleContainerModifier(); modifier.Width = reader.GetAttributeInt(nameof(RectangleContainerModifier.Width)); modifier.Height = reader.GetAttributeInt(nameof(RectangleContainerModifier.Height)); modifier.RestitutionCoefficient = reader.GetAttributeFloat(nameof(RectangleContainerModifier.RestitutionCoefficient)); return modifier; } private Modifier ReadRectangleLoopContainerModifier(XmlReader reader) { RectangleLoopContainerModifier modifier = new RectangleLoopContainerModifier(); modifier.Width = reader.GetAttributeInt(nameof(RectangleLoopContainerModifier.Width)); modifier.Height = reader.GetAttributeInt(nameof(RectangleLoopContainerModifier.Height)); return modifier; } private void ReadInterpolators(XmlReader reader, List interpolators) { using XmlReader subtree = reader.ReadSubtree(); while (subtree.Read()) { if (subtree.NodeType == XmlNodeType.Element && subtree.LocalName == nameof(Interpolator)) { Interpolator interpolator = ReadInterpolator(subtree); if (interpolator != null) { interpolators.Add(interpolator); } } } } private Interpolator ReadInterpolator(XmlReader reader) { string type = reader.GetAttribute(nameof(Type)); string name = reader.GetAttribute(nameof(Interpolator.Name)); switch (type) { case nameof(ColorInterpolator): ColorInterpolator colorInterpolator = new ColorInterpolator(); colorInterpolator.StartValue = reader.GetAttributeVector3(nameof(ColorInterpolator.StartValue)); colorInterpolator.EndValue = reader.GetAttributeVector3(nameof(ColorInterpolator.EndValue)); return colorInterpolator; case nameof(HueInterpolator): HueInterpolator hueInterpolator = new HueInterpolator(); hueInterpolator.StartValue = reader.GetAttributeFloat(nameof(HueInterpolator.StartValue)); hueInterpolator.EndValue = reader.GetAttributeFloat(nameof(HueInterpolator.EndValue)); return hueInterpolator; case nameof(OpacityInterpolator): OpacityInterpolator opacityInterpolator = new OpacityInterpolator(); opacityInterpolator.StartValue = reader.GetAttributeFloat(nameof(OpacityInterpolator.StartValue)); opacityInterpolator.EndValue = reader.GetAttributeFloat(nameof(OpacityInterpolator.EndValue)); return opacityInterpolator; case nameof(RotationInterpolator): RotationInterpolator rotationInterpolator = new RotationInterpolator(); rotationInterpolator.StartValue = reader.GetAttributeFloat(nameof(RotationInterpolator.StartValue)); rotationInterpolator.EndValue = reader.GetAttributeFloat(nameof(RotationInterpolator.EndValue)); return rotationInterpolator; case nameof(ScaleInterpolator): ScaleInterpolator scaleInterpolator = new ScaleInterpolator(); scaleInterpolator.StartValue = reader.GetAttributeVector2(nameof(ScaleInterpolator.StartValue)); scaleInterpolator.EndValue = reader.GetAttributeVector2(nameof(ScaleInterpolator.EndValue)); return scaleInterpolator; case nameof(VelocityInterpolator): VelocityInterpolator velocityInterpolator = new VelocityInterpolator(); velocityInterpolator.StartValue = reader.GetAttributeVector2(nameof(VelocityInterpolator.StartValue)); velocityInterpolator.EndValue = reader.GetAttributeVector2(nameof(VelocityInterpolator.EndValue)); return velocityInterpolator; default: return null; } } /// public void Dispose() { if (IsDisposed) { return; } _reader?.Dispose(); IsDisposed = true; GC.SuppressFinalize(this); } }