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 { /// /// Called when the local client's items are first retrieved, and when they change. /// Obviously not called on the server. /// public event Action OnUpdate; /// /// 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. /// public Item[] Items; /// /// You can send this data to a server, or another player who can then deserialize it /// and get a verified list of items. /// public byte[] SerializedItems; /// /// Serialized data exprires after an hour. This is the time the value in SerializedItems will expire. /// public DateTime SerializedExpireTime; /// /// Controls whether per-item properties () are available or not. Default true. /// This can improve performance of full inventory updates. /// public bool EnableItemProperties = true; internal uint LastTimestamp = 0; internal SteamNative.SteamInventory inventory; private bool IsServer { get; set; } public event Action OnDefinitionsUpdated; public event Action OnInventoryResultReady; internal Inventory( BaseSteamworks steamworks, SteamNative.SteamInventory c, bool server ) { IsServer = server; inventory = c; steamworks.RegisterCallback( onDefinitionsUpdated ); Result.Pending = new Dictionary(); FetchItemDefinitions(); LoadDefinitions(); UpdatePrices(); if ( !server ) { steamworks.RegisterCallback( onResultReady ); steamworks.RegisterCallback( onFullUpdate ); // // Get a list of our items immediately // Refresh(); } } /// /// Should get called when the definitions get updated from Steam. /// 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; } /// /// We've received a FULL update /// private void onFullUpdate( SteamInventoryFullUpdate_t data ) { var result = new Result( this, data.Handle, false ); result.Fill(); onResult( result, true ); } /// /// A generic result has returned. /// 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; } /// /// 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 /// 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; } /// /// 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. /// [Obsolete( "No longer required, will be removed in a later version" )] public void PlaytimeHeartbeat() { } /// /// 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 /// public void Refresh() { if ( IsServer ) return; SteamNative.SteamInventoryResult_t request = 0; if ( !inventory.GetAllItems( ref request ) || request == -1 ) { Console.WriteLine( "GetAllItems failed!?" ); return; } } /// /// 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. /// public Definition CreateDefinition( int id ) { return new Definition( this, id ); } /// /// Fetch item definitions in case new ones have been added since we've initialized /// public void FetchItemDefinitions() { inventory.LoadItemDefinitions(); } /// /// No need to call this manually if you're calling Update /// public void Update() { } /// /// A list of items defined for this app. /// This should be immediately populated and available. /// public Definition[] Definitions; /// /// A list of item definitions that have prices and so can be bought. /// public IEnumerable DefinitionsWithPrices { get { if ( Definitions == null ) yield break; for ( int i=0; i< Definitions.Length; i++ ) { if (Definitions[i].LocalPrice > 0) yield return Definitions[i]; } } } /// /// Utility, given a "1;VLV250" string, convert it to a 2.5 /// 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; } /// /// We might be better off using a dictionary for this, once there's 1000+ definitions /// 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; } } /// /// 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. /// 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 ); } /// /// Crafting! Uses the passed items to buy the target item. /// You need to have set up the appropriate exchange rules in your item /// definitions. /// 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 ); } /// /// Split stack into two items /// public Result SplitStack( Item item, int quantity = 1 ) { return item.SplitStack( quantity ); } /// /// Stack source item onto dest item /// 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 ); } /// /// 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. /// 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 ); /// /// 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 /// 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; } /// /// This might be null until Steam has actually recieved the prices. /// 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(); }); } } }