Code/LibroFusiones.cs

Manager for fusion recipes. It exposes all recipes (pack assets first, then user-created), loads/saves user recipes to FileSystem.Data as JSON, and provides add, delete and clear operations while tracking a Version counter.

File Access
namespace SBH;

using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// [SBHBase] El libro de recetas de fusión. Junta DOS fuentes:
///  - Las recetas de PACK: assets .fusion (RecetaFusion), solo lectura.
///  - Las recetas PROPIAS: creadas en el juego con la tool Merge en modo
///    crear; persisten como JSON en FileSystem.Data y sobreviven sesiones.
/// La tool y el recetario consumen Todas() sin distinguir origen (salvo
/// para permitir borrar: solo las propias).
/// </summary>
public static class LibroFusiones
{
	const string Archivo = "sbh_fusiones.json";

	/// <summary> Una receta lista para usar, venga del pack o del juego. </summary>
	public class Receta
	{
		public List<IngredienteFusion> Ingredientes { get; set; } = new();

		/// <summary> Ruta del prefab que aparece al fusionar. </summary>
		public string RutaResultado { get; set; } = "";

		/// <summary> Ruta de un SoundEvent opcional. </summary>
		public string RutaSonido { get; set; }

		/// <summary> Vino de un asset .fusion: no se puede borrar desde el juego. </summary>
		public bool DePack { get; set; }
	}

	static List<Receta> _propias;

	/// <summary> Sube con cada alta/baja de receta propia — para BuildHash de la UI. </summary>
	public static int Version { get; private set; }

	/// <summary> Todas las recetas válidas: primero las del pack, después las propias. </summary>
	public static IEnumerable<Receta> Todas()
	{
		foreach ( var r in ResourceLibrary.GetAll<RecetaFusion>() )
		{
			if ( r.Resultado == null ) continue;
			if ( r.Ingredientes == null || r.Ingredientes.Count == 0 ) continue;

			yield return new Receta
			{
				Ingredientes = r.Ingredientes,
				RutaResultado = r.Resultado.ResourcePath,
				RutaSonido = r.Sonido?.ResourcePath,
				DePack = true,
			};
		}

		foreach ( var r in Propias() )
			yield return r;
	}

	static List<Receta> Propias()
	{
		if ( _propias != null ) return _propias;

		_propias = new List<Receta>();

		try
		{
			if ( FileSystem.Data.FileExists( Archivo ) )
				_propias = Json.Deserialize<List<Receta>>( FileSystem.Data.ReadAllText( Archivo ) ) ?? new();
		}
		catch ( Exception e )
		{
			Log.Warning( $"LibroFusiones: no pude leer {Archivo} — {e.Message}" );
		}

		foreach ( var r in _propias )
			r.DePack = false;

		return _propias;
	}

	/// <summary>
	/// Guarda una receta creada en el juego. Si ya había una propia con los
	/// MISMOS ingredientes, se reemplaza — re-grabar una receta es la forma
	/// de cambiarle el resultado.
	/// </summary>
	public static void Guardar( Receta receta )
	{
		var propias = Propias();
		propias.RemoveAll( r => MismosIngredientes( r, receta ) );
		propias.Add( receta );
		Persistir();
	}

	/// <summary> Borra una receta propia (las de pack no se tocan). </summary>
	public static void Borrar( Receta receta )
	{
		if ( receta.DePack ) return;

		Propias().RemoveAll( r => MismosIngredientes( r, receta )
			&& r.RutaResultado == receta.RutaResultado );
		Persistir();
	}

	/// <summary> El botón nuclear: borra TODAS las recetas propias de un saque. </summary>
	public static void BorrarTodas()
	{
		Propias().Clear();
		Persistir();
	}

	static void Persistir()
	{
		try
		{
			FileSystem.Data.WriteAllText( Archivo, Json.Serialize( _propias ) );
		}
		catch ( Exception e )
		{
			Log.Warning( $"LibroFusiones: no pude guardar {Archivo} — {e.Message}" );
		}

		Version++;
	}

	// Mismo multiset de ingredientes (nombres sin mayúsculas, cantidades sumadas)
	static bool MismosIngredientes( Receta a, Receta b )
	{
		static Dictionary<string, int> Mapa( Receta r ) => r.Ingredientes
			.GroupBy( i => i.Nombre?.Trim().ToLowerInvariant() ?? "" )
			.ToDictionary( g => g.Key, g => g.Sum( i => Math.Max( 1, i.Cantidad ) ) );

		var ma = Mapa( a );
		var mb = Mapa( b );

		return ma.Count == mb.Count
			&& ma.All( kv => mb.TryGetValue( kv.Key, out var c ) && c == kv.Value );
	}
}