diff --git a/Facepunch.Steamworks.Test/Client/Workshop.cs b/Facepunch.Steamworks.Test/Client/Workshop.cs index 2ced142..b26539c 100644 --- a/Facepunch.Steamworks.Test/Client/Workshop.cs +++ b/Facepunch.Steamworks.Test/Client/Workshop.cs @@ -38,7 +38,7 @@ public void Query() Console.WriteLine( "Searching" ); Query.Order = Workshop.Order.RankedByTextSearch; - Query.QueryType = Workshop.QueryType.Items_Mtx; + Query.QueryType = Workshop.QueryType.MicrotransactionItems; Query.SearchText = "shit"; Query.RequireTags.Add( "LongTShirt Skin" ); Query.Run(); @@ -237,5 +237,34 @@ public void DownloadFile() } } + [TestMethod] + [TestCategory( "Run Manually" )] + public void CreatePublish() + { + using ( var client = new Facepunch.Steamworks.Client( 252490 ) ) + { + Assert.IsTrue( client.IsValid ); + + var item = client.Workshop.CreateItem( Workshop.ItemType.Microtransaction ); + + item.Title = "Facepunch.Steamworks Unit test"; + + item.Publish(); + + while ( item.Publishing ) + { + client.Update(); + Thread.Sleep( 100 ); + } + + Assert.IsFalse( item.Publishing ); + Assert.AreNotEqual( 0, item.Id ); + + Console.WriteLine( "item.Id: {0}", item.Id ); + + item.Delete(); + } + } + } } diff --git a/Facepunch.Steamworks/BaseSteamworks.cs b/Facepunch.Steamworks/BaseSteamworks.cs index e5554dd..42ee8dc 100644 --- a/Facepunch.Steamworks/BaseSteamworks.cs +++ b/Facepunch.Steamworks/BaseSteamworks.cs @@ -36,7 +36,7 @@ public void SetupCommonInterfaces() { Networking = new Steamworks.Networking( this, native.networking ); Inventory = new Steamworks.Inventory( native.inventory, IsGameServer ); - Workshop = new Steamworks.Workshop( this, native.ugc ); + Workshop = new Steamworks.Workshop( this, native.ugc, native.remoteStorage ); } public bool IsValid @@ -88,12 +88,14 @@ public virtual void Update() /// /// Call results are results to specific actions /// - internal void AddCallResult( CallResult c ) + internal void AddCallResult( CallResult call ) { - if ( FinishCallback( c ) ) + if ( call == null ) throw new ArgumentNullException( "call" ); + + if ( FinishCallback( call ) ) return; - Callbacks.Add( c ); + Callbacks.Add( call ); } void RunCallbackQueue() diff --git a/Facepunch.Steamworks/Callbacks/Workshop.cs b/Facepunch.Steamworks/Callbacks/Workshop.cs index e4570e5..c6a8a44 100644 --- a/Facepunch.Steamworks/Callbacks/Workshop.cs +++ b/Facepunch.Steamworks/Callbacks/Workshop.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; +using System.Runtime.InteropServices; using Facepunch.Steamworks.Interop; namespace Facepunch.Steamworks.Callbacks.Workshop @@ -32,8 +28,6 @@ internal class DownloadResult public const int CallbackId = Index.UGC + 6; }; - - internal class QueryCompleted : CallResult { public override int CallbackId { get { return Index.UGC + 1; } } @@ -50,5 +44,30 @@ internal struct Data }; } + internal class CreateItem : CallResult + { + public override int CallbackId { get { return Index.UGC + 3; } } + [StructLayout( LayoutKind.Sequential )] + internal struct Data + { + internal Result Result; + internal ulong FileId; + [MarshalAs(UnmanagedType.I1)] + internal bool NeedsLegalAgreement; + }; + } + + internal class SubmitItemUpdate : CallResult + { + public override int CallbackId { get { return Index.UGC + 4; } } + + [StructLayout( LayoutKind.Sequential )] + internal struct Data + { + internal Result Result; + [MarshalAs(UnmanagedType.I1)] + internal bool NeedsLegalAgreement; + }; + } } diff --git a/Facepunch.Steamworks/Facepunch.Steamworks.csproj b/Facepunch.Steamworks/Facepunch.Steamworks.csproj index 2c6bfb4..b28786b 100644 --- a/Facepunch.Steamworks/Facepunch.Steamworks.csproj +++ b/Facepunch.Steamworks/Facepunch.Steamworks.csproj @@ -137,7 +137,9 @@ + + diff --git a/Facepunch.Steamworks/Interfaces/Inventory.cs b/Facepunch.Steamworks/Interfaces/Inventory.cs index cc9dbb1..89dd992 100644 --- a/Facepunch.Steamworks/Interfaces/Inventory.cs +++ b/Facepunch.Steamworks/Interfaces/Inventory.cs @@ -98,10 +98,7 @@ internal void FetchItemDefinitions() int[] ids; if ( !inventory.GetItemDefinitionIDs( out ids ) ) - { - Console.WriteLine( "Couldn't load definitions" ); return; - } Definitions = ids.Select( x => { diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs new file mode 100644 index 0000000..0950718 --- /dev/null +++ b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs @@ -0,0 +1,120 @@ +using System; +using Facepunch.Steamworks.Callbacks.Workshop; + +namespace Facepunch.Steamworks +{ + public partial class Workshop + { + public class Editor + { + internal Workshop workshop; + + internal CreateItem CreateItem; + internal SubmitItemUpdate SubmitItemUpdate; + + public ulong Id { get; internal set; } + public string Title { get; set; } + public string Description { get; set; } + public bool Publishing { get; internal set; } + public ItemType? Type { get; set; } + + public string ChangeNote { get; set; } = ""; + + public bool NeedToAgreeToWorkshopLegal { get; internal set; } + + public void Publish() + { + Publishing = true; + + if ( Id == 0 ) + { + StartCreatingItem(); + return; + } + + PublishChanges(); + } + + private void StartCreatingItem() + { + if ( !Type.HasValue ) + throw new System.Exception( "Editor.Type must be set when creating a new item!" ); + + CreateItem = new CreateItem(); + CreateItem.Handle = workshop.ugc.CreateItem( workshop.steamworks.AppId, (uint)Type ); + CreateItem.OnResult = OnItemCreated; + workshop.steamworks.AddCallResult( CreateItem ); + } + + private void OnItemCreated( CreateItem.Data obj ) + { + NeedToAgreeToWorkshopLegal = obj.NeedsLegalAgreement; + CreateItem = null; + + if ( obj.Result == Callbacks.Result.OK ) + { + Id = obj.FileId; + PublishChanges(); + return; + } + + Console.WriteLine( "File publish error: " + obj ); + Publishing = false; + } + + private void PublishChanges() + { + Publishing = false; + + ulong UpdateId = workshop.ugc.StartItemUpdate( workshop.steamworks.AppId, Id ); + + if ( Title != null ) + workshop.ugc.SetItemTitle( UpdateId, Title ); + + if ( Description != null ) + workshop.ugc.SetItemDescription( UpdateId, Description ); + + /* + workshop.ugc.SetItemUpdateLanguage( UpdateId, const char *pchLanguage ) = 0; // specify the language of the title or description that will be set + workshop.ugc.SetItemMetadata( UpdateId, const char *pchMetaData ) = 0; // change the metadata of an UGC item (max = k_cchDeveloperMetadataMax) + workshop.ugc.SetItemVisibility( UpdateId, ERemoteStoragePublishedFileVisibility eVisibility ) = 0; // change the visibility of an UGC item + workshop.ugc.SetItemTags( UpdateId, const SteamParamStringArray_t *pTags ) = 0; // change the tags of an UGC item + workshop.ugc.SetItemContent( UpdateId, const char *pszContentFolder ) = 0; // update item content from this local folder + workshop.ugc.SetItemPreview( UpdateId, const char *pszPreviewFile ) = 0; // change preview image file for this item. pszPreviewFile points to local image file, which must be under 1MB in size + workshop.ugc.RemoveItemKeyValueTags( UpdateId, const char *pchKey ) = 0; // remove any existing key-value tags with the specified key + workshop.ugc.AddItemKeyValueTag( UpdateId, const char *pchKey, const char *pchValue ) = 0; // add new key-value tags for the item. Note that there can be multiple values for a tag. + workshop.ugc.AddItemPreviewFile( UpdateId, const char *pszPreviewFile, EItemPreviewType type ) = 0; // add preview file for this item. pszPreviewFile points to local file, which must be under 1MB in size + workshop.ugc.AddItemPreviewVideo( UpdateId, const char *pszVideoID ) = 0; // add preview video for this item + workshop.ugc.UpdateItemPreviewFile( UpdateId, uint32 index, const char *pszPreviewFile ) = 0; // updates an existing preview file for this item. pszPreviewFile points to local file, which must be under 1MB in size + workshop.ugc.UpdateItemPreviewVideo( UpdateId, uint32 index, const char *pszVideoID ) = 0; // updates an existing preview video for this item + workshop.ugc.RemoveItemPreview( UpdateId, uint32 index ) = 0; // remove a preview by index starting at 0 (previews are sorted) + + */ + + SubmitItemUpdate = new SubmitItemUpdate(); + SubmitItemUpdate.Handle = workshop.ugc.SubmitItemUpdate( UpdateId, ChangeNote ); + SubmitItemUpdate.OnResult = OnChangesSubmitted; + workshop.steamworks.AddCallResult( SubmitItemUpdate ); + } + + private void OnChangesSubmitted( SubmitItemUpdate.Data obj ) + { + SubmitItemUpdate = null; + + NeedToAgreeToWorkshopLegal = obj.NeedsLegalAgreement; + + if ( obj.Result == Callbacks.Result.OK ) + { + Publishing = false; + return; + } + } + + public void Delete() + { + workshop.remoteStorage.DeletePublishedFile( Id ); + Id = 0; + } + } + } +} diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Query.cs b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs new file mode 100644 index 0000000..dd453a2 --- /dev/null +++ b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Facepunch.Steamworks.Callbacks.Networking; +using Facepunch.Steamworks.Callbacks.Workshop; +using Facepunch.Steamworks.Interop; +using Valve.Steamworks; + +namespace Facepunch.Steamworks +{ + public partial class Workshop + { + public class Query : IDisposable + { + internal ulong Handle; + internal QueryCompleted Callback; + + /// + /// The AppId you're querying. This defaults to this appid. + /// + public uint AppId { get; set; } + + /// + /// The AppId of the app used to upload the item. This defaults to 0 + /// which means all/any. + /// + public uint UploaderAppId { get; set; } + + public QueryType QueryType { get; set; } = QueryType.Items; + public Order Order { get; set; } = Order.RankedByVote; + + public string SearchText { get; set; } + + public Item[] Items { get; set; } + + public int TotalResults { get; set; } + + /// + /// Page starts at 1 !! + /// + public int Page { get; set; } = 1; + internal Workshop workshop; + + public unsafe void Run() + { + if ( Callback != null ) + return; + + if ( Page <= 0 ) + throw new System.Exception( "Page should be 1 or above" ); + + if ( FileId.Count != 0 ) + { + var fileArray = FileId.ToArray(); + + fixed ( ulong* array = fileArray ) + { + Handle = workshop.ugc.CreateQueryUGCDetailsRequest( (IntPtr)array, (uint)fileArray.Length ); + } + } + else + { + Handle = workshop.ugc.CreateQueryAllUGCRequest( (uint)Order, (uint)QueryType, UploaderAppId, AppId, (uint)Page ); + } + + if ( !string.IsNullOrEmpty( SearchText ) ) + workshop.ugc.SetSearchText( Handle, SearchText ); + + foreach ( var tag in RequireTags ) + workshop.ugc.AddRequiredTag( Handle, tag ); + + if ( RequireTags.Count > 0 ) + workshop.ugc.SetMatchAnyTag( Handle, RequireAllTags ); + + foreach ( var tag in ExcludeTags ) + workshop.ugc.AddExcludedTag( Handle, tag ); + + Callback = new QueryCompleted(); + Callback.Handle = workshop.ugc.SendQueryUGCRequest( Handle ); + Callback.OnResult = OnResult; + workshop.steamworks.AddCallResult( Callback ); + } + + void OnResult( QueryCompleted.Data data ) + { + Items = new Item[data.m_unNumResultsReturned]; + for ( int i = 0; i < data.m_unNumResultsReturned; i++ ) + { + SteamUGCDetails_t details = new SteamUGCDetails_t(); + workshop.ugc.GetQueryUGCResult( data.Handle, (uint)i, ref details ); + + Items[i] = Item.From( details, workshop ); + } + + TotalResults = (int)data.m_unTotalMatchingResults; + + Callback.Dispose(); + Callback = null; + } + + public bool IsRunning + { + get { return Callback != null; } + } + + /// + /// Only return items with these tags + /// + public List RequireTags { get; set; } = new List(); + + /// + /// If true, return items that have all RequireTags + /// If false, return items that have any tags in RequireTags + /// + public bool RequireAllTags { get; set; } = false; + + /// + /// Don't return any items with this tag + /// + public List ExcludeTags { get; set; } = new List(); + + /// + /// If you're querying for a particular file or files, add them to this. + /// + public List FileId { get; set; } = new List(); + + /// + /// Don't call this in production! + /// + public void Block() + { + workshop.steamworks.Update(); + + while ( IsRunning ) + { + System.Threading.Thread.Sleep( 10 ); + workshop.steamworks.Update(); + } + } + + public void Dispose() + { + // ReleaseQueryUGCRequest + } + } + } +} diff --git a/Facepunch.Steamworks/Interfaces/Workshop.cs b/Facepunch.Steamworks/Interfaces/Workshop.cs index 8d241c9..4f2e8f9 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.cs @@ -1,11 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using Facepunch.Steamworks.Callbacks.Workshop; -using Facepunch.Steamworks.Interop; using Valve.Steamworks; namespace Facepunch.Steamworks @@ -16,35 +11,47 @@ public partial class Workshop internal ISteamUGC ugc; internal BaseSteamworks steamworks; + internal ISteamRemoteStorage remoteStorage; internal event Action OnFileDownloaded; internal event Action OnItemInstalled; - internal Workshop( BaseSteamworks sw, ISteamUGC ugc ) + internal Workshop( BaseSteamworks steamworks, ISteamUGC ugc, ISteamRemoteStorage remoteStorage ) { this.ugc = ugc; - this.steamworks = sw; + this.steamworks = steamworks; + this.remoteStorage = remoteStorage; - sw.AddCallback( onDownloadResult, DownloadResult.CallbackId ); - sw.AddCallback( onItemInstalled, ItemInstalled.CallbackId ); + steamworks.AddCallback( onDownloadResult, DownloadResult.CallbackId ); + steamworks.AddCallback( onItemInstalled, ItemInstalled.CallbackId ); } private void onItemInstalled( ItemInstalled obj ) { - Console.WriteLine( "OnItemInstalled" ); - if ( OnItemInstalled != null ) OnItemInstalled( obj.FileId ); } private void onDownloadResult( DownloadResult obj ) { - Console.WriteLine( "onDownloadResult" ); - if ( OnFileDownloaded != null ) OnFileDownloaded( obj.FileId, obj.Result ); } + public Query CreateQuery() + { + return new Query() + { + AppId = steamworks.AppId, + workshop = this + }; + } + + public Editor CreateItem( ItemType type ) + { + return new Editor() { workshop = this, Type = type }; + } + public enum Order { RankedByVote = 0, @@ -64,9 +71,18 @@ public enum Order public enum QueryType { - Items = 0, // both mtx items and ready-to-use items - Items_Mtx = 1, - Items_ReadyToUse = 2, + /// + /// Both MicrotransactionItems and subscriptionItems + /// + Items = 0, + /// + /// Workshop item that is meant to be voted on for the purpose of selling in-game + /// + MicrotransactionItems = 1, + /// + /// normal Workshop item that can be subscribed to + /// + subscriptionItems = 2, Collections = 3, Artwork = 4, Videos = 5, @@ -79,146 +95,27 @@ public enum QueryType GameManagedItems = 12, // game managed items (not managed by users) }; - public WorkshopQuery CreateQuery() + public enum ItemType { - var q = new WorkshopQuery(); - q.AppId = steamworks.AppId; - q.workshop = this; - return q; - } + Community = 0, // normal Workshop item that can be subscribed to + Microtransaction = 1, // Workshop item that is meant to be voted on for the purpose of selling in-game + Collection = 2, // a collection of Workshop or Greenlight items + Art = 3, // artwork + Video = 4, // external video + Screenshot = 5, // screenshot + Game = 6, // Greenlight game entry + Software = 7, // Greenlight software entry + Concept = 8, // Greenlight concept + WebGuide = 9, // Steam web guide + IntegratedGuide = 10, // application integrated guide + Merch = 11, // Workshop merchandise meant to be voted on for the purpose of being sold + ControllerBinding = 12, // Steam Controller bindings + SteamworksAccessInvite = 13, // internal + SteamVideo = 14, // Steam video + GameManagedItem = 15, // managed completely by the game, not the user, and not shown on the web + }; - public class WorkshopQuery : IDisposable - { - internal ulong Handle; - internal QueryCompleted Callback; - /// - /// The AppId you're querying. This defaults to this appid. - /// - public uint AppId { get; set; } - /// - /// The AppId of the app used to upload the item. This defaults to 0 - /// which means all/any. - /// - public uint UploaderAppId { get; set; } - - public QueryType QueryType { get; set; } = QueryType.Items; - public Order Order { get; set; } = Order.RankedByVote; - - public string SearchText { get; set; } - - public Item[] Items { get; set; } - - public int TotalResults { get; set; } - - /// - /// Page starts at 1 !! - /// - public int Page { get; set; } = 1; - internal Workshop workshop; - - public unsafe void Run() - { - if ( Callback != null ) - return; - - if ( Page <= 0 ) - throw new System.Exception( "Page should be 1 or above" ); - - if ( FileId.Count != 0 ) - { - var fileArray = FileId.ToArray(); - - fixed ( ulong* array = fileArray ) - { - Handle = workshop.ugc.CreateQueryUGCDetailsRequest( (IntPtr) array, (uint)fileArray.Length ); - } - } - else - { - Handle = workshop.ugc.CreateQueryAllUGCRequest( (uint)Order, (uint)QueryType, UploaderAppId, AppId, (uint)Page ); - } - - if ( !string.IsNullOrEmpty( SearchText ) ) - workshop.ugc.SetSearchText( Handle, SearchText ); - - foreach ( var tag in RequireTags ) - workshop.ugc.AddRequiredTag( Handle, tag ); - - if ( RequireTags.Count > 0 ) - workshop.ugc.SetMatchAnyTag( Handle, RequireAllTags ); - - foreach ( var tag in ExcludeTags ) - workshop.ugc.AddExcludedTag( Handle, tag ); - - Callback = new QueryCompleted(); - Callback.Handle = workshop.ugc.SendQueryUGCRequest( Handle ); - Callback.OnResult = OnResult; - workshop.steamworks.AddCallResult( Callback ); - } - - void OnResult( QueryCompleted.Data data ) - { - Items = new Item[data.m_unNumResultsReturned]; - for ( int i = 0; i < data.m_unNumResultsReturned; i++ ) - { - SteamUGCDetails_t details = new SteamUGCDetails_t(); - workshop.ugc.GetQueryUGCResult( data.Handle, (uint) i, ref details ); - - Items[i] = Item.From( details, workshop ); - } - - TotalResults = (int) data.m_unTotalMatchingResults; - - Callback.Dispose(); - Callback = null; - } - - public bool IsRunning - { - get { return Callback != null; } - } - - /// - /// Only return items with these tags - /// - public List RequireTags { get; set; } = new List(); - - /// - /// If true, return items that have all RequireTags - /// If false, return items that have any tags in RequireTags - /// - public bool RequireAllTags { get; set; } = false; - - /// - /// Don't return any items with this tag - /// - public List ExcludeTags { get; set; } = new List(); - - /// - /// If you're querying for a particular file or files, add them to this. - /// - public List FileId { get; set; } = new List(); - - /// - /// Don't call this in production! - /// - public void Block() - { - workshop.steamworks.Update(); - - while ( IsRunning ) - { - System.Threading.Thread.Sleep( 10 ); - workshop.steamworks.Update(); - } - } - - public void Dispose() - { - // ReleaseQueryUGCRequest - } - } } } diff --git a/Facepunch.Steamworks/Interop/Native.cs b/Facepunch.Steamworks/Interop/Native.cs index b0196d4..ba67aa2 100644 --- a/Facepunch.Steamworks/Interop/Native.cs +++ b/Facepunch.Steamworks/Interop/Native.cs @@ -21,6 +21,7 @@ internal class NativeInterface : IDisposable internal Valve.Steamworks.ISteamUGC ugc; internal Valve.Steamworks.ISteamGameServer gameServer; internal Valve.Steamworks.ISteamGameServerStats gameServerStats; + internal Valve.Steamworks.ISteamRemoteStorage remoteStorage; internal bool InitClient() { @@ -75,6 +76,7 @@ public void FillInterfaces( int hpipe, int huser ) servers = client.GetISteamMatchmakingServers( huser, hpipe, "SteamMatchMakingServers002" ); userstats = client.GetISteamUserStats( huser, hpipe, "STEAMUSERSTATS_INTERFACE_VERSION011" ); screenshots = client.GetISteamScreenshots( huser, hpipe, "STEAMSCREENSHOTS_INTERFACE_VERSION002" ); + remoteStorage = client.GetISteamRemoteStorage( huser, hpipe, "STEAMREMOTESTORAGE_INTERFACE_VERSION013" ); } public void Dispose() diff --git a/Facepunch.Steamworks/Interop/steam_api_interop.cs b/Facepunch.Steamworks/Interop/steam_api_interop.cs index f1effb0..7f6de1e 100644 --- a/Facepunch.Steamworks/Interop/steam_api_interop.cs +++ b/Facepunch.Steamworks/Interop/steam_api_interop.cs @@ -2513,7 +2513,7 @@ internal override ISteamRemoteStorage GetISteamRemoteStorage( int hSteamUser, in { CheckIfUsable(); IntPtr result = NativeEntrypoints.SteamAPI_ISteamClient_GetISteamRemoteStorage(m_pSteamClient,hSteamUser,hSteamPipe,pchVersion); - return (ISteamRemoteStorage)Marshal.PtrToStructure( result, typeof( ISteamRemoteStorage ) ); + return new CSteamRemoteStorage( result ); } internal override ISteamScreenshots GetISteamScreenshots( int hSteamUser, int hSteamPipe, string pchVersion ) {