From d9cd99d4b95c0d01feadb4a363cb52e512e2973f Mon Sep 17 00:00:00 2001
From: Christopher Whitley <103014489+AristurtleDev@users.noreply.github.com>
Date: Mon, 8 Sep 2025 00:36:37 -0400
Subject: [PATCH] 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
---
.../Particles/ParticleEffect.cs | 31 +++++------
.../Particles/ParticleEffectReader.cs | 55 ++++++++++++-------
2 files changed, 50 insertions(+), 36 deletions(-)
diff --git a/source/MonoGame.Extended/Particles/ParticleEffect.cs b/source/MonoGame.Extended/Particles/ParticleEffect.cs
index acc5aa13..918997c4 100644
--- a/source/MonoGame.Extended/Particles/ParticleEffect.cs
+++ b/source/MonoGame.Extended/Particles/ParticleEffect.cs
@@ -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 class from a file.
///
/// The path to the file containing the serialized effect data.
+ /// The content manager used to load graphical assets.
/// A new instance with properties and emitters as defined in the file.
- ///
- /// This method is not yet implemented.
- ///
- ///
- /// This method is intended to deserialize effect data from a file, but has not been implemented in this version.
- ///
- public static ParticleEffect FromFile(string path)
+ /// is
+ /// is or empty.
+ public static ParticleEffect FromFile(string path, ContentManager content)
{
- throw new NotImplementedException();
+ using ParticleEffectReader reader = new(path, content);
+ return reader.ReadParticleEffect();
}
///
/// Creates a new instance of the class from a stream.
///
- /// The stream containing the serialized effect data.
+ /// The stream to read from.
+ /// The to use for loading textures.
+ /// The base directory to use for resolving relative texture paths. If null, uses the ContentManager's RootDirectory.
/// A new instance with properties and emitters as defined in the stream.
- ///
- /// This method is not yet implemented.
- ///
- ///
- /// This method is intended to deserialize effect data from a stream, but has not been implemented in this version.
- ///
- public static ParticleEffect FromStream(Stream stream)
+ /// or is
+ public static ParticleEffect FromStream(Stream stream, ContentManager content, string baseDirectory)
{
- throw new NotImplementedException();
+ using ParticleEffectReader reader = new(stream, content);
+ return reader.ReadParticleEffect();
}
///
diff --git a/source/MonoGame.Extended/Particles/ParticleEffectReader.cs b/source/MonoGame.Extended/Particles/ParticleEffectReader.cs
index 02f81f4b..8879c2e4 100644
--- a/source/MonoGame.Extended/Particles/ParticleEffectReader.cs
+++ b/source/MonoGame.Extended/Particles/ParticleEffectReader.cs
@@ -27,6 +27,7 @@ public sealed class ParticleEffectReader : IDisposable
{
private readonly XmlReader _reader;
private readonly ContentManager _content;
+ private readonly string _baseDirectory;
///
/// Gets a value that indicates whether this 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));
}
///
@@ -61,6 +64,18 @@ public sealed class ParticleEffectReader : IDisposable
/// The to use for loading textures.
/// or is
public ParticleEffectReader(Stream stream, ContentManager content)
+ : this(stream, content, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class that reads from a stream.
+ ///
+ /// The stream to read from.
+ /// The to use for loading textures.
+ /// The base directory to use for resolving relative texture paths. If null, uses the ContentManager's RootDirectory.
+ /// or is
+ 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;
}
///
@@ -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;
}
}