WorldBuilder/World.SaveLoad.cs
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Clover.Data;
using Clover.Items;
using Clover.Persistence;

namespace Clover;

public sealed partial class World
{
	private string SaveFileName => $"{RealmManager.CurrentRealm.Path}/worlds/{Data.ResourceName}.json";

	private WorldSaveData _saveData = new();

	public Action OnSave;

	public void Save()
	{
		if ( IsProxy )
		{
			Log.Error( "Cannot save proxy world. Fix this call." );
			return;
		}

		Log.Info( $"Saving world {Data.ResourceName}" );

		Scene.RunEvent<IWorldSaved>( x => x.PreWorldSaved( this ) );

		var savedItems = new List<PersistentWorldItem>();

		// Save world items (dropped/placed items, furniture, etc)
		foreach ( var worldItem in WorldItems.ToList() )
		{
			if ( !worldItem.ShouldBeSaved() )
			{
				Log.Info( $"Skipping {worldItem}" );
				continue;
			}

			var persistentItem = PersistentItem.Create( worldItem.GameObject );

			var persistentWorldItem = new PersistentWorldItem
			{
				ItemId = worldItem.ItemData.Id,
				PlacementType = worldItem.ItemPlacement,
				// Position = worldItem.WorldPosition,
				// Rotation = worldItem.WorldRotation,
				WPosition = worldItem.LocalPosition,
				WAngles = worldItem.LocalRotation,
				Item = persistentItem
			};

			savedItems.Add( persistentWorldItem );
		}


		var savedObjects = new List<PersistentWorldObject>();

		foreach ( var worldObject in Scene.GetAllComponents<WorldObject>()
			         .Where( x => x.WorldLayerObject.Layer == Layer ) )
		{
			Log.Info( $"Saving object {worldObject}" );
			var persistentObject = worldObject.OnObjectSave();
			savedObjects.Add( persistentObject );
		}

		// var saveData = new WorldSaveData { Name = Data.ResourceName, Items = savedItems, LastSave = DateTime.Now };

		if ( _saveData == null )
		{
			Log.Warning( "Save data is null, creating new instance. Should probably do this when starting the game" );
			_saveData = new WorldSaveData();
		}

		_saveData.LastSave = DateTime.Now;

		Log.Info( $"Saving {savedItems.Count} items" );
		_saveData.Items = savedItems;

		Log.Info( $"Saving {savedObjects.Count} objects" );
		_saveData.Objects = savedObjects;

		FileSystem.Data.CreateDirectory( $"{RealmManager.CurrentRealm.Path}/worlds" );

		Log.Info( $"Writing save data to {SaveFileName}" );

		// FileSystem.Data.WriteJson( $"worlds/{Data.ResourceName}.json", saveData );
		var json = JsonSerializer.Serialize( _saveData, GameManager.JsonOptions );
		FileSystem.Data.WriteAllText( SaveFileName, json );

		OnSave?.Invoke();
		Scene.RunEvent<IWorldSaved>( x => x.PostWorldSaved( this ) );
	}

	public async Task Load()
	{
		Log.Info( $"Loading world {Data.ResourceName} async..." );

		if ( RealmManager.CurrentRealm == null )
		{
			Log.Error( "Current realm is null" );
			return;
		}

		if ( string.IsNullOrEmpty( RealmManager.CurrentRealm.Path ) )
		{
			Log.Error( "Current realm path is null or empty" );
			return;
		}

		if ( string.IsNullOrEmpty( SaveFileName ) )
		{
			Log.Error( "Save file name is null or empty" );
			return;
		}

		if ( !FileSystem.Data.FileExists( SaveFileName ) )
		{
			Log.Warning( $"File {SaveFileName} does not exist" );
			return;
		}

		var json = await FileSystem.Data.ReadAllTextAsync( SaveFileName );
		var saveData = JsonSerializer.Deserialize<WorldSaveData>( json, GameManager.JsonOptions );

		Log.Info( $"Loaded save data from {SaveFileName}" );

		foreach ( var persistentWorldItem in saveData.Items )
		{
			var itemData = ItemData.Get( persistentWorldItem.ItemId );
			if ( !itemData.IsValid() )
			{
				Log.Error( $"Item data for {persistentWorldItem.ItemId} is not valid" );
				continue;
			}

			Log.Info( $"Loading item {persistentWorldItem.ItemId}" );

			if ( !string.IsNullOrEmpty( persistentWorldItem.Item.PackageIdent ) )
			{
				var package = await Package.Fetch( persistentWorldItem.Item.PackageIdent, false );
				if ( package == null )
				{
					Log.Warning( $"Could not fetch package {persistentWorldItem.Item.PackageIdent}" );
					continue;
				}

				Log.Info( $"Fetched package {package.Title}" );
			}

			var prefab = persistentWorldItem.PlacementType == ItemPlacementType.Dropped
				? itemData.DropScene
				: itemData.PlaceScene;

			if ( !prefab.IsValid() )
			{
				Log.Error( $"Prefab for item {persistentWorldItem.ItemId} is not valid" );
				continue;
			}

			var gameObject = prefab.Clone();

			// Set parent to this world
			gameObject.Parent = GameObject;

			// Set position and rotation
			gameObject.LocalPosition = persistentWorldItem.WPosition;
			gameObject.LocalRotation = persistentWorldItem.WAngles;

			if ( !gameObject.Components.TryGet<WorldItem>( out var worldItem ) )
			{
				Log.Error( $"No WorldItem component found on {gameObject}" );
				gameObject.Destroy();
				continue;
			}

			worldItem.LoadPersistence( persistentWorldItem.Item );

			WorldItems.Add( worldItem );

			// gameObject.SetParent( GameObject ); // TODO: should items be parented to the world?

			gameObject.NetworkSpawn();
		}

		Log.Info( $"Loaded {saveData.Items.Count} items" );

		foreach ( var worldObject in saveData.Objects )
		{
			Log.Info( $"Loading object {worldObject}" );

			var gameObject = GameObject.GetPrefab( worldObject.PrefabPath ).Clone();

			if ( !gameObject.Components.TryGet<WorldObject>( out var worldObjectComponent ) )
			{
				Log.Warning( $"No WorldObject component found on {gameObject}" );
				gameObject.Destroy();
				continue;
			}

			worldObjectComponent.WorldLayerObject.SetLayer( Layer );
			worldObjectComponent.OnObjectLoad( worldObject );

			gameObject.SetParent( GameObject ); // TODO: should items be parented to the world?

			gameObject.NetworkSpawn();
		}

		Log.Info( $"Loaded {saveData.Objects.Count} objects" );

		_saveData = saveData;
	}
}

interface IWorldSaved
{
	void PreWorldSaved( World world );
	void PostWorldSaved( World world );
}