Code/ParticleResource.cs
using System;
using Sandbox;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace fxbox;
/// <summary>
/// Particle system resource containing multiple emitters
/// </summary>
[AssetType(Name = "Particle System", Extension = "fx", Category = "FX", Flags = AssetTypeFlags.NoEmbedding)]
public class ParticleResource : GameResource
{
public bool IsDirty { get; set; } = false;
/// <summary>
/// All emitters in this particle system
/// </summary>
public List<ParticleEmitter> Emitters { get; set; } = new();
/// <summary>
/// Named float parameters
/// </summary>
[InlineEditor, DisplayName("FloatParameters")] public List<FloatParameter> FloatParameters { get; set; } = new();
/// <summary>
/// Named vector parameters
/// </summary>
[InlineEditor] public List<VectorParameter> VectorParameters { get; set; } = new();
/// <summary>
/// Named color parameters
/// </summary>
[InlineEditor] public List<ColorParameter> ColorParameters { get; set; } = new();
/// <summary>
/// Global system properties
/// </summary>
public float Duration { get; set; } = 5.0f;
public bool Looping { get; set; } = true;
public int Version { get; set; } = 0;
/// <summary>
/// Preview settings for the editor
/// </summary>
public ParticlePreviewSettings PreviewSettings { get; set; } = new();
/// <summary>
/// Get a float parameter's default value by name
/// </summary>
public float GetParameterDefault(string name)
{
var param = FloatParameters.FirstOrDefault(p => p.Name == name);
return param?.DefaultValue ?? 0f;
}
/// <summary>
/// Get a vector parameter's default value by name
/// </summary>
public Vector3 GetVectorParameterDefault(string name)
{
var param = VectorParameters.FirstOrDefault(p => p.Name == name);
return param?.DefaultValue ?? Vector3.Zero;
}
/// <summary>
/// Get a color parameter's default value by name
/// </summary>
public ParticleGradient GetColorParameterDefault(string name)
{
var param = ColorParameters.FirstOrDefault(p => p.Name == name);
return param?.DefaultValue ?? Color.White;
}
/// <summary>
/// Add a new float parameter
/// </summary>
public FloatParameter AddParameter(string name, float defaultValue = 1.0f)
{
var param = new FloatParameter
{
Name = name,
DefaultValue = defaultValue
};
FloatParameters.Add(param);
return param;
}
/// <summary>
/// Add a new vector parameter
/// </summary>
public VectorParameter AddVectorParameter(string name, Vector3 defaultValue)
{
var param = new VectorParameter
{
Name = name,
DefaultValue = defaultValue
};
VectorParameters.Add(param);
return param;
}
/// <summary>
/// Add a new color parameter
/// </summary>
public ColorParameter AddColorParameter(string name, Color defaultValue)
{
var param = new ColorParameter
{
Name = name,
DefaultValue = defaultValue
};
ColorParameters.Add(param);
return param;
}
}
/// <summary>
/// Preview settings for the particle editor
/// </summary>
public class ParticlePreviewSettings
{
public bool ShowGround { get; set; } = true;
public bool ShowGrid { get; set; } = true;
public Color BackgroundColor { get; set; } = new Color(0.1f, 0.1f, 0.15f);
public float PlaybackSpeed { get; set; } = 1.0f;
}
/// <summary>
/// A single particle emitter with its own spawn and update logic
/// </summary>
public class ParticleEmitter
{
public string Name { get; set; } = "Emitter";
[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
public bool Enabled { get; set; } = true;
public int MaxParticles { get; set; } = 1000;
/// <summary>
/// Modules that run when spawning particles
/// </summary>
[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
public List<ParticleModule> SpawnModules { get; set; } = new();
/// <summary>
/// Modules that run once when a particle is created
/// </summary>
[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
public List<ParticleModule> InitializeModules { get; set; } = new();
/// <summary>
/// Modules that run every frame for each particle
/// </summary>
[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
public List<ParticleModule> UpdateModules { get; set; } = new();
/// <summary>
/// Modules that control how particles are rendered
/// </summary>
[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
public List<ParticleModule> RenderModules { get; set; } = new();
}
/// <summary>
/// Base class for all particle modules
/// </summary>
public abstract class ParticleModule
{
[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
[Hide] public string Name { get; set; }
[Hide] public bool Enabled { get; set; } = true;
/// <summary>
/// What stage this module belongs to
/// </summary>
[JsonIgnore]
public abstract ModuleStage Stage { get; }
/// <summary>
/// Execute this module
/// </summary>
public abstract void Execute(ParticleExecutionContext context);
public abstract void Initialize( ParticleExecutionContext context );
}
/// <summary>
/// JSON converter for List of ParticleModule
/// </summary>
public class ParticleModuleListConverter : JsonConverter<List<ParticleModule>>
{
public override List<ParticleModule> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var list = new List<ParticleModule>();
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException("Expected start of array");
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
using (var doc = JsonDocument.ParseValue(ref reader))
{
var root = doc.RootElement;
// Get the type name
if (!root.TryGetProperty("$type", out var typeProperty))
{
Log.Warning("Missing $type property for ParticleModule");
continue;
}
var typeName = typeProperty.GetString();
var type = TypeLibrary.GetType(typeName)?.TargetType;
if (type == null)
{
Log.Warning($"Unknown module type: {typeName}");
continue;
}
// Deserialize to the specific type
var json = root.GetRawText();
var module = (ParticleModule)JsonSerializer.Deserialize(json, type, options);
if (module != null)
{
list.Add(module);
}
}
}
return list;
}
public override void Write(Utf8JsonWriter writer, List<ParticleModule> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var module in value)
{
if (module == null) continue;
writer.WriteStartObject();
// Write the type information
writer.WriteString("$type", module.GetType().FullName);
// Serialize the module
var json = JsonSerializer.Serialize(module, module.GetType(), options);
using (var doc = JsonDocument.Parse(json))
{
foreach (var property in doc.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}