// 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
{
#if KNI
using FileStream stream = File.OpenRead(filePath);
Texture2D texture = Texture2D.FromStream(graphicsDeviceService.GraphicsDevice, stream);
#else
Texture2D texture = Texture2D.FromFile(graphicsDeviceService.GraphicsDevice, filePath);
#endif
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);
}
}