Player persistence logic. Defines PlayerSave and ItemSave structs, loads/saves player data to the engine data filesystem as JSON, applies loaded saves on the host, and sends owner-client saves to the host via an RPC.
using System;
using System.Collections.Generic;
using Sandbox;
using BrickJam.Upgrading;
namespace BrickJam;
public sealed partial class Player
{
public static bool DataChanged { get; set; }
private const float SaveInterval = 15f;
private TimeUntil nextSave = SaveInterval;
public struct ItemSave
{
public string PrefabPath { get; set; }
public LootRarity Rarity { get; set; }
public int Count { get; set; }
public static ItemSave FromEntry( ItemEntry entry, int count ) => new()
{
PrefabPath = entry.Prefab?.ResourceName,
Rarity = entry.Rarity,
Count = count
};
}
public struct PlayerSave
{
public int Money { get; set; }
public List<ItemSave> Inventory { get; set; }
public List<string> Upgrades { get; set; }
public bool SeenTips { get; set; }
private static string FileName => $"{Connection.Local.SteamId}.player";
public static PlayerSave? LoadStored()
{
if ( !FileSystem.Data.FileExists( FileName ) )
return null;
try { return Json.Deserialize<PlayerSave>( FileSystem.Data.ReadAllText( FileName ) ); }
catch { return null; }
}
public void SaveStored() => FileSystem.Data.WriteJson( FileName, this );
public static PlayerSave FromPlayer( Player player )
{
var save = new PlayerSave
{
Inventory = new List<ItemSave>(),
Upgrades = new List<string>(),
Money = player.Money,
SeenTips = player.SeenTips
};
if ( player.Inventory is not null )
{
foreach ( var (entry, count) in player.Inventory.Entries )
save.Inventory.Add( ItemSave.FromEntry( entry, count ) );
}
foreach ( var upgrade in player.Upgrades )
save.Upgrades.Add( upgrade );
return save;
}
}
/// <summary>Host-side: apply a save to this player.</summary>
public void LoadSaveData( PlayerSave save )
{
if ( !Networking.IsHost )
return;
Inventory?.Clear();
Upgrades.Clear();
if ( Inventory is not null )
{
foreach ( var item in save.Inventory )
{
var prefab = LootPrefab.Get( item.PrefabPath );
if ( prefab is null )
continue;
Inventory.Add( new ItemEntry { Prefab = prefab, Rarity = item.Rarity }, item.Count );
}
}
foreach ( var upgrade in save.Upgrades )
{
if ( Upgrade.Exists( upgrade ) && !Upgrades.Contains( upgrade ) )
Upgrades.Add( upgrade );
}
SeenTips = save.SeenTips;
SetMoney( save.Money );
}
/// <summary>Owner client: persist the local player's data to disk.</summary>
public static void StoreSave()
{
if ( !DataChanged || Local is null )
return;
PlayerSave.FromPlayer( Local ).SaveStored();
DataChanged = false;
}
/// <summary>Owner client: load the stored save (if any) and push it to the host.</summary>
private void SendStoredSaveToServer()
{
var save = PlayerSave.LoadStored();
if ( save is null )
return;
// File Explorer: we just successfully loaded our progress file (owner client).
AchievementTracker.OnSaveLoaded();
ReceiveSaveFromClient( Json.Serialize( save.Value ) );
}
[Rpc.Host]
private void ReceiveSaveFromClient( string json )
{
try { LoadSaveData( Json.Deserialize<PlayerSave>( json ) ); }
catch ( Exception e ) { Log.Warning( $"Invalid player save data: {e.Message}" ); }
}
private void TickSave()
{
if ( IsProxy )
return;
if ( nextSave )
{
DataChanged = true;
StoreSave();
nextSave = SaveInterval;
}
}
}