The Storage system provides a simple, unified way to manage user-generated content in your game. Whether you're saving game progress, storing player creations, or anything else, Storage handles everything from local file management to Steam Workshop integration.
Each storage entry is a self-contained folder with its own filesystem, metadata, and optional thumbnail, making it easy to organize, share, and distribute user content.
Storage entries are categorized by a string type (like "save" or "dupe"), automatically maintain metadata, and can be seamlessly published to or downloaded from the Steam Workshop. The API is designed to be straightforward: create entries, read/write files, and optionally share them with the community.
To create a new storage entry, use Storage.CreateEntry() with a type identifier. The entry provides a Files property (a BaseFileSystem) that you use to read and write files.
// Create a new save game entry
var saveEntry = Storage.CreateEntry( "save" );
// Write game data to files
saveEntry.Files.WriteAllText( "player.json", playerJson );
saveEntry.Files.WriteAllBytes( "world.dat", worldData );
// You can also use JSON serialization
saveEntry.Files.WriteJson( "config.json", gameConfig );
// Set metadata for searching and display
saveEntry.SetMeta( "playerName", "John Doe" );
saveEntry.SetMeta( "level", 42 );
saveEntry.SetMeta( "playtime", 3600 );
// Create and set a thumbnail
var thumbnail = CreateThumbnailBitmap(); // Your method to create a Bitmap
saveEntry.SetThumbnail( thumbnail );
Each entry has the following public properties:
**Id** - A unique identifier (GUID) for this entry**Type** - The category type (e.g., "save", "dupe")**Created** - When this entry was created**Files** - A BaseFileSystem for reading and writing files**Thumbnail** - The thumbnail image as a TextureThe Files property is a full-featured filesystem that supports:
// Write operations
entry.Files.WriteAllText( "notes.txt", "Player notes..." );
entry.Files.WriteAllBytes( "screenshot.png", imageBytes );
entry.Files.WriteJson( "data.json", myObject );
// Read operations
string notes = entry.Files.ReadAllText( "notes.txt" );
byte[] screenshot = entry.Files.ReadAllBytes( "screenshot.png" ).ToArray();
MyData data = entry.Files.ReadJson<MyData>( "data.json" );
// File queries
bool exists = entry.Files.FileExists( "notes.txt" );
var allFiles = entry.Files.FindFile( "/", "*", recursive: true );
// Directories
entry.Files.CreateDirectory( "levels" );
entry.Files.WriteAllText( "levels/level1.json", levelData );
To retrieve all storage entries of a specific type from disk:
// Get all saved games
var allSaves = Storage.GetAll( "save" );
foreach ( var save in allSaves )
{
// Access entry properties
Log.Info( $"Save: {save.Id}" );
Log.Info( $"Created: {save.Created}" );
// Read metadata
var playerName = save.GetMeta<string>( "playerName" );
var level = save.GetMeta<int>( "level" );
// Access the thumbnail
var thumbnail = save.Thumbnail;
// Read files from the entry
if ( save.Files.FileExists( "player.json" ) )
{
var playerJson = save.Files.ReadAllText( "player.json" );
// Load your game...
}
}
To remove an entry from disk:
entry.Delete();
This permanently removes the entry's folder and all its contents.
Publishing to Steam Workshop is made very simple:
saveEntry.Publish();
This pops open a dialog for the client, where they can name, add a description and change visibility of the entry.
To search for content on the Steam Workshop, create and configure a Query, then run it:
var query = new Storage.Query
{
// Search for items with specific tags
TagsRequired = { "save", "adventure" },
// Exclude certain tags
TagsExcluded = { "adult" },
// Key-value filters (set during publish)
KeyValues = { ["type"] = "save", ["package"] = "facepunch.sandbox" },
// Text search
SearchText = "epic quest",
// Sort order
SortOrder = Storage.SortOrder.RankedByVote,
};
// Run the query
var result = await query.Run();
Log.Info( $"Found {result.TotalCount} total items" );
Log.Info( $"Returned {result.ResultCount} items" );
foreach ( var item in result.Items )
{
Log.Info( $"{item.Title} by {item.Owner.Name}" );
Log.Info( $"Rating: {item.VotesUp}/{item.VotesDown}" );
Log.Info( $"Created: {item.Created}" );
Log.Info( $"Tags: {string.Join( ", ", item.Tags )}" );
}
The SortOrder enum provides various ranking options:
RankedByVote - Most popular itemsRankedByPublicationDate - Newest firstRankedByTrend - Currently trendingRankedByTotalPlaytime - Most playedRankedByTextSearch - Best search matchesAny storage published automatically have two keyvalues
package - the name of the package that published ittype - the name of the storage typeThese are obviously useful when you only want to find packages from a specific game
To download and use content from the Steam Workshop, just call Install on the Storage.QueryItem
// From a query result
var query = new Storage.Query { TagsRequired = { "save" } };
Storage.QueryResult result = await query.Run();
Storage.QueryItem chosenItem = result.Items.First();
// Install the workshop item
Storage.Entry entry = await chosenItem.Install();
if ( entry == null ) throw new Exception( "Failed to install the chosen item." );
// The entry is now available locally
Log.Info( $"Installed: {entry.Type} - {entry.Id}" );
// Read its metadata
var playerName = entry.GetMeta<string>( "playerName" );
// Access its files (read-only)
if ( entry.Files.FileExists( "player.json" ) )
{
var data = entry.Files.ReadAllText( "player.json" );
// Load the save...
}
// Workshop entries are read-only
if ( entry.Files.IsReadOnly )
{
Log.Info( "This is a workshop item (read-only)" );
}