It doesn't get better than this.
Edit: it got better than this.
SaveChunk is tightly coupled and can only contain one child per type. But it turns out that SaveChunks don't actually need to be coupled. All it needs in fact is 2 simple events and ways to invoke them: you heard that right! We can still save and load the data from one chunk onto another, but there is no reason for the SaveChunks to be coupled. I am sure that this will allow some clever people to do some very clever things with them!
// Expose events
public interface ISaveChunk<TData>
{
event Action<TData>? OnSave;
event Action<TData>? OnLoad;
}
// Note the new() restriction. This is intentional, as the new() restriction is also required for serialization scenarios.
// It also ensures that each OnSave callback may safely assume that the object is created from new().
// In this way, it works in cohesion with the unordered nature of events, wherein each callback assumes nothing.
public class SaveChunk<TData> : ISaveChunk<TData> where TData : new()
{
public event Action<TData>? OnSave;
public event Action<TData>? OnLoad;
public TData GetSaveData()
{
var data = new TData();
OnSave?.Invoke(data);
return data;
}
public void LoadSaveData(TData data) => OnLoad?.Invoke(data);
}
Additionally, because you're settings relevant data from the class you're setting from, you can structure your data representation however you want. Right now GameData.Players is filled via index, but it could just as easily be done via string or Guid by using a Dictionary<,> Players.
public record GameData
{
public List<PlayerData> Players;
}
public record PlayerData
{
public string Name;
public int Level;
}
[Meta(typeof(IAutoNode))]
public partial class Game : Node3D
{
public SaveFile SaveFile = SaveFile.CreateGZipJsonFile(SaveFilePath, JsonOptions);
public SaveChunk<GameData> GameChunk = new();
// Provide the game chunk to all descendant nodes.
ISaveChunk<GameData> IProvide<ISaveChunk<GameData>>.Value() => GameChunk;
void OnSave()
{
var gameData = GameChunk.GetSaveData();
SaveFile.Save(gameData);
}
void OnLoad()
{
var gameData = SaveFile.Load<GameData>();
GameChunk.LoadSaveData(gameData);
}
}
[Meta(typeof(IAutoNode))]
public partial class Player : CharacterBody3D
{
[Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();
public SaveChunk<GameData> PlayerChunk = new();
public int index;
public string Name;
public int Age;
public void Setup()
{
PlayerChunk.OnSave += playerData =>
{
playerData.Name = Name;
playerData.Level = Age;
};
PlayerChunk.OnLoad += playerData =>
{
Name = playerData.Name;
Age = playerData.Level;
};
}
public void OnResolved()
{
GameChunk.OnSave += gameData => gameData.Players[index] = PlayerChunk.GetSaveData();
GameChunk.OnLoad += gameData => PlayerChunk.LoadSaveData(gameData.Players[index]);
}
}
Note how we didn't need to define an OnSave or OnLoad inside of the Game class. It is added inside the Player class, only once there is something to save or load.
It doesn't get better than this.Edit: it got better than this.
SaveChunkis tightly coupled and can only contain one child per type. But it turns out thatSaveChunksdon't actually need to be coupled. All it needs in fact is 2 simple events and ways to invoke them: you heard that right! We can still save and load the data from one chunk onto another, but there is no reason for theSaveChunksto be coupled. I am sure that this will allow some clever people to do some very clever things with them!Additionally, because you're settings relevant data from the class you're setting from, you can structure your data representation however you want. Right now GameData.Players is filled via index, but it could just as easily be done via
stringorGuidby using aDictionary<,> Players.Note how we didn't need to define an
OnSaveorOnLoadinside of theGameclass. It is added inside thePlayerclass, only once there is something to save or load.