Storing and retrieving player-specific data

Rajen Kishna, Technical Account Manager, Epic Games |
December 7, 2021
In the previous article in our EOS getting started series, we covered retrieving game-specific data from the cloud using Title Storage. We covered the differences between Title Storage and Player Data Storage in that post, so in this one we’ll use the latter to save and retrieve player data to and from the cloud. It’s important to know that Player Data Storage is player-authoritative, so you should not use it in situations where tampering could lead to cheating (for example inventory management). In this article, we’ll go over:
 

Player Data Storage vs. Epic Games Store Cloud Saves

Before we dive into Player Data Storage, it’s worth calling out that the Epic Games Store (EGS) has a related feature called Cloud Saves. EGS Cloud Saves don’t require API implementation, as the feature is configured for the Epic Games Store directly and managed via the Epic Games Launcher.

While both implementations can be used to store player save data in the cloud, Player Data Storage is more flexible as it can be used to store/retrieve any files on any platform, regardless of which store the game is distributed through. You can use Player Data Storage to implement cross-save functionality for all your players. You can read more about the differences in the documentation.

Changing our Client Policy

As with all services, we have to add the appropriate actions to our Client Policy:
  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
  4. Scroll down to Features and click on the toggle button next to Player Data Storage.
  5. Tick the boxes next to the “access”, “delete”, and “copy” actions, as we’ll implement all these functionalities.
  6. Click Save & Exit to confirm.
Developer Portal Client Policy Player Data Storage
Player Data Storage Client Policy allowed features and actions

Querying files and getting file metadata

If you haven’t read the previous article on Title Storage, please read through the “EncryptionKey and CacheDirectory additions to SDK initialization” section to learn about how files are encrypted and cached. Once you’ve done that, we can start by implementing querying files:
 
  1. Create a new User Control in the Views folder called PlayerDataStorageView:

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

    <StackPanel Grid.Column="1">
        <Button Width="100" Height="23" Margin="2" Content="Query files" Command="{Binding PlayerDataStorageQueryFileList}" />
        <Button Width="100" Height="23" Margin="2" Content="Upload file" Command="{Binding PlayerDataStorageWriteFile}" />
        <StackPanel Orientation="Horizontal">
            <Button Width="100" Height="23" Margin="2" Content="Download file" Command="{Binding PlayerDataStorageReadFile}" />
            <Button Width="100" Height="23" Margin="2" Content="Duplicate file" Command="{Binding PlayerDataStorageDuplicateFile}" />
        </StackPanel>
        <Button Width="100" Height="23" Margin="2" Content="Delete file" Command="{Binding PlayerDataStorageDeleteFile}" />
    </StackPanel>

    <ListView x:Name="PlayerDataStorageFilesListView" Grid.Column="0" Margin="2" ItemsSource="{Binding PlayerDataStorageFiles}" SelectedItem="{Binding SelectedPlayerDataStorageFile}" SelectionChanged="PlayerDataStorageFilesListView_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="LastModifiedTime" Width="175" DisplayMemberBinding="{Binding LastModifiedTime}">
                    <GridViewColumn.HeaderContainerStyle>
                        <Style TargetType="{x:Type GridViewColumnHeader}">
                            <Setter Property="HorizontalContentAlignment" Value="Left" />
                        </Style>
                    </GridViewColumn.HeaderContainerStyle>
                </GridViewColumn>
                <GridViewColumn Header="FileSizeBytes" Width="75" 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>

 
  • Here we have a straightforward UI with a ListView to display our files and buttons to query, upload, download, duplicate, and delete files.
 
  1. Open PlayerDataStorageView.xaml.cs and set up the DataContext and placeholder event handler:

public partial class PlayerDataStorageView : UserControl
{
    public PlayerDataStorageViewModel ViewModel { get { return ViewModelLocator.PlayerDataStorage; } }

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

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

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

public class PlayerDataStorageViewModel : BindableBase
{
    private ObservableCollection<FileMetadata> _playerDataStorageFiles;
    public ObservableCollection<FileMetadata> PlayerDataStorageFiles
    {
        get { return _playerDataStorageFiles; }
        set { SetProperty(ref _playerDataStorageFiles, value); }
    }

    private FileMetadata _selectedPlayerDataStorageFile;
    public FileMetadata SelectedPlayerDataStorageFile
    {
        get { return _selectedPlayerDataStorageFile; }
        set { SetProperty(ref _selectedPlayerDataStorageFile, value); }
    }
}

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

private static PlayerDataStorageViewModel _playerDataStorage;
public static PlayerDataStorageViewModel PlayerDataStorage
{
    get { return _playerDataStorage ??= new PlayerDataStorageViewModel(); }
}

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

public static class PlayerDataStorageService
{
    public static void QueryFileList()
    {
        var queryFileListOptions = new QueryFileListOptions()
        {
            LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId)
        };

        ViewModelLocator.Main.StatusBarText = "Querying player data storage file list...";

        App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.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.GetPlayerDataStorageInterface()
.CopyFileMetadataAtIndex(copyFileMetadataAtIndexOptions, out var metadata);

                    if (result == Result.Success)
                    {
                        ViewModelLocator.PlayerDataStorage.PlayerDataStorageFiles
.Add(metadata);
                    }
                }
            }

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

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

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

    public override void Execute(object parameter)
    {
        ViewModelLocator.PlayerDataStorage.PlayerDataStorageFiles = new ObservableCollection<FileMetadata>();
        PlayerDataStorageService.QueryFileList();
    }
}

 
  1. Open PlayerDataStorageViewModel.cs to declare and instantiate the command:

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
}

 
  1. Add the following line to the RaiseConnectCanExecuteChanged() method in ViewModelLocator.cs to ensure we can only query after successfully logging in through the Connect Interface:

PlayerDataStorage.PlayerDataStorageQueryFileList.RaiseCanExecuteChanged();
 
  1. Lastly, add the PlayerDataStorageView to our TabControl in MainWindow.xaml:

<TabItem x:Name="PlayerDataStorage" Header="Player Data Storage">
    <views:PlayerDataStorageView />
</TabItem>


Now when we run the app and query for Player Data Storage files after authentication, we see the following in the log:

[Warning] LogEOSPlayerDataStorage - Querying file failed, got 0 results.
QueryFileList NotFound


This is expected, as we haven’t uploaded any files yet, so let’s do that next.

Uploading files

Your logic for uploading files might be different, but the following should give you a general sense of how you can implement this API.
 
  1. Add the following method to PlayerDataStorageService.cs:

public static void WriteFile(OpenFileDialog openFileDialog)
{
    var bytesWritten = 0;

    var writeFileOptions = new WriteFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = openFileDialog.SafeFileName,
        ChunkLengthBytes = 10485760,
        WriteFileDataCallback = (WriteFileDataCallbackInfo writeFileDataCallbackInfo, out byte[] buffer) =>
        {
            using var fs = new FileStream($"{openFileDialog.FileName}", FileMode.Open, FileAccess.Read);
            if (fs.Length > bytesWritten)
            {
                var readBytes = new byte[System.Math.Min(writeFileDataCallbackInfo.DataBufferLengthBytes, fs.Length)];
                fs.Seek(bytesWritten, SeekOrigin.Begin);
                bytesWritten += fs.Read(readBytes, 0, (int)System.Math.Min(writeFileDataCallbackInfo
.DataBufferLengthBytes, fs.Length));
                buffer = readBytes;
            }
            else
            {
                buffer = new byte[0];
                return WriteResult.CompleteRequest;
            }
            return WriteResult.ContinueWriting;
        },
        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 = $"Uploading file <{writeFileOptions.Filename}> (creating buffer)...";

    var fileTransferRequest = App.Settings.PlatformInterface
.GetPlayerDataStorageInterface().WriteFile(writeFileOptions, null, (WriteFileCallbackInfo writeFileCallbackInfo) =>
    {
        Debug.WriteLine($"WriteFile {writeFileCallbackInfo.ResultCode}");

        if (writeFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            Debug.WriteLine($"Successfully uploaded {writeFileCallbackInfo.Filename}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
        else
        {
            Debug.WriteLine($"Error uploading {writeFileCallbackInfo.Filename}: {writeFileCallbackInfo.ResultCode}.");
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }    
    });

    if (fileTransferRequest == null)
    {
        Debug.WriteLine("Error uploading file: bad handle");
        ViewModelLocator.Main.StatusBarText = string.Empty;
    }
}

 
  • This pattern is similar to what we implemented to read files from Title Storage: we initialize WriteFileOptions, which will hold our logic of uploading and reporting progress of our files in chunks.
  • We use PlayerDataStorage.WriteFile to initiate the actual upload and we watch for any result other than Success, which could indicate hitting the Usage limitations described below.
 
  1. Add a PlayerDataStorageWriteFileCommand.cs class to the Commands folder:

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

    public override void Execute(object parameter)
    {
        OpenFileDialog openFileDialog = new OpenFileDialog();
        if (openFileDialog.ShowDialog() == true)
        {
            PlayerDataStorageService.WriteFile(openFileDialog);
        }
    }
}

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

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
}

 
  1. Lastly, open ViewModelLocator.cs and add the following to the RaiseConnectCanExecuteChanged() method:

PlayerDataStorage.PlayerDataStorageWriteFile.RaiseCanExecuteChanged();

Now we can run the app and upload a file (considering the Usage limitations listed further below). Once uploaded, our code triggers a query and the new file will be shown in the ListView.
 
App Player Data Storage Added Files
Uploaded files in Player Data Storage

We can also go into the Developer Portal and navigate to our product > Game Services > Player Data Storage and look up the player by copying the PUID from our app’s UI. Here we’ll see the list of files for this user, as well as the current and maximum consumption.
 
Developer Portal Player Data Storage Added Files
Uploaded files in Epic Games Developer Portal

Reading files

The next step is going to be reading the files from the cloud, similar to what we’ve done with Title Storage. The logic here is largely the same:
 
  1. Add the following method to PlayerDataStorageService.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.ContinueReading;
        },
        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
.GetPlayerDataStorageInterface().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;
    }
}

 
  • We initialize ReadFileOptions, which will hold our logic of downloading and reporting progress of our files in chunks.
  • We use PlayerDataStorage.ReadFile to initiate the download to the configured CacheDirectory.
 
  1. Add a PlayerDataStorageReadFileCommand.cs class to the Commands folder:

public class PlayerDataStorageReadFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.ReadFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

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

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
}

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

ViewModel.PlayerDataStorageReadFile.RaiseCanExecuteChanged();

After you query for files now, you’ll be able to select one of the files in the ListView and click on the Download file button to get the file from the cloud. You should see output in the Debug window that indicates a download completed:

ReadFile Success
Successfully downloaded test_file.txt to C:\Users\<User>\AppData\Local\Temp\.

Duplicating files

At times, you’ll want to duplicate a file in the player’s storage space without downloading and uploading. This is where the duplication API comes in handy:
 
  1. Add the following method to PlayerDataStorageService.cs:

public static void DuplicateFile(FileMetadata fileMetadata)
{
    var duplicateFileOptions = new DuplicateFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        SourceFilename = fileMetadata.Filename,
        DestinationFilename = $"{fileMetadata.Filename}_(copy)"
    };

    ViewModelLocator.Main.StatusBarText = $"Copying <{duplicateFileOptions.SourceFilename}> as <{duplicateFileOptions.DestinationFilename}>...";

    App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.DuplicateFile(duplicateFileOptions, null, (DuplicateFileCallbackInfo duplicateFileCallbackInfo) =>
    {
        Debug.WriteLine($"DuplicateFile {duplicateFileCallbackInfo.ResultCode}");

        if (duplicateFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            ViewModelLocator.Main.StatusBarText = "Successfully copied file.";
        }
        else
        {
            Debug.WriteLine("Copying file failed: " + duplicateFileCallbackInfo.ResultCode);
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });
}

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

public class PlayerDataStorageDuplicateFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.DuplicateFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

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

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }
public PlayerDataStorageDuplicateFileCommand PlayerDataStorageDuplicateFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
    PlayerDataStorageDuplicateFile = new PlayerDataStorageDuplicateFileCommand();
}

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

ViewModel.PlayerDataStorageDuplicateFile.RaiseCanExecuteChanged();

When we select a file in the ListView and hit the Duplicate file button, we’ll see a new file show up with the same name, appended with “_(copy)”.
 
App Player Data Storage Duplicated File
Duplicated file in Player Data Storage cloud storage

Deleting files

The last thing we’ll implement is the ability to delete files:
 
  1. Add the following method to PlayerDataStorageService.cs:

public static void DeleteFile(FileMetadata fileMetadata)
{
    var deleteFileOptions = new DeleteFileOptions()
    {
        LocalUserId = ProductUserId.FromString(ViewModelLocator.Main.ProductUserId),
        Filename = fileMetadata.Filename
    };

    ViewModelLocator.Main.StatusBarText = $"Deleting <{deleteFileOptions.Filename}>...";

    App.Settings.PlatformInterface.GetPlayerDataStorageInterface()
.DeleteFile(deleteFileOptions, null, (DeleteFileCallbackInfo deleteFileCallbackInfo) =>
    {
        Debug.WriteLine($"DeleteFile {deleteFileCallbackInfo.ResultCode}");

        if (deleteFileCallbackInfo.ResultCode == Result.Success)
        {
            ViewModelLocator.PlayerDataStorage
.PlayerDataStorageQueryFileList.Execute(null);
            ViewModelLocator.Main.StatusBarText = "Successfully deleted file.";
        }
        else
        {
            Debug.WriteLine("Deleting file failed: " + deleteFileCallbackInfo.ResultCode);
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    });
}

 
  • The PlayerDataStorage.DeleteFile API is pretty straightforward, taking a filename as input after which the file will be deleted permanently.
 
  1. Add a PlayerDataStorageDeleteFileCommand.cs class to the Commands folder:

public class PlayerDataStorageDeleteFileCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return ViewModelLocator.PlayerDataStorage.SelectedPlayerDataStorageFile != null;
    }

    public override void Execute(object parameter)
    {
        PlayerDataStorageService.DeleteFile(ViewModelLocator
.PlayerDataStorage.SelectedPlayerDataStorageFile);
    }
}

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

public PlayerDataStorageQueryFileListCommand PlayerDataStorageQueryFileList { get; set; }
public PlayerDataStorageWriteFileCommand PlayerDataStorageWriteFile { get; set; }
public PlayerDataStorageReadFileCommand PlayerDataStorageReadFile { get; set; }
public PlayerDataStorageDuplicateFileCommand PlayerDataStorageDuplicateFile { get; set; }
public PlayerDataStorageDeleteFileCommand PlayerDataStorageDeleteFile { get; set; }

public PlayerDataStorageViewModel()
{
    PlayerDataStorageQueryFileList = new PlayerDataStorageQueryFileListCommand();
    PlayerDataStorageWriteFile = new PlayerDataStorageWriteFileCommand();
    PlayerDataStorageReadFile = new PlayerDataStorageReadFileCommand();
    PlayerDataStorageDuplicateFile = new PlayerDataStorageDuplicateFileCommand();
    PlayerDataStorageDeleteFile = new PlayerDataStorageDeleteFileCommand();
}

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

ViewModel.PlayerDataStorageDeleteFile.RaiseCanExecuteChanged();

Now we can delete files using the Delete file button after selecting a file in the ListView.

Usage limitations

Player Data Storage has some usage limitations in place to ensure reliability and availability for all users. At the time of writing, these are the limitations (be sure to check the documentation for the most up-to-date information):
  • 1000 read or write requests per minute
  • 200 MB maximum individual file size
  • 400 MB total storage space per user
  • 1000 files maximum per user

If a file is attempted to be uploaded that exceeds the 200 MB max individual file size limit, the PlayerDataStorage.WriteFile call will fail, returning a PlayerDataStorageFileSizeTooLarge result. Once the 400 MB total storage space for the user is exceeded, additional calls to PlayerDataStorage.WriteFile will fail, returning a PlayerDataStorageUserThrottled result, indicating a throttled state for that user. The storage space for this user will remain in a throttled state until enough files are removed to bring the storage space utilization under 400 MB. To do so, you can still query for files, retrieve file metadata, and delete files while in a throttled state.

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.