Examples/SaveStateTrackerExample.cs
// ============================================================
// SaveStateTrackerExample.cs — Automatic save state management
// Copy this into your game project's Code/ directory.
// ============================================================

using System.Text.Json;

namespace Sandbox;

/// <summary>
/// Example: Using SaveStateTracker for automatic state management.
///
/// SaveStateTracker wraps NetworkStorage.CallEndpoint() and tracks
/// whether you're Idle, Saving, Saved, or in Error state. Use this
/// to drive HUD indicators (spinner, checkmark, error icon).
///
/// It also provides CallAndApply() for the optimistic update pattern:
/// apply locally → call server → revert if failed.
/// </summary>
public class SaveStateExample : Component
{
	private readonly SaveStateTracker _tracker = new();

	// ── Read these from your UI to show save status ──
	public SaveStateTracker.SaveState State => _tracker.State;
	public bool IsSaving => _tracker.IsBusy;
	public string LastError => _tracker.LastError;
	public TimeSince TimeSinceStateChange => _tracker.TimeSinceStateChange;

	// ── Game state ──
	[Sync] public int Currency { get; set; }
	public Dictionary<string, float> Inventory { get; set; } = new();

	/// <summary>
	/// Simple tracked call — just tracks Saving/Saved/Error state.
	/// </summary>
	public async Task MineOre( string oreId, float kg )
	{
		var result = await _tracker.Call( "mine-ore", new { ore_id = oreId, kg } );

		if ( result.HasValue )
		{
			// Apply server response
			Currency = JsonHelpers.GetInt( result.Value, "currency", Currency );
			Log.Info( $"Mined {kg}kg {oreId} — Currency: {Currency}" );
		}
		// If null, _tracker.State is already SaveState.Error
		// and _tracker.LastError has the message
	}

	/// <summary>
	/// Full optimistic update pattern with auto-revert.
	/// CallAndApply does: applyOptimistic → call server → applyServer or revert.
	/// </summary>
	public async Task SellOre( string oreId, float kg )
	{
		var held = Inventory.GetValueOrDefault( oreId, 0f );
		var actual = MathF.Min( kg, held );
		if ( actual <= 0f ) return;

		var prevCurrency = Currency;
		var prevHeld = held;

		var success = await _tracker.CallAndApply(
			slug: "sell-ore",
			input: new { ore_id = oreId, kg = actual },

			// 1. Optimistic local update (runs immediately)
			applyOptimistic: () =>
			{
				Currency += (int)( actual * 10f ); // estimated value
				Inventory[oreId] = held - actual;
			},

			// 2. Apply authoritative server response (runs on success)
			applyServer: ( JsonElement data ) =>
			{
				Currency = JsonHelpers.GetInt( data, "currency", Currency );
				// Server response is authoritative — override estimate
			},

			// 3. Revert (runs on failure)
			revert: () =>
			{
				Currency = prevCurrency;
				Inventory[oreId] = prevHeld;
				Log.Warning( $"Sell failed — reverted" );
			}
		);

		if ( success )
			Log.Info( $"Sold {actual}kg {oreId} — Currency: {Currency}" );
	}

	/// <summary>
	/// Example: Show save state in a Razor UI component.
	/// </summary>
	// In your .razor file:
	//
	// @if ( SaveStateExample.Instance?.IsSaving == true )
	// {
	//     <div class="save-indicator saving">Saving...</div>
	// }
	// else if ( SaveStateExample.Instance?.State == SaveStateTracker.SaveState.Saved )
	// {
	//     <div class="save-indicator saved">Saved</div>
	// }
	// else if ( SaveStateExample.Instance?.State == SaveStateTracker.SaveState.Error )
	// {
	//     <div class="save-indicator error">@SaveStateExample.Instance.LastError</div>
	// }
}