Retrieve game-specific data from the cloud

Rajen Kishna, Technical Account Manager, Epic Games |
November 30, 2021
With our Game Services authentication set up in the previous article, we’re ready to start using Title Storage to securely retrieve game-specific data from the cloud. We’ll go over:
 

Title Storage vs. Player Data Storage

Epic Online Services offers two types of encrypted cloud storage for data: Title Storage and Player Data Storage. The encryption and decryption is done using an encryption key specified by you, so the files are never readable in the cloud. Title Storage can be used to centrally store data in the cloud that can be accessed by any player. Title Storage data is saved via the Developer Portal, but can be retrieved via EOS SDK APIs. This is useful when you have configuration data that you’d like to get to players without having to update the game, for example.

Player Data Storage is—as the name suggests—specific to each player and can be both saved and retrieved via APIs from any device where the player is logged in. This can be used to implement cloud saves (even across multiple platforms), for example. The next article will focus on Player Data Storage.

Usage limitations

In general, Epic Online Services has certain Service Usage Limitations to ensure we provide a stable and reliable ecosystem for all users. Some services have additional usage limitations and Title Storage is one of those services. At the time of writing, these are the limitations, but please refer to the documentation for the most up-to-date information:
 
  • 10GB total file size across all files in a deployment
  • 100 total files in a deployment

Changing our Client Policy

Before we can call the Title Storage Interface APIs in our sample app, we first have to adjust our Client Policy to allow access to this service:
 
  1. Log in to the Developer Portal at https://dev.epicgames.com/portal/.
  2. Navigate to your product > Product Settings in the left menu and click on the Clients tab in the product settings screen.
  3. Click on the three dots next to the client policy you’re using and click on Details.
 
Developer Portal Client Policy Details
Client Policy details
 
  1. Scroll down to Features and click on the toggle button next to Title Storage.
    • Note that a list of allowed actions appears on the right side that we can individually tick to further provide granular access to what our client can do.
  2. Tick the boxes next to both the “list” and “access” actions, as we’ll implement both functionalities.
  3. Click Save & Exit to confirm.
 
Developer Portal Client Policy Title Storage
Title Storage Client Policy allowed features and actions

Setting up files in the Developer Portal

The next step is to set up our files in the Developer Portal. As mentioned, Title Storage APIs are only for querying and retrieving data, so uploading and managing the files is done through the portal:
 
  1. Navigate to your product > Game Services > Title Storage in the left menu.
  2. Here we can see a few things:
    • The files we have added previously (including a count). The screen will likely say “No files found” right now.
    • How much of our 10GB quota is used.
    • The Deployment we’re currently looking at. You’ll likely only see “Release in Live Sandbox” here, as that’s the default, but if we add more deployments, they can be selected here.
    • A button to add new files.
    • A link to the documentation.
 
Developer Portal Title Storage
Title Storage settings in the Developer Portal
 
  1. Click on the Add New File button to add a new file.

In the flyout, you’ll see a screen to enter an encryption key. This key is a 256-bit hex string that you define as 64 characters containing numbers 0-9 and letters A-F. We’ll use this same key in our code, so encryption and decryption can be done through the SDK. It’s important to know that this key is not stored by Epic and will be required to encrypt/decrypt the files, so make sure you remember it. The flyout will also provide an option of saving the key as a .txt file for convenience.
 
  1. For demonstration purposes, we’ll use the following encryption key, but make sure you generate a secure key when using the SDK in a production environment: 1111111111111111111111111111111111111111111111111111111111111111
  2. Click on Next to move to the actual file details.

Here we can give our file a name of max 64 characters, add “tags” that will come in handy when querying for files through the API, upload the actual file, and any overrides we want to specify. 

Tags are used to query for multiple files at once and prevent you from having to hard code specific file names in your game. You can add multiple tags to files in Title Storage, so if you want to query all files available, simply tag all files with a common tag and query on that.

Overrides are cases where we want to use a specific variant of this file based on a specific PUID, identity provider ID (e.g. Epic Account ID), or EOS client. You can use this to get specific files for your game client and trusted server, for example, without having to maintain separate files and code/configuration differences. Another example would be if you use a different EOS client on Android and PC, they each get their own version of the file.
 
  1. As we’re only creating files for our sample app, enter the following:
    • File name: test_file
    • Add tag: test_tag (click Add after entering)
    • Upload file: can be any file, you can create a simple .txt file with a few words of text
    • Overrides: leave unchanged
  2. Click on Save & Close to submit.
 
Developer Portal Title Storage Test File
Adding a new file to Title Storage
 
  1. Finally, repeat steps six and seven one more time:
    • Encryption key: 1111111111111111111111111111111111111111111111111111111111111111
    • File name: another_test
    • Tag: test_tag
    • Add tag: second_tag
    • Upload file: any file
    • Overrides: leave unchanged

We’ll use both files to validate our querying logic by different tags. You should now see two files displayed in the Title Storage settings screen.
 
Developer Portal Title Storage Two Files Added
Two files added to Title Storage

EncryptionKey and CacheDirectory additions to SDK initialization

With our policy and files set up in the Developer Portal, we can now start implementing the code. The first thing we need to do is add the encryption key (as defined in the previous section) and cache directory location to our SDK initialization code, so it knows how to encrypt/decrypt the files and what folder it can use to cache downloads.

Title Storage and Player Data Storage will be cached in separate subfolders of the specified cache directory, so we only need to set a single directory during SDK initialization. When a file (with the same MD5 hash) already exists in cache, subsequent read requests will simply get the file contents from cache again, rather than re-downloading it from the cloud.
 
  1. Open ApplicationSettings.cs and add the following two members. Note that we’re using the default Temp directory as our cache directory, but this can be any folder your application has read/write access to.

public string EncryptionKey = "1111111111111111111111111111111111111111111111111111111111111111";
public string CacheDirectory = Path.GetTempPath();

 
  1. Open MainWindow.xaml.cs and add the following after IsServer = false in the initialization of the options variable:

,
EncryptionKey = App.Settings.EncryptionKey,
CacheDirectory = App.Settings.CacheDirectory


We can run the app to ensure there are no errors, but functionally nothing has changed yet.

Querying files by name or tag

  1. Create a new User Control in the Views folder called TitleStorageView:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="1">
        <TextBox x:Name="TitleStorageFileNameTextBox" Margin="2" TextChanged="TitleStorageFileNameTextBox_TextChanged" Text="{Binding TitleStorageFileName, UpdateSourceTrigger=PropertyChanged}" />
        <Button Width="100" Height="23" Margin="2" Content="Query file" Command="{Binding TitleStorageQueryFile}" />
        <TextBox x:Name="TitleStorageTagTextBox" Margin="2" TextChanged="TitleStorageTagTextBox_TextChanged" Text="{Binding TitleStorageTag, UpdateSourceTrigger=PropertyChanged}" />
        <Button Width="100" Height="23" Margin="2" Content="Query by tag" Command="{Binding TitleStorageQueryFileList}" />
        <Button Width="100" Height="23" Margin="2" Content="Download file" Command="{Binding TitleStorageReadFile}" />
    </StackPanel>

    <ListView x:Name="TitleStorageFilesListView" Grid.Column="0" Margin="2" ItemsSource="{Binding TitleStorageFiles}" SelectedItem="{Binding SelectedTitleStorageFile}" SelectionChanged="TitleStorageFilesListView_SelectionChanged">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Filename" Width="150" DisplayMemberBinding="{Binding Filename}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="MD5Hash" Width="250" DisplayMemberBinding="{Binding MD5Hash}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="FileSizeBytes" Width="100" DisplayMemberBinding="{Binding FileSizeBytes}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="UnencryptedDataSizeBytes" Width="150" DisplayMemberBinding="{Binding UnencryptedDataSizeBytes}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>
</Grid>

 
  • We’ll be using a ListView to display the files we get back from a query
  • We have two TextBoxes with accompanying buttons to query by file name or tag
  • Lastly, we have a button to download the actual file we select in the ListView
 
  1. Open TitleStorageView.xaml.cs to attach the ViewModel and add the UI event handlers:

public partial class TitleStorageView : UserControl
{
    public TitleStorageViewModel ViewModel { get { return ViewModelLocator.TitleStorage; } }

    public TitleStorageView()
    {
        InitializeComponent();
        DataContext = ViewModel;
    }

    private void TitleStorageFilesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
    }

    private void TitleStorageFileNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ViewModel.TitleStorageQueryFile.RaiseCanExecuteChanged();
    }

    private void TitleStorageTagTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ViewModel.TitleStorageQueryFileList.RaiseCanExecuteChanged();
    }
}

 
  1. Add a TitleStorageViewModel.cs class to the ViewModels folder:

public class TitleStorageViewModel : BindableBase
{
    private ObservableCollection<FileMetadata> _titleStorageFiles;
    public ObservableCollection<FileMetadata> TitleStorageFiles
    {
        get { return _titleStorageFiles; }
        set { SetProperty(ref _titleStorageFiles, value); }
    }

    private FileMetadata _selectedTitleStorageFile;
    public FileMetadata SelectedTitleStorageFile
    {
        get { return _selectedTitleStorageFile; }
        set { SetProperty(ref _selectedTitleStorageFile, value); }
    }

    private string _titleStorageFileName;
    public string TitleStorageFileName
    {
        get { return _titleStorageFileName; }
        set { SetProperty(ref _titleStorageFileName, value); }
    }

    private string _titleStorageTag;
    public string TitleStorageTag
    {
        get { return _titleStorageTag; }
        set { SetProperty(ref _titleStorageTag, value); }
    }
}

 
  1. Add a reference to TitleStorageViewModel in ViewModelLocator.cs:

private static TitleStorageViewModel _titleStorage;
public static TitleStorageViewModel TitleStorage
{
    get { return _titleStorage ??= new TitleStorageViewModel(); }
}

 
  1. Add a TitleStorageService.cs class to the Services folder to hold the query logic:

public static class TitleStorageService
{
    public static void QueryFile(string fileName)
    {
        var queryFileOptions = new QueryFileOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
            Filename = fileName
        };

        ViewModelLocator.Main.StatusBarText = $"Querying title storage file <{queryFileOptions.Filename}>...";

        App.Settings.PlatformInterface.GetTitleStorageInterface()
.QueryFile(queryFileOptions, null, (QueryFileCallbackInfo queryFileCallbackInfo) =>
        {
            Debug.WriteLine($"QueryFile {queryFileCallbackInfo.ResultCode}");

            if (queryFileCallbackInfo.ResultCode == Result.Success)
            {
                var copyFileMetadataByFilenameOptions = new CopyFileMetadataByFilenameOptions()
                {
                    LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
                    Filename = fileName
                };
                var result = App.Settings.PlatformInterface.GetTitleStorageInterface()
.CopyFileMetadataByFilename(copyFileMetadataByFilenameOptions, out var metadata);

                if (result == Result.Success)
                {
                    ViewModelLocator.TitleStorage.TitleStorageFiles.Add(metadata);
                }
            }

            ViewModelLocator.Main.StatusBarText = string.Empty;
        });
    }

    public static void QueryFileList(string tag)
    {
        var queryFileListOptions = new QueryFileListOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
            ListOfTags = new string[] { tag }
        };

        ViewModelLocator.Main.StatusBarText = $"Querying title storage files by tag <{tag}>...";

        App.Settings.PlatformInterface.GetTitleStorageInterface()
.QueryFileList(queryFileListOptions, null, (QueryFileListCallbackInfo queryFileListCallbackInfo) =>
        {
            Debug.WriteLine($"QueryFileList {queryFileListCallbackInfo.ResultCode}");

            if (queryFileListCallbackInfo.ResultCode == Result.Success)
            {
                for (uint i = 0; i < queryFileListCallbackInfo.FileCount; i++)
                {
                    var copyFileMetadataAtIndexOptions = new CopyFileMetadataAtIndexOptions()
                    {
                        Index = i,
                        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId)
                    };
                    var result = App.Settings.PlatformInterface.GetTitleStorageInterface()
.CopyFileMetadataAtIndex(copyFileMetadataAtIndexOptions, out var metadata);

                    if (result == Result.Success)
                    {
                        ViewModelLocator.TitleStorage.TitleStorageFiles.Add(metadata);
                    }
                }
            }

            ViewModelLocator.Main.StatusBarText = string.Empty;
        });
    }
}

   
  1. Add a TitleStorageQueryFileCommand.cs class to the Commands folder:

public class TitleStorageQueryFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.ProductUserId) && !string.IsNullOrWhiteSpace(ViewModelLocator.TitleStorage
.TitleStorageFileName);
    }

    public override void Execute(object parameter)
    {
        ViewModelLocator.TitleStorage.TitleStorageFiles = new ObservableCollection<FileMetadata>();
        TitleStorageService.QueryFile(ViewModelLocator.TitleStorage
.TitleStorageFileName);
    }
}

 
  1. Add a TitleStorageQueryFileListCommand.cs class to the Commands folder:

public class TitleStorageQueryFileListCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.ProductUserId) && !string.IsNullOrWhiteSpace(ViewModelLocator
.TitleStorage.TitleStorageTag);
    }

    public override void Execute(object parameter)
    {
        ViewModelLocator.TitleStorage.TitleStorageFiles = new ObservableCollection<FileMetadata>();
        TitleStorageService.QueryFileList(ViewModelLocator.TitleStorage
.TitleStorageTag);
    }
}

 
  1. Open TitleStorageViewModel.cs to declare and instantiate the two commands:

public TitleStorageQueryFileCommand TitleStorageQueryFile { get; set; }
public TitleStorageQueryFileListCommand TitleStorageQueryFileList { get; set; }

public TitleStorageViewModel()
{
    TitleStorageQueryFile = new TitleStorageQueryFileCommand();
    TitleStorageQueryFileList = new TitleStorageQueryFileListCommand();
}

 
  1. Add the following two lines to the RaiseConnectCanExecuteChanged() method in ViewModelLocator.cs to ensure we can only call Title Storage APIs after successfully logging in through the Connect Interface:

TitleStorage.TitleStorageQueryFile.RaiseCanExecuteChanged();
TitleStorage.TitleStorageQueryFileList.RaiseCanExecuteChanged();

 
  1. Lastly, add the TitleStorageView to our TabControl in MainWindow.xaml:

<TabItem x:Name="TitleStorage" Header="Title Storage">
    <views:TitleStorageView />
</TabItem>


Now when we run the app and authenticate through Auth and Connect, we can navigate to the Title Storage tab and query either of our files by name: “test_file” or “another_test”. You can also do a query by tag and note that when you use “test_tag”, both files are returned.
 
App Title Storage Queried Files
Title Storage files queried by tag

Reading files

Now we’ll implement reading the files from cloud storage. This implementation will vary depending on what your game needs, but for now we’ll do a simple download of the file to the same Temp directory location we’re using as our Cache Directory:
 
  1. Add the following method to TitleStorageService.cs:

public static void ReadFile(FileMetadata fileMetadata)
{
    var readFileOptions = new ReadFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = fileMetadata.Filename,
        ReadChunkLengthBytes = 1048576,
        ReadFileDataCallback = (ReadFileDataCallbackInfo readFileDataCallbackInfo) =>
        {
            using var fs = new FileStream($"{App.Settings.CacheDirectory}{readFileDataCallbackInfo.Filename}", FileMode.Append, FileAccess.Write);
            fs.Write(readFileDataCallbackInfo.DataChunk, 0, readFileDataCallbackInfo.DataChunk.Length);
            return ReadResult.RrContinuereading;
        },
        FileTransferProgressCallback = (FileTransferProgressCallbackInfo fileTransferProgressCallbackInfo) =>
        {
            var percentComplete = (double)fileTransferProgressCallbackInfo.BytesTransferred / (double)fileTransferProgressCallbackInfo.TotalFileSizeBytes * 100;
            ViewModelLocator.Main.StatusBarText = $"Downloading file <{fileTransferProgressCallbackInfo.Filename}> ({System.Math.Ceiling.(percentComplete)}%)...";
        }
    };

    ViewModelLocator.Main.StatusBarText = $"Downloading file <{readFileOptions.Filename}> (creating buffer)...";

    var fileTransferRequest = App.Settings.PlatformInterface.GetTitleStorageInterface().ReadFile(readFileOptions, null, (ReadFileCallbackInfo readFileCallbackInfo) =>
    {
        Debug.WriteLine($"ReadFile {readFileCallbackInfo.ResultCode}");

        if (readFileCallbackInfo.ResultCode == Result.Success)
        {
            Debug.WriteLine($"Successfully downloaded {readFileCallbackInfo.Filename} to {App.Settings.CacheDirectory}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });

    if (fileTransferRequest == null)
    {
        Debug.WriteLine("Error downloading file: bad handle");
        ViewModelLocator.Main.StatusBarText = string.Empty;
    }
}
 
  • As explained in the documentation, TitleStorage.ReadFile reads files in chunks. In TitleStorage.ReadFileOptions we specify a chunk size, as well as a callback method for reading the data and another callback for reporting progress.
  • We’re using the Temp folder (by default C:\Users\<Windows_User>\AppData\Local\Temp\ or %Temp%) to store the file.
 
  1. Add a TitleStorageReadFileCommand.cs class to the Commands folder:

public class TitleStorageReadFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.TitleStorage.SelectedTitleStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        TitleStorageService.ReadFile(ViewModelLocator.TitleStorage
.SelectedTitleStorageFile);
    }
}

 
  1. Open TitleStorageViewModel.cs to declare and instantiate our new command:

public TitleStorageQueryFileCommand TitleStorageQueryFile { get; set; }
public TitleStorageQueryFileListCommand TitleStorageQueryFileList { get; set; }
public TitleStorageReadFileCommand TitleStorageReadFile { get; set; }

public TitleStorageViewModel()
{
    TitleStorageQueryFile = new TitleStorageQueryFileCommand();
    TitleStorageQueryFileList = new TitleStorageQueryFileListCommand();
    TitleStorageReadFile = new TitleStorageReadFileCommand();
}

 
  1. Lastly, open TitleStorageView.xaml.cs and add the following to the TitleStorageFilesListView_SelectionChanged() method:

ViewModel.TitleStorageReadFile.RaiseCanExecuteChanged();

Now when we run the app, authenticate, query for files, and download a file, the file will be on our disk in the %Temp% directory. In this case, we can rename “test_file” or “another_test” to add the .txt file extension to be able to open the file and see its contents.
 
App Title Storage Downloaded File
File downloaded from Title Storage

Get the code

Get the code for this article below. Follow the Usage instructions in the GitHub repo to set up the downloaded code.
We’ve talked about the differences between Title Storage and Player Data Storage, so in the next article we’ll implement storing and retrieving player-specific data using Player Data Storage.

The full list of articles in this series can be found in the series reference. For feedback or questions, head over to the Community forum.

    We succeed when you succeed

    Epic believes in an open, integrated games community. By offering our online services to everyone for free, we aim to empower more developers to serve their own player communities.