using System.Text.Json.Nodes; namespace Sand.Core; public static class ParticleLibraryLoader { private static readonly HashSet StaticTypes = [ "wall", "stone", "rock", "iron", "gold", "copper", "wood", "brass", "glass", ]; public static ParticleLibrary LoadFromDirectory(string partRootPath) { var directories = new[] { Path.Combine(partRootPath, "coreparts"), Path.Combine(partRootPath, "mods"), }; var rawDefinitions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var directory in directories) { if (!Directory.Exists(directory)) { continue; } foreach (var filePath in Directory.EnumerateFiles(directory, "*.json", SearchOption.AllDirectories)) { var rootNode = JsonNode.Parse(File.ReadAllText(filePath)) as JsonObject; if (rootNode is null) { continue; } foreach (var pair in rootNode) { if (pair.Value is JsonObject obj) { rawDefinitions[pair.Key.ToLowerInvariant()] = obj; } } } } var orderedIds = rawDefinitions.Keys.OrderBy(static id => id, StringComparer.Ordinal).ToArray(); var definitions = new List(orderedIds.Length); foreach (var id in orderedIds) { definitions.Add(ParseDefinition(id, rawDefinitions[id])); } var knownIds = new HashSet(orderedIds, StringComparer.OrdinalIgnoreCase); foreach (var definition in definitions) { ValidateTarget(knownIds, definition.Id, definition.Melt, nameof(definition.Melt)); ValidateTarget(knownIds, definition.Id, definition.Evaporate, nameof(definition.Evaporate)); ValidateTarget(knownIds, definition.Id, definition.Solidify, nameof(definition.Solidify)); ValidateTarget(knownIds, definition.Id, definition.Freeze, nameof(definition.Freeze)); ValidateTarget(knownIds, definition.Id, definition.Broken, nameof(definition.Broken)); ValidateTarget(knownIds, definition.Id, definition.Produces, nameof(definition.Produces)); ValidateTarget(knownIds, definition.Id, definition.ProducesOnDeath, nameof(definition.ProducesOnDeath)); } return new ParticleLibrary(definitions); } private static ParticleDef ParseDefinition(string id, JsonObject source) { var isGas = GetBool(source, "is_gas"); var isLiquid = GetBool(source, "liquid"); var isSolid = GetBool(source, "solid", true); var isSpecial = !isGas && !isLiquid && !isSolid; return new ParticleDef { Id = id, Name = GetString(source, "name") ?? id, Kind = isGas ? ParticleKind.Gas : isLiquid ? ParticleKind.Liquid : ParticleKind.Solid, IsStatic = StaticTypes.Contains(id) || GetBool(source, "static"), IsSpecial = isSpecial, Mass = GetFloat(source, "mass", 1f), Hardness = GetFloat(source, "hardness", 0.5f), Velocity = GetFloat(source, "velocity", 0f), Conductivity = GetFloat(source, "conductivity", 0f), Conductive = GetBool(source, "conductive"), Flamability = GetFloat(source, "flamability", 0f), Durability = GetFloat(source, "durability", 100f), HeatCapacity = GetFloat(source, "heat_capacity", 1f), Friction = GetFloat(source, "friction", 0f), Viscosity = GetFloat(source, "viscosity", 0f), Pressure = GetFloat(source, "pressure", 0f), Temperature = GetFloat(source, "temperature", 22f), Melt = GetLowerString(source, "melt"), MeltTemperature = GetNullableFloat(source, "melt_temperature"), Evaporate = GetLowerString(source, "evaporate"), EvaporateTemperature = GetNullableFloat(source, "evaporate_temperature"), Solidify = GetLowerString(source, "solidify"), SolidifyTemperature = GetNullableFloat(source, "solidify_temperature"), Freeze = GetLowerString(source, "freeze"), FreezeTemperature = GetNullableFloat(source, "freeze_temperature"), BurnDuration = GetFloat(source, "burn_duration", 0f), BurnTemperature = GetFloat(source, "burn_temperature", 0f), BurnRate = GetFloat(source, "burn_rate", 1f), Burning = GetBool(source, "burning"), Lifetime = GetNullableFloat(source, "lifetime"), Explosive = GetBool(source, "explosive"), ExplosionRadius = GetInt(source, "explosion_radius", 0), ExplosionColor = GetOptionalColor(source, "explosion_color"), ExplosionForce = GetFloat(source, "explosion_force", 6f), ExplosionDuration = GetInt(source, "explosion_duration", 1), PressureResistance = GetFloat(source, "pressure_resistance", 0f), PressureTolerance = GetFloat(source, "pressure_tolerance", 0f), PressureThreshold = GetFloat(source, "pressure_threshold", 0f), PressureThresholdDuration = GetInt(source, "pressure_threshold_duration", 0), Broken = GetLowerString(source, "Broken") ?? GetLowerString(source, "broken"), Produces = GetLowerString(source, "produces"), ProducesOnDeath = GetLowerString(source, "produces_on_death"), HeatEmission = GetFloat(source, "heat_emission", 0f), EnergyTransfer = GetFloat(source, "energy_transfer", 0f), Radius = GetFloat(source, "radius", 0f), ForceFalloff = GetFloat(source, "force_falloff", 1f), Turbulence = GetFloat(source, "turbulence", 0f), Affects = GetStringArray(source, "affects"), IsWind = GetBool(source, "is_wind"), WindStrength = GetFloat(source, "wind_strength", 0f), WindDirection = GetFloatArray(source, "wind_direction"), IsGravity = GetBool(source, "is_gravity"), GravityStrength = GetFloat(source, "gravity_strength", 0f), PullDirection = GetLowerString(source, "pull_direction"), IsRepulsor = GetBool(source, "is_repulsor"), RepulsionStrength = GetFloat(source, "repulsion_strength", 0f), PushDirection = GetLowerString(source, "push_direction"), Color = GetColor(source), }; } private static void ValidateTarget(HashSet knownIds, string id, string? target, string propertyName) { if (target is not null && !knownIds.Contains(target)) { throw new InvalidDataException($"Particle '{id}' references unknown {propertyName} target '{target}'."); } } private static string? GetString(JsonObject source, string key) => source[key]?.GetValue(); private static string? GetLowerString(JsonObject source, string key) { var value = GetString(source, key); return value?.ToLowerInvariant(); } private static bool GetBool(JsonObject source, string key, bool defaultValue = false) { if (source[key] is null) { return defaultValue; } return source[key]!.GetValue(); } private static float GetFloat(JsonObject source, string key, float defaultValue) { if (source[key] is null) { return defaultValue; } return source[key]!.GetValue(); } private static int GetInt(JsonObject source, string key, int defaultValue) { if (source[key] is null) { return defaultValue; } return source[key]!.GetValue(); } private static float? GetNullableFloat(JsonObject source, string key) { if (source[key] is null) { return null; } return source[key]!.GetValue(); } private static Rgb24 GetColor(JsonObject source) { if (source["color"] is not JsonArray array || array.Count < 3) { return new Rgb24(255, 255, 255); } return new Rgb24( (byte)array[0]!.GetValue(), (byte)array[1]!.GetValue(), (byte)array[2]!.GetValue()); } private static Rgb24? GetOptionalColor(JsonObject source, string key) { if (source[key] is not JsonArray array || array.Count < 3) { return null; } return new Rgb24( (byte)array[0]!.GetValue(), (byte)array[1]!.GetValue(), (byte)array[2]!.GetValue()); } private static string[] GetStringArray(JsonObject source, string key) { if (source[key] is not JsonArray array) { return []; } return array .Where(static item => item is not null) .Select(static item => item!.GetValue().ToLowerInvariant()) .ToArray(); } private static float[]? GetFloatArray(JsonObject source, string key) { if (source[key] is not JsonArray array || array.Count == 0) { return null; } return array .Where(static item => item is not null) .Select(static item => item!.GetValue()) .ToArray(); } }