Restructuring of the files

This commit is contained in:
2025-02-25 12:58:41 +02:00
parent 8a25503432
commit b37fd411c6
154 changed files with 5 additions and 12 deletions

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct Achievement
{
internal string Value;
public Achievement( string name )
{
Value = name;
}
public override string ToString() => Value;
/// <summary>
/// True if unlocked
/// </summary>
public bool State
{
get
{
var state = false;
SteamUserStats.Internal.GetAchievement( Value, ref state );
return state;
}
}
public string Identifier => Value;
public string Name => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "name" );
public string Description => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "desc" );
/// <summary>
/// Should hold the unlock time if State is true
/// </summary>
public DateTime? UnlockTime
{
get
{
var state = false;
uint time = 0;
if ( !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state )
return null;
return Epoch.ToDateTime( time );
}
}
/// <summary>
/// Gets the icon of the achievement. This can return a null image even though the image exists if the image
/// hasn't been downloaded by Steam yet. You can use GetIconAsync if you want to wait for the image to be downloaded.
/// </summary>
public Image? GetIcon()
{
return SteamUtils.GetImage( SteamUserStats.Internal.GetAchievementIcon( Value ) );
}
/// <summary>
/// Gets the icon of the achievement, waits for it to load if we have to
/// </summary>
public async Task<Image?> GetIconAsync( int timeout = 5000 )
{
var i = SteamUserStats.Internal.GetAchievementIcon( Value );
if ( i != 0 ) return SteamUtils.GetImage( i );
var ident = Identifier;
bool gotCallback = false;
void f( string x, int icon )
{
if ( x != ident ) return;
i = icon;
gotCallback = true;
}
try
{
SteamUserStats.OnAchievementIconFetched += f;
int waited = 0;
while ( !gotCallback )
{
await Task.Delay( 10 );
waited += 10;
// Time out after x milliseconds
if ( waited > timeout )
return null;
}
if ( i == 0 ) return null;
return SteamUtils.GetImage( i );
}
finally
{
SteamUserStats.OnAchievementIconFetched -= f;
}
}
/// <summary>
/// Returns the fraction (0-1) of users who have unlocked the specified achievement, or -1 if no data available.
/// </summary>
public float GlobalUnlocked
{
get
{
float pct = 0;
if ( !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) )
return -1.0f;
return pct / 100.0f;
}
}
/// <summary>
/// Make this achievement earned
/// </summary>
public bool Trigger( bool apply = true )
{
var r = SteamUserStats.Internal.SetAchievement( Value );
if ( apply && r )
{
SteamUserStats.Internal.StoreStats();
}
return r;
}
/// <summary>
/// Reset this achievement to not achieved
/// </summary>
public bool Clear()
{
return SteamUserStats.Internal.ClearAchievement( Value );
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks
{
public struct AppId
{
public uint Value;
public override string ToString() => Value.ToString();
public static implicit operator AppId( uint value )
{
return new AppId{ Value = value };
}
public static implicit operator AppId( int value )
{
return new AppId { Value = (uint) value };
}
public static implicit operator uint( AppId value )
{
return value.Value;
}
}
}

View File

@@ -0,0 +1,97 @@
using Facepunch.Steamworks.Data;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks
{
public struct Controller
{
internal InputHandle_t Handle;
internal Controller( InputHandle_t inputHandle_t )
{
this.Handle = inputHandle_t;
}
public ulong Id => Handle.Value;
public InputType InputType => SteamInput.Internal.GetInputTypeForHandle( Handle );
/// <summary>
/// Reconfigure the controller to use the specified action set (ie 'Menu', 'Walk' or 'Drive')
/// This is cheap, and can be safely called repeatedly. It's often easier to repeatedly call it in
/// our state loops, instead of trying to place it in all of your state transitions.
/// </summary>
public string ActionSet
{
set => SteamInput.Internal.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) );
}
public void DeactivateLayer( string layer ) => SteamInput.Internal.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) );
public void ActivateLayer( string layer ) => SteamInput.Internal.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) );
public void ClearLayers() => SteamInput.Internal.DeactivateAllActionSetLayers( Handle );
/// <summary>
/// Returns the current state of the supplied digital game action
/// </summary>
public DigitalState GetDigitalState( string actionName )
{
return SteamInput.Internal.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) );
}
/// <summary>
/// Returns the current state of these supplied analog game action
/// </summary>
public AnalogState GetAnalogState( string actionName )
{
return SteamInput.Internal.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) );
}
public override string ToString() => $"{InputType}.{Handle.Value}";
public static bool operator ==( Controller a, Controller b ) => a.Equals( b );
public static bool operator !=( Controller a, Controller b ) => !(a == b);
public override bool Equals( object p ) => this.Equals( (Controller)p );
public override int GetHashCode() => Handle.GetHashCode();
public bool Equals( Controller p ) => p.Handle == Handle;
}
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public struct AnalogState
{
public InputSourceMode EMode; // eMode EInputSourceMode
public float X; // x float
public float Y; // y float
internal byte BActive; // bActive byte
public bool Active => BActive != 0;
}
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
internal struct MotionState
{
public float RotQuatX; // rotQuatX float
public float RotQuatY; // rotQuatY float
public float RotQuatZ; // rotQuatZ float
public float RotQuatW; // rotQuatW float
public float PosAccelX; // posAccelX float
public float PosAccelY; // posAccelY float
public float PosAccelZ; // posAccelZ float
public float RotVelX; // rotVelX float
public float RotVelY; // rotVelY float
public float RotVelZ; // rotVelZ float
}
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public struct DigitalState
{
[MarshalAs( UnmanagedType.I1 )]
internal byte BState; // bState byte
[MarshalAs( UnmanagedType.I1 )]
internal byte BActive; // bActive byte
public bool Pressed => BState != 0;
public bool Active => BActive != 0;
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct DepotId
{
public uint Value;
public static implicit operator DepotId( uint value )
{
return new DepotId { Value = value };
}
public static implicit operator DepotId( int value )
{
return new DepotId { Value = (uint) value };
}
public static implicit operator uint( DepotId value )
{
return value.Value;
}
public override string ToString() => Value.ToString();
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct DlcInformation
{
public AppId AppId { get; internal set; }
public string Name { get; internal set; }
public bool Available { get; internal set; }
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct DownloadProgress
{
public bool Active;
public ulong BytesDownloaded;
public ulong BytesTotal;
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
/// <summary>
/// Sent for games with enabled anti indulgence / duration control, for enabled users.
/// Lets the game know whether persistent rewards or XP should be granted at normal rate, half rate, or zero rate.
/// </summary>
public struct DurationControl
{
internal DurationControl_t _inner;
/// <summary>
/// appid generating playtime
/// </summary>
public AppId Appid => _inner.Appid;
/// <summary>
/// is duration control applicable to user + game combination
/// </summary>
public bool Applicable => _inner.Applicable;
/// <summary>
/// playtime since most recent 5 hour gap in playtime, only counting up to regulatory limit of playtime, in seconds
/// </summary>
internal TimeSpan PlaytimeInLastFiveHours => TimeSpan.FromSeconds( _inner.CsecsLast5h );
/// <summary>
/// playtime on current calendar day
/// </summary>
internal TimeSpan PlaytimeToday => TimeSpan.FromSeconds( _inner.CsecsLast5h );
/// <summary>
/// recommended progress
/// </summary>
internal DurationControlProgress Progress => _inner.Progress;
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct FileDetails
{
public ulong SizeInBytes;
public string Sha1;
public uint Flags;
}
}

View File

@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public struct Friend
{
public SteamId Id;
public Friend( SteamId steamid )
{
Id = steamid;
}
public override string ToString()
{
return $"{Name} ({Id.ToString()})";
}
/// <summary>
/// Returns true if this is the local user
/// </summary>
public bool IsMe => Id == SteamClient.SteamId;
/// <summary>
/// Return true if this is a friend
/// </summary>
public bool IsFriend => Relationship == Relationship.Friend;
/// <summary>
/// Returns true if you have this user blocked
/// </summary>
public bool IsBlocked => Relationship == Relationship.Blocked;
/// <summary>
/// Return true if this user is playing the game we're running
/// </summary>
public bool IsPlayingThisGame => GameInfo?.GameID == SteamClient.AppId;
/// <summary>
/// Returns true if this friend is online
/// </summary>
public bool IsOnline => State != FriendState.Offline;
/// <summary>
/// Sometimes we don't know the user's name. This will wait until we have
/// downloaded the information on this user.
/// </summary>
public async Task RequestInfoAsync()
{
await SteamFriends.CacheUserInformationAsync( Id, true );
}
/// <summary>
/// Returns true if this friend is marked as away
/// </summary>
public bool IsAway => State == FriendState.Away;
/// <summary>
/// Returns true if this friend is marked as busy
/// </summary>
public bool IsBusy => State == FriendState.Busy;
/// <summary>
/// Returns true if this friend is marked as snoozing
/// </summary>
public bool IsSnoozing => State == FriendState.Snooze;
public Relationship Relationship => SteamFriends.Internal.GetFriendRelationship( Id );
public FriendState State => SteamFriends.Internal.GetFriendPersonaState( Id );
public string Name => SteamFriends.Internal.GetFriendPersonaName( Id );
public IEnumerable<string> NameHistory
{
get
{
for( int i=0; i<32; i++ )
{
var n = SteamFriends.Internal.GetFriendPersonaNameHistory( Id, i );
if ( string.IsNullOrEmpty( n ) )
break;
yield return n;
}
}
}
public int SteamLevel => SteamFriends.Internal.GetFriendSteamLevel( Id );
public FriendGameInfo? GameInfo
{
get
{
FriendGameInfo_t gameInfo = default;
if ( !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) )
return null;
return FriendGameInfo.From( gameInfo );
}
}
public bool IsIn( SteamId group_or_room )
{
return SteamFriends.Internal.IsUserInSource( Id, group_or_room );
}
public struct FriendGameInfo
{
internal ulong GameID; // m_gameID class CGameID
internal uint GameIP; // m_unGameIP uint32
internal ulong SteamIDLobby; // m_steamIDLobby class CSteamID
public int ConnectionPort;
public int QueryPort;
public uint IpAddressRaw => GameIP;
public System.Net.IPAddress IpAddress => Utility.Int32ToIp( GameIP );
public Lobby? Lobby
{
get
{
if ( SteamIDLobby == 0 ) return null;
return new Lobby( SteamIDLobby );
}
}
internal static FriendGameInfo From( FriendGameInfo_t i )
{
return new FriendGameInfo
{
GameID = i.GameID,
GameIP = i.GameIP,
ConnectionPort = i.GamePort,
QueryPort = i.QueryPort,
SteamIDLobby = i.SteamIDLobby,
};
}
}
public async Task<Data.Image?> GetSmallAvatarAsync()
{
return await SteamFriends.GetSmallAvatarAsync( Id );
}
public async Task<Data.Image?> GetMediumAvatarAsync()
{
return await SteamFriends.GetMediumAvatarAsync( Id );
}
public async Task<Data.Image?> GetLargeAvatarAsync()
{
return await SteamFriends.GetLargeAvatarAsync( Id );
}
public string GetRichPresence( string key )
{
var val = SteamFriends.Internal.GetFriendRichPresence( Id, key );
if ( string.IsNullOrEmpty( val ) ) return null;
return val;
}
/// <summary>
/// Invite this friend to the game that we are playing
/// </summary>
public bool InviteToGame( string Text )
{
return SteamFriends.Internal.InviteUserToGame( Id, Text );
}
/// <summary>
/// Sends a message to a Steam friend. Returns true if success
/// </summary>
public bool SendMessage( string message )
{
return SteamFriends.Internal.ReplyToFriendMessage( Id, message );
}
/// <summary>
/// Tries to get download the latest user stats
/// </summary>
/// <returns>True if successful, False if failure</returns>
public async Task<bool> RequestUserStatsAsync()
{
var result = await SteamUserStats.Internal.RequestUserStats( Id );
return result.HasValue && result.Value.Result == Result.OK;
}
/// <summary>
/// Gets a user stat. Must call RequestUserStats first.
/// </summary>
/// <param name="statName">The name of the stat you want to get</param>
/// <param name="defult">Will return this value if not available</param>
/// <returns>The value, or defult if not available</returns>
public float GetStatFloat( string statName, float defult = 0 )
{
var val = defult;
if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) )
return defult;
return val;
}
/// <summary>
/// Gets a user stat. Must call RequestUserStats first.
/// </summary>
/// <param name="statName">The name of the stat you want to get</param>
/// <param name="defult">Will return this value if not available</param>
/// <returns>The value, or defult if not available</returns>
public int GetStatInt( string statName, int defult = 0 )
{
var val = defult;
if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) )
return defult;
return val;
}
/// <summary>
/// Gets a user achievement state. Must call RequestUserStats first.
/// </summary>
/// <param name="statName">The name of the achievement you want to get</param>
/// <param name="defult">Will return this value if not available</param>
/// <returns>The value, or defult if not available</returns>
public bool GetAchievement( string statName, bool defult = false )
{
var val = defult;
if ( !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) )
return defult;
return val;
}
/// <summary>
/// Gets a the time this achievement was unlocked.
/// </summary>
/// <param name="statName">The name of the achievement you want to get</param>
/// <returns>The time unlocked. If it wasn't unlocked, or you haven't downloaded the stats yet - will return DateTime.MinValue</returns>
public DateTime GetAchievementUnlockTime( string statName )
{
bool val = false;
uint time = 0;
if ( !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val )
return DateTime.MinValue;
return Epoch.ToDateTime( time );
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct GameId
{
// TODO - Be able to access these vars
/*
enum EGameIDType
{
k_EGameIDTypeApp = 0,
k_EGameIDTypeGameMod = 1,
k_EGameIDTypeShortcut = 2,
k_EGameIDTypeP2P = 3,
};
# ifdef VALVE_BIG_ENDIAN
unsigned int m_nModID : 32;
unsigned int m_nType : 8;
unsigned int m_nAppID : 24;
#else
unsigned int m_nAppID : 24;
unsigned int m_nType : 8;
unsigned int m_nModID : 32;
#endif
*/
public ulong Value;
public static implicit operator GameId( ulong value )
{
return new GameId { Value = value };
}
public static implicit operator ulong( GameId value )
{
return value.Value;
}
}
}

View File

@@ -0,0 +1,37 @@

namespace Facepunch.Steamworks.Data
{
public struct Image
{
public uint Width;
public uint Height;
public byte[] Data;
public Color GetPixel( int x, int y )
{
if ( x < 0 || x >= Width ) throw new System.Exception( "x out of bounds" );
if ( y < 0 || y >= Height ) throw new System.Exception( "y out of bounds" );
Color c = new Color();
var i = (y * Width + x) * 4;
c.r = Data[i + 0];
c.g = Data[i + 1];
c.b = Data[i + 2];
c.a = Data[i + 3];
return c;
}
public override string ToString()
{
return $"{Width}x{Height} ({Data.Length}bytes)";
}
}
public struct Color
{
public byte r, g, b, a;
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public class InventoryDef : IEquatable<InventoryDef>
{
internal InventoryDefId _id;
internal Dictionary<string, string> _properties;
public InventoryDef( InventoryDefId defId )
{
_id = defId;
}
public int Id => _id.Value;
/// <summary>
/// Shortcut to call GetProperty( "name" )
/// </summary>
public string Name => GetProperty( "name" );
/// <summary>
/// Shortcut to call GetProperty( "description" )
/// </summary>
public string Description => GetProperty( "description" );
/// <summary>
/// Shortcut to call GetProperty( "icon_url" )
/// </summary>
public string IconUrl => GetProperty( "icon_url" );
/// <summary>
/// Shortcut to call GetProperty( "icon_url_large" )
/// </summary>
public string IconUrlLarge => GetProperty( "icon_url_large" );
/// <summary>
/// Shortcut to call GetProperty( "price_category" )
/// </summary>
public string PriceCategory => GetProperty( "price_category" );
/// <summary>
/// Shortcut to call GetProperty( "type" )
/// </summary>
public string Type => GetProperty( "type" );
/// <summary>
/// Returns true if this is an item that generates an item, rather
/// than something that is actual an item
/// </summary>
public bool IsGenerator => Type == "generator";
/// <summary>
/// Shortcut to call GetProperty( "exchange" )
/// </summary>
public string ExchangeSchema => GetProperty( "exchange" );
/// <summary>
/// Get a list of exchanges that are available to make this item
/// </summary>
public InventoryRecipe[] GetRecipes()
{
if ( string.IsNullOrEmpty( ExchangeSchema ) ) return null;
var parts = ExchangeSchema.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries );
return parts.Select( x => InventoryRecipe.FromString( x, this ) ).ToArray();
}
/// <summary>
/// Shortcut to call GetBoolProperty( "marketable" )
/// </summary>
public bool Marketable => GetBoolProperty( "marketable" );
/// <summary>
/// Shortcut to call GetBoolProperty( "tradable" )
/// </summary>
public bool Tradable => GetBoolProperty( "tradable" );
/// <summary>
/// Gets the property timestamp
/// </summary>
public DateTime Created => GetProperty<DateTime>( "timestamp" );
/// <summary>
/// Gets the property modified
/// </summary>
public DateTime Modified => GetProperty<DateTime>( "modified" );
/// <summary>
/// Get a specific property by name
/// </summary>
public string GetProperty( string name )
{
if ( _properties!= null && _properties.TryGetValue( name, out string val ) )
return val;
uint _ = (uint)Helpers.MemoryBufferSize;
if ( !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) )
return null;
if (name == null) //return keys string
return vl;
if ( _properties == null )
_properties = new Dictionary<string, string>();
_properties[name] = vl;
return vl;
}
/// <summary>
/// Read a raw property from the definition schema
/// </summary>
public bool GetBoolProperty( string name )
{
string val = GetProperty( name );
if ( val.Length == 0 ) return false;
if ( val[0] == '0' || val[0] == 'F' || val[0] == 'f' ) return false;
return true;
}
/// <summary>
/// Read a raw property from the definition schema
/// </summary>
public T GetProperty<T>( string name )
{
string val = GetProperty( name );
if ( string.IsNullOrEmpty( val ) )
return default;
try
{
return (T)Convert.ChangeType( val, typeof( T ) );
}
catch ( System.Exception )
{
return default;
}
}
/// <summary>
/// Gets a list of all properties on this item
/// </summary>
public IEnumerable<KeyValuePair<string, string>> Properties
{
get
{
var list = GetProperty( null );
var keys = list.Split( ',' );
foreach ( var key in keys )
{
yield return new KeyValuePair<string, string>( key, GetProperty( key ) );
}
}
}
/// <summary>
/// Returns the price of this item in the local currency (SteamInventory.Currency)
/// </summary>
public int LocalPrice
{
get
{
ulong curprice = 0;
ulong baseprice = 0;
if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) )
return 0;
return (int) curprice;
}
}
public string LocalPriceFormatted => Utility.FormatPrice( SteamInventory.Currency, LocalPrice / 100.0 );
/// <summary>
/// If the price has been discounted, LocalPrice will differ from LocalBasePrice
/// (assumed, this isn't documented)
/// </summary>
public int LocalBasePrice
{
get
{
ulong curprice = 0;
ulong baseprice = 0;
if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) )
return 0;
return (int)baseprice;
}
}
public string LocalBasePriceFormatted => Utility.FormatPrice( SteamInventory.Currency, LocalPrice / 100.0 );
InventoryRecipe[] _recContaining;
/// <summary>
/// Return a list of recepies that contain this item
/// </summary>
public InventoryRecipe[] GetRecipesContainingThis()
{
if ( _recContaining != null ) return _recContaining;
var allRec = SteamInventory.Definitions
.Select( x => x.GetRecipes() )
.Where( x => x != null )
.SelectMany( x => x );
_recContaining = allRec.Where( x => x.ContainsIngredient( this ) ).ToArray();
return _recContaining;
}
public static bool operator ==( InventoryDef a, InventoryDef b )
{
if ( Object.ReferenceEquals( a, null ) )
return Object.ReferenceEquals( b, null );
return a.Equals( b );
}
public static bool operator !=( InventoryDef a, InventoryDef b ) => !(a == b);
public override bool Equals( object p ) => this.Equals( (InventoryDef)p );
public override int GetHashCode() => Id.GetHashCode();
public bool Equals( InventoryDef p )
{
if ( p == null ) return false;
return p.Id == Id;
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public struct InventoryItem : IEquatable<InventoryItem>
{
internal InventoryItemId _id;
internal InventoryDefId _def;
internal SteamItemFlags _flags;
internal ushort _quantity;
internal Dictionary<string, string> _properties;
public InventoryItemId Id => _id;
public InventoryDefId DefId => _def;
public int Quantity => _quantity;
public InventoryDef Def => SteamInventory.FindDefinition( DefId );
/// <summary>
/// Only available if the result set was created with the getproperties
/// </summary>
public Dictionary<string, string> Properties => _properties;
/// <summary>
/// This item is account-locked and cannot be traded or given away.
/// This is an item status flag which is permanently attached to specific item instances
/// </summary>
public bool IsNoTrade => _flags.HasFlag( SteamItemFlags.NoTrade );
/// <summary>
/// The item has been destroyed, traded away, expired, or otherwise invalidated.
/// This is an action confirmation flag which is only set one time, as part of a result set.
/// </summary>
public bool IsRemoved => _flags.HasFlag( SteamItemFlags.Removed );
/// <summary>
/// The item quantity has been decreased by 1 via ConsumeItem API.
/// This is an action confirmation flag which is only set one time, as part of a result set.
/// </summary>
public bool IsConsumed => _flags.HasFlag( SteamItemFlags.Consumed );
/// <summary>
/// Consumes items from a user's inventory. If the quantity of the given item goes to zero, it is permanently removed.
/// Once an item is removed it cannot be recovered.This is not for the faint of heart - if your game implements item removal at all,
/// a high-friction UI confirmation process is highly recommended.ConsumeItem can be restricted to certain item definitions or fully
/// blocked via the Steamworks website to minimize support/abuse issues such as the classic "my brother borrowed my laptop and deleted all of my rare items".
/// </summary>
public async Task<InventoryResult?> ConsumeAsync( int amount = 1 )
{
var sresult = Defines.k_SteamInventoryResultInvalid;
if ( !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) )
return null;
return await InventoryResult.GetAsync( sresult );
}
/// <summary>
/// Split stack into two items
/// </summary>
public async Task<InventoryResult?> SplitStackAsync( int quantity = 1 )
{
var sresult = Defines.k_SteamInventoryResultInvalid;
if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) )
return null;
return await InventoryResult.GetAsync( sresult );
}
/// <summary>
/// Add x units of the target item to this item
/// </summary>
public async Task<InventoryResult?> AddAsync( InventoryItem add, int quantity = 1 )
{
var sresult = Defines.k_SteamInventoryResultInvalid;
if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) )
return null;
return await InventoryResult.GetAsync( sresult );
}
internal static InventoryItem From( SteamItemDetails_t details )
{
var i = new InventoryItem
{
_id = details.ItemId,
_def = details.Definition,
_flags = (SteamItemFlags) details.Flags,
_quantity = details.Quantity
};
return i;
}
internal static Dictionary<string, string> GetProperties( SteamInventoryResult_t result, int index )
{
var strlen = (uint) Helpers.MemoryBufferSize;
if ( !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) )
return null;
var props = new Dictionary<string, string>();
foreach ( var propertyName in propNames.Split( ',' ) )
{
strlen = (uint)Helpers.MemoryBufferSize;
if ( SteamInventory.Internal.GetResultItemProperty( result, (uint)index, propertyName, out var strVal, ref strlen ) )
{
props.Add( propertyName, strVal );
}
}
return props;
}
/// <summary>
/// Will try to return the date that this item was aquired. You need to have for the items
/// with their properties for this to work.
/// </summary>
public DateTime Acquired
{
get
{
if ( Properties == null ) return DateTime.UtcNow;
if ( Properties.TryGetValue( "acquired", out var str ) )
{
var y = int.Parse( str.Substring( 0, 4 ) );
var m = int.Parse( str.Substring( 4, 2 ) );
var d = int.Parse( str.Substring( 6, 2 ) );
var h = int.Parse( str.Substring( 9, 2 ) );
var mn = int.Parse( str.Substring( 11, 2 ) );
var s = int.Parse( str.Substring( 13, 2 ) );
return new DateTime( y, m, d, h, mn, s, DateTimeKind.Utc );
}
return DateTime.UtcNow;
}
}
/// <summary>
/// Tries to get the origin property. Need properties for this to work.
/// Will return a string like "market"
/// </summary>
public string Origin
{
get
{
if ( Properties == null ) return null;
if ( Properties.TryGetValue( "origin", out var str ) )
return str;
return null;
}
}
/// <summary>
/// Small utility class to describe an item with a quantity
/// </summary>
public struct Amount
{
public InventoryItem Item;
public int Quantity;
}
public static bool operator ==( InventoryItem a, InventoryItem b ) => a._id == b._id;
public static bool operator !=( InventoryItem a, InventoryItem b ) => a._id != b._id;
public override bool Equals( object p ) => this.Equals( (InventoryItem)p );
public override int GetHashCode() => _id.GetHashCode();
public bool Equals( InventoryItem p ) => p._id == _id;
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct InventoryPurchaseResult
{
public Result Result;
public ulong OrderID;
public ulong TransID;
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
/// <summary>
/// A structured description of an item exchange
/// </summary>
public struct InventoryRecipe : IEquatable<InventoryRecipe>
{
public struct Ingredient
{
/// <summary>
/// The definition ID of the ingredient.
/// </summary>
public int DefinitionId;
/// <summary>
/// If we don't know about this item definition this might be null.
/// In which case, DefinitionId should still hold the correct id.
/// </summary>
public InventoryDef Definition;
/// <summary>
/// The amount of this item needed. Generally this will be 1.
/// </summary>
public int Count;
internal static Ingredient FromString( string part )
{
var i = new Ingredient();
i.Count = 1;
try
{
if ( part.Contains( "x" ) )
{
var idx = part.IndexOf( 'x' );
int count = 0;
if ( int.TryParse( part.Substring( idx + 1 ), out count ) )
i.Count = count;
part = part.Substring( 0, idx );
}
i.DefinitionId = int.Parse( part );
i.Definition = SteamInventory.FindDefinition( i.DefinitionId );
}
catch ( System.Exception )
{
return i;
}
return i;
}
}
/// <summary>
/// The item that this will create.
/// </summary>
public InventoryDef Result;
/// <summary>
/// The items, with quantity required to create this item.
/// </summary>
public Ingredient[] Ingredients;
public string Source;
internal static InventoryRecipe FromString( string part, InventoryDef Result )
{
var r = new InventoryRecipe
{
Result = Result,
Source = part
};
var parts = part.Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries );
r.Ingredients = parts.Select( x => Ingredient.FromString( x ) ).Where( x => x.DefinitionId != 0 ).ToArray();
return r;
}
internal bool ContainsIngredient( InventoryDef inventoryDef )
{
return Ingredients.Any( x => x.DefinitionId == inventoryDef.Id );
}
public static bool operator ==( InventoryRecipe a, InventoryRecipe b ) => a.GetHashCode() == b.GetHashCode();
public static bool operator !=( InventoryRecipe a, InventoryRecipe b ) => a.GetHashCode() != b.GetHashCode();
public override bool Equals( object p ) => this.Equals( (InventoryRecipe)p );
public override int GetHashCode()
{
return Source.GetHashCode();
}
public bool Equals( InventoryRecipe p ) => p.GetHashCode() == GetHashCode();
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public struct InventoryResult : IDisposable
{
internal SteamInventoryResult_t _id;
public bool Expired { get; internal set; }
internal InventoryResult( SteamInventoryResult_t id, bool expired )
{
_id = id;
Expired = expired;
}
public int ItemCount
{
get
{
uint cnt = 0;
if ( !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) )
return 0;
return (int) cnt;
}
}
/// <summary>
/// Checks whether an inventory result handle belongs to the specified Steam ID.
/// This is important when using Deserialize, to verify that a remote player is not pretending to have a different user's inventory
/// </summary>
public bool BelongsTo( SteamId steamId )
{
return SteamInventory.Internal.CheckResultSteamID( _id, steamId );
}
public InventoryItem[] GetItems( bool includeProperties = false )
{
uint cnt = (uint) ItemCount;
if ( cnt <= 0 ) return null;
var pOutItemsArray = new SteamItemDetails_t[cnt];
if ( !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) )
return null;
var items = new InventoryItem[cnt];
for( int i=0; i< cnt; i++ )
{
var item = InventoryItem.From( pOutItemsArray[i] );
if ( includeProperties )
item._properties = InventoryItem.GetProperties( _id, i );
items[i] = item;
}
return items;
}
public void Dispose()
{
if ( _id.Value == -1 ) return;
SteamInventory.Internal.DestroyResult( _id );
}
internal static async Task<InventoryResult?> GetAsync( SteamInventoryResult_t sresult )
{
var _result = Result.Pending;
while ( _result == Result.Pending )
{
_result = SteamInventory.Internal.GetResultStatus( sresult );
await Task.Delay( 10 );
}
if ( _result != Result.OK && _result != Result.Expired )
return null;
return new InventoryResult( sresult, _result == Result.Expired );
}
/// <summary>
/// Serialized result sets contain a short signature which can't be forged or replayed across different game sessions.
/// A result set can be serialized on the local client, transmitted to other players via your game networking, and
/// deserialized by the remote players.This is a secure way of preventing hackers from lying about posessing
/// rare/high-value items. Serializes a result set with signature bytes to an output buffer.The size of a serialized
/// result depends on the number items which are being serialized.When securely transmitting items to other players,
/// it is recommended to use GetItemsByID first to create a minimal result set.
/// Results have a built-in timestamp which will be considered "expired" after an hour has elapsed.See DeserializeResult
/// for expiration handling.
/// </summary>
public unsafe byte[] Serialize()
{
uint size = 0;
if ( !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) )
return null;
var data = new byte[size];
fixed ( byte* ptr = data )
{
if ( !SteamInventory.Internal.SerializeResult( _id, (IntPtr)ptr, ref size ) )
return null;
}
return data;
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct Leaderboard
{
internal SteamLeaderboard_t Id;
/// <summary>
/// the name of a leaderboard
/// </summary>
public string Name => SteamUserStats.Internal.GetLeaderboardName( Id );
public LeaderboardSort Sort => SteamUserStats.Internal.GetLeaderboardSortMethod( Id );
public LeaderboardDisplay Display => SteamUserStats.Internal.GetLeaderboardDisplayType( Id );
public int EntryCount => SteamUserStats.Internal.GetLeaderboardEntryCount(Id);
static int[] detailsBuffer = new int[64];
static int[] noDetails = Array.Empty<int>();
/// <summary>
/// Submit your score and replace your old score even if it was better
/// </summary>
public async Task<LeaderboardUpdate?> ReplaceScore( int score, int[] details = null )
{
if ( details == null ) details = noDetails;
var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.ForceUpdate, score, details, details.Length );
if ( !r.HasValue ) return null;
return LeaderboardUpdate.From( r.Value );
}
/// <summary>
/// Submit your new score, but won't replace your high score if it's lower
/// </summary>
public async Task<LeaderboardUpdate?> SubmitScoreAsync( int score, int[] details = null )
{
if ( details == null ) details = noDetails;
var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.KeepBest, score, details, details.Length );
if ( !r.HasValue ) return null;
return LeaderboardUpdate.From( r.Value );
}
/// <summary>
/// Attaches a piece of user generated content the user's entry on a leaderboard
/// </summary>
public async Task<Result> AttachUgc( Ugc file )
{
var r = await SteamUserStats.Internal.AttachLeaderboardUGC( Id, file.Handle );
if ( !r.HasValue ) return Result.Fail;
return r.Value.Result;
}
/// <summary>
/// Used to query for a sequential range of leaderboard entries by leaderboard Sort.
/// </summary>
public async Task<LeaderboardEntry[]> GetScoresAsync( int count, int offset = 1 )
{
if ( offset <= 0 ) throw new System.ArgumentException( "Should be 1+", nameof( offset ) );
var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Global, offset, offset + count );
if ( !r.HasValue )
return null;
return await LeaderboardResultToEntries( r.Value );
}
/// <summary>
/// Used to retrieve leaderboard entries relative a user's entry. If there are not enough entries in the leaderboard
/// before or after the user's entry, Steam will adjust the range to try to return the number of entries requested.
/// For example, if the user is #1 on the leaderboard and start is set to -2, end is set to 2, Steam will return the first
/// 5 entries in the leaderboard. If The current user has no entry, this will return null.
/// </summary>
public async Task<LeaderboardEntry[]> GetScoresAroundUserAsync( int start = -10, int end = 10 )
{
var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.GlobalAroundUser, start, end );
if ( !r.HasValue )
return null;
return await LeaderboardResultToEntries( r.Value );
}
/// <summary>
/// Used to retrieve all leaderboard entries for friends of the current user
/// </summary>
public async Task<LeaderboardEntry[]> GetScoresFromFriendsAsync()
{
var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Friends, 0, 0 );
if ( !r.HasValue )
return null;
return await LeaderboardResultToEntries( r.Value );
}
#region util
internal async Task<LeaderboardEntry[]> LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r )
{
if ( r.CEntryCount <= 0 )
return null;
var output = new LeaderboardEntry[r.CEntryCount];
var e = default( LeaderboardEntry_t );
for ( int i = 0; i < output.Length; i++ )
{
if ( SteamUserStats.Internal.GetDownloadedLeaderboardEntry( r.SteamLeaderboardEntries, i, ref e, detailsBuffer, detailsBuffer.Length ) )
{
output[i] = LeaderboardEntry.From( e, detailsBuffer );
}
}
await WaitForUserNames( output );
return output;
}
internal static async Task WaitForUserNames( LeaderboardEntry[] entries)
{
bool gotAll = false;
while ( !gotAll )
{
gotAll = true;
foreach ( var entry in entries )
{
if ( entry.User.Id == 0 ) continue;
if ( !SteamFriends.Internal.RequestUserInformation( entry.User.Id, true ) ) continue;
gotAll = false;
}
await Task.Delay( 1 );
}
}
#endregion
}
}

View File

@@ -0,0 +1,31 @@
using System.Linq;
namespace Facepunch.Steamworks.Data
{
public struct LeaderboardEntry
{
public Friend User;
public int GlobalRank;
public int Score;
public int[] Details;
// UGCHandle_t m_hUGC
internal static LeaderboardEntry From( LeaderboardEntry_t e, int[] detailsBuffer )
{
var r = new LeaderboardEntry
{
User = new Friend( e.SteamIDUser ),
GlobalRank = e.GlobalRank,
Score = e.Score,
Details = null
};
if ( e.CDetails > 0 )
{
r.Details = detailsBuffer.Take( e.CDetails ).ToArray();
}
return r;
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Linq;
namespace Facepunch.Steamworks.Data
{
public struct LeaderboardUpdate
{
public int Score;
public bool Changed;
public int NewGlobalRank;
public int OldGlobalRank;
public int RankChange => NewGlobalRank - OldGlobalRank;
internal static LeaderboardUpdate From( LeaderboardScoreUploaded_t e ) =>
new LeaderboardUpdate
{
Score = e.Score,
Changed = e.ScoreChanged == 1,
NewGlobalRank = e.GlobalRankNew,
OldGlobalRank = e.GlobalRankPrevious
};
}
}

View File

@@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct Lobby
{
public SteamId Id { get; internal set; }
public Lobby( SteamId id )
{
Id = id;
}
/// <summary>
/// Try to join this room. Will return RoomEnter.Success on success,
/// and anything else is a failure
/// </summary>
public async Task<RoomEnter> Join()
{
var result = await SteamMatchmaking.Internal.JoinLobby( Id );
if ( !result.HasValue ) return RoomEnter.Error;
return (RoomEnter) result.Value.EChatRoomEnterResponse;
}
/// <summary>
/// Leave a lobby; this will take effect immediately on the client side
/// other users in the lobby will be notified by a LobbyChatUpdate_t callback
/// </summary>
public void Leave()
{
SteamMatchmaking.Internal.LeaveLobby( Id );
}
/// <summary>
/// Invite another user to the lobby
/// will return true if the invite is successfully sent, whether or not the target responds
/// returns false if the local user is not connected to the Steam servers
/// </summary>
public bool InviteFriend( SteamId steamid )
{
return SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid );
}
/// <summary>
/// returns the number of users in the specified lobby
/// </summary>
public int MemberCount => SteamMatchmaking.Internal.GetNumLobbyMembers( Id );
/// <summary>
/// Returns current members. Need to be in the lobby to see the users.
/// </summary>
public IEnumerable<Friend> Members
{
get
{
for( int i = 0; i < MemberCount; i++ )
{
yield return new Friend( SteamMatchmaking.Internal.GetLobbyMemberByIndex( Id, i ) );
}
}
}
/// <summary>
/// Get data associated with this lobby
/// </summary>
public string GetData( string key )
{
return SteamMatchmaking.Internal.GetLobbyData( Id, key );
}
/// <summary>
/// Get data associated with this lobby
/// </summary>
public bool SetData( string key, string value )
{
if ( key.Length > 255 ) throw new System.ArgumentException( "Key should be < 255 chars", nameof( key ) );
if ( value.Length > 8192 ) throw new System.ArgumentException( "Value should be < 8192 chars", nameof( key ) );
return SteamMatchmaking.Internal.SetLobbyData( Id, key, value );
}
/// <summary>
/// Removes a metadata key from the lobby
/// </summary>
public bool DeleteData( string key )
{
return SteamMatchmaking.Internal.DeleteLobbyData( Id, key );
}
/// <summary>
/// Get all data for this lobby
/// </summary>
public IEnumerable<KeyValuePair<string, string>> Data
{
get
{
var cnt = SteamMatchmaking.Internal.GetLobbyDataCount( Id );
for ( int i =0; i<cnt; i++)
{
if ( SteamMatchmaking.Internal.GetLobbyDataByIndex( Id, i, out var a, out var b ) )
{
yield return new KeyValuePair<string, string>( a, b );
}
}
}
}
/// <summary>
/// Gets per-user metadata for someone in this lobby
/// </summary>
public string GetMemberData( Friend member, string key )
{
return SteamMatchmaking.Internal.GetLobbyMemberData( Id, member.Id, key );
}
/// <summary>
/// Sets per-user metadata (for the local user implicitly)
/// </summary>
public void SetMemberData( string key, string value )
{
SteamMatchmaking.Internal.SetLobbyMemberData( Id, key, value );
}
/// <summary>
/// Sends a string to the chat room
/// </summary>
public bool SendChatString( string message )
{
var data = System.Text.Encoding.UTF8.GetBytes( message );
return SendChatBytes( data );
}
/// <summary>
/// Sends bytes the the chat room
/// this isn't exposed because there's no way to read raw bytes atm,
/// and I figure people can send json if they want something more advanced
/// </summary>
internal unsafe bool SendChatBytes( byte[] data )
{
fixed ( byte* ptr = data )
{
return SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length );
}
}
/// <summary>
/// Refreshes metadata for a lobby you're not necessarily in right now
/// you never do this for lobbies you're a member of, only if your
/// this will send down all the metadata associated with a lobby
/// this is an asynchronous call
/// returns false if the local user is not connected to the Steam servers
/// results will be returned by a LobbyDataUpdate_t callback
/// if the specified lobby doesn't exist, LobbyDataUpdate_t::m_bSuccess will be set to false
/// </summary>
public bool Refresh()
{
return SteamMatchmaking.Internal.RequestLobbyData( Id );
}
/// <summary>
/// Max members able to join this lobby. Cannot be over 250.
/// Can only be set by the owner
/// </summary>
public int MaxMembers
{
get => SteamMatchmaking.Internal.GetLobbyMemberLimit( Id );
set => SteamMatchmaking.Internal.SetLobbyMemberLimit( Id, value );
}
public bool SetPublic()
{
return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public );
}
public bool SetPrivate()
{
return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private );
}
public bool SetInvisible()
{
return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible );
}
public bool SetFriendsOnly()
{
return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly );
}
public bool SetJoinable( bool b )
{
return SteamMatchmaking.Internal.SetLobbyJoinable( Id, b );
}
/// <summary>
/// [SteamID variant]
/// Allows the owner to set the game server associated with the lobby. Triggers the
/// Steammatchmaking.OnLobbyGameCreated event.
/// </summary>
public void SetGameServer( SteamId steamServer )
{
if ( !steamServer.IsValid )
throw new ArgumentException( $"SteamId for server is invalid" );
SteamMatchmaking.Internal.SetLobbyGameServer( Id, 0, 0, steamServer );
}
/// <summary>
/// [IP/Port variant]
/// Allows the owner to set the game server associated with the lobby. Triggers the
/// Steammatchmaking.OnLobbyGameCreated event.
/// </summary>
public void SetGameServer( string ip, ushort port )
{
if ( !IPAddress.TryParse( ip, out IPAddress add ) )
throw new ArgumentException( $"IP address for server is invalid" );
SteamMatchmaking.Internal.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() );
}
/// <summary>
/// Gets the details of the lobby's game server, if set. Returns true if the lobby is
/// valid and has a server set, otherwise returns false.
/// </summary>
public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId )
{
return SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId );
}
/// <summary>
/// You must be the lobby owner to set the owner
/// </summary>
public Friend Owner
{
get => new Friend( SteamMatchmaking.Internal.GetLobbyOwner( Id ) );
set => SteamMatchmaking.Internal.SetLobbyOwner( Id, value.Id );
}
/// <summary>
/// Check if the specified SteamId owns the lobby
/// </summary>
public bool IsOwnedBy( SteamId k ) => Owner.Id == k;
}
}

View File

@@ -0,0 +1,240 @@
using System.Threading.Tasks;
using System.Collections.Generic;
namespace Facepunch.Steamworks.Data
{
public struct LobbyQuery
{
// TODO FILTERS
// AddRequestLobbyListStringFilter
// - WithoutKeyValue
#region Distance Filter
internal LobbyDistanceFilter? distance;
/// <summary>
/// only lobbies in the same immediate region will be returned
/// </summary>
public LobbyQuery FilterDistanceClose()
{
distance = LobbyDistanceFilter.Close;
return this;
}
/// <summary>
/// only lobbies in the same immediate region will be returned
/// </summary>
public LobbyQuery FilterDistanceFar()
{
distance = LobbyDistanceFilter.Far;
return this;
}
/// <summary>
/// only lobbies in the same immediate region will be returned
/// </summary>
public LobbyQuery FilterDistanceWorldwide()
{
distance = LobbyDistanceFilter.Worldwide;
return this;
}
#endregion
#region String key/value filter
internal Dictionary<string, string> stringFilters;
/// <summary>
/// Filter by specified key/value pair; string parameters
/// </summary>
public LobbyQuery WithKeyValue( string key, string value )
{
if ( string.IsNullOrEmpty( key ) )
throw new System.ArgumentException( "Key string provided for LobbyQuery filter is null or empty", nameof( key ) );
if ( key.Length > SteamMatchmaking.MaxLobbyKeyLength )
throw new System.ArgumentException( $"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof( key ) );
if ( stringFilters == null )
stringFilters = new Dictionary<string, string>();
stringFilters.Add( key, value );
return this;
}
#endregion
#region Numerical filters
internal List<NumericalFilter> numericalFilters;
/// <summary>
/// Numerical filter where value is less than the value provided
/// </summary>
public LobbyQuery WithLower( string key, int value )
{
AddNumericalFilter( key, value, LobbyComparison.LessThan );
return this;
}
/// <summary>
/// Numerical filter where value is greater than the value provided
/// </summary>
public LobbyQuery WithHigher( string key, int value )
{
AddNumericalFilter( key, value, LobbyComparison.GreaterThan );
return this;
}
/// <summary>
/// Numerical filter where value must be equal to the value provided
/// </summary>
public LobbyQuery WithEqual( string key, int value )
{
AddNumericalFilter( key, value, LobbyComparison.Equal );
return this;
}
/// <summary>
/// Numerical filter where value must not equal the value provided
/// </summary>
public LobbyQuery WithNotEqual( string key, int value )
{
AddNumericalFilter( key, value, LobbyComparison.NotEqual );
return this;
}
/// <summary>
/// Test key, initialize numerical filter list if necessary, then add new numerical filter
/// </summary>
internal void AddNumericalFilter( string key, int value, LobbyComparison compare )
{
if ( string.IsNullOrEmpty( key ) )
throw new System.ArgumentException( "Key string provided for LobbyQuery filter is null or empty", nameof( key ) );
if ( key.Length > SteamMatchmaking.MaxLobbyKeyLength )
throw new System.ArgumentException( $"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof( key ) );
if ( numericalFilters == null )
numericalFilters = new List<NumericalFilter>();
numericalFilters.Add( new NumericalFilter( key, value, compare ) );
}
#endregion
#region Near value filter
internal Dictionary<string, int> nearValFilters;
/// <summary>
/// Order filtered results according to key/values nearest the provided key/value pair.
/// Can specify multiple near value filters; each successive filter is lower priority than the previous.
/// </summary>
public LobbyQuery OrderByNear( string key, int value )
{
if ( string.IsNullOrEmpty( key ) )
throw new System.ArgumentException( "Key string provided for LobbyQuery filter is null or empty", nameof( key ) );
if ( key.Length > SteamMatchmaking.MaxLobbyKeyLength )
throw new System.ArgumentException( $"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof( key ) );
if ( nearValFilters == null )
nearValFilters = new Dictionary<string, int>();
nearValFilters.Add( key, value );
return this;
}
#endregion
#region Slots Filter
internal int? slotsAvailable;
/// <summary>
/// returns only lobbies with the specified number of slots available
/// </summary>
public LobbyQuery WithSlotsAvailable( int minSlots )
{
slotsAvailable = minSlots;
return this;
}
#endregion
#region Max results filter
internal int? maxResults;
/// <summary>
/// sets how many results to return, the lower the count the faster it is to download the lobby results
/// </summary>
public LobbyQuery WithMaxResults( int max )
{
maxResults = max;
return this;
}
#endregion
void ApplyFilters()
{
if ( distance.HasValue )
{
SteamMatchmaking.Internal.AddRequestLobbyListDistanceFilter( distance.Value );
}
if ( slotsAvailable.HasValue )
{
SteamMatchmaking.Internal.AddRequestLobbyListFilterSlotsAvailable( slotsAvailable.Value );
}
if ( maxResults.HasValue )
{
SteamMatchmaking.Internal.AddRequestLobbyListResultCountFilter( maxResults.Value );
}
if ( stringFilters != null )
{
foreach ( var k in stringFilters )
{
SteamMatchmaking.Internal.AddRequestLobbyListStringFilter( k.Key, k.Value, LobbyComparison.Equal );
}
}
if( numericalFilters != null )
{
foreach ( var n in numericalFilters )
{
SteamMatchmaking.Internal.AddRequestLobbyListNumericalFilter( n.Key, n.Value, n.Comparer );
}
}
if( nearValFilters != null )
{
foreach (var v in nearValFilters )
{
SteamMatchmaking.Internal.AddRequestLobbyListNearValueFilter( v.Key, v.Value );
}
}
}
/// <summary>
/// Run the query, get the matching lobbies
/// </summary>
public async Task<Lobby[]> RequestAsync()
{
ApplyFilters();
LobbyMatchList_t? list = await SteamMatchmaking.Internal.RequestLobbyList();
if ( !list.HasValue || list.Value.LobbiesMatching == 0 )
{
return null;
}
Lobby[] lobbies = new Lobby[list.Value.LobbiesMatching];
for ( int i = 0; i < list.Value.LobbiesMatching; i++ )
{
lobbies[i] = new Lobby { Id = SteamMatchmaking.Internal.GetLobbyByIndex( i ) };
}
return lobbies;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Sequential, Pack = Platform.StructPackSize )]
internal partial struct MatchMakingKeyValuePair
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
internal string Key;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
internal string Value;
}
}

View File

@@ -0,0 +1,16 @@
namespace Facepunch.Steamworks.Data
{
struct NumericalFilter
{
public string Key { get; internal set; }
public int Value { get; internal set; }
public LobbyComparison Comparer { get; internal set; }
internal NumericalFilter ( string k, int v, LobbyComparison c )
{
Key = k;
Value = v;
Comparer = c;
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Facepunch.Steamworks.Data
{
/// <summary>
/// A server query packet.
/// </summary>
public struct OutgoingPacket
{
/// <summary>
/// Target IP address
/// </summary>
public uint Address { get; internal set; }
/// <summary>
/// Target port
/// </summary>
public ushort Port { get; internal set; }
/// <summary>
/// This data is pooled. Make a copy if you don't use it immediately.
/// This buffer is also quite large - so pay attention to Size.
/// </summary>
public byte[] Data { get; internal set; }
/// <summary>
/// Size of the data
/// </summary>
public int Size { get; internal set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace Facepunch.Steamworks.Data
{
public struct P2Packet
{
public SteamId SteamId;
public byte[] Data;
}
}

View File

@@ -0,0 +1,80 @@
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public struct PartyBeacon
{
static ISteamParties Internal => SteamParties.Internal;
internal PartyBeaconID_t Id;
/// <summary>
/// Creator of the beacon
/// </summary>
public SteamId Owner
{
get
{
var owner = default( SteamId );
var location = default( SteamPartyBeaconLocation_t );
Internal.GetBeaconDetails( Id, ref owner, ref location, out _ );
return owner;
}
}
/// <summary>
/// Creator of the beacon
/// </summary>
public string MetaData
{
get
{
var owner = default( SteamId );
var location = default( SteamPartyBeaconLocation_t );
_ = Internal.GetBeaconDetails( Id, ref owner, ref location, out var strVal );
return strVal;
}
}
/// <summary>
/// Will attempt to join the party. If successful will return a connection string.
/// If failed, will return null
/// </summary>
public async Task<string> JoinAsync()
{
var result = await Internal.JoinParty( Id );
if ( !result.HasValue || result.Value.Result != Result.OK )
return null;
return result.Value.ConnectStringUTF8();
}
/// <summary>
/// When a user follows your beacon, Steam will reserve one of the open party slots for them, and send your game a ReservationNotification callback.
/// When that user joins your party, call OnReservationCompleted to notify Steam that the user has joined successfully
/// </summary>
public void OnReservationCompleted( SteamId steamid )
{
Internal.OnReservationCompleted( Id, steamid );
}
/// <summary>
/// To cancel a reservation (due to timeout or user input), call this.
/// Steam will open a new reservation slot.
/// Note: The user may already be in-flight to your game, so it's possible they will still connect and try to join your party.
/// </summary>
public void CancelReservation( SteamId steamid )
{
Internal.CancelReservation( Id, steamid );
}
/// <summary>
/// Turn off the beacon
/// </summary>
public bool Destroy()
{
return Internal.DestroyBeacon( Id );
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace Facepunch.Steamworks.Data
{
/// <summary>
/// Represents a RemotePlaySession from the SteamRemotePlay interface
/// </summary>
public struct RemotePlaySession
{
public uint Id { get; set; }
public override string ToString() => Id.ToString();
public static implicit operator RemotePlaySession( uint value ) => new RemotePlaySession() { Id = value };
public static implicit operator uint( RemotePlaySession value ) => value.Id;
/// <summary>
/// Returns true if this session was valid when created. This will stay true even
/// after disconnection - so be sure to watch SteamRemotePlay.OnSessionDisconnected
/// </summary>
public bool IsValid => Id > 0;
/// <summary>
/// Get the SteamID of the connected user
/// </summary>
public SteamId SteamId => SteamRemotePlay.Internal.GetSessionSteamID( Id );
/// <summary>
/// Get the name of the session client device
/// </summary>
public string ClientName => SteamRemotePlay.Internal.GetSessionClientName( Id );
/// <summary>
/// Get the name of the session client device
/// </summary>
public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal.GetSessionClientFormFactor( Id );
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
public struct Screenshot
{
internal ScreenshotHandle Value;
/// <summary>
/// Tags a user as being visible in the screenshot
/// </summary>
public bool TagUser( SteamId user )
{
return SteamScreenshots.Internal.TagUser( Value, user );
}
/// <summary>
/// Tags a user as being visible in the screenshot
/// </summary>
public bool SetLocation( string location )
{
return SteamScreenshots.Internal.SetLocation( Value, location );
}
/// <summary>
/// Tags a user as being visible in the screenshot
/// </summary>
public bool TagPublishedFile( PublishedFileId file )
{
return SteamScreenshots.Internal.TagPublishedFile( Value, file );
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct ServerInfo : IEquatable<ServerInfo>
{
public string Name { get; set; }
public int Ping { get; set; }
public string GameDir { get; set; }
public string Map { get; set; }
public string Description { get; set; }
public uint AppId { get; set; }
public int Players { get; set; }
public int MaxPlayers { get; set; }
public int BotPlayers { get; set; }
public bool Passworded { get; set; }
public bool Secure { get; set; }
public uint LastTimePlayed { get; set; }
public int Version { get; set; }
public string TagString { get; set; }
public ulong SteamId { get; set; }
public uint AddressRaw { get; set; }
public IPAddress Address { get; set; }
public int ConnectionPort { get; set; }
public int QueryPort { get; set; }
string[] _tags;
/// <summary>
/// Gets the individual tags for this server
/// </summary>
public string[] Tags
{
get
{
if ( _tags == null )
{
if ( !string.IsNullOrEmpty( TagString ) )
{
_tags = TagString.Split( ',' );
}
}
return _tags;
}
}
internal static ServerInfo From( gameserveritem_t item )
{
return new ServerInfo()
{
AddressRaw = item.NetAdr.IP,
Address = Utility.Int32ToIp( item.NetAdr.IP ),
ConnectionPort = item.NetAdr.ConnectionPort,
QueryPort = item.NetAdr.QueryPort,
Name = item.ServerNameUTF8(),
Ping = item.Ping,
GameDir = item.GameDirUTF8(),
Map = item.MapUTF8(),
Description = item.GameDescriptionUTF8(),
AppId = item.AppID,
Players = item.Players,
MaxPlayers = item.MaxPlayers,
BotPlayers = item.BotPlayers,
Passworded = item.Password,
Secure = item.Secure,
LastTimePlayed = item.TimeLastPlayed,
Version = item.ServerVersion,
TagString = item.GameTagsUTF8(),
SteamId = item.SteamID
};
}
public ServerInfo( uint ip, ushort cport, ushort qport, uint timeplayed ) : this()
{
AddressRaw = ip;
Address = Utility.Int32ToIp( ip );
ConnectionPort = cport;
QueryPort = qport;
LastTimePlayed = timeplayed;
}
internal const uint k_unFavoriteFlagNone = 0x00;
internal const uint k_unFavoriteFlagFavorite = 0x01; // this game favorite entry is for the favorites list
internal const uint k_unFavoriteFlagHistory = 0x02; // this game favorite entry is for the history list
/// <summary>
/// Add this server to our history list
/// If we're already in the history list, weill set the last played time to now
/// </summary>
public void AddToHistory()
{
SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current );
}
/// <summary>
/// If this server responds to source engine style queries, we'll be able to get a list of rules here
/// </summary>
public async Task<Dictionary<string, string>> QueryRulesAsync()
{
return await SourceServerQuery.GetRules( this );
}
/// <summary>
/// Remove this server from our history list
/// </summary>
public void RemoveFromHistory()
{
SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory );
}
/// <summary>
/// Add this server to our favourite list
/// </summary>
public void AddToFavourites()
{
SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current );
}
/// <summary>
/// Remove this server from our favourite list
/// </summary>
public void RemoveFromFavourites()
{
SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite );
}
public bool Equals( ServerInfo other )
{
return this.GetHashCode() == other.GetHashCode();
}
public override int GetHashCode()
{
return Address.GetHashCode() + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode();
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks
{
/// <summary>
/// Used to set up the server.
/// The variables in here are all required to be set, and can't be changed once the server is created.
/// </summary>
public struct SteamServerInit
{
public IPAddress IpAddress;
public ushort SteamPort;
public ushort GamePort;
public ushort QueryPort;
public bool Secure;
/// <summary>
/// The version string is usually in the form x.x.x.x, and is used by the master server to detect when the server is out of date.
/// If you go into the dedicated server tab on steamworks you'll be able to server the latest version. If this version number is
/// less than that latest version then your server won't show.
/// </summary>
public string VersionString;
/// <summary>
/// This should be the same directory game where gets installed into. Just the folder name, not the whole path. I.e. "Rust", "Garrysmod".
/// </summary>
public string ModDir;
/// <summary>
/// The game description. Setting this to the full name of your game is recommended.
/// </summary>
public string GameDescription;
/// <summary>
/// Is a dedicated server
/// </summary>
public bool DedicatedServer;
public SteamServerInit( string modDir, string gameDesc )
{
DedicatedServer = true;
ModDir = modDir;
GameDescription = gameDesc;
GamePort = 27015;
QueryPort = 27016;
Secure = true;
VersionString = "1.0.0.0";
IpAddress = null;
SteamPort = 0;
}
/// <summary>
/// Set the Steam quert port
/// </summary>
public SteamServerInit WithRandomSteamPort()
{
SteamPort = (ushort)new Random().Next( 10000, 60000 );
return this;
}
/// <summary>
/// If you pass MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE into usQueryPort, then it causes the game server API to use
/// "GameSocketShare" mode, which means that the game is responsible for sending and receiving UDP packets for the master
/// server updater.
///
/// More info about this here: https://partner.steamgames.com/doc/api/ISteamGameServer#HandleIncomingPacket
/// </summary>
public SteamServerInit WithQueryShareGamePort()
{
QueryPort = 0xFFFF;
return this;
}
}
}

View File

@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Facepunch.Steamworks.Data
{
public struct Stat
{
public string Name { get; internal set; }
public SteamId UserId { get; internal set; }
public Stat( string name )
{
Name = name;
UserId = 0;
}
public Stat( string name, SteamId user )
{
Name = name;
UserId = user;
}
internal void LocalUserOnly( [CallerMemberName] string caller = null )
{
if ( UserId == 0 ) return;
throw new System.Exception( $"Stat.{caller} can only be called for the local user" );
}
public double GetGlobalFloat()
{
double val = 0.0;
if ( SteamUserStats.Internal.GetGlobalStat( Name, ref val ) )
return val;
return 0;
}
public long GetGlobalInt()
{
long val = 0;
SteamUserStats.Internal.GetGlobalStat( Name, ref val );
return val;
}
public async Task<long[]> GetGlobalIntDaysAsync( int days )
{
var result = await SteamUserStats.Internal.RequestGlobalStats( days );
if ( result?.Result != Result.OK ) return null;
var r = new long[days];
var rows = SteamUserStats.Internal.GetGlobalStatHistory( Name, r, (uint) r.Length * sizeof(long) );
if ( days != rows )
r = r.Take( rows ).ToArray();
return r;
}
public async Task<double[]> GetGlobalFloatDays( int days )
{
var result = await SteamUserStats.Internal.RequestGlobalStats( days );
if ( result?.Result != Result.OK ) return null;
var r = new double[days];
var rows = SteamUserStats.Internal.GetGlobalStatHistory( Name, r, (uint)r.Length * sizeof( double ) );
if ( days != rows )
r = r.Take( rows ).ToArray();
return r;
}
public float GetFloat()
{
float val = 0.0f;
if ( UserId > 0 )
{
SteamUserStats.Internal.GetUserStat( UserId, Name, ref val );
}
else
{
SteamUserStats.Internal.GetStat( Name, ref val );
}
return 0;
}
public int GetInt()
{
int val = 0;
if ( UserId > 0 )
{
SteamUserStats.Internal.GetUserStat( UserId, Name, ref val );
}
else
{
SteamUserStats.Internal.GetStat( Name, ref val );
}
return val;
}
public bool Set( int val )
{
LocalUserOnly();
return SteamUserStats.Internal.SetStat( Name, val );
}
public bool Set( float val )
{
LocalUserOnly();
return SteamUserStats.Internal.SetStat( Name, val );
}
public bool Add( int val )
{
LocalUserOnly();
return Set( GetInt() + val );
}
public bool Add( float val )
{
LocalUserOnly();
return Set( GetFloat() + val );
}
public bool UpdateAverageRate( float count, float sessionlength )
{
LocalUserOnly();
return SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength );
}
public bool Store()
{
LocalUserOnly();
return SteamUserStats.Internal.StoreStats();
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks
{
public struct SteamId
{
public ulong Value;
public static implicit operator SteamId( ulong value )
{
return new SteamId { Value = value };
}
public static implicit operator ulong( SteamId value )
{
return value.Value;
}
public override string ToString() => Value.ToString();
public uint AccountId => (uint) (Value & 0xFFFFFFFFul);
public bool IsValid => Value != default;
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Explicit, Pack = Platform.StructPlatformPackSize )]
internal partial struct SteamIPAddress
{
[FieldOffset( 0 )]
public uint Ip4Address; // Host Order
[FieldOffset( 16 )]
internal SteamIPType Type; // m_eType ESteamIPType
public static implicit operator System.Net.IPAddress( SteamIPAddress value )
{
if ( value.Type == SteamIPType.Type4 )
return Utility.Int32ToIp( value.Ip4Address );
throw new System.Exception( $"Oops - can't convert SteamIPAddress to System.Net.IPAddress because no-one coded support for {value.Type} yet" );
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Runtime.InteropServices;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks.Ugc
{
internal struct SteamParamStringArray : IDisposable
{
public SteamParamStringArray_t Value;
IntPtr[] NativeStrings;
IntPtr NativeArray;
public static SteamParamStringArray From( string[] array )
{
var a = new SteamParamStringArray();
a.NativeStrings = new IntPtr[array.Length];
for ( int i = 0; i < a.NativeStrings.Length; i++ )
{
a.NativeStrings[i] = Marshal.StringToHGlobalAnsi( array[i] );
}
var size = Marshal.SizeOf( typeof( IntPtr ) ) * a.NativeStrings.Length;
a.NativeArray = Marshal.AllocHGlobal( size );
Marshal.Copy( a.NativeStrings, 0, a.NativeArray, a.NativeStrings.Length );
a.Value = new SteamParamStringArray_t
{
Strings = a.NativeArray,
NumStrings = array.Length
};
return a;
}
public void Dispose()
{
foreach ( var x in NativeStrings )
Marshal.FreeHGlobal( x );
Marshal.FreeHGlobal( NativeArray );
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Linq;
#pragma warning disable 649
namespace Facepunch.Steamworks.Data
{
public struct Ugc
{
internal UGCHandle_t Handle;
}
}
#pragma warning restore 649

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
using QueryType = Facepunch.Steamworks.Ugc.Query;
namespace Facepunch.Steamworks.Ugc
{
public struct Editor
{
PublishedFileId fileId;
bool creatingNew;
WorkshopFileType creatingType;
AppId consumerAppId;
internal Editor( WorkshopFileType filetype ) : this()
{
this.creatingNew = true;
this.creatingType = filetype;
}
public Editor( PublishedFileId fileId ) : this()
{
this.fileId = fileId;
}
/// <summary>
/// Create a Normal Workshop item that can be subscribed to
/// </summary>
public static Editor NewCommunityFile => new Editor( WorkshopFileType.Community );
/// <summary>
/// Workshop item that is meant to be voted on for the purpose of selling in-game
/// </summary>
public static Editor NewMicrotransactionFile => new Editor( WorkshopFileType.Microtransaction );
public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; }
string Title;
public Editor WithTitle( string t ) { this.Title = t; return this; }
string Description;
public Editor WithDescription( string t ) { this.Description = t; return this; }
string MetaData;
public Editor WithMetaData( string t ) { this.MetaData = t; return this; }
string ChangeLog;
public Editor WithChangeLog( string t ) { this.ChangeLog = t; return this; }
string Language;
public Editor InLanguage( string t ) { this.Language = t; return this; }
string PreviewFile;
public Editor WithPreviewFile( string t ) { this.PreviewFile = t; return this; }
System.IO.DirectoryInfo ContentFolder;
public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; }
public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); }
RemoteStoragePublishedFileVisibility? Visibility;
public Editor WithPublicVisibility() { Visibility = RemoteStoragePublishedFileVisibility.Public; return this; }
public Editor WithFriendsOnlyVisibility() { Visibility = RemoteStoragePublishedFileVisibility.FriendsOnly; return this; }
public Editor WithPrivateVisibility() { Visibility = RemoteStoragePublishedFileVisibility.Private; return this; }
List<string> Tags;
Dictionary<string, string> KeyValueTags;
public Editor WithTag( string tag )
{
if ( Tags == null ) Tags = new List<string>();
Tags.Add( tag );
return this;
}
public Editor AddKeyValueTag(string key, string value)
{
if (KeyValueTags == null) KeyValueTags = new Dictionary<string, string>();
KeyValueTags.Add(key, value);
return this;
}
public async Task<PublishResult> SubmitAsync( IProgress<float> progress = null )
{
var result = default( PublishResult );
progress?.Report( 0 );
if ( consumerAppId == 0 )
consumerAppId = SteamClient.AppId;
//
// Item Create
//
if ( creatingNew )
{
result.Result = Steamworks.Result.Fail;
var created = await SteamUGC.Internal.CreateItem( consumerAppId, creatingType );
if ( !created.HasValue ) return result;
result.Result = created.Value.Result;
if ( result.Result != Steamworks.Result.OK )
return result;
fileId = created.Value.PublishedFileId;
result.NeedsWorkshopAgreement = created.Value.UserNeedsToAcceptWorkshopLegalAgreement;
result.FileId = fileId;
}
result.FileId = fileId;
//
// Item Update
//
{
var handle = SteamUGC.Internal.StartItemUpdate( consumerAppId, fileId );
if ( handle == 0xffffffffffffffff )
return result;
if ( Title != null ) SteamUGC.Internal.SetItemTitle( handle, Title );
if ( Description != null ) SteamUGC.Internal.SetItemDescription( handle, Description );
if ( MetaData != null ) SteamUGC.Internal.SetItemMetadata( handle, MetaData );
if ( Language != null ) SteamUGC.Internal.SetItemUpdateLanguage( handle, Language );
if ( ContentFolder != null ) SteamUGC.Internal.SetItemContent( handle, ContentFolder.FullName );
if ( PreviewFile != null ) SteamUGC.Internal.SetItemPreview( handle, PreviewFile );
if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, Visibility.Value );
if ( Tags != null && Tags.Count > 0 )
{
using ( var a = SteamParamStringArray.From( Tags.ToArray() ) )
{
var val = a.Value;
SteamUGC.Internal.SetItemTags( handle, ref val );
}
}
if (KeyValueTags != null && KeyValueTags.Count > 0)
{
foreach (var keyValueTag in KeyValueTags)
{
SteamUGC.Internal.AddItemKeyValueTag(handle, keyValueTag.Key, keyValueTag.Value);
}
}
result.Result = Steamworks.Result.Fail;
if ( ChangeLog == null )
ChangeLog = "";
var updating = SteamUGC.Internal.SubmitItemUpdate( handle, ChangeLog );
while ( !updating.IsCompleted )
{
if ( progress != null )
{
ulong total = 0;
ulong processed = 0;
var r = SteamUGC.Internal.GetItemUpdateProgress( handle, ref processed, ref total );
switch ( r )
{
case ItemUpdateStatus.PreparingConfig:
{
progress?.Report( 0.1f );
break;
}
case ItemUpdateStatus.PreparingContent:
{
progress?.Report( 0.2f );
break;
}
case ItemUpdateStatus.UploadingContent:
{
var uploaded = total > 0 ? ((float)processed / (float)total) : 0.0f;
progress?.Report( 0.2f + uploaded * 0.7f );
break;
}
case ItemUpdateStatus.UploadingPreviewFile:
{
progress?.Report( 0.8f );
break;
}
case ItemUpdateStatus.CommittingChanges:
{
progress?.Report( 1 );
break;
}
}
}
await Task.Delay( 1000 / 60 );
}
progress?.Report( 1 );
var updated = updating.GetResult();
if ( !updated.HasValue ) return result;
result.Result = updated.Value.Result;
if ( result.Result != Steamworks.Result.OK )
return result;
result.NeedsWorkshopAgreement = updated.Value.UserNeedsToAcceptWorkshopLegalAgreement;
result.FileId = fileId;
}
return result;
}
}
public struct PublishResult
{
public bool Success => Result == Steamworks.Result.OK;
public Steamworks.Result Result;
public PublishedFileId FileId;
/// <summary>
/// https://partner.steamgames.com/doc/features/workshop/implementation#Legal
/// </summary>
public bool NeedsWorkshopAgreement;
}
}

View File

@@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
using QueryType = Facepunch.Steamworks.Ugc.Query;
namespace Facepunch.Steamworks.Ugc
{
public struct Item
{
internal SteamUGCDetails_t details;
internal PublishedFileId _id;
public Item( PublishedFileId id ) : this()
{
_id = id;
}
/// <summary>
/// The actual ID of this file
/// </summary>
public PublishedFileId Id => _id;
/// <summary>
/// The given title of this item
/// </summary>
public string Title { get; internal set; }
/// <summary>
/// The description of this item, in your local language if available
/// </summary>
public string Description { get; internal set; }
/// <summary>
/// A list of tags for this item, all lowercase
/// </summary>
public string[] Tags { get; internal set; }
/// <summary>
/// App Id of the app that created this item
/// </summary>
public AppId CreatorApp => details.CreatorAppID;
/// <summary>
/// App Id of the app that will consume this item.
/// </summary>
public AppId ConsumerApp => details.ConsumerAppID;
/// <summary>
/// User who created this content
/// </summary>
public Friend Owner => new Friend( details.SteamIDOwner );
/// <summary>
/// The bayesian average for up votes / total votes, between [0,1]
/// </summary>
public float Score => details.Score;
/// <summary>
/// Time when the published item was created
/// </summary>
public DateTime Created => Epoch.ToDateTime( details.TimeCreated );
/// <summary>
/// Time when the published item was last updated
/// </summary>
public DateTime Updated => Epoch.ToDateTime( details.TimeUpdated );
/// <summary>
/// True if this is publically visible
/// </summary>
public bool IsPublic => details.Visibility == RemoteStoragePublishedFileVisibility.Public;
/// <summary>
/// True if this item is only visible by friends of the creator
/// </summary>
public bool IsFriendsOnly => details.Visibility == RemoteStoragePublishedFileVisibility.FriendsOnly;
/// <summary>
/// True if this is only visible to the creator
/// </summary>
public bool IsPrivate => details.Visibility == RemoteStoragePublishedFileVisibility.Private;
/// <summary>
/// True if this item has been banned
/// </summary>
public bool IsBanned => details.Banned;
/// <summary>
/// Whether the developer of this app has specifically flagged this item as accepted in the Workshop
/// </summary>
public bool IsAcceptedForUse => details.AcceptedForUse;
/// <summary>
/// The number of upvotes of this item
/// </summary>
public uint VotesUp => details.VotesUp;
/// <summary>
/// The number of downvotes of this item
/// </summary>
public uint VotesDown => details.VotesDown;
public bool IsInstalled => (State & ItemState.Installed) == ItemState.Installed;
public bool IsDownloading => (State & ItemState.Downloading) == ItemState.Downloading;
public bool IsDownloadPending => (State & ItemState.DownloadPending) == ItemState.DownloadPending;
public bool IsSubscribed => (State & ItemState.Subscribed) == ItemState.Subscribed;
public bool NeedsUpdate => (State & ItemState.NeedsUpdate) == ItemState.NeedsUpdate;
public string Directory
{
get
{
ulong size = 0;
uint ts = 0;
if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out var strVal, ref ts ) )
return null;
return strVal;
}
}
/// <summary>
/// Start downloading this item.
/// If this returns false the item isn't getting downloaded.
/// </summary>
public bool Download( bool highPriority = false )
{
return SteamUGC.Download( Id, highPriority );
}
/// <summary>
/// If we're downloading, how big the total download is
/// </summary>
public long DownloadBytesTotal
{
get
{
if ( !NeedsUpdate )
return SizeBytes;
ulong downloaded = 0;
ulong total = 0;
if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) )
return (long) total;
return -1;
}
}
/// <summary>
/// If we're downloading, how much we've downloaded
/// </summary>
public long DownloadBytesDownloaded
{
get
{
if ( !NeedsUpdate )
return SizeBytes;
ulong downloaded = 0;
ulong total = 0;
if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) )
return (long)downloaded;
return -1;
}
}
/// <summary>
/// If we're installed, how big is the install
/// </summary>
public long SizeBytes
{
get
{
if ( NeedsUpdate )
return DownloadBytesDownloaded;
ulong size = 0;
uint ts = 0;
if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) )
return 0;
return (long) size;
}
}
/// <summary>
/// If we're downloading our current progress as a delta betwen 0-1
/// </summary>
public float DownloadAmount
{
get
{
//changed from NeedsUpdate as it's false when validating and redownloading ugc
//possibly similar properties should also be changed
if ( !IsDownloading ) return 1;
ulong downloaded = 0;
ulong total = 0;
if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) && total > 0 )
return (float)((double)downloaded / (double)total);
if ( NeedsUpdate || !IsInstalled || IsDownloading )
return 0;
return 1;
}
}
private ItemState State => (ItemState) SteamUGC.Internal.GetItemState( Id );
public static async Task<Item?> GetAsync( PublishedFileId id, int maxageseconds = 60 * 30 )
{
var file = await Steamworks.Ugc.Query.All
.WithFileId( id )
.WithLongDescription( true )
.GetPageAsync( 1 );
if ( !file.HasValue ) return null;
if ( file.Value.ResultCount == 0 ) return null;
return file.Value.Entries.First();
}
internal static Item From( SteamUGCDetails_t details )
{
var d = new Item
{
_id = details.PublishedFileId,
details = details,
Title = details.TitleUTF8(),
Description = details.DescriptionUTF8(),
Tags = details.TagsUTF8().ToLower().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries )
};
return d;
}
/// <summary>
/// A case insensitive check for tag
/// </summary>
public bool HasTag( string find )
{
if ( Tags.Length == 0 ) return false;
return Tags.Contains( find, StringComparer.OrdinalIgnoreCase );
}
/// <summary>
/// Allows the user to subscribe to this item
/// </summary>
public async Task<bool> Subscribe ()
{
var result = await SteamUGC.Internal.SubscribeItem( _id );
return result?.Result == Result.OK;
}
/// <summary>
/// Allows the user to subscribe to download this item asyncronously
/// If CancellationToken is default then there is 60 seconds timeout
/// Progress will be set to 0-1
/// </summary>
public async Task<bool> DownloadAsync( Action<float> progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default )
{
return await SteamUGC.DownloadAsync( Id, progress, milisecondsUpdateDelay, ct );
}
/// <summary>
/// Allows the user to unsubscribe from this item
/// </summary>
public async Task<bool> Unsubscribe ()
{
var result = await SteamUGC.Internal.UnsubscribeItem( _id );
return result?.Result == Result.OK;
}
/// <summary>
/// Adds item to user favorite list
/// </summary>
public async Task<bool> AddFavorite()
{
var result = await SteamUGC.Internal.AddItemToFavorites(details.ConsumerAppID, _id);
return result?.Result == Result.OK;
}
/// <summary>
/// Removes item from user favorite list
/// </summary>
public async Task<bool> RemoveFavorite()
{
var result = await SteamUGC.Internal.RemoveItemFromFavorites(details.ConsumerAppID, _id);
return result?.Result == Result.OK;
}
/// <summary>
/// Allows the user to rate a workshop item up or down.
/// </summary>
public async Task<Result?> Vote( bool up )
{
var r = await SteamUGC.Internal.SetUserItemVote( Id, up );
return r?.Result;
}
/// <summary>
/// Gets the current users vote on the item
/// </summary>
public async Task<UserItemVote?> GetUserVote()
{
var result = await SteamUGC.Internal.GetUserItemVote(_id);
if (!result.HasValue)
return null;
return UserItemVote.From(result.Value);
}
/// <summary>
/// Return a URL to view this item online
/// </summary>
public string Url => $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}";
/// <summary>
/// The URl to view this item's changelog
/// </summary>
public string ChangelogUrl => $"http://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}";
/// <summary>
/// The URL to view the comments on this item
/// </summary>
public string CommentsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/comments/{Id}";
/// <summary>
/// The URL to discuss this item
/// </summary>
public string DiscussUrl => $"http://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}";
/// <summary>
/// The URL to view this items stats online
/// </summary>
public string StatsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/stats/{Id}";
public ulong NumSubscriptions { get; internal set; }
public ulong NumFavorites { get; internal set; }
public ulong NumFollowers { get; internal set; }
public ulong NumUniqueSubscriptions { get; internal set; }
public ulong NumUniqueFavorites { get; internal set; }
public ulong NumUniqueFollowers { get; internal set; }
public ulong NumUniqueWebsiteViews { get; internal set; }
public ulong ReportScore { get; internal set; }
public ulong NumSecondsPlayed { get; internal set; }
public ulong NumPlaytimeSessions { get; internal set; }
public ulong NumComments { get; internal set; }
public ulong NumSecondsPlayedDuringTimePeriod { get; internal set; }
public ulong NumPlaytimeSessionsDuringTimePeriod { get; internal set; }
/// <summary>
/// The URL to the preview image for this item
/// </summary>
public string PreviewImageUrl { get; internal set; }
/// <summary>
/// Edit this item
/// </summary>
public Ugc.Editor Edit()
{
return new Ugc.Editor( Id );
}
public Result Result => details.Result;
}
}

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Facepunch.Steamworks.Data;
using QueryType = Facepunch.Steamworks.Ugc.Query;
namespace Facepunch.Steamworks.Ugc
{
public struct Query
{
UgcType matchingType;
UGCQuery queryType;
AppId consumerApp;
AppId creatorApp;
string searchText;
public Query( UgcType type ) : this()
{
matchingType = type;
}
public static Query All => new Query( UgcType.All );
public static Query Items => new Query( UgcType.Items );
public static Query ItemsMtx => new Query( UgcType.Items_Mtx );
public static Query ItemsReadyToUse => new Query( UgcType.Items_ReadyToUse );
public static Query Collections => new Query( UgcType.Collections );
public static Query Artwork => new Query( UgcType.Artwork );
public static Query Videos => new Query( UgcType.Videos );
public static Query Screenshots => new Query( UgcType.Screenshots );
public static Query AllGuides => new Query( UgcType.AllGuides );
public static Query WebGuides => new Query( UgcType.WebGuides );
public static Query IntegratedGuides => new Query( UgcType.IntegratedGuides );
public static Query UsableInGame => new Query( UgcType.UsableInGame );
public static Query ControllerBindings => new Query( UgcType.ControllerBindings );
public static Query GameManagedItems => new Query( UgcType.GameManagedItems );
public Query RankedByVote() { queryType = UGCQuery.RankedByVote; return this; }
public Query RankedByPublicationDate() { queryType = UGCQuery.RankedByPublicationDate; return this; }
public Query RankedByAcceptanceDate() { queryType = UGCQuery.AcceptedForGameRankedByAcceptanceDate; return this; }
public Query RankedByTrend() { queryType = UGCQuery.RankedByTrend; return this; }
public Query FavoritedByFriends() { queryType = UGCQuery.FavoritedByFriendsRankedByPublicationDate; return this; }
public Query CreatedByFriends() { queryType = UGCQuery.CreatedByFriendsRankedByPublicationDate; return this; }
public Query RankedByNumTimesReported() { queryType = UGCQuery.RankedByNumTimesReported; return this; }
public Query CreatedByFollowedUsers() { queryType = UGCQuery.CreatedByFollowedUsersRankedByPublicationDate; return this; }
public Query NotYetRated() { queryType = UGCQuery.NotYetRated; return this; }
public Query RankedByTotalVotesAsc() { queryType = UGCQuery.RankedByTotalVotesAsc; return this; }
public Query RankedByVotesUp() { queryType = UGCQuery.RankedByVotesUp; return this; }
public Query RankedByTextSearch() { queryType = UGCQuery.RankedByTextSearch; return this; }
public Query RankedByTotalUniqueSubscriptions() { queryType = UGCQuery.RankedByTotalUniqueSubscriptions; return this; }
public Query RankedByPlaytimeTrend() { queryType = UGCQuery.RankedByPlaytimeTrend; return this; }
public Query RankedByTotalPlaytime() { queryType = UGCQuery.RankedByTotalPlaytime; return this; }
public Query RankedByAveragePlaytimeTrend() { queryType = UGCQuery.RankedByAveragePlaytimeTrend; return this; }
public Query RankedByLifetimeAveragePlaytime() { queryType = UGCQuery.RankedByLifetimeAveragePlaytime; return this; }
public Query RankedByPlaytimeSessionsTrend() { queryType = UGCQuery.RankedByPlaytimeSessionsTrend; return this; }
public Query RankedByLifetimePlaytimeSessions() { queryType = UGCQuery.RankedByLifetimePlaytimeSessions; return this; }
#region UserQuery
SteamId? steamid;
UserUGCList userType;
UserUGCListSortOrder userSort;
internal Query LimitUser( SteamId steamid )
{
if ( steamid.Value == 0 )
steamid = SteamClient.SteamId;
this.steamid = steamid;
return this;
}
public Query WhereUserPublished( SteamId user = default ) { userType = UserUGCList.Published; LimitUser( user ); return this; }
public Query WhereUserVotedOn( SteamId user = default ) { userType = UserUGCList.VotedOn; LimitUser( user ); return this; }
public Query WhereUserVotedUp( SteamId user = default ) { userType = UserUGCList.VotedUp; LimitUser( user ); return this; }
public Query WhereUserVotedDown( SteamId user = default ) { userType = UserUGCList.VotedDown; LimitUser( user ); return this; }
public Query WhereUserWillVoteLater( SteamId user = default ) { userType = UserUGCList.WillVoteLater; LimitUser( user ); return this; }
public Query WhereUserFavorited( SteamId user = default ) { userType = UserUGCList.Favorited; LimitUser( user ); return this; }
public Query WhereUserSubscribed( SteamId user = default ) { userType = UserUGCList.Subscribed; LimitUser( user ); return this; }
public Query WhereUserUsedOrPlayed( SteamId user = default ) { userType = UserUGCList.UsedOrPlayed; LimitUser( user ); return this; }
public Query WhereUserFollowed( SteamId user = default ) { userType = UserUGCList.Followed; LimitUser( user ); return this; }
public Query SortByCreationDate() { userSort = UserUGCListSortOrder.CreationOrderDesc; return this; }
public Query SortByCreationDateAsc() { userSort = UserUGCListSortOrder.CreationOrderAsc; return this; }
public Query SortByTitleAsc() { userSort = UserUGCListSortOrder.TitleAsc; return this; }
public Query SortByUpdateDate() { userSort = UserUGCListSortOrder.LastUpdatedDesc; return this; }
public Query SortBySubscriptionDate() { userSort = UserUGCListSortOrder.SubscriptionDateDesc; return this; }
public Query SortByVoteScore() { userSort = UserUGCListSortOrder.VoteScoreDesc; return this; }
public Query SortByModeration() { userSort = UserUGCListSortOrder.ForModeration; return this; }
public Query WhereSearchText(string searchText) { this.searchText = searchText; return this; }
#endregion
#region Files
PublishedFileId[] Files;
public Query WithFileId( params PublishedFileId[] files )
{
Files = files;
return this;
}
#endregion
public async Task<ResultPage?> 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;
if ( Files != null )
{
handle = SteamUGC.Internal.CreateQueryUGCDetailsRequest( Files, (uint)Files.Length );
}
else if ( steamid.HasValue )
{
handle = SteamUGC.Internal.CreateQueryUserUGCRequest( steamid.Value.AccountId, userType, matchingType, userSort, creatorApp.Value, consumerApp.Value, (uint)page );
}
else
{
handle = SteamUGC.Internal.CreateQueryAllUGCRequest( queryType, matchingType, creatorApp.Value, consumerApp.Value, (uint)page );
}
ApplyReturns(handle);
if (maxCacheAge.HasValue)
{
SteamUGC.Internal.SetAllowCachedResponse(handle, (uint)maxCacheAge.Value);
}
ApplyConstraints( handle );
var result = await SteamUGC.Internal.SendQueryUGCRequest( handle );
if ( !result.HasValue )
return null;
if ( result.Value.Result != Steamworks.Result.OK )
return null;
return new ResultPage
{
Handle = result.Value.Handle,
ResultCount = (int) result.Value.NumResultsReturned,
TotalCount = (int)result.Value.TotalMatchingResults,
CachedData = result.Value.CachedData
};
}
#region SharedConstraints
public QueryType WithType( UgcType type ) { matchingType = type; return this; }
int? maxCacheAge;
public QueryType AllowCachedResponse( int maxSecondsAge ) { maxCacheAge = maxSecondsAge; return this; }
string language;
public QueryType InLanguage( string lang ) { language = lang; return this; }
int? trendDays;
public QueryType WithTrendDays( int days ) { trendDays = days; return this; }
List<string> requiredTags;
bool? matchAnyTag;
List<string> excludedTags;
Dictionary<string, string> requiredKv;
/// <summary>
/// Found items must have at least one of the defined tags
/// </summary>
public QueryType MatchAnyTag() { matchAnyTag = true; return this; }
/// <summary>
/// Found items must have all defined tags
/// </summary>
public QueryType MatchAllTags() { matchAnyTag = false; return this; }
public QueryType WithTag( string tag )
{
if ( requiredTags == null ) requiredTags = new List<string>();
requiredTags.Add( tag );
return this;
}
public QueryType AddRequiredKeyValueTag(string key, string value)
{
if (requiredKv == null) requiredKv = new Dictionary<string, string>();
requiredKv.Add(key, value);
return this;
}
public QueryType WithoutTag( string tag )
{
if ( excludedTags == null ) excludedTags = new List<string>();
excludedTags.Add( tag );
return this;
}
void ApplyConstraints( UGCQueryHandle_t handle )
{
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 );
}
if ( matchAnyTag.HasValue )
{
SteamUGC.Internal.SetMatchAnyTag( handle, matchAnyTag.Value );
}
if ( trendDays.HasValue )
{
SteamUGC.Internal.SetRankedByTrendDays( handle, (uint)trendDays.Value );
}
if ( !string.IsNullOrEmpty( searchText ) )
{
SteamUGC.Internal.SetSearchText( handle, searchText );
}
}
#endregion
#region ReturnValues
bool? WantsReturnOnlyIDs;
public QueryType WithOnlyIDs(bool b) { WantsReturnOnlyIDs = b; return this; }
bool? WantsReturnKeyValueTags;
public QueryType WithKeyValueTag(bool b) { WantsReturnKeyValueTags = b; return this; }
bool? WantsReturnLongDescription;
public QueryType WithLongDescription(bool b) { WantsReturnLongDescription = b; return this; }
bool? WantsReturnMetadata;
public QueryType WithMetadata(bool b) { WantsReturnMetadata = b; return this; }
bool? WantsReturnChildren;
public QueryType WithChildren(bool b) { WantsReturnChildren = b; return this; }
bool? WantsReturnAdditionalPreviews;
public QueryType WithAdditionalPreviews(bool b) { WantsReturnAdditionalPreviews = b; return this; }
bool? WantsReturnTotalOnly;
public QueryType WithTotalOnly(bool b) { WantsReturnTotalOnly = b; return this; }
uint? WantsReturnPlaytimeStats;
public QueryType WithPlaytimeStats(uint unDays) { WantsReturnPlaytimeStats = unDays; return this; }
private void ApplyReturns(UGCQueryHandle_t handle)
{
if (WantsReturnOnlyIDs.HasValue)
{
SteamUGC.Internal.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value);
}
if (WantsReturnKeyValueTags.HasValue)
{
SteamUGC.Internal.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value);
}
if (WantsReturnLongDescription.HasValue)
{
SteamUGC.Internal.SetReturnLongDescription(handle, WantsReturnLongDescription.Value);
}
if (WantsReturnMetadata.HasValue)
{
SteamUGC.Internal.SetReturnMetadata(handle, WantsReturnMetadata.Value);
}
if (WantsReturnChildren.HasValue)
{
SteamUGC.Internal.SetReturnChildren(handle, WantsReturnChildren.Value);
}
if (WantsReturnAdditionalPreviews.HasValue)
{
SteamUGC.Internal.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value);
}
if (WantsReturnTotalOnly.HasValue)
{
SteamUGC.Internal.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value);
}
if (WantsReturnPlaytimeStats.HasValue)
{
SteamUGC.Internal.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value);
}
}
#endregion
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks.Ugc
{
public struct ResultPage : System.IDisposable
{
internal UGCQueryHandle_t Handle;
public int ResultCount;
public int TotalCount;
public bool CachedData;
public IEnumerable<Item> Entries
{
get
{
var details = default( SteamUGCDetails_t );
for ( uint i=0; i< ResultCount; i++ )
{
if ( SteamUGC.Internal.GetQueryUGCResult( Handle, i, ref details ) )
{
var item = Item.From( details );
item.NumSubscriptions = GetStat( i, ItemStatistic.NumSubscriptions );
item.NumFavorites = GetStat( i, ItemStatistic.NumFavorites );
item.NumFollowers = GetStat( i, ItemStatistic.NumFollowers );
item.NumUniqueSubscriptions = GetStat( i, ItemStatistic.NumUniqueSubscriptions );
item.NumUniqueFavorites = GetStat( i, ItemStatistic.NumUniqueFavorites );
item.NumUniqueFollowers = GetStat( i, ItemStatistic.NumUniqueFollowers );
item.NumUniqueWebsiteViews = GetStat( i, ItemStatistic.NumUniqueWebsiteViews );
item.ReportScore = GetStat( i, ItemStatistic.ReportScore );
item.NumSecondsPlayed = GetStat( i, ItemStatistic.NumSecondsPlayed );
item.NumPlaytimeSessions = GetStat( i, ItemStatistic.NumPlaytimeSessions );
item.NumComments = GetStat( i, ItemStatistic.NumComments );
item.NumSecondsPlayedDuringTimePeriod = GetStat( i, ItemStatistic.NumSecondsPlayedDuringTimePeriod );
item.NumPlaytimeSessionsDuringTimePeriod = GetStat( i, ItemStatistic.NumPlaytimeSessionsDuringTimePeriod );
if ( SteamUGC.Internal.GetQueryUGCPreviewURL( Handle, i, out string preview ) )
{
item.PreviewImageUrl = preview;
}
// TODO GetQueryUGCAdditionalPreview
// TODO GetQueryUGCChildren
// TODO GetQueryUGCKeyValueTag
// TODO GetQueryUGCMetadata
yield return item;
}
}
}
}
private ulong GetStat( uint index, ItemStatistic stat )
{
ulong val = 0;
if ( !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) )
return 0;
return val;
}
public void Dispose()
{
if ( Handle > 0 )
{
SteamUGC.Internal.ReleaseQueryUGCRequest( Handle );
Handle = 0;
}
}
}
}

View File

@@ -0,0 +1,21 @@
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks.Ugc
{
public struct UserItemVote
{
public bool VotedUp;
public bool VotedDown;
public bool VoteSkipped;
internal static UserItemVote? From(GetUserItemVoteResult_t result)
{
return new UserItemVote
{
VotedUp = result.VotedUp,
VotedDown = result.VotedDown,
VoteSkipped = result.VoteSkipped
};
}
}
}