38f1ddb...178a853: v0.8.9.1, removed content folder

This commit is contained in:
Joonas Rikkonen
2019-03-18 19:46:58 +02:00
parent 38f1ddb6fe
commit 6c0679c297
1054 changed files with 151673 additions and 144931 deletions
@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using SteamNative;
namespace Facepunch.Steamworks
{
public partial class Inventory
{
/// <summary>
/// An item definition. This describes an item in your Steam inventory, but is
/// not unique to that item. For example, this might be a tshirt, but you might be able to own
/// multiple tshirts.
/// </summary>
public class Definition
{
internal Inventory inventory;
public int Id { get; private set; }
public string Name { get; set; }
public string Description { get; set; }
/// <summary>
/// URL to an image specified by the schema, else empty
/// </summary>
public string IconUrl { get; set; }
/// <summary>
/// URL to an image specified by the schema, else empty
/// </summary>
public string IconLargeUrl { get; set; }
/// <summary>
/// Type can be whatever the schema defines.
/// </summary>
public string Type { get; set; }
/// <summary>
/// If this item can be created using other items this string will contain a comma seperated
/// list of definition ids that can be used, ie "100,101;102x5;103x3,104x3"
/// </summary>
public string ExchangeSchema { get; set; }
/// <summary>
/// A list of recepies for creating this item. Can be null if none.
/// </summary>
public Recipe[] Recipes { get; set; }
/// <summary>
/// A list of recepies we're included in
/// </summary>
public Recipe[] IngredientFor { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
/// <summary>
/// The raw contents of price_category from the schema
/// </summary>
public string PriceCategory { get; set; }
/// <summary>
/// The dollar price from PriceRaw
/// </summary>
public double PriceDollars { get; internal set; }
/// <summary>
/// The price in the local player's currency. The local player's currency
/// is available in Invetory.Currency
/// </summary>
public double LocalPrice { get; internal set; }
/// <summary>
/// Local Price but probably how you want to display it (ie, $3.99, £1.99 etc )
/// </summary>
public string LocalPriceFormatted { get; internal set; }
/// <summary>
/// Returns true if this item can be sold on the marketplace
/// </summary>
public bool Marketable { get; set; }
public bool IsGenerator
{
get { return Type == "generator"; }
}
private Dictionary<string, string> customProperties;
internal Definition( Inventory i, int id )
{
inventory = i;
Id = id;
SetupCommonProperties();
UpdatePrice();
}
/// <summary>
/// If you're manually occupying the Definition (because maybe you're on a server
/// and want to hack around the fact that definitions aren't presented to you),
/// you can use this to set propertis.
/// </summary>
public void SetProperty( string name, string value )
{
if ( customProperties == null )
customProperties = new Dictionary<string, string>();
if ( !customProperties.ContainsKey( name ) )
customProperties.Add( name, value );
else
customProperties[name] = value;
}
/// <summary>
/// Read a raw property from the definition schema
/// </summary>
public T GetProperty<T>( string name )
{
string val = GetStringProperty( name );
if ( string.IsNullOrEmpty( val ) )
return default( T );
try
{
return (T)Convert.ChangeType( val, typeof( T ) );
}
catch ( System.Exception )
{
return default( T );
}
}
/// <summary>
/// Read a raw property from the definition schema
/// </summary>
public string GetStringProperty( string name )
{
string val = string.Empty;
if ( customProperties != null && customProperties.ContainsKey( name ) )
return customProperties[name];
if ( !inventory.inventory.GetItemDefinitionProperty( Id, name, out val ) )
return string.Empty;
return val;
}
/// <summary>
/// Read a raw property from the definition schema
/// </summary>
public bool GetBoolProperty( string name )
{
string val = GetStringProperty( name );
if ( val.Length == 0 ) return false;
if ( val[0] == '0' || val[0] == 'F'|| val[0] == 'f' ) return false;
return true;
}
internal void SetupCommonProperties()
{
Name = GetStringProperty( "name" );
Description = GetStringProperty( "description" );
Created = GetProperty<DateTime>( "timestamp" );
Modified = GetProperty<DateTime>( "modified" );
ExchangeSchema = GetStringProperty( "exchange" );
IconUrl = GetStringProperty( "icon_url" );
IconLargeUrl = GetStringProperty( "icon_url_large" );
Type = GetStringProperty( "type" );
PriceCategory = GetStringProperty( "price_category" );
Marketable = GetBoolProperty( "marketable" );
if ( !string.IsNullOrEmpty( PriceCategory ) )
{
PriceDollars = PriceCategoryToFloat( PriceCategory );
}
}
/// <summary>
/// Trigger an item drop. Call this when it's a good time to award
/// an item drop to a player. This won't automatically result in giving
/// an item to a player. Just call it every minute or so, or on launch.
/// ItemDefinition is usually a generator
/// </summary>
public void TriggerItemDrop()
{
inventory.TriggerItemDrop( Id );
}
/// <summary>
/// Trigger a promo item drop. You can call this at startup, it won't
/// give users multiple promo drops.
/// </summary>
public void TriggerPromoDrop()
{
inventory.TriggerPromoDrop( Id );
}
internal void Link( Definition[] definitions )
{
LinkExchange( definitions );
}
private void LinkExchange( Definition[] definitions )
{
if ( string.IsNullOrEmpty( ExchangeSchema ) ) return;
var parts = ExchangeSchema.Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries );
Recipes = parts.Select( x => Recipe.FromString( x, definitions, this ) ).ToArray();
}
internal void InRecipe( Recipe r )
{
if ( IngredientFor == null )
IngredientFor = new Recipe[0];
var list = new List<Recipe>( IngredientFor );
list.Add( r );
IngredientFor = list.ToArray();
}
internal void UpdatePrice()
{
if ( inventory.inventory.GetItemPrice( Id, out ulong price) )
{
LocalPrice = price / 100.0;
LocalPriceFormatted = Utility.FormatPrice( inventory.Currency, price );
}
else
{
LocalPrice = 0;
LocalPriceFormatted = null;
}
}
}
/// <summary>
/// Trigger a promo item drop. You can call this at startup, it won't
/// give users multiple promo drops.
/// </summary>
public void TriggerPromoDrop( int definitionId )
{
SteamNative.SteamInventoryResult_t result = 0;
inventory.AddPromoItem( ref result, definitionId );
inventory.DestroyResult( result );
}
/// <summary>
/// Trigger an item drop for this user. This is for timed drops. For promo
/// drops use TriggerPromoDrop.
/// </summary>
public void TriggerItemDrop( int definitionId )
{
SteamNative.SteamInventoryResult_t result = 0;
inventory.TriggerItemDrop( ref result, definitionId );
inventory.DestroyResult( result );
}
/// <summary>
/// Grant all promotional items the user is eligible for.
/// </summary>
public void GrantAllPromoItems()
{
SteamNative.SteamInventoryResult_t result = 0;
inventory.GrantPromoItems( ref result );
inventory.DestroyResult( result );
}
/// <summary>
/// Represents a crafting recepie which was defined using the exchange
/// section in the item schema.
/// </summary>
public struct Recipe
{
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 Definition Definition;
/// <summary>
/// The amount of this item needed. Generally this will be 1.
/// </summary>
public int Count;
internal static Ingredient FromString( string part, Definition[] definitions )
{
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 = definitions.FirstOrDefault( x => x.Id == i.DefinitionId );
}
catch ( System.Exception )
{
return i;
}
return i;
}
}
/// <summary>
/// The item that this will create.
/// </summary>
public Definition Result;
/// <summary>
/// The items, with quantity required to create this item.
/// </summary>
public Ingredient[] Ingredients;
internal static Recipe FromString( string part, Definition[] definitions, Definition Result )
{
var r = new Recipe();
r.Result = Result;
var parts = part.Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries );
r.Ingredients = parts.Select( x => Ingredient.FromString( x, definitions ) ).Where( x => x.DefinitionId != 0 ).ToArray();
foreach ( var i in r.Ingredients )
{
if ( i.Definition == null )
continue;
i.Definition.InRecipe( r );
}
return r;
}
}
}
}
@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks
{
public partial class Inventory
{
/// <summary>
/// An item in your inventory.
/// </summary>
public class Item : IEquatable<Item>
{
internal Item( Inventory Inventory, ulong Id, int Quantity, int DefinitionId )
{
this.Inventory = Inventory;
this.Id = Id;
this.Quantity = Quantity;
this.DefinitionId = DefinitionId;
}
public struct Amount
{
public Item Item;
public int Quantity;
}
public ulong Id;
public int Quantity;
public int DefinitionId;
internal Inventory Inventory;
public Dictionary<string, string> Properties { get; internal set; }
private Definition _cachedDefinition;
/// <summary>
/// Careful, this might not be available. Especially on a game server.
/// </summary>
public Definition Definition
{
get
{
if ( _cachedDefinition != null )
return _cachedDefinition;
_cachedDefinition = Inventory.FindDefinition( DefinitionId );
return _cachedDefinition;
}
}
public bool TradeLocked;
public bool Equals(Item other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id == other.Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Item)obj);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public static bool operator ==(Item left, Item right)
{
return Equals(left, right);
}
public static bool operator !=(Item left, Item right)
{
return !Equals(left, right);
}
/// <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 Result Consume( int amount = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !Inventory.inventory.ConsumeItem( ref resultHandle, Id, (uint)amount ) )
return null;
return new Result( Inventory, resultHandle, true );
}
/// <summary>
/// Split stack into two items
/// </summary>
public Result SplitStack( int quantity = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !Inventory.inventory.TransferItemQuantity( ref resultHandle, Id, (uint)quantity, ulong.MaxValue ) )
return null;
return new Result( Inventory, resultHandle, true );
}
SteamNative.SteamInventoryUpdateHandle_t updateHandle;
private void UpdatingProperties()
{
if (!Inventory.EnableItemProperties)
throw new InvalidOperationException("Item properties are disabled.");
if (updateHandle != 0) return;
updateHandle = Inventory.inventory.StartUpdateProperties();
}
public bool SetProperty( string name, string value )
{
UpdatingProperties();
Properties[name] = value.ToString();
return Inventory.inventory.SetProperty(updateHandle, Id, name, value);
}
public bool SetProperty(string name, bool value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Inventory.inventory.SetProperty0(updateHandle, Id, name, value);
}
public bool SetProperty(string name, long value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Inventory.inventory.SetProperty1(updateHandle, Id, name, value);
}
public bool SetProperty(string name, float value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Inventory.inventory.SetProperty2(updateHandle, Id, name, value);
}
/// <summary>
/// Called to finalize any changes made using SetProperty
/// </summary>
public bool SubmitProperties()
{
if (updateHandle == 0)
throw new Exception("SubmitProperties called without updating properties");
try
{
SteamNative.SteamInventoryResult_t result = -1;
if (!Inventory.inventory.SubmitUpdateProperties(updateHandle, ref result))
{
return false;
}
Inventory.inventory.DestroyResult(result);
return true;
}
finally
{
updateHandle = 0;
}
}
}
}
}
@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using SteamNative;
namespace Facepunch.Steamworks
{
public partial class Inventory
{
public class Result : IDisposable
{
internal static Dictionary< int, Result > Pending;
internal Inventory inventory;
private SteamNative.SteamInventoryResult_t Handle { get; set; } = -1;
/// <summary>
/// Called when result is successfully returned
/// </summary>
public Action<Result> OnResult;
/// <summary>
/// Items that exist, or that have been created, or changed
/// </summary>
public Item[] Items { get; internal set; }
/// <summary>
/// Items that have been removed or somehow destroyed
/// </summary>
public Item[] Removed { get; internal set; }
/// <summary>
/// Items that have been consumed, like in a craft or something
/// </summary>
public Item[] Consumed { get; internal set; }
protected bool _gotResult = false;
/// <summary>
/// Returns true if this result is still pending
/// </summary>
public bool IsPending
{
get
{
if ( _gotResult ) return false;
if ( Status() == Callbacks.Result.OK )
{
Fill();
return false;
}
return Status() == Callbacks.Result.Pending;
}
}
internal uint Timestamp { get; private set; }
internal bool IsSuccess
{
get
{
if ( Items != null ) return true;
if ( Handle == -1 ) return false;
return Status() == Callbacks.Result.OK;
}
}
internal Callbacks.Result Status()
{
if ( Handle == -1 ) return Callbacks.Result.InvalidParam;
return (Callbacks.Result)inventory.inventory.GetResultStatus( Handle );
}
internal Result( Inventory inventory, int Handle, bool pending )
{
if ( pending )
{
Pending.Add( Handle, this );
}
this.Handle = Handle;
this.inventory = inventory;
}
internal void Fill()
{
if ( _gotResult )
return;
if ( Items != null )
return;
if ( Status() != Callbacks.Result.OK )
return;
_gotResult = true;
Timestamp = inventory.inventory.GetResultTimestamp( Handle );
SteamNative.SteamItemDetails_t[] steamItems = inventory.inventory.GetResultItems( Handle );
if ( steamItems == null )
return;
var tempItems = new List<Item>();
var tempRemoved = new List<Item>();
var tempConsumed = new List<Item>();
for ( int i=0; i< steamItems.Length; i++ )
{
var item = inventory.ItemFrom( Handle, steamItems[i], i );
if ( item == null )
{
continue;
}
if ( ( steamItems[i].Flags & (int)SteamNative.SteamItemFlags.Removed ) != 0 )
{
tempRemoved.Add(item);
}
else if ((steamItems[i].Flags & (int)SteamNative.SteamItemFlags.Consumed) != 0)
{
tempConsumed.Add(item);
}
else
{
tempItems.Add(item);
}
}
Items = tempItems.ToArray();
Removed = tempRemoved.ToArray();
Consumed = tempConsumed.ToArray();
if ( OnResult != null )
{
OnResult( this );
}
}
internal void OnSteamResult( SteamInventoryResultReady_t data )
{
var success = data.Result == SteamNative.Result.OK;
if ( success )
{
Fill();
}
}
internal unsafe byte[] Serialize()
{
uint size = 0;
if ( !inventory.inventory.SerializeResult( Handle, IntPtr.Zero, out size ) )
return null;
var data = new byte[size];
fixed ( byte* ptr = data )
{
if ( !inventory.inventory.SerializeResult( Handle, (IntPtr)ptr, out size ) )
return null;
}
return data;
}
public void Dispose()
{
if ( Handle != -1 && inventory != null )
{
inventory.inventory.DestroyResult( Handle );
Handle = -1;
}
inventory = null;
}
}
internal Item ItemFrom( SteamInventoryResult_t handle, SteamItemDetails_t detail, int index )
{
Dictionary<string, string> props = null;
if ( EnableItemProperties && inventory.GetResultItemProperty(handle, (uint) index, null, out string propertyNames) )
{
props = new Dictionary<string, string>();
foreach ( var propertyName in propertyNames.Split( ',' ) )
{
if ( inventory.GetResultItemProperty(handle, (uint)index, propertyName, out string propertyValue ) )
{
if (propertyName == "error")
{
Console.Write("Steam item error: ");
Console.WriteLine(propertyValue);
return null;
}
props.Add(propertyName, propertyValue);
}
}
}
var item = new Item( this, detail.ItemId, detail.Quantity, detail.Definition );
item.Properties = props;
return item;
}
}
}
@@ -0,0 +1,487 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using SteamNative;
namespace Facepunch.Steamworks
{
public partial class Inventory : IDisposable
{
/// <summary>
/// Called when the local client's items are first retrieved, and when they change.
/// Obviously not called on the server.
/// </summary>
public event Action OnUpdate;
/// <summary>
/// A list of items owned by this user. You should call Refresh() before trying to access this,
/// and then wait until it's non null or listen to OnUpdate to find out immediately when it's populated.
/// </summary>
public Item[] Items;
/// <summary>
/// You can send this data to a server, or another player who can then deserialize it
/// and get a verified list of items.
/// </summary>
public byte[] SerializedItems;
/// <summary>
/// Serialized data exprires after an hour. This is the time the value in SerializedItems will expire.
/// </summary>
public DateTime SerializedExpireTime;
/// <summary>
/// Controls whether per-item properties (<see cref="Item.Properties"/>) are available or not. Default true.
/// This can improve performance of full inventory updates.
/// </summary>
public bool EnableItemProperties = true;
internal uint LastTimestamp = 0;
internal SteamNative.SteamInventory inventory;
private bool IsServer { get; set; }
public event Action OnDefinitionsUpdated;
public event Action<Result> OnInventoryResultReady;
internal Inventory( BaseSteamworks steamworks, SteamNative.SteamInventory c, bool server )
{
IsServer = server;
inventory = c;
steamworks.RegisterCallback<SteamNative.SteamInventoryDefinitionUpdate_t>( onDefinitionsUpdated );
Result.Pending = new Dictionary<int, Result>();
FetchItemDefinitions();
LoadDefinitions();
UpdatePrices();
if ( !server )
{
steamworks.RegisterCallback<SteamNative.SteamInventoryResultReady_t>( onResultReady );
steamworks.RegisterCallback<SteamNative.SteamInventoryFullUpdate_t>( onFullUpdate );
//
// Get a list of our items immediately
//
Refresh();
}
}
/// <summary>
/// Should get called when the definitions get updated from Steam.
/// </summary>
private void onDefinitionsUpdated( SteamInventoryDefinitionUpdate_t obj )
{
LoadDefinitions();
UpdatePrices();
if ( OnDefinitionsUpdated != null )
{
OnDefinitionsUpdated.Invoke();
}
}
private bool LoadDefinitions()
{
var ids = inventory.GetItemDefinitionIDs();
if ( ids == null )
return false;
Definitions = ids.Select( x => CreateDefinition( x ) ).ToArray();
foreach ( var def in Definitions )
{
def.Link( Definitions );
}
return true;
}
/// <summary>
/// We've received a FULL update
/// </summary>
private void onFullUpdate( SteamInventoryFullUpdate_t data )
{
var result = new Result( this, data.Handle, false );
result.Fill();
onResult( result, true );
}
/// <summary>
/// A generic result has returned.
/// </summary>
private void onResultReady( SteamInventoryResultReady_t data )
{
Result result;
if ( Result.Pending.TryGetValue( data.Handle, out result ) )
{
result.OnSteamResult( data );
if ( data.Result == SteamNative.Result.OK )
{
onResult( result, false );
}
Result.Pending.Remove( data.Handle );
result.Dispose();
}
else
{
result = new Result(this, data.Handle, false);
result.Fill();
}
OnInventoryResultReady?.Invoke(result);
}
private void onResult( Result r, bool isFullUpdate )
{
if ( r.IsSuccess )
{
//
// We only serialize FULL updates
//
if ( isFullUpdate )
{
//
// Only serialize if this result is newer than the last one
//
if ( r.Timestamp < LastTimestamp )
return;
SerializedItems = r.Serialize();
SerializedExpireTime = DateTime.Now.Add( TimeSpan.FromMinutes( 60 ) );
}
LastTimestamp = r.Timestamp;
ApplyResult( r, isFullUpdate );
}
r.Dispose();
r = null;
}
/// <summary>
/// Apply this result to our current stack of Items
/// Here we're trying to keep our stack up to date with whatever happens
/// with the crafting, stacking etc
/// </summary>
internal void ApplyResult( Result r, bool isFullUpdate )
{
if ( IsServer ) return;
if ( r.IsSuccess && r.Items != null )
{
if ( Items == null )
Items = new Item[0];
if (isFullUpdate)
{
Items = r.Items;
}
else
{
// keep the new item instance because it might have a different quantity, properties, etc
Items = Items
.UnionSelect(r.Items, (oldItem, newItem) => newItem)
.Where(x => !r.Removed.Contains(x))
.Where(x => !r.Consumed.Contains(x))
.ToArray();
}
//
// Tell everyone we've got new items!
//
OnUpdate?.Invoke();
}
}
public void Dispose()
{
inventory = null;
Items = null;
SerializedItems = null;
Result.Pending = null;
}
/// <summary>
/// Call this at least every two minutes, every frame doesn't hurt.
/// You should call it when you consider it active play time.
/// IE - your player is alive, and playing.
/// Don't stress on it too much tho cuz it's super hijackable anyway.
/// </summary>
[Obsolete( "No longer required, will be removed in a later version" )]
public void PlaytimeHeartbeat()
{
}
/// <summary>
/// Call this to retrieve the items.
/// Note that if this has already been called it won't
/// trigger a call to OnUpdate unless the items have changed
/// </summary>
public void Refresh()
{
if ( IsServer ) return;
SteamNative.SteamInventoryResult_t request = 0;
if ( !inventory.GetAllItems( ref request ) || request == -1 )
{
Console.WriteLine( "GetAllItems failed!?" );
return;
}
}
/// <summary>
/// Some definitions aren't sent to the client, and all aren't available on the server.
/// Manually getting a Definition here lets you call functions on those definitions.
/// </summary>
public Definition CreateDefinition( int id )
{
return new Definition( this, id );
}
/// <summary>
/// Fetch item definitions in case new ones have been added since we've initialized
/// </summary>
public void FetchItemDefinitions()
{
inventory.LoadItemDefinitions();
}
/// <summary>
/// No need to call this manually if you're calling Update
/// </summary>
public void Update()
{
}
/// <summary>
/// A list of items defined for this app.
/// This should be immediately populated and available.
/// </summary>
public Definition[] Definitions;
/// <summary>
/// A list of item definitions that have prices and so can be bought.
/// </summary>
public IEnumerable<Definition> DefinitionsWithPrices
{
get
{
if ( Definitions == null )
yield break;
for ( int i=0; i< Definitions.Length; i++ )
{
if (Definitions[i].LocalPrice > 0)
yield return Definitions[i];
}
}
}
/// <summary>
/// Utility, given a "1;VLV250" string, convert it to a 2.5
/// </summary>
public static float PriceCategoryToFloat( string price )
{
if ( string.IsNullOrEmpty( price ) )
return 0.0f;
price = price.Replace( "1;VLV", "" );
int iPrice = 0;
if ( !int.TryParse( price, out iPrice ) )
return 0.0f;
return int.Parse( price ) / 100.0f;
}
/// <summary>
/// We might be better off using a dictionary for this, once there's 1000+ definitions
/// </summary>
public Definition FindDefinition( int DefinitionId )
{
if ( Definitions == null ) return null;
for( int i=0; i< Definitions.Length; i++ )
{
if ( Definitions[i].Id == DefinitionId )
return Definitions[i];
}
return null;
}
public unsafe Result Deserialize( byte[] data, int dataLength = -1 )
{
if (data == null)
throw new ArgumentException("data should nto be null");
if ( dataLength == -1 )
dataLength = data.Length;
SteamNative.SteamInventoryResult_t resultHandle = -1;
fixed ( byte* ptr = data )
{
var result = inventory.DeserializeResult( ref resultHandle, (IntPtr) ptr, (uint)dataLength, false );
if ( !result || resultHandle == -1 )
return null;
var r = new Result( this, resultHandle, false );
r.Fill();
return r;
}
}
/// <summary>
/// Crafting! Uses the passed items to buy the target item.
/// You need to have set up the appropriate exchange rules in your item
/// definitions. This assumes all the items passed in aren't stacked.
/// </summary>
public Result CraftItem( Item[] list, Definition target )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
var newItems = new SteamNative.SteamItemDef_t[] { new SteamNative.SteamItemDef_t() { Value = target.Id } };
var newItemC = new uint[] { 1 };
var takeItems = list.Select( x => (SteamNative.SteamItemInstanceID_t)x.Id ).ToArray();
var takeItemsC = list.Select( x => (uint)1 ).ToArray();
if ( !inventory.ExchangeItems( ref resultHandle, newItems, newItemC, 1, takeItems, takeItemsC, (uint)takeItems.Length ) )
return null;
return new Result( this, resultHandle, true );
}
/// <summary>
/// Crafting! Uses the passed items to buy the target item.
/// You need to have set up the appropriate exchange rules in your item
/// definitions.
/// </summary>
public Result CraftItem( Item.Amount[] list, Definition target )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
var newItems = new SteamNative.SteamItemDef_t[] { new SteamNative.SteamItemDef_t() { Value = target.Id } };
var newItemC = new uint[] { 1 };
var takeItems = list.Select( x => (SteamNative.SteamItemInstanceID_t)x.Item.Id ).ToArray();
var takeItemsC = list.Select( x => (uint)x.Quantity ).ToArray();
if ( !inventory.ExchangeItems( ref resultHandle, newItems, newItemC, 1, takeItems, takeItemsC, (uint)takeItems.Length ) )
return null;
return new Result( this, resultHandle, true );
}
/// <summary>
/// Split stack into two items
/// </summary>
public Result SplitStack( Item item, int quantity = 1 )
{
return item.SplitStack( quantity );
}
/// <summary>
/// Stack source item onto dest item
/// </summary>
public Result Stack( Item source, Item dest, int quantity = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !inventory.TransferItemQuantity( ref resultHandle, source.Id, (uint)quantity, dest.Id ) )
return null;
return new Result( this, resultHandle, true );
}
/// <summary>
/// This is used to grant a specific item to the user. This should
/// only be used for development prototyping, from a trusted server,
/// or if you don't care about hacked clients granting arbitrary items.
/// This call can be disabled by a setting on Steamworks.
/// </summary>
public Result GenerateItem( Definition target, int amount )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
var newItems = new SteamNative.SteamItemDef_t[] { new SteamNative.SteamItemDef_t() { Value = target.Id } };
var newItemC = new uint[] { (uint) amount };
if ( !inventory.GenerateItems( ref resultHandle, newItems, newItemC, 1 ) )
return null;
return new Result( this, resultHandle, true );
}
public delegate void StartPurchaseSuccess( ulong orderId, ulong transactionId );
/// <summary>
/// Starts the purchase process for the user, given a "shopping cart" of item definitions that the user would like to buy.
/// The user will be prompted in the Steam Overlay to complete the purchase in their local currency, funding their Steam Wallet if necessary, etc.
///
/// If was succesful the callback orderId and transactionId will be non 0
/// </summary>
public bool StartPurchase( Definition[] items, StartPurchaseSuccess callback = null )
{
var itemGroup = items.GroupBy(x => x.Id);
var newItems = itemGroup.Select( x => new SteamItemDef_t { Value = x.Key } ).ToArray();
var newItemC = itemGroup.Select( x => (uint) x.Count() ).ToArray();
var h = inventory.StartPurchase( newItems, newItemC, (uint) newItemC.Length, ( result, error ) =>
{
if ( error )
{
callback?.Invoke(0, 0);
}
else
{
callback?.Invoke(result.OrderID, result.TransID);
}
});
return h != null;
}
/// <summary>
/// This might be null until Steam has actually recieved the prices.
/// </summary>
public string Currency { get; private set; }
public void UpdatePrices()
{
if (IsServer)
return;
inventory.RequestPrices((result, b) =>
{
Currency = result.Currency;
if ( Definitions == null )
return;
for (int i = 0; i < Definitions.Length; i++)
{
Definitions[i].UpdatePrice();
}
OnUpdate?.Invoke();
});
}
}
}
@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Facepunch.Steamworks
{
public class Networking : IDisposable
{
private static byte[] ReceiveBuffer = new byte[1024 * 64];
public delegate void OnRecievedP2PData( ulong steamid, byte[] data, int dataLength, int channel );
public OnRecievedP2PData OnP2PData;
public Func<ulong, bool> OnIncomingConnection;
public Action<ulong, SessionError> OnConnectionFailed;
private List<int> ListenChannels = new List<int>();
private System.Diagnostics.Stopwatch UpdateTimer = System.Diagnostics.Stopwatch.StartNew();
internal SteamNative.SteamNetworking networking;
internal Networking( BaseSteamworks steamworks, SteamNative.SteamNetworking networking )
{
this.networking = networking;
steamworks.RegisterCallback<SteamNative.P2PSessionRequest_t>( onP2PConnectionRequest );
steamworks.RegisterCallback<SteamNative.P2PSessionConnectFail_t>( onP2PConnectionFailed );
}
public void Dispose()
{
networking = null;
OnIncomingConnection = null;
OnConnectionFailed = null;
OnP2PData = null;
ListenChannels.Clear();
}
/// <summary>
/// No need to call this manually if you're calling Update()
/// </summary>
public void Update()
{
if ( OnP2PData == null )
return;
// Update every 60th of a second
if ( UpdateTimer.Elapsed.TotalSeconds < 1.0 / 60.0 )
return;
UpdateTimer.Reset();
UpdateTimer.Start();
foreach ( var channel in ListenChannels )
{
while ( ReadP2PPacket( channel ) )
{
// Nothing Here.
}
}
}
/// <summary>
/// Enable or disable listening on a specific channel.
/// If you donp't enable the channel we won't listen to it,
/// so you won't be able to receive messages on it.
/// </summary>
public void SetListenChannel( int ChannelId, bool Listen )
{
ListenChannels.RemoveAll( x => x == ChannelId );
if ( Listen )
{
ListenChannels.Add( ChannelId );
}
}
private void onP2PConnectionRequest( SteamNative.P2PSessionRequest_t o )
{
if ( OnIncomingConnection != null )
{
var accept = OnIncomingConnection( o.SteamIDRemote );
if ( accept )
{
networking.AcceptP2PSessionWithUser( o.SteamIDRemote );
}
else
{
networking.CloseP2PSessionWithUser( o.SteamIDRemote );
}
return;
}
//
// Default is to reject the session
//
networking.CloseP2PSessionWithUser( o.SteamIDRemote );
}
public enum SessionError : byte
{
None = 0,
NotRunningApp = 1, // target is not running the same game
NoRightsToApp = 2, // local user doesn't own the app that is running
DestinationNotLoggedIn = 3, // target user isn't connected to Steam
Timeout = 4, // target isn't responding, perhaps not calling AcceptP2PSessionWithUser()
// corporate firewalls can also block this (NAT traversal is not firewall traversal)
// make sure that UDP ports 3478, 4379, and 4380 are open in an outbound direction
Max = 5
};
private void onP2PConnectionFailed( SteamNative.P2PSessionConnectFail_t o )
{
if ( OnConnectionFailed != null )
{
OnConnectionFailed( o.SteamIDRemote, (SessionError) o.P2PSessionError );
}
}
public enum SendType : int
{
/// <summary>
/// Basic UDP send. Packets can't be bigger than 1200 bytes (your typical MTU size). Can be lost, or arrive out of order (rare).
/// The sending API does have some knowledge of the underlying connection, so if there is no NAT-traversal accomplished or
/// there is a recognized adjustment happening on the connection, the packet will be batched until the connection is open again.
/// </summary>
Unreliable = 0,
/// <summary>
/// As above, but if the underlying p2p connection isn't yet established the packet will just be thrown away. Using this on the first
/// packet sent to a remote host almost guarantees the packet will be dropped.
/// This is only really useful for kinds of data that should never buffer up, i.e. voice payload packets
/// </summary>
UnreliableNoDelay = 1,
/// <summary>
/// Reliable message send. Can send up to 1MB of data in a single message.
/// Does fragmentation/re-assembly of messages under the hood, as well as a sliding window for efficient sends of large chunks of data.
/// </summary>
Reliable = 2,
/// <summary>
/// As above, but applies the Nagle algorithm to the send - sends will accumulate
/// until the current MTU size (typically ~1200 bytes, but can change) or ~200ms has passed (Nagle algorithm).
/// Useful if you want to send a set of smaller messages but have the coalesced into a single packet
/// Since the reliable stream is all ordered, you can do several small message sends with k_EP2PSendReliableWithBuffering and then
/// do a normal k_EP2PSendReliable to force all the buffered data to be sent.
/// </summary>
ReliableWithBuffering = 3,
}
public unsafe bool SendP2PPacket( ulong steamid, byte[] data, int length, SendType eP2PSendType = SendType.Reliable, int nChannel = 0 )
{
fixed ( byte* p = data )
{
return networking.SendP2PPacket( steamid, (IntPtr) p, (uint)length, (SteamNative.P2PSend)(int)eP2PSendType, nChannel );
}
}
private unsafe bool ReadP2PPacket( int channel )
{
uint DataAvailable = 0;
if ( !networking.IsP2PPacketAvailable( out DataAvailable, channel ) )
return false;
if ( ReceiveBuffer.Length < DataAvailable )
ReceiveBuffer = new byte[ DataAvailable + 1024 ];
fixed ( byte* p = ReceiveBuffer )
{
SteamNative.CSteamID steamid = 1;
if ( !networking.ReadP2PPacket( (IntPtr)p, DataAvailable, out DataAvailable, out steamid, channel ) || DataAvailable == 0 )
return false;
OnP2PData?.Invoke( steamid, ReceiveBuffer, (int) DataAvailable, channel );
return true;
}
}
/// <summary>
/// This should be called when you're done communicating with a user, as this will free up all of the resources allocated for the connection under-the-hood.
/// If the remote user tries to send data to you again, a new onP2PConnectionRequest callback will be posted.
/// </summary>
public bool CloseSession( ulong steamId )
{
return networking.CloseP2PSessionWithUser( steamId );
}
}
}
@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using SteamNative;
using Result = Facepunch.Steamworks.Callbacks.Result;
namespace Facepunch.Steamworks
{
public partial class Workshop
{
public class Editor
{
internal Workshop workshop;
internal CallbackHandle CreateItem;
internal CallbackHandle SubmitItemUpdate;
internal SteamNative.UGCUpdateHandle_t UpdateHandle;
public ulong Id { get; internal set; }
public string Title { get; set; } = null;
public string Description { get; set; } = null;
public string Folder { get; set; } = null;
public string PreviewImage { get; set; } = null;
public List<string> Tags { get; set; } = new List<string>();
public bool Publishing { get; internal set; }
public ItemType? Type { get; set; }
public string Error { get; internal set; } = null;
public string ChangeNote { get; set; } = "";
public uint WorkshopUploadAppId { get; set; }
public string MetaData { get; set; } = null;
public Dictionary<string, string[]> KeyValues { get; set; } = new Dictionary<string, string[]>();
public enum VisibilityType : int
{
Public = 0,
FriendsOnly = 1,
Private = 2
}
public VisibilityType ? Visibility { get; set; }
public bool NeedToAgreeToWorkshopLegal { get; internal set; }
/// <summary>
/// Called when published changes have finished being submitted.
/// </summary>
public event Action<Result> OnChangesSubmitted;
public double Progress
{
get
{
var bt = BytesTotal;
if (bt == 0) return 0;
return (double)BytesUploaded / (double)bt;
}
}
private int bytesUploaded = 0;
public int BytesUploaded
{
get
{
if ( !Publishing ) return bytesUploaded;
if (UpdateHandle == 0) return bytesUploaded;
ulong b = 0;
ulong t = 0;
workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out b, out t );
bytesUploaded = Math.Max( bytesUploaded, (int) b );
return (int)bytesUploaded;
}
}
private int bytesTotal = 0;
public int BytesTotal
{
get
{
if ( !Publishing ) return bytesTotal;
if (UpdateHandle == 0 ) return bytesTotal;
ulong b = 0;
ulong t = 0;
workshop.steamworks.native.ugc.GetItemUpdateProgress( UpdateHandle, out b, out t );
bytesTotal = Math.Max(bytesTotal, (int)t);
return (int)bytesTotal;
}
}
public void Publish()
{
bytesUploaded = 0;
bytesTotal = 0;
Publishing = true;
Error = null;
if ( Id == 0 )
{
StartCreatingItem();
return;
}
PublishChanges();
}
private void StartCreatingItem()
{
if ( !Type.HasValue )
throw new System.Exception( "Editor.Type must be set when creating a new item!" );
if ( WorkshopUploadAppId == 0 )
throw new Exception( "WorkshopUploadAppId should not be 0" );
CreateItem = workshop.ugc.CreateItem( WorkshopUploadAppId, (SteamNative.WorkshopFileType)(uint)Type, OnItemCreated );
}
private void OnItemCreated( SteamNative.CreateItemResult_t obj, bool Failed )
{
NeedToAgreeToWorkshopLegal = obj.UserNeedsToAcceptWorkshopLegalAgreement;
CreateItem.Dispose();
CreateItem = null;
if ( obj.Result == SteamNative.Result.OK && !Failed )
{
Error = null;
Id = obj.PublishedFileId;
PublishChanges();
return;
}
Error = $"Error creating new file: {obj.Result} ({obj.PublishedFileId})";
Publishing = false;
OnChangesSubmitted?.Invoke( (Result) obj.Result );
}
private void PublishChanges()
{
if ( WorkshopUploadAppId == 0 )
throw new Exception( "WorkshopUploadAppId should not be 0" );
UpdateHandle = workshop.ugc.StartItemUpdate(WorkshopUploadAppId, Id );
if ( Title != null )
workshop.ugc.SetItemTitle( UpdateHandle, Title );
if ( Description != null )
workshop.ugc.SetItemDescription( UpdateHandle, Description );
if ( Folder != null )
{
var info = new System.IO.DirectoryInfo( Folder );
if ( !info.Exists )
throw new System.Exception( $"Folder doesn't exist ({Folder})" );
workshop.ugc.SetItemContent( UpdateHandle, Folder );
}
if ( Tags != null && Tags.Count > 0 )
workshop.ugc.SetItemTags( UpdateHandle, Tags.ToArray() );
if ( Visibility.HasValue )
workshop.ugc.SetItemVisibility( UpdateHandle, (SteamNative.RemoteStoragePublishedFileVisibility)(uint)Visibility.Value );
if ( PreviewImage != null )
{
var info = new System.IO.FileInfo( PreviewImage );
if ( !info.Exists )
throw new System.Exception( $"PreviewImage doesn't exist ({PreviewImage})" );
if ( info.Length >= 1024 * 1024 )
throw new System.Exception( $"PreviewImage should be under 1MB ({info.Length})" );
workshop.ugc.SetItemPreview( UpdateHandle, PreviewImage );
}
if ( MetaData != null )
{
workshop.ugc.SetItemMetadata( UpdateHandle, MetaData );
}
if ( KeyValues != null )
{
foreach ( var key in KeyValues )
{
foreach ( var value in key.Value )
{
workshop.ugc.AddItemKeyValueTag( UpdateHandle, key.Key, value );
}
}
}
/*
workshop.ugc.SetItemUpdateLanguage( UpdateId, const char *pchLanguage ) = 0; // specify the language of the title or description that will be set
workshop.ugc.RemoveItemKeyValueTags( UpdateId, const char *pchKey ) = 0; // remove any existing key-value tags with the specified key
workshop.ugc.AddItemPreviewFile( UpdateId, const char *pszPreviewFile, EItemPreviewType type ) = 0; // add preview file for this item. pszPreviewFile points to local file, which must be under 1MB in size
workshop.ugc.AddItemPreviewVideo( UpdateId, const char *pszVideoID ) = 0; // add preview video for this item
workshop.ugc.UpdateItemPreviewFile( UpdateId, uint32 index, const char *pszPreviewFile ) = 0; // updates an existing preview file for this item. pszPreviewFile points to local file, which must be under 1MB in size
workshop.ugc.UpdateItemPreviewVideo( UpdateId, uint32 index, const char *pszVideoID ) = 0; // updates an existing preview video for this item
workshop.ugc.RemoveItemPreview( UpdateId, uint32 index ) = 0; // remove a preview by index starting at 0 (previews are sorted)
*/
SubmitItemUpdate = workshop.ugc.SubmitItemUpdate( UpdateHandle, ChangeNote, OnChangesSubmittedInternal );
}
private void OnChangesSubmittedInternal( SteamNative.SubmitItemUpdateResult_t obj, bool Failed )
{
if ( Failed )
throw new System.Exception( "CreateItemResult_t Failed" );
UpdateHandle = 0;
SubmitItemUpdate = null;
NeedToAgreeToWorkshopLegal = obj.UserNeedsToAcceptWorkshopLegalAgreement;
Publishing = false;
Error = obj.Result != SteamNative.Result.OK
? $"Error publishing changes: {obj.Result} ({NeedToAgreeToWorkshopLegal})"
: null;
OnChangesSubmitted?.Invoke( (Result) obj.Result );
}
public void Delete()
{
workshop.ugc.DeleteItem( Id );
Id = 0;
}
}
}
}
@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using SteamNative;
namespace Facepunch.Steamworks
{
public partial class Workshop
{
public class Item
{
internal Workshop workshop;
internal Action onInstalled;
public string Description { get; private set; }
public ulong Id { get; private set; }
public ulong OwnerId { get; private set; }
public float Score { get; private set; }
public string[] Tags { get; private set; }
public string Title { get; private set; }
public uint VotesDown { get; private set; }
public uint VotesUp { get; private set; }
public DateTime Modified { get; private set; }
public DateTime Created { get; private set; }
public Item( ulong Id, Workshop workshop )
{
this.Id = Id;
this.workshop = workshop;
}
internal static Item From( SteamNative.SteamUGCDetails_t details, Workshop workshop )
{
var item = new Item( details.PublishedFileId, workshop);
item.Title = details.Title;
item.Description = details.Description;
item.OwnerId = details.SteamIDOwner;
item.Tags = details.Tags.Split( ',' ).Select( x=> x.ToLower() ).ToArray();
item.Score = details.Score;
item.VotesUp = details.VotesUp;
item.VotesDown = details.VotesDown;
item.Modified = Utility.Epoch.ToDateTime( details.TimeUpdated );
item.Created = Utility.Epoch.ToDateTime( details.TimeCreated );
return item;
}
public bool Download( bool highPriority = true, Action onInstalled = null )
{
if ( Installed ) return true;
if ( Downloading ) return true;
if ( !workshop.ugc.DownloadItem( Id, highPriority ) )
{
Console.WriteLine( "Download Failed" );
return false;
}
this.onInstalled = onInstalled;
workshop.OnFileDownloaded += OnFileDownloaded;
workshop.OnItemInstalled += OnItemInstalled;
return true;
}
public void Subscribe()
{
workshop.ugc.SubscribeItem(Id);
SubscriptionCount++;
}
public void UnSubscribe()
{
workshop.ugc.UnsubscribeItem(Id);
SubscriptionCount--;
}
private void OnFileDownloaded( ulong fileid, Callbacks.Result result )
{
if ( fileid != Id ) return;
workshop.OnFileDownloaded -= OnFileDownloaded;
}
private void OnItemInstalled( ulong fileid )
{
if ( fileid != Id ) return;
onInstalled?.Invoke();
workshop.OnItemInstalled -= OnItemInstalled;
}
public ulong BytesDownloaded { get { UpdateDownloadProgress(); return _BytesDownloaded; } }
public ulong BytesTotalDownload { get { UpdateDownloadProgress(); return _BytesTotal; } }
public double DownloadProgress
{
get
{
UpdateDownloadProgress();
if ( _BytesTotal == 0 ) return 0;
return (double)_BytesDownloaded / (double)_BytesTotal;
}
}
public bool Installed { get { return ( State & ItemState.Installed ) != 0; } }
public bool Downloading { get { return ( State & ItemState.Downloading ) != 0; } }
public bool DownloadPending { get { return ( State & ItemState.DownloadPending ) != 0; } }
public bool Subscribed { get { return ( State & ItemState.Subscribed ) != 0; } }
public bool NeedsUpdate { get { return ( State & ItemState.NeedsUpdate ) != 0; } }
private SteamNative.ItemState State { get { return ( SteamNative.ItemState) workshop.ugc.GetItemState( Id ); } }
private DirectoryInfo _directory;
public DirectoryInfo Directory
{
get
{
if ( _directory != null )
return _directory;
if ( !Installed )
return null;
ulong sizeOnDisk;
string folder;
uint timestamp;
if ( workshop.ugc.GetItemInstallInfo( Id, out sizeOnDisk, out folder, out timestamp ) )
{
_directory = new DirectoryInfo( folder );
Size = sizeOnDisk;
if ( !_directory.Exists )
{
// Size = 0;
// _directory = null;
}
}
return _directory;
}
}
public ulong Size { get; private set; }
private ulong _BytesDownloaded, _BytesTotal;
internal void UpdateDownloadProgress()
{
workshop.ugc.GetItemDownloadInfo( Id, out _BytesDownloaded, out _BytesTotal );
}
private int YourVote = 0;
public void VoteUp()
{
if ( YourVote == 1 ) return;
if ( YourVote == -1 ) VotesDown--;
VotesUp++;
workshop.ugc.SetUserItemVote( Id, true );
YourVote = 1;
}
public void VoteDown()
{
if ( YourVote == -1 ) return;
if ( YourVote == 1 ) VotesUp--;
VotesDown++;
workshop.ugc.SetUserItemVote( Id, false );
YourVote = -1;
}
public Editor Edit()
{
return workshop.EditItem( Id );
}
/// <summary>
/// Return a URL to view this item online
/// </summary>
public string Url { get { return string.Format( "http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={0}", Id ); } }
public string ChangelogUrl { get { return string.Format( "http://steamcommunity.com/sharedfiles/filedetails/changelog/{0}", Id ); } }
public string CommentsUrl { get { return string.Format( "http://steamcommunity.com/sharedfiles/filedetails/comments/{0}", Id ); } }
public string DiscussUrl { get { return string.Format( "http://steamcommunity.com/sharedfiles/filedetails/discussions/{0}", Id ); } }
public string StartsUrl { get { return string.Format( "http://steamcommunity.com/sharedfiles/filedetails/stats/{0}", Id ); } }
public int SubscriptionCount { get; internal set; }
public int FavouriteCount { get; internal set; }
public int FollowerCount { get; internal set; }
public int WebsiteViews { get; internal set; }
public int ReportScore { get; internal set; }
public string PreviewImageUrl { get; internal set; }
string _ownerName = null;
public string OwnerName
{
get
{
if ( _ownerName == null && workshop.friends != null )
{
_ownerName = workshop.friends.GetName( OwnerId );
if ( _ownerName == "[unknown]" )
{
_ownerName = null;
return string.Empty;
}
}
if ( _ownerName == null )
return string.Empty;
return _ownerName;
}
}
}
}
}
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Facepunch.Steamworks
{
public partial class Workshop
{
public class Query : IDisposable
{
internal const int SteamResponseSize = 50;
internal SteamNative.UGCQueryHandle_t Handle;
internal SteamNative.CallbackHandle Callback;
/// <summary>
/// The AppId you're querying. This defaults to this appid.
/// </summary>
public uint AppId { get; set; }
/// <summary>
/// The AppId of the app used to upload the item. This defaults to 0
/// which means all/any.
/// </summary>
public uint UploaderAppId { get; set; }
public QueryType QueryType { get; set; } = QueryType.Items;
public Order Order { get; set; } = Order.RankedByVote;
public string SearchText { get; set; }
public Item[] Items { get; set; }
public int TotalResults { get; set; }
public ulong? UserId { get; set; }
/// <summary>
/// If order is RankedByTrend, this value represents how many days to take
/// into account.
/// </summary>
public int RankedByTrendDays { get; set; }
public UserQueryType UserQueryType { get; set; } = UserQueryType.Published;
/// <summary>
/// Called when the query finishes
/// </summary>
public Action<Query> OnResult;
/// <summary>
/// Page starts at 1 !!
/// </summary>
public int Page { get; set; } = 1;
public int PerPage { get; set; } = SteamResponseSize;
internal Workshop workshop;
internal Friends friends;
private int _resultPage = 0;
private int _resultsRemain = 0;
private int _resultSkip = 0;
private List<Item> _results;
public void Run()
{
if ( Callback != null )
return;
if ( Page <= 0 )
throw new System.Exception( "Page should be 1 or above" );
var actualOffset = ((Page-1) * PerPage);
TotalResults = 0;
_resultSkip = actualOffset % SteamResponseSize;
_resultsRemain = PerPage;
_resultPage = (int) Math.Floor( (float) actualOffset / (float)SteamResponseSize );
_results = new List<Item>();
RunInternal();
}
unsafe void RunInternal()
{
if ( FileId.Count != 0 )
{
var fileArray = FileId.Select( x => (SteamNative.PublishedFileId_t)x ).ToArray();
_resultsRemain = fileArray.Length;
Handle = workshop.ugc.CreateQueryUGCDetailsRequest( fileArray );
}
else if ( UserId.HasValue )
{
uint accountId = (uint)( UserId.Value & 0xFFFFFFFFul );
Handle = workshop.ugc.CreateQueryUserUGCRequest( accountId, (SteamNative.UserUGCList)( int)UserQueryType, (SteamNative.UGCMatchingUGCType)( int)QueryType, SteamNative.UserUGCListSortOrder.LastUpdatedDesc, UploaderAppId, AppId, (uint)_resultPage + 1 );
}
else
{
Handle = workshop.ugc.CreateQueryAllUGCRequest( (SteamNative.UGCQuery)(int)Order, (SteamNative.UGCMatchingUGCType)(int)QueryType, UploaderAppId, AppId, (uint)_resultPage + 1 );
}
if ( !string.IsNullOrEmpty( SearchText ) )
workshop.ugc.SetSearchText( Handle, SearchText );
foreach ( var tag in RequireTags )
workshop.ugc.AddRequiredTag( Handle, tag );
if ( RequireTags.Count > 0 )
workshop.ugc.SetMatchAnyTag( Handle, !RequireAllTags );
if ( RankedByTrendDays > 0 )
workshop.ugc.SetRankedByTrendDays( Handle, (uint) RankedByTrendDays );
foreach ( var tag in ExcludeTags )
workshop.ugc.AddExcludedTag( Handle, tag );
Callback = workshop.ugc.SendQueryUGCRequest( Handle, ResultCallback );
}
void ResultCallback( SteamNative.SteamUGCQueryCompleted_t data, bool bFailed )
{
if ( bFailed )
throw new System.Exception( "bFailed!" );
var gotFiles = 0;
for ( int i = 0; i < data.NumResultsReturned; i++ )
{
if ( _resultSkip > 0 )
{
_resultSkip--;
continue;
}
SteamNative.SteamUGCDetails_t details = new SteamNative.SteamUGCDetails_t();
if ( !workshop.ugc.GetQueryUGCResult( data.Handle, (uint)i, ref details ) )
continue;
// We already have this file, so skip it
if ( _results.Any( x => x.Id == details.PublishedFileId ) )
continue;
var item = Item.From( details, workshop );
item.SubscriptionCount = GetStat( data.Handle, i, ItemStatistic.NumSubscriptions );
item.FavouriteCount = GetStat( data.Handle, i, ItemStatistic.NumFavorites );
item.FollowerCount = GetStat( data.Handle, i, ItemStatistic.NumFollowers );
item.WebsiteViews = GetStat( data.Handle, i, ItemStatistic.NumUniqueWebsiteViews );
item.ReportScore = GetStat( data.Handle, i, ItemStatistic.ReportScore );
string url = null;
if ( workshop.ugc.GetQueryUGCPreviewURL( data.Handle, (uint)i, out url ) )
item.PreviewImageUrl = url;
_results.Add( item );
_resultsRemain--;
gotFiles++;
if ( _resultsRemain <= 0 )
break;
}
TotalResults = TotalResults > data.TotalMatchingResults ? TotalResults : (int)data.TotalMatchingResults;
Callback.Dispose();
Callback = null;
_resultPage++;
if ( _resultsRemain > 0 && gotFiles > 0 )
{
RunInternal();
}
else
{
Items = _results.ToArray();
if ( OnResult != null )
{
OnResult( this );
}
}
}
private int GetStat( ulong handle, int index, ItemStatistic stat )
{
ulong val = 0;
if ( !workshop.ugc.GetQueryUGCStatistic( handle, (uint)index, (SteamNative.ItemStatistic)(uint)stat, out val ) )
return 0;
return (int) val;
}
public bool IsRunning
{
get { return Callback != null; }
}
/// <summary>
/// Only return items with these tags
/// </summary>
public List<string> RequireTags { get; set; } = new List<string>();
/// <summary>
/// If true, return items that have all RequireTags
/// If false, return items that have any tags in RequireTags
/// </summary>
public bool RequireAllTags { get; set; } = false;
/// <summary>
/// Don't return any items with this tag
/// </summary>
public List<string> ExcludeTags { get; set; } = new List<string>();
/// <summary>
/// If you're querying for a particular file or files, add them to this.
/// </summary>
public List<ulong> FileId { get; set; } = new List<ulong>();
/// <summary>
/// Don't call this in production!
/// </summary>
public void Block()
{
const int sleepMs = 10;
workshop.steamworks.Update();
while ( IsRunning )
{
#if NET_CORE
System.Threading.Tasks.Task.Delay( sleepMs ).Wait();
#else
System.Threading.Thread.Sleep( sleepMs );
#endif
workshop.steamworks.Update();
}
}
public void Dispose()
{
// ReleaseQueryUGCRequest
}
}
private enum ItemStatistic : uint
{
NumSubscriptions = 0,
NumFavorites = 1,
NumFollowers = 2,
NumUniqueSubscriptions = 3,
NumUniqueFavorites = 4,
NumUniqueFollowers = 5,
NumUniqueWebsiteViews = 6,
ReportScore = 7,
};
}
}
@@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using SteamNative;
namespace Facepunch.Steamworks
{
/// <summary>
/// Allows you to interact with Steam's UGC stuff (User Generated Content).
/// To put simply, this allows you to upload a folder of files to Steam.
///
/// To upload a new file use CreateItem. This returns an Editor object.
/// This object is also used to edit existing items.
///
/// To get a list of items you can call CreateQuery. From there you can download
/// an item and retrieve the folder that it's downloaded to.
///
/// Generally there's no need to compress and decompress your uploads, so you should
/// usually be able to use the content straight from the destination folder.
///
/// </summary>
public partial class Workshop : IDisposable
{
static Workshop()
{
Debug.Assert( Marshal.SizeOf( typeof(PublishedFileId_t) ) == Marshal.SizeOf( typeof(ulong) ),
$"sizeof({nameof(PublishedFileId_t)}) != sizeof({nameof(UInt64)})" );
}
internal const ulong InvalidHandle = 0xffffffffffffffff;
internal SteamNative.SteamUGC ugc;
internal Friends friends;
internal BaseSteamworks steamworks;
internal SteamNative.SteamRemoteStorage remoteStorage;
/// <summary>
/// Called when an item has been downloaded. This could have been
/// because of a call to Download.
/// </summary>
public event Action<ulong, Callbacks.Result> OnFileDownloaded;
/// <summary>
/// Called when an item has been installed. This could have been
/// because of a call to Download or because of a subscription triggered
/// via the browser/app.
/// </summary>
public event Action<ulong> OnItemInstalled;
internal Workshop( BaseSteamworks steamworks, SteamNative.SteamUGC ugc, SteamNative.SteamRemoteStorage remoteStorage )
{
this.ugc = ugc;
this.steamworks = steamworks;
this.remoteStorage = remoteStorage;
steamworks.RegisterCallback<SteamNative.DownloadItemResult_t>( onDownloadResult );
steamworks.RegisterCallback<SteamNative.ItemInstalled_t>( onItemInstalled );
}
/// <summary>
/// You should never have to call this manually
/// </summary>
public void Dispose()
{
ugc = null;
steamworks = null;
remoteStorage = null;
friends = null;
OnFileDownloaded = null;
OnItemInstalled = null;
}
private void onItemInstalled( SteamNative.ItemInstalled_t obj )
{
if ( OnItemInstalled != null && obj.AppID == Client.Instance.AppId )
OnItemInstalled( obj.PublishedFileId );
}
private void onDownloadResult( SteamNative.DownloadItemResult_t obj )
{
if ( OnFileDownloaded != null && obj.AppID == Client.Instance.AppId )
OnFileDownloaded( obj.PublishedFileId, (Callbacks.Result) obj.Result );
}
/// <summary>
/// Get the IDs of all subscribed workshop items. Not all items may be currently installed.
/// </summary>
public unsafe ulong[] GetSubscribedItemIds()
{
var count = ugc.GetNumSubscribedItems();
var array = new ulong[count];
fixed ( ulong* ptr = array )
{
ugc.GetSubscribedItems( (PublishedFileId_t*) ptr, count );
}
return array;
}
[ThreadStatic]
private static ulong[] _sSubscribedItemBuffer;
/// <summary>
/// Get the IDs of all subscribed workshop items, avoiding repeated allocations.
/// Not all items may be currently installed.
/// </summary>
public unsafe int GetSubscribedItemIds( List<ulong> destList )
{
const int bufferSize = 1024;
var count = ugc.GetNumSubscribedItems();
if ( count >= bufferSize )
{
// Fallback for exceptional cases
destList.AddRange( GetSubscribedItemIds() );
return (int) count;
}
if ( _sSubscribedItemBuffer == null )
{
_sSubscribedItemBuffer = new ulong[bufferSize];
}
fixed ( ulong* ptr = _sSubscribedItemBuffer)
{
count = ugc.GetSubscribedItems( (PublishedFileId_t*) ptr, bufferSize );
}
for ( var i = 0; i < count; ++i )
{
destList.Add( _sSubscribedItemBuffer[i] );
}
return (int) count;
}
/// <summary>
/// Creates a query object, which is used to get a list of items.
///
/// This could be a list of the most popular items, or a search,
/// or just getting a list of the items you've uploaded.
/// </summary>
public Query CreateQuery()
{
return new Query()
{
AppId = steamworks.AppId,
workshop = this,
friends = friends
};
}
/// <summary>
/// Create a new Editor object with the intention of creating a new item.
/// Your item won't actually be created until you call Publish() on the object.
/// </summary>
public Editor CreateItem( ItemType type = ItemType.Community )
{
return CreateItem(this.steamworks.AppId, type);
}
/// <summary>
/// Create a new Editor object with the intention of creating a new item.
/// Your item won't actually be created until you call Publish() on the object.
/// Your item will be published to the provided appId.
/// </summary>
/// <remarks>You need to add app publish permissions for cross app uploading to work.</remarks>
public Editor CreateItem( uint workshopUploadAppId, ItemType type = ItemType.Community )
{
return new Editor() { workshop = this, WorkshopUploadAppId = workshopUploadAppId, Type = type };
}
/// <summary>
/// Returns a class representing this ItemId. We don't query
/// item name, description etc. We don't verify that item exists.
/// We don't verify that this item belongs to your app.
/// </summary>
public Editor EditItem( ulong itemId )
{
return new Editor() { workshop = this, Id = itemId, WorkshopUploadAppId = steamworks.AppId };
}
/// <summary>
/// Gets an Item object for a specific item. This doesn't currently
/// query the item's name and description. It's only really useful
/// if you know an item's ID and want to download it, or check its
/// current download status.
/// </summary>
public Item GetItem( ulong itemid )
{
return new Item( itemid, this );
}
/// <summary>
/// How a query should be ordered.
/// </summary>
public enum Order
{
RankedByVote = 0,
RankedByPublicationDate = 1,
AcceptedForGameRankedByAcceptanceDate = 2,
RankedByTrend = 3,
FavoritedByFriendsRankedByPublicationDate = 4,
CreatedByFriendsRankedByPublicationDate = 5,
RankedByNumTimesReported = 6,
CreatedByFollowedUsersRankedByPublicationDate = 7,
NotYetRated = 8,
RankedByTotalVotesAsc = 9,
RankedByVotesUp = 10,
RankedByTextSearch = 11,
RankedByTotalUniqueSubscriptions = 12,
};
/// <summary>
/// The type of item you are querying for
/// </summary>
public enum QueryType
{
/// <summary>
/// Both MicrotransactionItems and subscriptionItems
/// </summary>
Items = 0,
/// <summary>
/// Workshop item that is meant to be voted on for the purpose of selling in-game
/// </summary>
MicrotransactionItems = 1,
/// <summary>
/// normal Workshop item that can be subscribed to
/// </summary>
SubscriptionItems = 2,
Collections = 3,
Artwork = 4,
Videos = 5,
Screenshots = 6,
AllGuides = 7, // both web guides and integrated guides
WebGuides = 8,
IntegratedGuides = 9,
UsableInGame = 10, // ready-to-use items and integrated guides
ControllerBindings = 11,
GameManagedItems = 12, // game managed items (not managed by users)
};
/// <summary>
/// Used to define the item type when creating
/// </summary>
public enum ItemType
{
Community = 0, // normal Workshop item that can be subscribed to
Microtransaction = 1, // Workshop item that is meant to be voted on for the purpose of selling in-game
Collection = 2, // a collection of Workshop or Greenlight items
Art = 3, // artwork
Video = 4, // external video
Screenshot = 5, // screenshot
Game = 6, // Greenlight game entry
Software = 7, // Greenlight software entry
Concept = 8, // Greenlight concept
WebGuide = 9, // Steam web guide
IntegratedGuide = 10, // application integrated guide
Merch = 11, // Workshop merchandise meant to be voted on for the purpose of being sold
ControllerBinding = 12, // Steam Controller bindings
SteamworksAccessInvite = 13, // internal
SteamVideo = 14, // Steam video
GameManagedItem = 15, // managed completely by the game, not the user, and not shown on the web
};
/// <summary>
/// When querying a specific user's items this defines what
/// type of items you're looking for.
/// </summary>
public enum UserQueryType : uint
{
Published = 0,
VotedOn,
VotedUp,
VotedDown,
WillVoteLater,
Favorited,
Subscribed,
UsedOrPlayed,
Followed,
}
}
}