chickensoft-games / chickensoft-games/SaveFileBuilder

Decoupling parent chunks from child chunks advice

Open
#22 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
30
Forks
4
Avg merge
6h 7m
Merged PRs (30d)
4

Description

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;
    }
  );
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the README's tightly coupled example and the SaveChunk/SaveFileBuilder usage, then compare them with the proposed SaveChunkBag workaround. Done means agreeing whether this should be a documentation update or an API change and recording clear acceptance criteria before implementation.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.