Persistence/ISaveDataProperty.cs
using System;
using System.Collections.Immutable;
using System.Text.Json.Nodes;
namespace HC3.Persistence;
#nullable enable
/// <summary>
/// <para>
/// Handles writing a particular property to the root of a save file.
/// </para>
/// <para>
/// If implemented by a <see cref="Component"/>, we'll look for a component in the scene to handle this property.
/// This should only be implemented by singleton components that are always in the scene.
/// </para>
/// <para>
/// If implemented by a non-component non-abstract type with a parameterless constructor, we'll create a singleton
/// instance of it when saving / loading.
/// </para>
/// </summary>
public interface ISaveDataProperty
{
/// <summary>
/// JSON property name for this property.
/// </summary>
string PropertyName => GetType().Name;
/// <summary>
/// Serialization / deserialization order of this property.
/// </summary>
int PropertyOrder => 0;
/// <summary>
/// Called when the game is saving. Store this component's state as JSON.
/// </summary>
JsonNode? WriteValue( Scene scene );
/// <summary>
/// Called when the game is loading. Restore this component's state from JSON.
/// </summary>
void ReadValue( Scene scene, JsonNode node );
}
/// <inheritdoc cref="ISaveDataProperty"/>
/// <typeparam name="T">Property value type, must be JSON serializable.</typeparam>
public interface ISaveDataProperty<T> : ISaveDataProperty
{
new T WriteValue( Scene scene );
void ReadValue( Scene scene, T model );
JsonNode? ISaveDataProperty.WriteValue( Scene scene )
{
var model = WriteValue( scene );
return Json.ToNode( model, typeof( T ) );
}
void ISaveDataProperty.ReadValue( Scene scene, JsonNode node )
{
var model = Json.FromNode<T>( node )
?? throw new Exception( $"Found null when expecting to read a {typeof( T ).Name}." );
ReadValue( scene, model );
}
}
/// <summary>
/// Helper for saving data for prefab instances, used with <see cref="SpawnedPrefabSaveData{TComponent,TSaveData}"/>.
/// </summary>
public interface IPrefabSourceComponent<T>
{
/// <summary>
/// This needs to be set manually when the component wakes up.
/// </summary>
string? PrefabSource { get; }
T GetSaveData();
void Spawn( T saveData );
}
/// <summary>
/// Helper for saving prefab instances that all have a common component that implements <see cref="IPrefabSourceComponent{T}"/>.
/// </summary>
/// <typeparam name="TComponent">Component to look for when saving.</typeparam>
/// <typeparam name="TSaveData">Save data to write for each instance.</typeparam>
public abstract class SpawnedPrefabSaveData<TComponent, TSaveData>
: ISaveDataProperty<ImmutableDictionary<string, ImmutableArray<TSaveData>>>
where TComponent : Component, IPrefabSourceComponent<TSaveData>
{
public abstract string PropertyName { get; }
public virtual int PropertyOrder => 0;
protected virtual GameObject? Parent => null;
public ImmutableDictionary<string, ImmutableArray<TSaveData>> WriteValue( Scene scene )
{
return scene.GetAllComponents<TComponent>()
.Where( x => x.PrefabSource is not null )
.GroupBy( x => x.PrefabSource! )
.ToImmutableDictionary( x => x.Key,
x => x.Select( y => y.GetSaveData() ).ToImmutableArray() );
}
public void ReadValue( Scene scene, ImmutableDictionary<string, ImmutableArray<TSaveData>> model )
{
foreach ( var scenery in scene.GetAllComponents<TComponent>() )
{
scenery.DestroyGameObject();
}
foreach ( var group in model )
{
if ( GameObject.GetPrefab( group.Key ) is not { IsValid: true } prefab )
{
Log.Warning( $"Can't find prefab {group.Key}" );
continue;
}
foreach ( var saveData in group.Value )
{
var clone = prefab.Clone( transform: Transform.Zero, parent: Parent );
var component = clone.GetComponent<TComponent>();
component?.Spawn( saveData );
}
}
}
string ISaveDataProperty.PropertyName => PropertyName;
int ISaveDataProperty.PropertyOrder => PropertyOrder;
}