Code/Attributes/AutoSaved.cs
using Sandbox;
using Sandbox.Internal;
using System;
using System.Linq;

namespace SandbankDatabase;

/// <summary>
/// Add this attribute to a property to allow it to be saved to file. When the property
/// is modified, the whole class will be saved to file. Nothing will happen if the UID is not set.
/// 
/// While this is the most convenient approach, as this will save the document every time data
/// is changed, this will generally perform worse than [Saved].
/// </summary>
[AttributeUsage( AttributeTargets.Property )]
[CodeGenerator(CodeGeneratorFlags.WrapPropertySet | CodeGeneratorFlags.Instance, "SandbankDatabase.SandbankAutoSavedEventHandler.AutoSave" )]
public class AutoSaved : Attribute
{
	public string CollectionName { get; set; }

	public AutoSaved( string collectionName )
	{
		CollectionName = collectionName;
	}
}

/// <summary>
/// This class is only visible because codegen needs it. Don't use it directly.
/// </summary>
public static class SandbankAutoSavedEventHandler
{
	private static object _autoSaveLock = new();
	private static object _objectBeingAutoSaved = null;

	public static void WipeStaticFields()
	{
		_objectBeingAutoSaved = null;
	}

	public static void AutoSave<T>( WrappedPropertySet<T> p )
	{
		p.Setter( p.Value );

		string id = (string)GlobalGameNamespace.TypeLibrary.GetPropertyValue( p.Object, "UID" );

		// If the UID is not set then we can assume this document hasn't even been fully created yet.
		if ( string.IsNullOrEmpty( id ) )
			return;

		lock ( _autoSaveLock )
		{
			// When we save this in a moment, the cache will create a copy of it. This will basically send it right
			// back here. So to avoid an infinite loop, don't auto save if we're already auto saving an object.
			//
			// This means that only one object can be auto saved at a time, but in practice this isn't really a big
			// deal since a) 95% of users won't be saving things in multiple threads and b) auto save is meant for
			// people who don't care about performance. Also, creating a system for locking each object could just
			// end up adding more latency than it's worth.
			if ( _objectBeingAutoSaved != null )
				return;
			
			try
			{
				_objectBeingAutoSaved = p.Object;

				var collectionName = (string)GlobalGameNamespace.TypeLibrary.GetPropertyValue(
					p.Attributes.First( x => x.GetType().ToString() == "SandbankDatabase.AutoSaved" ),
					"CollectionName" );

				Sandbank.Insert( collectionName, p.Object, p.Object.GetType() );
			}
			finally
			{
				_objectBeingAutoSaved = null;
			}
		}
	}
}