Feature: Load Ember Particle Effects From File/Stream (#1023)

* Fix texture path resolution in ParticleEffectReader

This fixes texture loading issues when particle effect files reference textures with relative paths by properly resolving them against the effect file's location.

* Implement ParticleEffect FromFile/Stream methods
This commit is contained in:
Christopher Whitley
2025-09-08 00:36:37 -04:00
committed by GitHub
parent fd8eead241
commit d9cd99d4b9
2 changed files with 50 additions and 36 deletions
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using MonoGame.Extended.Particles.Primitives;
namespace MonoGame.Extended.Particles;
@@ -238,32 +239,28 @@ public class ParticleEffect : IDisposable
/// Creates a new instance of the <see cref="ParticleEffect"/> class from a file.
/// </summary>
/// <param name="path">The path to the file containing the serialized effect data.</param>
/// <param name="content">The content manager used to load graphical assets.</param>
/// <returns>A new <see cref="ParticleEffect"/> instance with properties and emitters as defined in the file.</returns>
/// <exception cref="NotImplementedException">
/// This method is not yet implemented.
/// </exception>
/// <remarks>
/// This method is intended to deserialize effect data from a file, but has not been implemented in this version.
/// </remarks>
public static ParticleEffect FromFile(string path)
/// <exception cref="ArgumentNullException"><paramref name="content"/> is <see langword="null"/></exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or empty.</exception>
public static ParticleEffect FromFile(string path, ContentManager content)
{
throw new NotImplementedException();
using ParticleEffectReader reader = new(path, content);
return reader.ReadParticleEffect();
}
/// <summary>
/// Creates a new instance of the <see cref="ParticleEffect"/> class from a stream.
/// </summary>
/// <param name="stream">The stream containing the serialized effect data.</param>
/// <param name="stream">The stream to read from.</param>
/// <param name="content">The <see cref="ContentManager"/> to use for loading textures.</param>
/// <param name="baseDirectory">The base directory to use for resolving relative texture paths. If null, uses the ContentManager's RootDirectory.</param>
/// <returns>A new <see cref="ParticleEffect"/> instance with properties and emitters as defined in the stream.</returns>
/// <exception cref="NotImplementedException">
/// This method is not yet implemented.
/// </exception>
/// <remarks>
/// This method is intended to deserialize effect data from a stream, but has not been implemented in this version.
/// </remarks>
public static ParticleEffect FromStream(Stream stream)
/// <exception cref="ArgumentNullException"><paramref name="stream"/> or <paramref name="content"/> is <see langword="null"/></exception>
public static ParticleEffect FromStream(Stream stream, ContentManager content, string baseDirectory)
{
throw new NotImplementedException();
using ParticleEffectReader reader = new(stream, content);
return reader.ReadParticleEffect();
}
/// <summary>
@@ -27,6 +27,7 @@ public sealed class ParticleEffectReader : IDisposable
{
private readonly XmlReader _reader;
private readonly ContentManager _content;
private readonly string _baseDirectory;
/// <summary>
/// Gets a value that indicates whether this <see cref="ParticleEffectReader"/> has been disposed of.
@@ -52,6 +53,8 @@ public sealed class ParticleEffectReader : IDisposable
_content = content;
_reader = XmlReader.Create(fileName, settings);
_baseDirectory = Path.GetDirectoryName(Path.GetFullPath(fileName));
}
/// <summary>
@@ -61,6 +64,18 @@ public sealed class ParticleEffectReader : IDisposable
/// <param name="content">The <see cref="ContentManager"/> to use for loading textures.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> or <paramref name="content"/> is <see langword="null"/></exception>
public ParticleEffectReader(Stream stream, ContentManager content)
: this(stream, content, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ParticleEffectReader"/> class that reads from a stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="content">The <see cref="ContentManager"/> to use for loading textures.</param>
/// <param name="baseDirectory">The base directory to use for resolving relative texture paths. If null, uses the ContentManager's RootDirectory.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> or <paramref name="content"/> is <see langword="null"/></exception>
public ParticleEffectReader(Stream stream, ContentManager content, string baseDirectory)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentNullException.ThrowIfNull(content);
@@ -72,6 +87,8 @@ public sealed class ParticleEffectReader : IDisposable
_content = content;
_reader = XmlReader.Create(stream, settings);
_baseDirectory = baseDirectory ?? content.RootDirectory;
}
/// <summary/>
@@ -218,31 +235,31 @@ public sealed class ParticleEffectReader : IDisposable
}
// Try common image extensions
string[] extensions = { ".png", ".jpg", ".jpeg", ".bmp" };
string baseDirectory = _content.RootDirectory;
string filePath = Path.Combine(_baseDirectory, name);
foreach(string extension in extensions)
if (File.Exists(filePath))
{
string filePath = Path.Combine(baseDirectory, name + extension);
if(File.Exists(filePath))
try
{
try
{
#if KNI
using FileStream stream = File.OpenRead(filePath);
Texture2D texture = Texture2D.FromStream(graphicsDeviceService.GraphicsDevice, stream);
using FileStream stream = File.OpenRead(filePath);
Texture2D texture = Texture2D.FromStream(graphicsDeviceService.GraphicsDevice, stream);
#else
Texture2D texture = Texture2D.FromFile(graphicsDeviceService.GraphicsDevice, filePath);
Texture2D texture = Texture2D.FromFile(graphicsDeviceService.GraphicsDevice, filePath);
#endif
texture.Name = name;
return new Texture2DRegion(texture, bounds);
}
catch
{
// Continue to next extension
continue;
}
texture.Name = name;
return new Texture2DRegion(texture, bounds);
}
catch
{
// TODO: 6.0.0
// Since the file name is baked into the .ember file including
// the extension, we no longer check extensions in a for each
// loop. This means we can't just "continue" in the catch.
// We'll need to throw an exception, but doing so would be
// a breaking change, so we'll return null for now, document
// it and update it for 6.0.0
return null;
}
}