38f1ddb...178a853: v0.8.9.1, removed content folder
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user