From 524c687b83cebd604f29b655e12429a32b60abaf Mon Sep 17 00:00:00 2001 From: James King Date: Mon, 7 Aug 2017 17:15:20 +0100 Subject: [PATCH] Added some workshop API members for Chunks Currently OnItemCreated doesn't seem to get called --- Facepunch.Steamworks.Test/Client/Workshop.cs | 2 +- Facepunch.Steamworks/Client/RemoteStorage.cs | 16 ++++ .../Interfaces/Workshop.Editor.cs | 95 +++++++++++++------ .../Interfaces/Workshop.Item.cs | 94 ++++++++++++------ .../Interfaces/Workshop.Query.cs | 54 +++++++++-- Facepunch.Steamworks/Interfaces/Workshop.cs | 53 +++++++++++ Facepunch.Steamworks/Utility.cs | 13 +++ 7 files changed, 256 insertions(+), 71 deletions(-) diff --git a/Facepunch.Steamworks.Test/Client/Workshop.cs b/Facepunch.Steamworks.Test/Client/Workshop.cs index 6107a0c..f01dac4 100644 --- a/Facepunch.Steamworks.Test/Client/Workshop.cs +++ b/Facepunch.Steamworks.Test/Client/Workshop.cs @@ -254,7 +254,7 @@ namespace Facepunch.Steamworks.Test { var gotCallback = false; - Query.OnResult = ( q ) => + Query.OnResult += ( q ) => { Assert.AreEqual( q.Items.Length, 1 ); Console.WriteLine( "Query.TotalResults: {0}", q.TotalResults ); diff --git a/Facepunch.Steamworks/Client/RemoteStorage.cs b/Facepunch.Steamworks/Client/RemoteStorage.cs index c33afc5..8b11d7c 100644 --- a/Facepunch.Steamworks/Client/RemoteStorage.cs +++ b/Facepunch.Steamworks/Client/RemoteStorage.cs @@ -27,8 +27,24 @@ namespace Facepunch.Steamworks { client = c; native = client.native.remoteStorage; + + RemoteStoragePublishedFileSubscribed_t.RegisterCallback( c, onRemoteStoragePublishedFileSubscribed ); + RemoteStoragePublishedFileUnsubscribed_t.RegisterCallback( c, onRemoteStoragePublishedFileUnsubscribed ); } + private void onRemoteStoragePublishedFileSubscribed( RemoteStoragePublishedFileSubscribed_t value, bool ioFailure ) + { + if ( ItemSubscribed != null && value.AppID == client.AppId ) ItemSubscribed( value.PublishedFileId ); + } + + private void onRemoteStoragePublishedFileUnsubscribed( RemoteStoragePublishedFileUnsubscribed_t value, bool ioFailure ) + { + if ( ItemUnsubscribed != null && value.AppID == client.AppId ) ItemUnsubscribed( value.PublishedFileId ); + } + + public event Action ItemSubscribed; + public event Action ItemUnsubscribed; + /// /// True if Steam Cloud is currently enabled by the current user. /// diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs index 761cb64..213d2e3 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs @@ -20,6 +20,7 @@ namespace Facepunch.Steamworks public string Folder { get; set; } = null; public string PreviewImage { get; set; } = null; public List Tags { get; set; } = new List(); + public Dictionary KeyValueTags { get; set; } = new Dictionary(); public bool Publishing { get; internal set; } public ItemType? Type { get; set; } public string Error { get; internal set; } = null; @@ -36,60 +37,63 @@ namespace Facepunch.Steamworks public bool NeedToAgreeToWorkshopLegal { get; internal set; } + public event Action PublishSucceeded; + public event Action PublishFailed; + private PublishStatus _publishStatus; + public PublishStatus PublishStatus + { + get + { + UpdatePublishProgress(); + return _publishStatus; + } + } public double Progress { get { - if ( !Publishing ) return 1.0; - if ( CreateItem != null ) return 0.0; - if ( SubmitItemUpdate == null ) return 1.0; + if ( CreateItem != null ) return 0d; + if ( SubmitItemUpdate == null ) return 1d; + if ( !Publishing ) return 1d; - ulong b = 0; - ulong t = 0; - - workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out b, out t ); - - if ( t == 0 ) - return 0; - - return (double)b / (double) t; + UpdatePublishProgress(); + return _bytesTotal > 0 ? _bytesUploaded / (double) _bytesTotal : 0d; } } + private ulong _bytesUploaded; public int BytesUploaded { get { - if ( !Publishing ) return 0; - if ( CreateItem != null ) return 0; - if ( SubmitItemUpdate == null ) return 0; - - ulong b = 0; - ulong t = 0; - - workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out b, out t ); - return (int) b; + UpdatePublishProgress(); + return (int) _bytesUploaded; } } + private ulong _bytesTotal; public int BytesTotal { get { - if ( !Publishing ) return 0; - if ( CreateItem != null ) return 0; - if ( SubmitItemUpdate == null ) return 0; - - ulong b = 0; - ulong t = 0; - - workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out b, out t ); - return (int)t; + UpdatePublishProgress(); + return (int) _bytesTotal; } } + private void UpdatePublishProgress() + { + if ( SubmitItemUpdate == null ) + { + _publishStatus = PublishStatus.Invalid; + return; + } + + _publishStatus = (PublishStatus) workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out _bytesUploaded, out _bytesTotal ); + } + public void Publish() { Publishing = true; @@ -109,13 +113,17 @@ namespace Facepunch.Steamworks if ( !Type.HasValue ) throw new System.Exception( "Editor.Type must be set when creating a new item!" ); + System.Diagnostics.Debug.WriteLine( "StartCreatingItem()" ); CreateItem = workshop.ugc.CreateItem( workshop.steamworks.AppId, (SteamNative.WorkshopFileType)(uint)Type, OnItemCreated ); } private void OnItemCreated( SteamNative.CreateItemResult_t obj, bool Failed ) { + System.Diagnostics.Debug.WriteLine( $"OnItemCreated({obj.PublishedFileId}, {Failed})" ); + NeedToAgreeToWorkshopLegal = obj.UserNeedsToAcceptWorkshopLegalAgreement; CreateItem.Dispose(); + CreateItem = null; if ( obj.Result == SteamNative.Result.OK && !Failed ) { @@ -126,6 +134,8 @@ namespace Facepunch.Steamworks Error = "Error creating new file: " + obj.Result.ToString() + "("+ obj.PublishedFileId+ ")"; Publishing = false; + + PublishFailed?.Invoke( this ); } private void PublishChanges() @@ -151,6 +161,14 @@ namespace Facepunch.Steamworks if ( Tags != null && Tags.Count > 0 ) workshop.ugc.SetItemTags( UpdateHandle, Tags.ToArray() ); + if ( KeyValueTags != null ) + { + foreach ( var keyValue in KeyValueTags ) + { + workshop.ugc.AddItemKeyValueTag( UpdateHandle, keyValue.Key, keyValue.Value ); + } + } + if ( Visibility.HasValue ) workshop.ugc.SetItemVisibility( UpdateHandle, (SteamNative.RemoteStoragePublishedFileVisibility)(uint)Visibility.Value ); @@ -185,7 +203,10 @@ namespace Facepunch.Steamworks private void OnChangesSubmitted( SteamNative.SubmitItemUpdateResult_t obj, bool Failed ) { if ( Failed ) - throw new System.Exception( "CreateItemResult_t Failed" ); + { + if ( PublishFailed != null ) PublishFailed( this ); + else throw new System.Exception( "CreateItemResult_t Failed" ); + } SubmitItemUpdate = null; NeedToAgreeToWorkshopLegal = obj.UserNeedsToAcceptWorkshopLegalAgreement; @@ -193,10 +214,12 @@ namespace Facepunch.Steamworks if ( obj.Result == SteamNative.Result.OK ) { + PublishSucceeded?.Invoke( this ); return; } Error = "Error publishing changes: " + obj.Result.ToString() + " ("+ NeedToAgreeToWorkshopLegal + ")"; + PublishFailed?.Invoke( this ); } public void Delete() @@ -205,5 +228,15 @@ namespace Facepunch.Steamworks Id = 0; } } + + public enum PublishStatus : int + { + Invalid = 0, + PreparingConfig = 1, + PreparingContent = 2, + UploadingContent = 3, + UploadingPreviewFile = 4, + CommittingChanges = 5, + } } } diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Item.cs b/Facepunch.Steamworks/Interfaces/Workshop.Item.cs index 21d943d..b61d30b 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.Item.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.Item.cs @@ -48,19 +48,20 @@ namespace Facepunch.Steamworks return item; } - public void Download( bool highPriority = true ) + public bool Download( bool highPriority = true ) { - if ( Installed ) return; - if ( Downloading ) return; + if ( Installed ) return false; + if ( Downloading ) return false; if ( !workshop.ugc.DownloadItem( Id, highPriority ) ) { - Console.WriteLine( "Download Failed" ); - return; + return false; } workshop.OnFileDownloaded += OnFileDownloaded; workshop.OnItemInstalled += OnItemInstalled; + + return true; } public void Subscribe() @@ -109,8 +110,7 @@ namespace Facepunch.Steamworks public bool Subscribed { get { return ( State & ItemState.Subscribed ) != 0; } } public bool NeedsUpdate { get { return ( State & ItemState.NeedsUpdate ) != 0; } } - private SteamNative.ItemState State { get { return ( SteamNative.ItemState) workshop.ugc.GetItemState( Id ); } } - + public ItemState State { get { return (ItemState) workshop.ugc.GetItemState( Id ); } } private DirectoryInfo _directory; @@ -118,33 +118,55 @@ namespace Facepunch.Steamworks { get { - if ( _directory != null ) - return _directory; - - if ( !Installed ) - return null; - - ulong sizeOnDisk; - string folder; - uint timestamp; - - if ( workshop.ugc.GetItemInstallInfo( Id, out sizeOnDisk, out folder, out timestamp ) ) - { - _directory = new DirectoryInfo( folder ); - Size = sizeOnDisk; - - if ( !_directory.Exists ) - { - // Size = 0; - // _directory = null; - } - } - + UpdateInstallInfo(); return _directory; } } - public ulong Size { get; private set; } + private ulong _size; + + public ulong Size + { + get + { + UpdateInstallInfo(); + return _size; + } + } + + private DateTime _timestamp; + + public DateTime Timestamp + { + get + { + UpdateInstallInfo(); + return _timestamp; + } + } + + internal void UpdateInstallInfo() + { + if ( _directory != null ) return; + if ( !Installed ) return; + + ulong sizeOnDisk; + string folder; + uint timestamp; + + if ( workshop.ugc.GetItemInstallInfo( Id, out sizeOnDisk, out folder, out timestamp ) ) + { + _directory = new DirectoryInfo( folder ); + _size = sizeOnDisk; + _timestamp = Utility.Epoch.ToDateTime( timestamp ); + + if ( !_directory.Exists ) + { + // Size = 0; + // _directory = null; + } + } + } private ulong _BytesDownloaded, _BytesTotal; @@ -227,5 +249,17 @@ namespace Facepunch.Steamworks } } } + + [Flags] + public enum ItemState + { + None = 0, + Subscribed = 1, + LegacyItem = 2, + Installed = 4, + NeedsUpdate = 8, + Downloading = 16, + DownloadPending = 32 + } } } diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Query.cs b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs index e847ee3..3b1dbd6 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.Query.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SteamNative; namespace Facepunch.Steamworks { @@ -44,9 +45,15 @@ namespace Facepunch.Steamworks public UserQueryType UserQueryType { get; set; } = UserQueryType.Published; /// - /// Called when the query finishes + /// Called when the query succeeds /// - public Action OnResult; + public event Action OnResult; + + /// + /// Called when the query fails. An exception will be thrown on IO failure + /// if no callbacks are added to this event. + /// + public event Action OnFailure; /// /// Page starts at 1 !! @@ -105,15 +112,27 @@ namespace Facepunch.Steamworks if ( !string.IsNullOrEmpty( SearchText ) ) workshop.ugc.SetSearchText( Handle, SearchText ); - foreach ( var tag in RequireTags ) - workshop.ugc.AddRequiredTag( Handle, tag ); + if ( RequireTags != null ) + { + foreach ( var tag in RequireTags ) + workshop.ugc.AddRequiredTag( Handle, tag ); - if ( RequireTags.Count > 0 ) - workshop.ugc.SetMatchAnyTag( Handle, !RequireAllTags ); + if ( RequireTags.Count > 0 ) + workshop.ugc.SetMatchAnyTag( Handle, !RequireAllTags ); + } + + if ( RequireKeyValueTags != null ) + { + foreach ( var keyValue in RequireKeyValueTags ) + workshop.ugc.AddRequiredKeyValueTag( Handle, keyValue.Key, keyValue.Value ); + } if ( RankedByTrendDays > 0 ) workshop.ugc.SetRankedByTrendDays( Handle, (uint) RankedByTrendDays ); + if ( MaxCachedAge > TimeSpan.Zero ) + workshop.ugc.SetAllowCachedResponse( Handle, (uint) MaxCachedAge.TotalSeconds ); + foreach ( var tag in ExcludeTags ) workshop.ugc.AddExcludedTag( Handle, tag ); @@ -122,8 +141,14 @@ namespace Facepunch.Steamworks void ResultCallback( SteamNative.SteamUGCQueryCompleted_t data, bool bFailed ) { - if ( bFailed ) - throw new System.Exception( "bFailed!" ); + if ( bFailed || OnFailure != null && data.Result != Result.OK ) + { + // This used to always throw (before OnFailure was added), so for backwards + // compatibility it will still throw if OnFailure isn't subscribed to. + + if ( OnFailure != null ) OnFailure( this, bFailed ? Callbacks.Result.IOFailure : (Callbacks.Result) data.Result ); + else if ( bFailed ) throw new System.Exception( "bFailed!" ); + } var gotFiles = 0; for ( int i = 0; i < data.NumResultsReturned; i++ ) @@ -177,6 +202,7 @@ namespace Facepunch.Steamworks else { Items = _results.ToArray(); + IsCachedData = data.CachedData; if ( OnResult != null ) { @@ -199,11 +225,18 @@ namespace Facepunch.Steamworks get { return Callback != null; } } + public bool IsCachedData { get; private set; } + /// /// Only return items with these tags /// public List RequireTags { get; set; } = new List(); + /// + /// Only return items with these key-value tags + /// + public Dictionary RequireKeyValueTags { get; set; } = new Dictionary(); + /// /// If true, return items that have all RequireTags /// If false, return items that have any tags in RequireTags @@ -220,7 +253,10 @@ namespace Facepunch.Steamworks /// public List FileId { get; set; } = new List(); - + /// + /// Maximum age of a previously cached item. Zero by default, so items are not cached. + /// + public TimeSpan MaxCachedAge { get; set; } = TimeSpan.Zero; /// /// Don't call this in production! diff --git a/Facepunch.Steamworks/Interfaces/Workshop.cs b/Facepunch.Steamworks/Interfaces/Workshop.cs index e35bc4b..e2423ca 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; using SteamNative; namespace Facepunch.Steamworks @@ -40,6 +43,12 @@ namespace Facepunch.Steamworks /// public event Action OnItemInstalled; + /// + /// Gets the total number of items the current user is subscribed to for + /// the game or application. Returns 0 if called from a game server. + /// + public int NumSubscribedItems => (int) ugc.GetNumSubscribedItems(); + internal Workshop( BaseSteamworks steamworks, SteamNative.SteamUGC ugc, SteamNative.SteamRemoteStorage remoteStorage ) { this.ugc = ugc; @@ -76,6 +85,50 @@ namespace Facepunch.Steamworks OnFileDownloaded( obj.PublishedFileId, (Callbacks.Result) obj.Result ); } + [ThreadStatic] + private static ulong[] _sItemBuffer; + + /// + /// Gets a list of all of the items the current user is subscribed to for the current game. The + /// IDs for each item are written into . + /// + /// + /// Destination array to write the item IDs to. Should typically be large enough to fit + /// . + /// + /// The number of subscribed workshop items that were populated into . + /// + public unsafe int GetSubscribedItems( ulong[] destIds ) + { + var count = Math.Min( destIds.Length, NumSubscribedItems ); + + fixed ( ulong* ptr = destIds ) + { + ugc.GetSubscribedItems( (PublishedFileId_t*) ptr, (uint) count ); + } + + return count; + } + + /// + /// Gets a list of all of the items the current user is subscribed to for the current game. The + /// IDs for each item are appended to . + /// + /// + /// Destination list to append the item IDs to. + /// + /// The number of subscribed workshop items that were populated into . + /// + public int GetSubscribedItems( List destIds ) + { + var items = Utility.EnsureBufferCapacity( ref _sItemBuffer, NumSubscribedItems ); + var count = GetSubscribedItems( items ); + + destIds.AddRange( items.Take( count ) ); + + return count; + } + /// /// Creates a query object, which is used to get a list of items. /// diff --git a/Facepunch.Steamworks/Utility.cs b/Facepunch.Steamworks/Utility.cs index e3cf995..186af20 100644 --- a/Facepunch.Steamworks/Utility.cs +++ b/Facepunch.Steamworks/Utility.cs @@ -15,6 +15,19 @@ namespace Facepunch.Steamworks ( ( x & 0xff000000 ) >> 24 ); } + static internal int NextPowerOf2( int x ) + { + var po2 = 1; + while ( po2 < x ) po2 <<= 1; + return po2; + } + + static internal T[] EnsureBufferCapacity( ref T[] buffer, int size ) + { + if ( buffer == null || buffer.Length < size ) buffer = new T[NextPowerOf2( size )]; + return buffer; + } + static internal class Epoch { private static readonly DateTime epoch = new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc );