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; } }