Create/Publish/Delete workshop files

This commit is contained in:
Garry Newman 2016-10-06 16:07:43 +01:00
parent 3ff2b304a5
commit d4eb325606
10 changed files with 387 additions and 169 deletions

View File

@ -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();
}
}
}
}

View File

@ -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()
/// <summary>
/// Call results are results to specific actions
/// </summary>
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()

View File

@ -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<QueryCompleted.Data>
{
public override int CallbackId { get { return Index.UGC + 1; } }
@ -50,5 +44,30 @@ internal struct Data
};
}
internal class CreateItem : CallResult<CreateItem.Data>
{
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<SubmitItemUpdate.Data>
{
public override int CallbackId { get { return Index.UGC + 4; } }
[StructLayout( LayoutKind.Sequential )]
internal struct Data
{
internal Result Result;
[MarshalAs(UnmanagedType.I1)]
internal bool NeedsLegalAgreement;
};
}
}

View File

@ -137,7 +137,9 @@
<Compile Include="Config.cs" />
<Compile Include="Callbacks\Networking.cs" />
<Compile Include="Interfaces\Workshop.cs" />
<Compile Include="Interfaces\Workshop.Editor.cs" />
<Compile Include="Interfaces\Workshop.Item.cs" />
<Compile Include="Interfaces\Workshop.Query.cs" />
<Compile Include="Interop\Callback.cs" />
<Compile Include="Interop\CallResult.cs" />
<Compile Include="Interop\Native.cs" />

View File

@ -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 =>
{

View File

@ -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;
}
}
}
}

View File

@ -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;
/// <summary>
/// The AppId you're querying. This defaults to this appid.
/// </summary>
public uint AppId { get; set; }
/// <summary>
/// The AppId of the app used to upload the item. This defaults to 0
/// which means all/any.
/// </summary>
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; }
/// <summary>
/// Page starts at 1 !!
/// </summary>
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; }
}
/// <summary>
/// Only return items with these tags
/// </summary>
public List<string> RequireTags { get; set; } = new List<string>();
/// <summary>
/// If true, return items that have all RequireTags
/// If false, return items that have any tags in RequireTags
/// </summary>
public bool RequireAllTags { get; set; } = false;
/// <summary>
/// Don't return any items with this tag
/// </summary>
public List<string> ExcludeTags { get; set; } = new List<string>();
/// <summary>
/// If you're querying for a particular file or files, add them to this.
/// </summary>
public List<ulong> FileId { get; set; } = new List<ulong>();
/// <summary>
/// Don't call this in production!
/// </summary>
public void Block()
{
workshop.steamworks.Update();
while ( IsRunning )
{
System.Threading.Thread.Sleep( 10 );
workshop.steamworks.Update();
}
}
public void Dispose()
{
// ReleaseQueryUGCRequest
}
}
}
}

View File

@ -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<ulong, Callbacks.Result> OnFileDownloaded;
internal event Action<ulong> 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<DownloadResult>( onDownloadResult, DownloadResult.CallbackId );
sw.AddCallback<ItemInstalled>( onItemInstalled, ItemInstalled.CallbackId );
steamworks.AddCallback<DownloadResult>( onDownloadResult, DownloadResult.CallbackId );
steamworks.AddCallback<ItemInstalled>( 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,
/// <summary>
/// Both MicrotransactionItems and subscriptionItems
/// </summary>
Items = 0,
/// <summary>
/// Workshop item that is meant to be voted on for the purpose of selling in-game
/// </summary>
MicrotransactionItems = 1,
/// <summary>
/// normal Workshop item that can be subscribed to
/// </summary>
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;
/// <summary>
/// The AppId you're querying. This defaults to this appid.
/// </summary>
public uint AppId { get; set; }
/// <summary>
/// The AppId of the app used to upload the item. This defaults to 0
/// which means all/any.
/// </summary>
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; }
/// <summary>
/// Page starts at 1 !!
/// </summary>
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; }
}
/// <summary>
/// Only return items with these tags
/// </summary>
public List<string> RequireTags { get; set; } = new List<string>();
/// <summary>
/// If true, return items that have all RequireTags
/// If false, return items that have any tags in RequireTags
/// </summary>
public bool RequireAllTags { get; set; } = false;
/// <summary>
/// Don't return any items with this tag
/// </summary>
public List<string> ExcludeTags { get; set; } = new List<string>();
/// <summary>
/// If you're querying for a particular file or files, add them to this.
/// </summary>
public List<ulong> FileId { get; set; } = new List<ulong>();
/// <summary>
/// Don't call this in production!
/// </summary>
public void Block()
{
workshop.steamworks.Update();
while ( IsRunning )
{
System.Threading.Thread.Sleep( 10 );
workshop.steamworks.Update();
}
}
public void Dispose()
{
// ReleaseQueryUGCRequest
}
}
}
}

View File

@ -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()

View File

@ -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 )
{