Skip to content

Decoupling parent chunks from child chunks advice #22

@millsaj

Description

@millsaj

Hi, been loving basically all of the libraries. Had a few teething issues with the SaveFileBuilder and thought I'd go over my thoughts and see what you think and if there's a PR to be raised out of it or not. Maybe just a documentation change in the README or changes to SaveChunk.

Tight coupling

In the example code in the README we have the parent node somewhat tightly coupled to its children's save data.

var gameData = new GameData() {
  MapData = chunk.GetChunkSaveData<MapData>(),
  PlayerData = chunk.GetChunkSaveData<PlayerData>(),
  PlayerCameraData = chunk.GetChunkSaveData<PlayerCameraData>()
};

In the above example we need to know that there is a MapData chunk and a PlayerData chunk etc. We also need to know about the GameData chunk in children:

[Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();

Whilst I think this pattern is useful I found myself wanting to decouple this relationship.

Also: In the above example, if I wanted 10 PlayerData objects then I need a list but there isn't a built in way to register each PlayerNode's PlayerData without finding them and looping over them in the parent.

Workaround

In my own game I've ended up creating a class like this to facilitate the kind of relationship I wanted between the nodes that serve these chunks

public partial class SaveChunkBag
{
  public ISaveChunk<ChunkBagSave> SaveChunk { get; private set; }
  
  private readonly ConcurrentDictionary<string, ISaveChunk<SaveObject>> _children = new();
  
  public SaveChunkBag()
  {
    SaveChunk = new SaveChunk<ChunkBagSave>(
      onSave: GetSaveData,
      onLoad: LoadSaveData
    );
  }
  
  public void AddChunk(string name, ISaveChunk<SaveObject> chunk)
  {
    _children[name] = new SaveChunk<SaveObject>(
      onSave: c => chunk.GetSaveData(),
      onLoad: (c, newData) => chunk.LoadSaveData(newData)
    );
  }
  
  public void AddChunk<T>(string name, Func<ISaveChunk<SaveObject>, T> onSave, Action<ISaveChunk<SaveObject>, T> onLoad)
    where T : SaveObject
  {
    _children[name] = new SaveChunk<SaveObject>(
      onSave: (c) => (T)onSave(c),
      onLoad: (c, newData) => onLoad(c, (T)newData)
    );
  }
  
  private ChunkBagSave GetSaveData(ISaveChunk<ChunkBagSave> _chunk)
  {
    var save = new ChunkBagSave();
    
    foreach (var kvp in _children)
    {
      if (kvp.Value == null)
      {
        throw new KeyNotFoundException($"Chunk '{kvp.Key}' not found in SaveChunkList.");
      }
      
      save.Children[kvp.Key] = kvp.Value.GetSaveData();
    }
    
    return save;
  }
  
  private void LoadSaveData(ISaveChunk<ChunkBagSave> chunk, ChunkBagSave newChunkBag)
  {
    foreach (var child in newChunkBag.Children)
    {
      if (!_children.ContainsKey(child.Key))
      {
        throw new KeyNotFoundException($"Chunk '{child.Key}' not found in SaveChunkList.");
      }
      
      _children[child.Key].LoadSaveData(child.Value);
    }
  }
  
  
  [Meta, Id("chunk_bag")]
  public partial class ChunkBagSave
  {
    [Save("children")]
    public Dictionary<string, SaveObject> Children { get; set; } = new();
  }
}

// This is the base class for all save objects in the game.
// It allows for polymorphic serialization and deserialization of game objects.
[Meta]
public partial record SaveObject
{
}

Usage Examples

I think these examples show a more decoupled structure that might be useful.

[Dependency]
private SaveChunkBag ChunkBag => this.DependOn<SaveChunkBag>();

private string NpcName;
 
public void OnResolved()
{
  ChunkBag.AddChunk("npcs." + NpcName,
    onSave: (chunk) =>
    {
      // Do something
    },
    onLoad: (chunk, data) =>
    {
      // Do something
    }
  );
}
[Dependency]
private SaveChunkBag ChunkBag => this.DependOn<SaveChunkBag>();

SaveChunkBag IProvide<SaveChunkBag>.Value() => this.PlayerData.ChunkBag;

public void OnResolved()
{
  ChunkBag.AddChunk("Player",
    onSave: (chunk) => this.PlayerData,
    onLoad: (chunk, data) =>
    {
      this.PlayerData = data;
    }
  );
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions