From e9f5898b6c1a37ab02b53e97a6a78d9711dc9dd0 Mon Sep 17 00:00:00 2001 From: Garry Newman Date: Wed, 17 Apr 2019 16:41:06 +0100 Subject: [PATCH] Basic Ugc Querying --- .../Facepunch.Steamworks.Test.csproj | 1 + Facepunch.Steamworks.Test/UgcTest.cs | 46 ++++ Facepunch.Steamworks.Test/UtilsTest.cs | 2 +- .../Generated/Interfaces/ISteamUGC.cs | 10 +- Facepunch.Steamworks/ServerList/Base.cs | 2 +- Facepunch.Steamworks/SteamClient.cs | 7 + Facepunch.Steamworks/SteamUgc.cs | 32 +++ Facepunch.Steamworks/SteamUtils.cs | 5 - Facepunch.Steamworks/Structs/Friend.cs | 2 +- Facepunch.Steamworks/Structs/UgcQuery.cs | 241 ++++++++++++++++++ Generator/CodeWriter/ClassVTable.cs | 6 + Generator/CodeWriter/Types/BaseType.cs | 2 +- 12 files changed, 342 insertions(+), 14 deletions(-) create mode 100644 Facepunch.Steamworks.Test/UgcTest.cs create mode 100644 Facepunch.Steamworks/SteamUgc.cs create mode 100644 Facepunch.Steamworks/Structs/UgcQuery.cs diff --git a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj index 6a8400f..efda689 100644 --- a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj +++ b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj @@ -91,6 +91,7 @@ + diff --git a/Facepunch.Steamworks.Test/UgcTest.cs b/Facepunch.Steamworks.Test/UgcTest.cs new file mode 100644 index 0000000..b8fffc2 --- /dev/null +++ b/Facepunch.Steamworks.Test/UgcTest.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Steamworks.Data; + +namespace Steamworks +{ + [TestClass] + [DeploymentItem( "steam_api64.dll" )] + public class UgcTests + { + [TestMethod] + public async Task QueryAll() + { + var q = UgcQuery.All(); + + var result = await q.GetPageAsync( 1 ); + Assert.IsNotNull( result ); + + Console.WriteLine( $"ResultCount: {result?.ResultCount}" ); + Console.WriteLine( $"TotalCount: {result?.TotalCount}" ); + } + + [TestMethod] + public async Task QueryAllFromFriends() + { + var q = UgcQuery.All() + .CreatedByFriends(); + + var result = await q.GetPageAsync( 1 ); + Assert.IsNotNull( result ); + + Console.WriteLine( $"ResultCount: {result?.ResultCount}" ); + Console.WriteLine( $"TotalCount: {result?.TotalCount}" ); + + foreach ( var entry in result.Value.Entries ) + { + Console.WriteLine( $" {entry.Title}" ); + } + } + } + +} diff --git a/Facepunch.Steamworks.Test/UtilsTest.cs b/Facepunch.Steamworks.Test/UtilsTest.cs index ecc1778..1d0fabd 100644 --- a/Facepunch.Steamworks.Test/UtilsTest.cs +++ b/Facepunch.Steamworks.Test/UtilsTest.cs @@ -63,7 +63,7 @@ public void CurrentBatteryPower() [TestMethod] public void AppId() { - var cnt = SteamUtils.AppId; + var cnt = SteamClient.AppId; Assert.IsTrue( cnt.Value > 0 ); diff --git a/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs b/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs index 88e46d9..f9daf7d 100644 --- a/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs +++ b/Facepunch.Steamworks/Generated/Interfaces/ISteamUGC.cs @@ -18,8 +18,8 @@ public ISteamUGC( bool server = false ) : base( server ) public override void InitInternals() { _CreateQueryUserUGCRequest = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 0) ); - _CreateQueryAllUGCRequest1 = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 8) ); - _CreateQueryAllUGCRequest2 = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 16) ); + _CreateQueryAllUGCRequest1 = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 16) ); + _CreateQueryAllUGCRequest2 = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 8) ); _CreateQueryUGCDetailsRequest = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 24) ); _SendQueryUGCRequest = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 32) ); _GetQueryUGCResult = Marshal.GetDelegateForFunctionPointer( Marshal.ReadIntPtr( VTable, 40) ); @@ -153,13 +153,13 @@ internal UGCQueryHandle_t CreateQueryUGCDetailsRequest( [In,Out] PublishedFileId #region FunctionMeta [UnmanagedFunctionPointer( CallingConvention.ThisCall )] [return: MarshalAs( UnmanagedType.I1 )] - private delegate bool FGetQueryUGCResult( IntPtr self, UGCQueryHandle_t handle, uint index, [In,Out] SteamUGCDetails_t[] pDetails ); + private delegate bool FGetQueryUGCResult( IntPtr self, UGCQueryHandle_t handle, uint index, ref SteamUGCDetails_t pDetails ); private FGetQueryUGCResult _GetQueryUGCResult; #endregion - internal bool GetQueryUGCResult( UGCQueryHandle_t handle, uint index, [In,Out] SteamUGCDetails_t[] pDetails ) + internal bool GetQueryUGCResult( UGCQueryHandle_t handle, uint index, ref SteamUGCDetails_t pDetails ) { - return _GetQueryUGCResult( Self, handle, index, pDetails ); + return _GetQueryUGCResult( Self, handle, index, ref pDetails ); } #region FunctionMeta diff --git a/Facepunch.Steamworks/ServerList/Base.cs b/Facepunch.Steamworks/ServerList/Base.cs index 153f3b2..fff68ae 100644 --- a/Facepunch.Steamworks/ServerList/Base.cs +++ b/Facepunch.Steamworks/ServerList/Base.cs @@ -56,7 +56,7 @@ internal static ISteamMatchmakingServers Internal public Base() { - AppId = SteamUtils.AppId; // Default AppId is this + AppId = SteamClient.AppId; // Default AppId is this } /// diff --git a/Facepunch.Steamworks/SteamClient.cs b/Facepunch.Steamworks/SteamClient.cs index a0150b5..5ea8790 100644 --- a/Facepunch.Steamworks/SteamClient.cs +++ b/Facepunch.Steamworks/SteamClient.cs @@ -25,6 +25,8 @@ public static void Init( uint appid ) throw new System.Exception( "SteamApi_Init returned false. Steam isn't running, couldn't find Steam, AppId is ureleased, Don't own AppId." ); } + AppId = appid; + initialized = true; SteamApps.InstallEvents(); @@ -108,5 +110,10 @@ internal static void UnregisterCallback( IntPtr intPtr ) /// gets the status of the current user /// public static FriendState State => SteamFriends.Internal.GetPersonaState(); + + /// + /// returns the appID of the current process + /// + public static AppId AppId { get; internal set; } } } \ No newline at end of file diff --git a/Facepunch.Steamworks/SteamUgc.cs b/Facepunch.Steamworks/SteamUgc.cs new file mode 100644 index 0000000..7ea00ce --- /dev/null +++ b/Facepunch.Steamworks/SteamUgc.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Steamworks.Data; + +namespace Steamworks +{ + /// + /// Functions for accessing and manipulating Steam user information. + /// This is also where the APIs for Steam Voice are exposed. + /// + public static class SteamUGC + { + static ISteamUGC _internal; + internal static ISteamUGC Internal + { + get + { + if ( _internal == null ) + { + _internal = new ISteamUGC(); + } + + return _internal; + } + } + + } +} \ No newline at end of file diff --git a/Facepunch.Steamworks/SteamUtils.cs b/Facepunch.Steamworks/SteamUtils.cs index 2c4dc12..30a079d 100644 --- a/Facepunch.Steamworks/SteamUtils.cs +++ b/Facepunch.Steamworks/SteamUtils.cs @@ -121,11 +121,6 @@ public static bool GetImageSize( int image, out uint width, out uint height ) /// public static float CurrentBatteryPower => Math.Min( Internal.GetCurrentBatteryPower() / 100, 1.0f ); - /// - /// returns the appID of the current process - /// - public static AppId AppId => Internal.GetAppID(); - static NotificationPosition overlayNotificationPosition = NotificationPosition.BottomRight; /// diff --git a/Facepunch.Steamworks/Structs/Friend.cs b/Facepunch.Steamworks/Structs/Friend.cs index c3cf09f..eeaf5ef 100644 --- a/Facepunch.Steamworks/Structs/Friend.cs +++ b/Facepunch.Steamworks/Structs/Friend.cs @@ -40,7 +40,7 @@ public override string ToString() /// /// Return true if this user is playing the game we're running /// - public bool IsPlayingThisGame => GameInfo?.GameID == SteamUtils.AppId; + public bool IsPlayingThisGame => GameInfo?.GameID == SteamClient.AppId; /// /// Returns true if this friend is online diff --git a/Facepunch.Steamworks/Structs/UgcQuery.cs b/Facepunch.Steamworks/Structs/UgcQuery.cs new file mode 100644 index 0000000..7d9932c --- /dev/null +++ b/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Steamworks.Data +{ + public struct UgcQuery + { + UGCMatchingUGCType matching; + + UserUGCList userGc; + UGCQuery queryType; + + AppId consumerApp; + AppId creatorApp; + + List requiredTags; + bool? matchAnyTag; + + List excludedTags; + + Dictionary requiredKv; + + bool? WantsReturnOnlyIDs; + bool? WantsReturnKeyValueTags; + bool? WantsReturnLongDescription; + bool? WantsReturnMetadata; + bool? WantsReturnChildren; + bool? WantsReturnAdditionalPreviews; + bool? WantsReturnTotalOnly; + bool? WantsReturnPlaytimeStats; + + string searchText; + string language; + int? trendDays; + int? maxCacheAge; + + public static UgcQuery Items() => new UgcQuery { matching = UGCMatchingUGCType.Items }; + public static UgcQuery ItemsMtx() => new UgcQuery { matching = UGCMatchingUGCType.Items_Mtx }; + public static UgcQuery ItemsReadyToUse() => new UgcQuery { matching = UGCMatchingUGCType.Items_ReadyToUse }; + public static UgcQuery Collections() => new UgcQuery { matching = UGCMatchingUGCType.Collections }; + public static UgcQuery Artwork() => new UgcQuery { matching = UGCMatchingUGCType.Artwork }; + public static UgcQuery Videos() => new UgcQuery { matching = UGCMatchingUGCType.Videos }; + public static UgcQuery Screenshots() => new UgcQuery { matching = UGCMatchingUGCType.Screenshots }; + public static UgcQuery AllGuides() => new UgcQuery { matching = UGCMatchingUGCType.AllGuides }; + public static UgcQuery WebGuides() => new UgcQuery { matching = UGCMatchingUGCType.WebGuides }; + public static UgcQuery IntegratedGuides() => new UgcQuery { matching = UGCMatchingUGCType.IntegratedGuides }; + public static UgcQuery UsableInGame() => new UgcQuery { matching = UGCMatchingUGCType.UsableInGame }; + public static UgcQuery ControllerBindings() => new UgcQuery { matching = UGCMatchingUGCType.ControllerBindings }; + public static UgcQuery GameManagedItems() => new UgcQuery { matching = UGCMatchingUGCType.GameManagedItems }; + public static UgcQuery All() => new UgcQuery { matching = UGCMatchingUGCType.All }; + + public UgcQuery RankedByVote() { queryType = UGCQuery.RankedByVote; return this; } + public UgcQuery RankedByPublicationDate() { queryType = UGCQuery.RankedByPublicationDate; return this; } + public UgcQuery RankedByAcceptanceDate() { queryType = UGCQuery.AcceptedForGameRankedByAcceptanceDate; return this; } + public UgcQuery RankedByTrend() { queryType = UGCQuery.RankedByTrend; return this; } + public UgcQuery FavoritedByFriends() { queryType = UGCQuery.FavoritedByFriendsRankedByPublicationDate; return this; } + public UgcQuery CreatedByFriends() { queryType = UGCQuery.CreatedByFriendsRankedByPublicationDate; return this; } + public UgcQuery RankedByNumTimesReported() { queryType = UGCQuery.RankedByNumTimesReported; return this; } + public UgcQuery CreatedByFollowedUsers() { queryType = UGCQuery.CreatedByFollowedUsersRankedByPublicationDate; return this; } + public UgcQuery NotYetRated() { queryType = UGCQuery.NotYetRated; return this; } + public UgcQuery RankedByTotalVotesAsc() { queryType = UGCQuery.RankedByTotalVotesAsc; return this; } + public UgcQuery RankedByVotesUp() { queryType = UGCQuery.RankedByVotesUp; return this; } + public UgcQuery RankedByTextSearch() { queryType = UGCQuery.RankedByTextSearch; return this; } + public UgcQuery RankedByTotalUniqueSubscriptions() { queryType = UGCQuery.RankedByTotalUniqueSubscriptions; return this; } + public UgcQuery RankedByPlaytimeTrend() { queryType = UGCQuery.RankedByPlaytimeTrend; return this; } + public UgcQuery RankedByTotalPlaytime() { queryType = UGCQuery.RankedByTotalPlaytime; return this; } + public UgcQuery RankedByAveragePlaytimeTrend() { queryType = UGCQuery.RankedByAveragePlaytimeTrend; return this; } + public UgcQuery RankedByLifetimeAveragePlaytime() { queryType = UGCQuery.RankedByLifetimeAveragePlaytime; return this; } + public UgcQuery RankedByPlaytimeSessionsTrend() { queryType = UGCQuery.RankedByPlaytimeSessionsTrend; return this; } + public UgcQuery RankedByLifetimePlaytimeSessions() { queryType = UGCQuery.RankedByLifetimePlaytimeSessions; return this; } + + public UgcQuery ReturnOnlyIDs( bool b) { WantsReturnOnlyIDs = b; return this; } + public UgcQuery ReturnKeyValueTag( bool b ) { WantsReturnKeyValueTags = b; return this; } + public UgcQuery ReturnLongDescription( bool b ) { WantsReturnLongDescription = b; return this; } + public UgcQuery ReturnMetadata( bool b ) { WantsReturnMetadata = b; return this; } + public UgcQuery ReturnChildren( bool b ) { WantsReturnChildren = b; return this; } + public UgcQuery ReturnAdditionalPreviews( bool b ) { WantsReturnAdditionalPreviews = b; return this; } + public UgcQuery ReturnTotalOnly( bool b ) { WantsReturnTotalOnly = b; return this; } + public UgcQuery ReturnPlaytimeStats( bool b ) { WantsReturnPlaytimeStats = b; return this; } + public UgcQuery AllowCachedResponse( int maxSecondsAge ) { maxCacheAge = maxSecondsAge; return this; } + + public UgcQuery InLanguage( string lang ) { language = lang; return this; } + public UgcQuery MatchAnyTag( bool b ) { matchAnyTag = b; return this; } + + public UgcQuery WithTag( string tag ) + { + if ( requiredTags == null ) requiredTags = new List(); + requiredTags.Add( tag ); + return this; + } + + public UgcQuery WithoutTag( string tag ) + { + if ( excludedTags == null ) excludedTags = new List(); + excludedTags.Add( tag ); + return this; + } + + public async Task GetPageAsync( int page ) + { + if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); + + if ( consumerApp == 0 ) consumerApp = SteamClient.AppId; + if ( creatorApp == 0 ) creatorApp = consumerApp; + + UGCQueryHandle_t handle; + + handle = SteamUGC.Internal.CreateQueryAllUGCRequest1( queryType, matching, creatorApp.Value, consumerApp.Value, (uint) page ); + + // Apply stored constraints + { + if ( requiredTags != null ) + { + foreach ( var tag in requiredTags ) + SteamUGC.Internal.AddRequiredTag( handle, tag ); + } + + if ( excludedTags != null ) + { + foreach ( var tag in excludedTags ) + SteamUGC.Internal.AddExcludedTag( handle, tag ); + } + + if ( requiredKv != null ) + { + foreach ( var tag in requiredKv ) + SteamUGC.Internal.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); + } + + // + // TODO - add more + // + } + + var result = await SteamUGC.Internal.SendQueryUGCRequest( handle ); + if ( !result.HasValue ) + return null; + + if ( result.Value.Result != Result.OK ) + return null; + + return new UgcQueryPage + { + Handle = result.Value.Handle, + ResultCount = (int) result.Value.NumResultsReturned, + TotalCount = (int)result.Value.TotalMatchingResults, + CachedData = result.Value.CachedData + }; + } + + } + + public struct UgcQueryPage : System.IDisposable + { + internal UGCQueryHandle_t Handle; + + public int ResultCount; + public int TotalCount; + + public bool CachedData; + + public IEnumerable Entries + { + get + { + var details = default( SteamUGCDetails_t ); + for ( uint i=0; i< ResultCount; i++ ) + { + if ( SteamUGC.Internal.GetQueryUGCResult( Handle, i, ref details ) ) + { + yield return UgcDetails.From( details, Handle ); + } + } + } + } + + + public void Dispose() + { + if ( Handle > 0 ) + { + SteamUGC.Internal.ReleaseQueryUGCRequest( Handle ); + Handle = 0; + } + } + } + + public struct UgcDetails + { + public PublishedFileId Id; + + public string Title; + public string Description; + + + // + // TODO; + // + internal Result Result; // m_eResult enum EResult + internal WorkshopFileType FileType; // m_eFileType enum EWorkshopFileType + internal uint CreatorAppID; // m_nCreatorAppID AppId_t + internal uint ConsumerAppID; // m_nConsumerAppID AppId_t + + internal ulong SteamIDOwner; // m_ulSteamIDOwner uint64 + internal uint TimeCreated; // m_rtimeCreated uint32 + internal uint TimeUpdated; // m_rtimeUpdated uint32 + internal uint TimeAddedToUserList; // m_rtimeAddedToUserList uint32 + internal RemoteStoragePublishedFileVisibility Visibility; // m_eVisibility enum ERemoteStoragePublishedFileVisibility + internal bool Banned; // m_bBanned _Bool + internal bool AcceptedForUse; // m_bAcceptedForUse _Bool + internal bool TagsTruncated; // m_bTagsTruncated _Bool + internal string Tags; // m_rgchTags char [1025] + internal ulong File; // m_hFile UGCHandle_t + internal ulong PreviewFile; // m_hPreviewFile UGCHandle_t + internal string PchFileName; // m_pchFileName char [260] + internal int FileSize; // m_nFileSize int32 + internal int PreviewFileSize; // m_nPreviewFileSize int32 + internal string URL; // m_rgchURL char [256] + internal uint VotesUp; // m_unVotesUp uint32 + internal uint VotesDown; // m_unVotesDown uint32 + internal float Score; // m_flScore float + internal uint NumChildren; // m_unNumChildren uint32 + + internal static UgcDetails From( SteamUGCDetails_t details, UGCQueryHandle_t handle ) + { + var d = new UgcDetails + { + Id = details.PublishedFileId, + FileType = details.FileType, + + Title = details.Title, + Description = details.Description, + + }; + + return d; + } + } +} \ No newline at end of file diff --git a/Generator/CodeWriter/ClassVTable.cs b/Generator/CodeWriter/ClassVTable.cs index 7dc30fd..9d49d4a 100644 --- a/Generator/CodeWriter/ClassVTable.cs +++ b/Generator/CodeWriter/ClassVTable.cs @@ -80,6 +80,12 @@ void WriteFunctionPointerReader( CodeParser.Class clss ) Swap( clss, "GetGlobalStatHistory1", "GetGlobalStatHistory2", locations ); } + if ( clss.Name == "ISteamUGC" ) + { + Swap( clss, "CreateQueryAllUGCRequest1", "CreateQueryAllUGCRequest2", locations ); + } + + StartBlock( $"public override void InitInternals()" ); { for (int i=0; i< clss.Functions.Count; i++ ) diff --git a/Generator/CodeWriter/Types/BaseType.cs b/Generator/CodeWriter/Types/BaseType.cs index 61db8d0..54b64bb 100644 --- a/Generator/CodeWriter/Types/BaseType.cs +++ b/Generator/CodeWriter/Types/BaseType.cs @@ -64,7 +64,7 @@ public virtual bool IsVector if ( VarName == "psteamIDClans" ) return true; if ( VarName == "pScoreDetails" ) return true; if ( VarName == "prgUsers" ) return true; - if ( VarName == "pDetails" ) return true; + if ( VarName == "pDetails" && Func == "GetDownloadedLeaderboardEntry" ) return true; if ( VarName == "pData" && NativeType.EndsWith( "*" ) && Func.StartsWith( "GetGlobalStatHistory" ) ) return true; if ( NativeType.EndsWith( "**" ) ) return true;