using System.Collections.Generic;
using System.Text.RegularExpressions;
using CleverCrow.Fluid.UniqueIds;
using UnchartedLimbo.Core.Runtime.Attributes;
namespace UnchartedLimbo.Core.Runtime.ProjectManagement
/// Saves the values of every <see cref="PresetableAttribute"/> field found on
/// the configured <see cref="trackedObjects"/> into a JSON-backed preset file
/// Each GameObject must own a <see cref="UniqueId"/> component so the loader
/// can match objects at restore-time.
/// The file name is generated as
/// <c>{prefix}_{yyyy-MM-dd}_{version:D2}.ulcpreset</c> where <em>version</em>
/// auto-increments per day.
public sealed class ULC_PresetSaver : MonoBehaviour
// -------------------------- constants & fields --------------------------
private const BindingFlags _flags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; citeturn0search7
private const string _fileExtension = "ulcpreset"; // no leading '.'
/// <summary>Prefix used when generating a preset filename.</summary>
[Tooltip("Prefix used when generating a preset filename.")]
public string prefix = "Preset";
[SerializeField, Tooltip("Scene objects whose Presetable fields will be captured.")]
private List<GameObject> trackedObjects = new();
/// <summary>The DTO instance representing the last saved preset.</summary>
[HideInInspector] public PresetDTO current;
[ReadOnly, SerializeField]
private string _fullPath; // Path of the last write for inspector visibility
// -------------------------- public API --------------------------
/// Serialises all marked fields on the <see cref="trackedObjects"/> list
/// and writes them to disk. The process is fully logged to the Console.
/// <exception cref="InvalidOperationException">
/// Thrown when the saver is invoked with an empty
/// <see cref="trackedObjects"/> collection.
[ContextMenu("Save preset")]
if (trackedObjects == null || trackedObjects.Count == 0)
throw new InvalidOperationException(
"[PresetSaver] No tracked objects configured – aborting.");
_fullPath = GetFullPath(prefix);
current = new PresetDTO();
var log = new StringBuilder();
log.AppendLine($"[PresetSaver] ▶ Begin save into {_fullPath}");
foreach (var obj in trackedObjects)
if (!obj) { log.AppendLine("[PresetSaver] ⚠ Tracked object is null"); continue; }
string guid = obj.GetComponent<UniqueId>()?.Id;
if (string.IsNullOrEmpty(guid))
log.AppendLine($"[PresetSaver] ⚠ {obj.name} lacks UniqueId – skipped");
var objDto = new ObjectDTO { name = obj.name, guid = guid };
int compCount = obj.GetComponentCount(); // extension method
for (int i = 0; i < compCount; i++)
var comp = obj.GetComponentAtIndex(i);
if (comp == null) continue;
string typeName = comp.GetType().AssemblyQualifiedName;
string simple = typeName?[..typeName.IndexOf(',')] ?? "Unknown";
simple = simple.Split('.').Last();
foreach (var field in comp.GetType().GetFields(_flags))
if (!field.IsDefined(typeof(PresetableAttribute), true))
object value = field.GetValue(comp);
objDto.fields.Add(new FieldDTO
name = $"{simple}/{field.Name}",
componentType = typeName,
jsonValue = JsonConvert.SerializeObject(value,
$"[PresetSaver] ✖ Failed serialising {simple}.{field.Name}: {ex.Message}");
current.objects.Add(objDto);
log.AppendLine($"[PresetSaver] ✓ Captured {objDto.fields.Count} fields from {obj.name}");
// ---------------- write file with basic exception handling -----------
string json = JsonConvert.SerializeObject(current, Formatting.Indented);
File.WriteAllText(_fullPath, json); citeturn0search4turn0search1
log.AppendLine("[PresetSaver] ▲ Preset successfully written.");
Debug.LogError($"[PresetSaver] ✖ I/O error when writing preset: {ioEx.Message}");
Debug.Log(log.ToString());
AssetDatabase.Refresh(); // import freshly written preset as asset citeturn0search5
// -------------------------- helpers --------------------------
/// Returns an absolute path for a new preset file using the configured
/// <paramref name="prefix"/>. Ensures <c>Assets/ULC_PRESETS</c>
/// exists and calculates the next version number for today.
/// Uses <see cref="Directory.EnumerateFiles"/> (lazy enumeration) for
/// efficiency on large folders. citeturn0search1
private static string GetFullPath(string prefix = "Preset")
string dir = Path.Combine(Application.dataPath, "ULC_PRESETS");
Directory.CreateDirectory(dir); // idempotent
string datePart = DateTime.Now.ToString("yyyy-MM-dd"); // ISO date citeturn0search3
string pattern = $"{prefix}_{datePart}_*.{_fileExtension}";
var versionRx = new Regex(
$"_(\\d+?)\\.{Regex.Escape(_fileExtension)}$",
RegexOptions.Compiled | RegexOptions.IgnoreCase); // escape '.' citeturn0search2turn0search10
int nextVersion = Directory
.EnumerateFiles(dir, pattern)
.Select(Path.GetFileName)
var m = versionRx.Match(f);
return m.Success ? int.Parse(m.Groups[1].Value) : -1;
string fileName = $"{prefix}_{datePart}_{nextVersion:D2}.{_fileExtension}";
return Path.Combine(dir, fileName);