Examples/PlayerDataExample.cs
// ============================================================
// PlayerDataExample.cs — Manage player data with Network Storage
// Copy this into your game project's Code/ directory.
// ============================================================
using System.Text.Json;
namespace Sandbox;
/// <summary>
/// Example: Player data manager using Network Storage endpoints.
///
/// This shows the typical pattern:
/// 1. Load player data on join (or init for new players)
/// 2. Call endpoints for game actions (mine, sell, buy)
/// 3. Apply server responses to local state
/// 4. Optimistic updates with revert on failure
///
/// Your endpoints are defined on sboxcool.com and synced
/// via the Editor Sync Tool.
/// </summary>
public class MyPlayerData : Component
{
// ── Synced to all clients (for nameplates, leaderboards) ──
[Sync] public string PlayerName { get; set; } = "";
[Sync] public int Level { get; set; } = 1;
[Sync] public int Currency { get; set; }
// ── Local-only state ──
public Dictionary<string, float> Inventory { get; private set; } = new();
public bool IsLoaded { get; private set; }
protected override void OnStart()
{
if ( IsProxy ) return;
_ = LoadAsync();
}
private async Task LoadAsync()
{
// Ensure library is configured
if ( !NetworkStorage.IsConfigured )
MyNetStorageConfig.Initialize();
// Load existing player data
var data = await NetworkStorage.CallEndpoint( "load-player" );
if ( data.HasValue )
{
ApplyServerData( data.Value );
IsLoaded = true;
Log.Info( $"Loaded: {PlayerName} Lv.{Level}" );
return;
}
// New player — call init endpoint
var init = await NetworkStorage.CallEndpoint( "init-player", new
{
playerName = Connection.Local?.DisplayName ?? "Player"
} );
if ( init.HasValue )
ApplyServerData( init.Value );
IsLoaded = true;
}
/// <summary>
/// Mine ore — optimistic local update, confirmed by server.
/// </summary>
public async Task MineOre( string oreId, float kg )
{
if ( kg <= 0f ) return;
// Optimistic update
Inventory[oreId] = Inventory.GetValueOrDefault( oreId, 0f ) + kg;
var result = await NetworkStorage.CallEndpoint( "mine-ore", new
{
ore_id = oreId,
kg
} );
if ( result.HasValue )
{
ApplyServerData( result.Value );
}
else
{
// Revert on failure
Inventory[oreId] = Inventory.GetValueOrDefault( oreId, 0f ) - kg;
if ( Inventory[oreId] <= 0f ) Inventory.Remove( oreId );
Log.Warning( $"Mine failed — reverted {kg}kg {oreId}" );
}
}
/// <summary>
/// Sell ore for currency.
/// </summary>
public async Task SellOre( string oreId, float kg )
{
var held = Inventory.GetValueOrDefault( oreId, 0f );
var actual = MathF.Min( kg, held );
if ( actual <= 0f ) return;
var result = await NetworkStorage.CallEndpoint( "sell-ore", new
{
ore_id = oreId,
kg = actual
} );
if ( result.HasValue )
ApplyServerData( result.Value );
}
/// <summary>
/// Read a document directly from a collection (e.g. leaderboard entry).
/// </summary>
public async Task<JsonElement?> GetLeaderboardEntry( string steamId )
{
return await NetworkStorage.GetDocument( "leaderboard", steamId );
}
private void ApplyServerData( JsonElement data )
{
// Use JsonHelpers for safe extraction with fallbacks
PlayerName = JsonHelpers.GetString( data, "playerName", PlayerName );
Level = JsonHelpers.GetInt( data, "level", Level );
Currency = JsonHelpers.GetInt( data, "currency", Currency );
// Use extension methods for complex types
if ( data.TryGetProperty( "inventory", out var inv ) && inv.ValueKind == JsonValueKind.Object )
{
foreach ( var prop in inv.EnumerateObject() )
{
if ( prop.Value.ValueKind == JsonValueKind.Number )
Inventory[prop.Name] = (float)prop.Value.GetDouble();
}
}
}
}