diff --git a/Facepunch.Steamworks.Test/Client/RemoteStorage.cs b/Facepunch.Steamworks.Test/Client/RemoteStorage.cs new file mode 100644 index 0000000..11e209c --- /dev/null +++ b/Facepunch.Steamworks.Test/Client/RemoteStorage.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Facepunch.Steamworks.Test +{ + [TestClass] + [DeploymentItem( "steam_api.dll" )] + [DeploymentItem( "steam_api64.dll" )] + [DeploymentItem( "steam_appid.txt" )] + public class RemoteStorage + { + [TestMethod] + public void GetQuota() + { + using ( var client = new Steamworks.Client( 252490 ) ) + { + ulong total, available; + client.RemoteStorage.GetQuota( out total, out available ); + + Console.WriteLine( $"Total quota: {total} bytes" ); + Console.WriteLine( $"Available: {available} bytes" ); + } + } + + [TestMethod] + public void WriteFile() + { + using ( var client = new Steamworks.Client( 252490 ) ) + { + var file = client.RemoteStorage.CreateFile( "test.txt" ); + + const string text = "Hello world!"; + + file.WriteAllText( text ); + + Assert.IsTrue( file.Exists ); + + var read = file.ReadAllText(); + Assert.AreEqual( text, read ); + } + } + + [TestMethod] + public void WriteFiles() + { + using ( var client = new Steamworks.Client( 252490 ) ) + { + for ( var i = 0; i < 10; ++i ) + { + client.RemoteStorage + .CreateFile( $"test_{i}/example.txt" ) + .WriteAllText( Guid.NewGuid().ToString() ); + } + + Console.WriteLine( $"File count: {client.RemoteStorage.FileCount}" ); + + foreach ( var file in client.RemoteStorage.Files ) + { + Console.WriteLine( $"- {file.FileName} ({file.SizeInBytes} bytes)" ); + } + } + } + } +} diff --git a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj index c1f53dd..82d2554 100644 --- a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj +++ b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj @@ -96,6 +96,7 @@ + diff --git a/Facepunch.Steamworks/Client/RemoteStorage.cs b/Facepunch.Steamworks/Client/RemoteStorage.cs new file mode 100644 index 0000000..ca762d7 --- /dev/null +++ b/Facepunch.Steamworks/Client/RemoteStorage.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using SteamNative; + +namespace Facepunch.Steamworks +{ + partial class Client + { + RemoteStorage _remoteStorage; + + public RemoteStorage RemoteStorage + { + get + { + if ( _remoteStorage == null ) + _remoteStorage = new RemoteStorage( this ); + + return _remoteStorage; + } + } + } + + internal class RemoteFileWriteStream : Stream + { + internal readonly RemoteStorage remoteStorage; + + private readonly UGCFileWriteStreamHandle_t _handle; + private readonly RemoteFile _file; + + private int _written; + private bool _closed; + + internal RemoteFileWriteStream( RemoteStorage r, RemoteFile file ) + { + remoteStorage = r; + + _handle = remoteStorage.native.FileWriteStreamOpen( file.FileName ); + _file = file; + } + + public override void Flush() { } + + public override int Read( byte[] buffer, int offset, int count ) + { + throw new NotImplementedException(); + } + + public override long Seek( long offset, SeekOrigin origin ) + { + throw new NotImplementedException(); + } + + public override void SetLength( long value ) + { + throw new NotImplementedException(); + } + + public override unsafe void Write( byte[] buffer, int offset, int count ) + { + if ( _closed ) throw new ObjectDisposedException( ToString() ); + + fixed ( byte* bufferPtr = buffer ) + { + if ( remoteStorage.native.FileWriteStreamWriteChunk( _handle, (IntPtr) (bufferPtr + offset), count ) ) + { + _written += count; + } + } + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => _written; + public override long Position { get { return _written; } set { throw new NotImplementedException(); } } + + public void Cancel() + { + if ( _closed ) return; + + _closed = true; + remoteStorage.native.FileWriteStreamCancel( _handle ); + } + + public void Close() + { + if ( _closed ) return; + + _closed = true; + remoteStorage.native.FileWriteStreamClose( _handle ); + + _file.remoteStorage.OnWrittenNewFile( _file ); + } + + protected override void Dispose( bool disposing ) + { + if ( disposing ) Close(); + base.Dispose( disposing ); + } + } + + public class RemoteFile + { + internal readonly RemoteStorage remoteStorage; + + private readonly bool _isUgc; + private string _fileName; + private int _sizeInBytes = -1; + private UGCHandle_t _handle; + private ulong _ownerId; + + public bool Exists { get; internal set; } + + public string FileName + { + get + { + if ( _fileName != null ) return _fileName; + GetUGCDetails(); + return _fileName; + } + } + + public ulong OwnerId + { + get + { + if ( _ownerId != 0 ) return _ownerId; + GetUGCDetails(); + return _ownerId; + } + } + + public int SizeInBytes + { + get + { + if ( _sizeInBytes != -1 ) return _sizeInBytes; + if ( _isUgc ) throw new NotImplementedException(); + _sizeInBytes = remoteStorage.native.GetFileSize( FileName ); + return _sizeInBytes; + } + internal set { _sizeInBytes = value; } + } + + internal RemoteFile( RemoteStorage r, UGCHandle_t handle ) + { + Exists = true; + + remoteStorage = r; + + _isUgc = true; + _handle = handle; + } + + internal RemoteFile( RemoteStorage r, string name, ulong ownerId, int sizeInBytes = -1 ) + { + remoteStorage = r; + + _isUgc = false; + _fileName = name; + _ownerId = ownerId; + _sizeInBytes = sizeInBytes; + } + + public Stream OpenWrite() + { + return new RemoteFileWriteStream( remoteStorage, this ); + } + + public void WriteAllBytes( byte[] buffer ) + { + using ( var stream = OpenWrite() ) + { + stream.Write( buffer, 0, buffer.Length ); + } + } + + public void WriteAllText( string text, Encoding encoding = null ) + { + if ( encoding == null ) encoding = Encoding.UTF8; + WriteAllBytes( encoding.GetBytes( text ) ); + } + + public Stream OpenRead() + { + return new MemoryStream( ReadAllBytes(), false ); + } + + public unsafe byte[] ReadAllBytes() + { + if ( _isUgc ) + { + // Need to download + throw new NotImplementedException(); + } + + var size = SizeInBytes; + var buffer = new byte[size]; + + fixed ( byte* bufferPtr = buffer ) + { + remoteStorage.native.FileRead( FileName, (IntPtr) bufferPtr, size ); + } + + return buffer; + } + + public string ReadAllText( Encoding encoding = null ) + { + if ( encoding == null ) encoding = Encoding.UTF8; + return encoding.GetString( ReadAllBytes() ); + } + + public delegate void ShareCallback( bool success ); + public bool Share( ShareCallback callback = null ) + { + if ( _isUgc ) return false; + + // Already shared + if ( _handle.Value != 0 ) return false; + + remoteStorage.native.FileShare( FileName, ( result, error ) => + { + var success = !error && result.Result == Result.OK; + if ( success ) + { + _handle.Value = result.File; + } + + callback?.Invoke( success ); + } ); + + return true; + } + + public bool Delete() + { + if ( !Exists ) return false; + if ( _isUgc ) return false; + if ( !remoteStorage.native.FileDelete( FileName ) ) return false; + + Exists = false; + remoteStorage.InvalidateFiles(); + + return true; + } + + private void GetUGCDetails() + { + if ( !_isUgc ) throw new InvalidOperationException(); + + var appId = new AppId_t { Value = remoteStorage.native.steamworks.AppId }; + + CSteamID ownerId; + remoteStorage.native.GetUGCDetails( _handle, ref appId, out _fileName, out ownerId ); + + _ownerId = ownerId.Value; + } + } + + /// + /// Handles Steam Cloud related actions. + /// + public class RemoteStorage + { + private static string NormalizePath( string path ) + { + return new FileInfo( $"x:/{path}" ).FullName.Substring( 3 ); + } + + internal readonly Client client; + internal readonly SteamNative.SteamRemoteStorage native; + + private bool _filesInvalid = true; + private readonly List _files = new List(); + + internal RemoteStorage( Client c ) + { + client = c; + native = client.native.remoteStorage; + } + + /// + /// True if Steam Cloud is currently enabled by the current user. + /// + public bool IsCloudEnabledForAccount + { + get { return native.IsCloudEnabledForAccount(); } + } + + /// + /// True if Steam Cloud is currently enabled for this app by the current user. + /// + public bool IsCloudEnabledForApp + { + get { return native.IsCloudEnabledForApp(); } + } + + public int FileCount + { + get { return native.GetFileCount(); } + } + + public IEnumerable Files + { + get + { + UpdateFiles(); + return _files; + } + } + + public RemoteFile CreateFile( string path ) + { + path = NormalizePath( path ); + + InvalidateFiles(); + var existing = Files.FirstOrDefault( x => x.FileName == path ); + return existing ?? new RemoteFile( this, path, client.SteamId, 0 ); + } + + internal void OnWrittenNewFile( RemoteFile file ) + { + if ( _files.Any( x => x.FileName == file.FileName ) ) return; + + _files.Add( file ); + file.Exists = true; + + InvalidateFiles(); + } + + internal void InvalidateFiles() + { + _filesInvalid = true; + } + + private void UpdateFiles() + { + if ( !_filesInvalid ) return; + _filesInvalid = false; + + foreach ( var file in _files ) + { + file.Exists = false; + } + + var count = FileCount; + for ( var i = 0; i < count; ++i ) + { + int size; + var name = NormalizePath( GetFileNameAndSize( i, out size ) ); + + var existing = _files.FirstOrDefault( x => x.FileName == name ); + if ( existing == null ) + { + existing = new RemoteFile( this, name, client.SteamId, size ); + _files.Add( existing ); + } + else + { + existing.SizeInBytes = size; + } + + existing.Exists = true; + } + + for ( var i = _files.Count - 1; i >= 0; --i ) + { + if ( !_files[i].Exists ) _files.RemoveAt( i ); + } + } + + public bool FileExists( string path ) + { + return native.FileExists( path ); + } + + /// + /// Gets both the total and available remote storage in bytes for this user and app. + /// + /// True if successful + public unsafe bool GetQuota( out ulong totalBytes, out ulong availableBytes ) + { + fixed ( ulong* totalPtr = &totalBytes) + fixed ( ulong* availablePtr = &availableBytes ) + { + return native.GetQuota( (IntPtr) totalPtr, (IntPtr) availablePtr ); + } + } + + private unsafe string GetFileNameAndSize( int file, out int size ) + { + fixed ( int* sizePtr = &size ) + { + return native.GetFileNameAndSize( file, (IntPtr) sizePtr ); + } + } + } +}