update: 完善库存系统, 和中台接口统一, 接入 Sqlit4Unity 插件
							parent
							
								
									0888bc4658
								
							
						
					
					
						commit
						3f10cf718e
					
				|  | @ -114,6 +114,7 @@ namespace Guru | |||
| 		// 经济相关 | ||||
| 		public static readonly string ParameterBalance = "balance"; // 用于余额 | ||||
| 		public static readonly string ParameterSku = "sku"; // sku | ||||
| 		public static readonly string ParameterScene = "scene"; // sku | ||||
| 		public static readonly string ParameterVirtualCurrencyName = "virtual_currency_name"; // 虚拟货币名称 | ||||
| 	} | ||||
| } | ||||
|  | @ -72,65 +72,59 @@ namespace Guru | |||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 获取虚拟货币 | ||||
|         /// 获取道具/货币 | ||||
|         /// </summary> | ||||
|         /// <param name="currencyName"></param> | ||||
|         /// <param name="value"></param> | ||||
|         /// <param name="balance"></param> | ||||
|         /// <param name="virtualCurrencyName"></param> | ||||
|         /// <param name="method"></param> | ||||
|         /// <param name="levelName"></param> | ||||
|         /// <param name="isIap"></param> | ||||
|         /// <param name="sku"></param> | ||||
|         /// <param name="scene"></param> | ||||
|         public static void EarnVirtualCurrency(string currencyName, int value, int balance,  | ||||
|             string method = "",  | ||||
|             string levelName = "", | ||||
|             bool isIap = false, | ||||
|             string sku = "", | ||||
|             string scene = "") | ||||
|         /// <param name="balance"></param> | ||||
|         /// <param name="value"></param> | ||||
|         /// <param name="methodDetails"></param> | ||||
|         public static void EarnVirtualCurrency(string virtualCurrencyName, string method, int balance, int value,   | ||||
|             string methodDetails = "") | ||||
|         { | ||||
|             if (isIap) method = "iap_buy";   | ||||
|             var data = new Dictionary<string, dynamic>() | ||||
|             { | ||||
|                 { ParameterVirtualCurrencyName, currencyName }, | ||||
|                 { ParameterVirtualCurrencyName, virtualCurrencyName }, | ||||
|                 { ParameterItemCategory, method }, | ||||
|                 { ParameterItemName, methodDetails }, | ||||
|                 { ParameterValue, value }, | ||||
|                 { ParameterBalance, balance }, | ||||
|                 { ParameterLevelName, levelName }, | ||||
|                 { ParameterItemCategory, method }, | ||||
|             }; | ||||
|              | ||||
|             if (!string.IsNullOrEmpty(scene)) data[ParameterItemName] = scene; // 获取的虚拟货币或者道具的场景 | ||||
|             if (!string.IsNullOrEmpty(sku)) data[ParameterSku] = sku; // 商品的 sku | ||||
|              | ||||
|             LogEvent(EventEarnVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true }); | ||||
|              | ||||
|             // FB 上报收入点 | ||||
|             FB.LogAppEvent(EventEarnVirtualCurrency, value, data); | ||||
|             // FB.LogAppEvent(EventEarnVirtualCurrency, value, data); | ||||
|         } | ||||
|          | ||||
|          | ||||
|         public static void SpendVirtualCurrency(string currencyName, int value, int balance,  | ||||
|             string method = "",  | ||||
|             string levelName = "", | ||||
|             string scene = "") | ||||
|         /// <summary> | ||||
|         /// 消耗道具/货币 | ||||
|         /// </summary> | ||||
|         /// <param name="contentId"></param> | ||||
|         /// <param name="contentType"></param> | ||||
|         /// <param name="price"></param> | ||||
|         /// <param name="virtualCurrencyName"></param> | ||||
|         /// <param name="balance"></param> | ||||
|         /// <param name="scene"></param> | ||||
|         public static void SpendVirtualCurrency(string contentId, string contentType, int price, | ||||
|             string virtualCurrencyName, int balance, string scene = "") | ||||
|         { | ||||
|             var data = new Dictionary<string, dynamic>() | ||||
|             { | ||||
|                 { ParameterVirtualCurrencyName, currencyName }, | ||||
|                 { ParameterValue, value }, | ||||
|                 { ParameterVirtualCurrencyName, virtualCurrencyName }, | ||||
|                 { ParameterValue, price }, | ||||
|                 { ParameterBalance, balance }, | ||||
|                 { ParameterLevelName, levelName }, | ||||
|                 { ParameterItemCategory, method }, | ||||
|                 { ParameterItemName, contentId }, | ||||
|                 { ParameterItemCategory, contentType }, | ||||
|             }; | ||||
|              | ||||
|             if (!string.IsNullOrEmpty(scene)) data[ParameterItemName] = scene; // 获取的虚拟货币或者道具的场景 | ||||
|             if (!string.IsNullOrEmpty(scene)) data[ParameterScene] = scene; // 获取的虚拟货币或者道具的场景 | ||||
|              | ||||
|             LogEvent(EventSpendVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true }); | ||||
|              | ||||
|             // FB 上报消费点 | ||||
|             FB.LogAppEvent(EventSpendVirtualCurrency, value, data); | ||||
|             // FB.LogAppEvent(EventSpendVirtualCurrency, value, data); | ||||
|              | ||||
|             // FB 上报消耗事件买量点 | ||||
|             FBSpentCredits(value, scene, method);  // 点位信息有变化 | ||||
|             FBSpentCredits(contentId, contentType, price);  // 点位信息有变化 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -140,7 +134,7 @@ namespace Guru | |||
|         /// <param name="amount"></param> | ||||
|         /// <param name="contentId"></param> | ||||
|         /// <param name="contentType"></param> | ||||
|         private static void FBSpentCredits(int amount, string contentId, string contentType) | ||||
|         private static void FBSpentCredits(string contentId, string contentType, float amount) | ||||
|         { | ||||
|             FB.LogAppEvent(AppEventName.SpentCredits, amount,  | ||||
|                 new Dictionary<string, object>() | ||||
|  |  | |||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 93bb1ade31b44cc9bb3dbad3051cbe78 | ||||
| timeCreated: 1706922645 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 1f4820a46a3a4b02ac85a48a4e25014c | ||||
| timeCreated: 1706922699 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: e921121b06404396b164c6538e68c6aa | ||||
| timeCreated: 1706922657 | ||||
|  | @ -0,0 +1,131 @@ | |||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|     using System; | ||||
|     using System.Collections.Generic; | ||||
|     using Newtonsoft.Json; | ||||
| 
 | ||||
|     [Serializable] | ||||
|     public class InventoryData: IJsonData | ||||
|     { | ||||
|         [JsonProperty("v")]  | ||||
|         public List<LimitedBalance> valid; | ||||
|         [JsonProperty("e")]  | ||||
|         public List<LimitedBalance> expired; | ||||
| 
 | ||||
|         public static InventoryData FromJson(string json) | ||||
|         { | ||||
|             if (JsonDataHelper.Parse<InventoryData>(json, out var d)) | ||||
|             { | ||||
|                 return d; | ||||
|             } | ||||
|             return new InventoryData(); | ||||
|         } | ||||
| 
 | ||||
|         public InventoryData() | ||||
|         { | ||||
|             valid = new List<LimitedBalance>(); | ||||
|             expired = new List<LimitedBalance>(); | ||||
|         } | ||||
| 
 | ||||
|         public InventoryData(List<LimitedBalance> valid, List<LimitedBalance> expired) | ||||
|         { | ||||
|             this.valid = valid; | ||||
|             this.expired = expired; | ||||
|         } | ||||
|          | ||||
|         public static InventoryData Create(LimitedBalance balance = null) | ||||
|         { | ||||
|             var t = new InventoryData(); | ||||
|             if (balance != null) | ||||
|             { | ||||
|                 if (balance.expireAt > InventoryManager.CurrentTimeInMillis) | ||||
|                 { | ||||
|                     t.valid.Add(balance); | ||||
|                 } | ||||
|             } | ||||
|             return t; | ||||
|         } | ||||
| 
 | ||||
|          | ||||
|         public InventoryData AttachLimitedBalance(LimitedBalance balance) | ||||
|         { | ||||
|             if (balance.expireAt > InventoryManager.CurrentTimeInMillis) | ||||
|             { | ||||
|                 valid.Add(balance); | ||||
|                 return new InventoryData() | ||||
|                 { | ||||
|                     valid =  this.valid, | ||||
|                     expired = this.expired, | ||||
|                 }; | ||||
|             } | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 回收过期的道具 | ||||
|         /// </summary> | ||||
|         /// <returns></returns> | ||||
|         public RecycleResult RecycleExpiredBalance() | ||||
|         { | ||||
|             long now = InventoryManager.CurrentTimeInMillis; | ||||
|             List<LimitedBalance> newValid = new List<LimitedBalance>(); | ||||
|             List<LimitedBalance> newExpired = new List<LimitedBalance>(); | ||||
|             int expiredBalance = 0; | ||||
|             foreach(var item in valid) { | ||||
|                 if (now >= item.expireAt) { | ||||
|                     expiredBalance += item.amount; | ||||
|                     newExpired.Add(item); | ||||
|                 } else { | ||||
|                     newValid.Add(item); | ||||
|                 } | ||||
|             } | ||||
|             return new RecycleResult(expiredBalance, new InventoryData(newValid, newExpired)); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     [Serializable] | ||||
|     public class LimitedBalance | ||||
|     { | ||||
|         [JsonProperty("a")] | ||||
|         public int amount = 0; | ||||
|         [JsonProperty("e")] | ||||
|         public long expireAt = -1; | ||||
|     } | ||||
|      | ||||
|     public class RecycleResult { | ||||
|         public int expiredBalance; | ||||
|         public InventoryData InventoryData; | ||||
|          | ||||
|         public RecycleResult(int expiredBalance, InventoryData inventoryData) { | ||||
|             this.expiredBalance = expiredBalance; | ||||
|             this.InventoryData = inventoryData; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public class ConsumeResult { | ||||
|         public bool consumed; | ||||
|         public InventoryItem item; | ||||
|          | ||||
|         public ConsumeResult(InventoryItem item, bool consumed) | ||||
|         { | ||||
|             this.item = item; | ||||
|             this.consumed = consumed; | ||||
|          | ||||
|         } | ||||
|          | ||||
|         public static ConsumeResult Success(InventoryItem item) | ||||
|         { | ||||
|             return new ConsumeResult(item, true); | ||||
|         } | ||||
| 
 | ||||
|         public static ConsumeResult Error(InventoryItem item) | ||||
|         { | ||||
|             return new ConsumeResult(item, false); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 54c27103f7594aa9bc337f5e076fb550 | ||||
| timeCreated: 1706959298 | ||||
|  | @ -0,0 +1,55 @@ | |||
| namespace Guru | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 交易方式 | ||||
|     /// </summary> | ||||
|     public enum TransactionMethod | ||||
|     { | ||||
|         unknown  = 0, | ||||
|         iap, // IAP购买 | ||||
|         igc, // In-game currency 购买(coin/gems..) | ||||
|         reward, // 奖励获得 | ||||
|         bonus, // 优惠 | ||||
|         prop, // 道具 | ||||
|         free, | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// 道具列别 | ||||
|     /// </summary> | ||||
|     public class InventoryCategory | ||||
|     { | ||||
|         public const string Prop = "prop"; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public partial class InventoryManager | ||||
|     { | ||||
|       | ||||
|         /// <summary> | ||||
|         /// 获取交易方式的字段值 | ||||
|         /// </summary> | ||||
|         /// <param name="method"></param> | ||||
|         /// <returns></returns> | ||||
|         private string ConvertTransactionMethodName(TransactionMethod method) { | ||||
|             switch (method) { | ||||
|                 case TransactionMethod.iap: | ||||
|                     return "iap_buy"; | ||||
|                 case TransactionMethod.igc: | ||||
|                     return "igc"; | ||||
|                 case TransactionMethod.reward: | ||||
|                     return "reward"; | ||||
|                 case TransactionMethod.bonus: | ||||
|                     return "bonus"; | ||||
|                 case TransactionMethod.prop: | ||||
|                     return "prop"; | ||||
|                 case TransactionMethod.free: | ||||
|                     return "prop"; | ||||
|                 default: | ||||
|                     return "unknown"; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|          | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 0b4e29d06a7f4ddb860e08d3aa81610e | ||||
| timeCreated: 1706945881 | ||||
|  | @ -0,0 +1,578 @@ | |||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|     using SQLite4Unity3d; | ||||
|     using UnityEngine; | ||||
|     using System.Collections.Generic; | ||||
|     using Newtonsoft.Json; | ||||
|     using System; | ||||
| 
 | ||||
|     public interface IInventoryDelegate | ||||
|     { | ||||
|         string GetInventoryCategory(string id); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// 道具管理器 | ||||
|     /// </summary> | ||||
|     public partial class InventoryManager | ||||
|     { | ||||
|          | ||||
|         private static InventoryManager _instance; | ||||
|         public  static InventoryManager Instance | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if(_instance == null) _instance = new InventoryManager(); | ||||
|                 return _instance; | ||||
|             } | ||||
|         } | ||||
|         private IInventoryDelegate _delegate; | ||||
|         private InventoryTable _table; | ||||
| 
 | ||||
|         public bool IsReady { get; private set; } = false; | ||||
|          | ||||
|         public static long CurrentTimeInMillis => TimeUtil.GetCurrentTimeStamp(); | ||||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 初始化 | ||||
|         /// </summary> | ||||
|         /// <param name="service"></param> | ||||
|         public static void Install(DBService service) | ||||
|         { | ||||
|             if (service != null) | ||||
|             { | ||||
|                 Instance._table = InventoryTable.LoadOrCreate(service); | ||||
|                 Instance.IsReady = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 获取道具分类 | ||||
|         /// </summary> | ||||
|         /// <param name="id"></param> | ||||
|         /// <returns></returns> | ||||
|         public string GetInventoryCategory(string id) | ||||
|         { | ||||
|             return _delegate?.GetInventoryCategory(id) ?? InventoryCategory.Prop; | ||||
|         } | ||||
| 
 | ||||
|         private InventoryItem GetData(string sku) => _table.GetItem(sku); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 获取道具(组) | ||||
|         /// 通过[method]中的的特定[specific]方式 获得了指定的 [items] | ||||
|         /// * method: iap -> specific: sku | ||||
|         /// * method: igc -> specific: coin/gems... | ||||
|         /// * method: reward -> specific: ads/lottery/daily/... | ||||
|         /// * method: bonus -> specific: ads/other/... | ||||
|         /// * method: prop -> specific: hint/hammer/swap/magic/.. | ||||
|         /// | ||||
|         /// method 最终会在 earnVirtualCurrency 中成为 item_category | ||||
|         /// specific 最终会在 earnVirtualCurrency 中成为 item_name | ||||
|         /// </summary> | ||||
|         /// <param name="items"></param> | ||||
|         /// <param name="method"></param> | ||||
|         /// <param name="specific"></param> | ||||
|         public void Acquire(List<StockItem> items, TransactionMethod method, string specific = "") | ||||
|         { | ||||
|             if (!IsReady) return; | ||||
|              | ||||
|             List<InventoryItem> acquired = new List<InventoryItem>(items.Count); | ||||
|             string category; | ||||
|             StockItem item; | ||||
|             InventoryItem invItem; | ||||
|              | ||||
|             for (int i = 0; i < items.Count; i++) | ||||
|             { | ||||
|                 item = items[i]; | ||||
|                 category = GetInventoryCategory(item.sku); | ||||
|                 invItem = GetData(item.sku)?.Acquire(method, item.amount)  | ||||
|                           ?? InventoryItem.Create(item.sku, category, item.attr,  balance:item.amount, method: method); | ||||
|                 acquired.Add(invItem); | ||||
|                  | ||||
|                 Analytics.EarnVirtualCurrency( | ||||
|                     item.sku, | ||||
|                     method:ConvertTransactionMethodName(method), | ||||
|                     methodDetails:specific, | ||||
|                     balance: invItem.balance, | ||||
|                     value: item.amount); | ||||
|             } | ||||
| 
 | ||||
|             _table.UpdateInventoryItems(acquired); // 更新数据库信息 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public bool Consume(List<StockItem> items, string contentId, string scene, string category = "") | ||||
|         { | ||||
|             if (!IsReady) return false; | ||||
| 
 | ||||
|             List<InventoryItem> consumed = new List<InventoryItem>(items.Count); | ||||
|             bool isConsumed = true; | ||||
| 
 | ||||
|             StockItem item; | ||||
|             InventoryItem invItem; | ||||
| 
 | ||||
|             for (int i = 0; i < items.Count; i++) | ||||
|             { | ||||
|                 item = items[i]; | ||||
|                 var result = GetData(item.sku)?.Consume(scene, item.amount) ?? null; | ||||
|                 if (result == null) | ||||
|                 { | ||||
|                     Debug.LogError($"consume item not found: {item.sku}"); | ||||
|                     return false; | ||||
|                 } | ||||
| 
 | ||||
|                 consumed.Add(result.item); | ||||
|                 // 这里如果没有消耗掉,将跳出 | ||||
|                 if (!result.consumed) | ||||
|                 { | ||||
|                     Debug.Log($"consume failed: {item.sku}:{item.amount}"); | ||||
|                     isConsumed = false; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (consumed.Count == 0) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (isConsumed && consumed.Count == items.Count) | ||||
|                 { | ||||
|                     for (int i = 0; i < items.Count; ++i) | ||||
|                     { | ||||
|                         Analytics.SpendVirtualCurrency( | ||||
|                             contentId, category, items[i].amount, | ||||
|                             virtualCurrencyName: consumed[i].sku, balance: consumed[i].balance); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             _table.UpdateInventoryItems(consumed); // 更新数据库信息 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public bool Consume(List<StockItem> items, Manifest redeem) | ||||
|         { | ||||
|             return Consume(items, redeem.scene, redeem.contentId, redeem.category); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 判断是否可以支付 | ||||
|         /// </summary> | ||||
|         /// <param name="id"></param> | ||||
|         /// <param name="amount"></param> | ||||
|         /// <returns></returns> | ||||
|         public bool CanAfford(String id, int amount) { | ||||
|             var item = GetData(id); | ||||
|             return item != null && item.balance > amount; | ||||
|         } | ||||
|          | ||||
|          | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     // <summary> | ||||
|     /// 库存道具 | ||||
|     /// </summary> | ||||
|     public class StockItem | ||||
|     { | ||||
|         [PrimaryKey]  | ||||
|         public string sku { get; set; } | ||||
|         public int amount { get; set; } | ||||
|         public int attr { get; set; } | ||||
|         public long expired { get; set; } | ||||
|          | ||||
| 
 | ||||
|         public static StockItem Consumable(string sku, int amount) | ||||
|         { | ||||
|             return Create(sku, amount, DetailsAttr.Consumable); | ||||
|         } | ||||
|          | ||||
|         public static StockItem Permanent(string sku, int amount) | ||||
|         { | ||||
|             return Create (sku, amount, DetailsAttr.Permanent); | ||||
|         } | ||||
|          | ||||
|          | ||||
|         public static StockItem Create(string sku, int amount, int att, long expiredStamp = 0) | ||||
|         { | ||||
|             return new StockItem | ||||
|             { | ||||
|                 sku = sku, | ||||
|                 amount = amount, | ||||
|                 attr = att, | ||||
|                 expired = expiredStamp | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     [Serializable] | ||||
|     public class InventoryItem: IJsonData | ||||
|     { | ||||
|         [JsonProperty(InventoryTable.dbSku)] | ||||
|         public string sku { get; set; } | ||||
|         [JsonProperty(InventoryTable.dbBalance)] | ||||
|         public int balance { get; set; } | ||||
|         [JsonProperty(InventoryTable.dbCategory)] | ||||
|         public string category { get; set; } | ||||
|         [JsonProperty(InventoryTable.dbAttr)] | ||||
|         public int attr { get; set; } | ||||
|         [JsonProperty(InventoryTable.dbTimeSensitive)] | ||||
|         public InventoryData Inventory { get; set; } | ||||
|         [JsonProperty(InventoryTable.dbUpdateAt)] | ||||
|         public long updateAt{ get; set; } | ||||
|         [JsonProperty(InventoryTable.dbCreateAt)] | ||||
|         public long createAt{ get; set; } | ||||
|         [JsonProperty(InventoryTable.dbDetails)] | ||||
|         public InventoryDetails details { get; set; } | ||||
|          | ||||
|         public static Action<Exception> ExceptionHandler; | ||||
| 
 | ||||
| 
 | ||||
|          | ||||
|          | ||||
| 
 | ||||
|         public static InventoryItem FromJson(string json) | ||||
|         { | ||||
|             if (JsonDataHelper.Parse<InventoryItem>(json, out var d)) | ||||
|             { | ||||
|                 return d; | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         public InventoryItem(string sku, int balance, string category, int attr, InventoryDetails details = null, InventoryData inventory = null, long createAt = -1, long updateAt = -1) | ||||
|         { | ||||
|             this.sku = sku; | ||||
|             this.category = category; | ||||
|             this.balance = balance; | ||||
|             this.attr = attr; | ||||
|             this.details = details; | ||||
|             this.Inventory = inventory; | ||||
|             this.createAt = createAt; | ||||
|             this.updateAt = updateAt; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static InventoryItem Create(string sku,  string category, int attr,  | ||||
|             int balance = 0, InventoryDetails details = null,  | ||||
|             InventoryData inventory = null, long createAt = -1, long updateAt = -1,  | ||||
|             int expireAt = -1, TransactionMethod method = TransactionMethod.unknown) | ||||
|         { | ||||
|             long stamp = InventoryManager.CurrentTimeInMillis; | ||||
|             return new InventoryItem( | ||||
|                 sku, | ||||
|                 balance, | ||||
|                 category, | ||||
|                 attr, | ||||
|                 details ?? InventoryDetails.Create(balance), | ||||
|                 inventory?? InventoryData.Create(new LimitedBalance() { amount = balance}), | ||||
|                 stamp, | ||||
|                 stamp | ||||
|             ); | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         /// 获取道具 | ||||
|         /// </summary> | ||||
|         /// <param name="method"></param> | ||||
|         /// <param name="amount"></param> | ||||
|         /// <returns></returns> | ||||
|         public InventoryItem Acquire(TransactionMethod method, int amount) | ||||
|         { | ||||
|             var now = InventoryManager.CurrentTimeInMillis; | ||||
|             var recycled = Inventory.RecycleExpiredBalance(); | ||||
|             var target = balance + amount; | ||||
|             var newBalance = Math.Clamp(target - recycled.expiredBalance, 0, target); | ||||
|             return new InventoryItem(this.sku, newBalance,this.category, this.attr,  | ||||
|                 details.Acquire(method, amount), | ||||
|                 recycled.InventoryData, | ||||
|                 createAt, now); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 消耗道具 | ||||
|         /// </summary> | ||||
|         /// <param name="scene"></param> | ||||
|         /// <param name="amount"></param> | ||||
|         /// <returns></returns> | ||||
|         public ConsumeResult Consume(string scene, int amount) | ||||
|         { | ||||
|             var now = InventoryManager.CurrentTimeInMillis; | ||||
|             var recycled = Inventory.RecycleExpiredBalance(); | ||||
|             var target = Math.Clamp(balance - recycled.expiredBalance, 0, balance); | ||||
|             if (target > amount && attr == DetailsAttr.Consumable) | ||||
|             { | ||||
|                 target -= amount; | ||||
|                 return ConsumeResult.Success(new InventoryItem(sku, target, category, attr, | ||||
|                     details.Consume(scene, amount), recycled.InventoryData, createAt, now)); | ||||
|             } | ||||
|             return ConsumeResult.Error(new InventoryItem( | ||||
|                 sku, target, category, attr, details, recycled.InventoryData, createAt, now)); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     [Serializable] | ||||
|     public class InventoryDetails: IJsonData | ||||
|     { | ||||
|         [JsonProperty("a")] | ||||
|         public Dictionary<TransactionMethod, int> acquired { get; set; }  | ||||
|          | ||||
|         [JsonProperty("c")] | ||||
|         public Dictionary<string, int> consumed { get; set; } | ||||
|          | ||||
|         [JsonProperty("d")] | ||||
|         public Dictionary<string, dynamic> data { get; set; } | ||||
| 
 | ||||
| 
 | ||||
|         public InventoryDetails() | ||||
|         { | ||||
|             acquired = new Dictionary<TransactionMethod, int>(10); | ||||
|             consumed = new Dictionary<string, int>(10); | ||||
|             data = new Dictionary<string, dynamic>(10); | ||||
|         } | ||||
| 
 | ||||
|         public static InventoryDetails Create(int amount) | ||||
|         { | ||||
|             var d = new InventoryDetails(); | ||||
|             if (amount > 0) | ||||
|             { | ||||
|                 d.acquired[TransactionMethod.unknown] = amount; | ||||
|             } | ||||
|             return d; | ||||
|         } | ||||
| 
 | ||||
|         public InventoryDetails Acquire(TransactionMethod method, int amount) | ||||
|         { | ||||
|             if (acquired.TryGetValue(method, out var a)) | ||||
|             { | ||||
|                 acquired[method] = a + amount; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 acquired[method] = amount; | ||||
|             } | ||||
|              | ||||
|             return this; | ||||
|         } | ||||
|          | ||||
|         public InventoryDetails Consume(string scene, int amount) | ||||
|         { | ||||
|             if (consumed.TryGetValue(scene, out var a)) | ||||
|             { | ||||
|                 consumed[scene] = a + amount; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 consumed[scene] = amount; | ||||
|             } | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 从 JSON 中解析 | ||||
|         /// </summary> | ||||
|         /// <param name="json"></param> | ||||
|         /// <returns></returns> | ||||
|         public InventoryDetails FromJson(string json) | ||||
|         { | ||||
|             if (JsonDataHelper.Parse<InventoryDetails>(json, out var d)) | ||||
|             { | ||||
|                 return d; | ||||
|             } | ||||
|             return new InventoryDetails(); | ||||
|         } | ||||
| 
 | ||||
|         public void SetValue(String key, object value) { | ||||
|             data[key] = value; | ||||
|         } | ||||
| 
 | ||||
|         public int GetInt(string key, int defaultValue = 0) | ||||
|         { | ||||
|             if(data.TryGetValue(key, out var value)) | ||||
|             { | ||||
|                 return (int)value; | ||||
|             } | ||||
|             return defaultValue;       | ||||
|         } | ||||
|          | ||||
|         public double GetDouble(string key, double defaultValue = 0) | ||||
|         { | ||||
|             if(data.TryGetValue(key, out var value)) | ||||
|             { | ||||
|                 return (double)value; | ||||
|             } | ||||
|             return defaultValue;       | ||||
|         } | ||||
|          | ||||
|         public string GetString(string key, string defaultValue = "") | ||||
|         { | ||||
|             if(data.TryGetValue(key, out var value)) | ||||
|             { | ||||
|                 return (string)value; | ||||
|             } | ||||
|             return defaultValue;       | ||||
|         } | ||||
|          | ||||
|         public bool GetBool(string key, bool defaultValue = false) | ||||
|         { | ||||
|             if(data.TryGetValue(key, out var value)) | ||||
|             { | ||||
|                 return (bool)value; | ||||
|             } | ||||
|             return defaultValue;       | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// 内置物品表维护器 | ||||
|     /// </summary> | ||||
|     internal class InventoryTable | ||||
|     { | ||||
|          | ||||
|         internal const string tbName = "inventory"; // Product Transaction Table | ||||
|         internal const string dbSku = "sku"; | ||||
|         internal const string dbBalance = "balance"; | ||||
|         internal const string dbCategory = "cat"; | ||||
|         internal const string dbAttr = "attr"; | ||||
|         internal const string dbDetails = "details"; | ||||
|         internal const string dbTimeSensitive = "tsv"; | ||||
|         internal const string dbUpdateAt = "update_at"; | ||||
|         internal const string dbCreateAt = "create_at"; | ||||
|          | ||||
|          | ||||
|         public static InventoryTable LoadOrCreate(DBService db) | ||||
|         { | ||||
|             var table = new InventoryTable(); | ||||
|             table.Setup(db); | ||||
|             return table; | ||||
|         } | ||||
| 
 | ||||
|         private List<inventory> _dataList; | ||||
|          | ||||
|         private DBService _db; | ||||
|         private void Setup(DBService db) | ||||
|         { | ||||
|             _db = db; | ||||
|             Refresh(); | ||||
|              | ||||
|             // 执行命令 | ||||
|             // _db.Execute($"CREATE INDEX inventory_item_idx ON {tbName} ({dbSku});", ts => | ||||
|             // { | ||||
|             //     _db.Execute($"CREATE INDEX inventory_item_category_idx ON {tbName} ({dbCategory});"); | ||||
|             // }); | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         private void Refresh() | ||||
|         { | ||||
|             _dataList = _db.GetTableList<inventory>(); | ||||
|         } | ||||
| 
 | ||||
|         private bool HasItem(string id) | ||||
|         { | ||||
|             return _dataList.Exists(c => c.id == id); | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         /// 更新道具 | ||||
|         /// </summary> | ||||
|         /// <param name="items"></param> | ||||
|         public void UpdateInventoryItems(List<InventoryItem> items) | ||||
|         { | ||||
|             List<inventory> updates = new List<inventory>(items.Count); | ||||
|             List<inventory> insets = new List<inventory>(items.Count); | ||||
|             InventoryItem item; | ||||
|             inventory inv; | ||||
|             for (int i = 0; i < items.Count; i++) | ||||
|             { | ||||
|                 item = items[i]; | ||||
|                 inv = inventory.Create(item); | ||||
|                 if (!HasItem(item.sku)) | ||||
|                 { | ||||
|                     insets.Add(inv); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     updates.Add(inv); | ||||
|                 } | ||||
|             } | ||||
|             _db.InsertAll(insets); | ||||
|             _db.UpdateAll(updates); | ||||
|              | ||||
|             Refresh(); | ||||
|         } | ||||
| 
 | ||||
|         public InventoryItem GetItem(string sku) | ||||
|         { | ||||
|             if (HasItem(sku)) | ||||
|             { | ||||
|                 var inv = _dataList.Find(c => c.sku == sku); | ||||
|                 if (inv != null) return inv.ToItem(); | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// 表头及数据结构 | ||||
|     /// </summary> | ||||
|     internal class inventory | ||||
|     { | ||||
|         [Indexed(InventoryTable.dbSku, 1)] | ||||
|         public string sku { get; set; } | ||||
|         public int balance { get; set; } | ||||
|         [Indexed(InventoryTable.dbCategory, 2)] | ||||
|         public string cat{ get; set; } | ||||
|         public int attr { get; set; } | ||||
|         public string tsv { get; set; } | ||||
|         public long update_at{ get; set; } | ||||
|         public long create_at{ get; set; } | ||||
| 
 | ||||
|         [PrimaryKey] | ||||
|         public string id => sku; | ||||
| 
 | ||||
|         public static inventory Create(InventoryItem item) | ||||
|         { | ||||
|             return new inventory() | ||||
|             { | ||||
|                 sku = item.sku, | ||||
|                 balance = item.balance, | ||||
|                 cat = item.category, | ||||
|                 attr = item.attr, | ||||
|                 tsv = item.Inventory.ToJson(), | ||||
|                 update_at = item.updateAt, | ||||
|                 create_at = item.createAt | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         public InventoryItem ToItem() | ||||
|         { | ||||
|             return InventoryItem.Create( | ||||
|                 sku, cat, attr, balance, | ||||
|                 null, | ||||
|                 InventoryData.FromJson(tsv), | ||||
|                 this.update_at, | ||||
|                 this.create_at); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 21e2bced768541b5befa3b4397783424 | ||||
| timeCreated: 1706944831 | ||||
|  | @ -0,0 +1,47 @@ | |||
| using System.Collections.Generic; | ||||
| using Unity.Plastic.Newtonsoft.Json; | ||||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|      | ||||
|     public class DetailsAttr | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 永久物品 | ||||
|         /// </summary> | ||||
|         public const int Permanent = 1; | ||||
|         /// <summary> | ||||
|         /// 可消耗 | ||||
|         /// </summary> | ||||
|         public const int Consumable = 2; | ||||
|     } | ||||
|      | ||||
|     public class ExtraReservedField { | ||||
|         public const string scene = "__scene"; | ||||
|         public const string offerId = "__offer_id"; | ||||
|         public const string basePlanId = "__base_plan_id"; | ||||
|         public const string sales = "__sales"; | ||||
|         public const string rate = "__rate"; | ||||
|         public const string contentId = "__content_id"; | ||||
|     } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// 商品清单 | ||||
|     /// </summary> | ||||
|     public class Manifest: IJsonData | ||||
|     { | ||||
|          | ||||
|         [JsonProperty("category")] | ||||
|         public string category; | ||||
| 
 | ||||
|         [JsonProperty("extra")] | ||||
|         public Dictionary<string, dynamic> extra; | ||||
| 
 | ||||
|         public string scene => extra.TryGetValue(ExtraReservedField.scene, out var v)? v : ""; | ||||
|          | ||||
|         public string basePlanId => extra.TryGetValue(ExtraReservedField.basePlanId, out var v)? v : ""; | ||||
|          | ||||
|         public string offerId => extra.TryGetValue(ExtraReservedField.offerId, out var v)? v : ""; | ||||
|         public string contentId => extra.TryGetValue(ExtraReservedField.contentId, out var v)? v : ""; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: a1e83edde24c4a46bd8ce00aaf09f273 | ||||
| timeCreated: 1706945145 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: c176d24ea24445f9ab67763aa8ef3f7c | ||||
| timeCreated: 1706925964 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 42b7e7382b1d45d39f1542ed00f4e890 | ||||
| timeCreated: 1706923167 | ||||
|  | @ -0,0 +1,190 @@ | |||
| 
 | ||||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|     using SQLite4Unity3d; | ||||
|     using UnityEngine; | ||||
|     using System.Collections; | ||||
|     using System; | ||||
|     using System.IO; | ||||
|     using System.Collections.Generic; | ||||
|     using System.Linq; | ||||
|     using System.Linq.Expressions; | ||||
|     using UnityEngine.Networking; | ||||
|      | ||||
|     public class DBService | ||||
|     { | ||||
| 	    internal const string DefaultDBName = "data"; | ||||
| 	    internal const string DefaultDBExtension = ".db"; | ||||
| 	    internal static string DebugDBPath = "mocks_dir/db"; | ||||
| 	    private SQLiteConnection _connection; | ||||
|         private string _dbName; | ||||
|         private string _dbPath; | ||||
| 		private bool _isDebug = false; | ||||
| 		 | ||||
| 		/// <summary> | ||||
| 		/// 获取数据库的路径 | ||||
| 		/// </summary> | ||||
| 		public string DatabasePath => _dbPath; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 启动服务 | ||||
|         /// </summary> | ||||
|         /// <param name="name"></param> | ||||
|         /// <param name="isDebug"></param> | ||||
|         public DBService(string name, bool isDebug = false) | ||||
|         { | ||||
| 	        _isDebug = isDebug; | ||||
| 	        if (string.IsNullOrEmpty(name)) name = DefaultDBName; | ||||
| 	        _dbName = name; | ||||
| 	        _dbPath = Path.GetFullPath($"{Application.persistentDataPath}/{_dbName}{DefaultDBExtension}"); | ||||
| 
 | ||||
| 	        if (isDebug) | ||||
| 	        { | ||||
| 		        _dbPath = Path.GetFullPath($"{Application.persistentDataPath}/{_dbName}_debug{DefaultDBExtension}"); | ||||
| 	        } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static DBService Open(string name, bool isDebug = false) | ||||
|         { | ||||
| 	        var ds = new DBService(name, isDebug); | ||||
| 	        ds.Connect(); | ||||
| 	        return ds; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// 关闭连接 | ||||
|         /// </summary> | ||||
|         public void Close() => _connection?.Close(); | ||||
|          | ||||
|         /// <summary> | ||||
| 		/// 建立连接, 若不存在则创建数据库 | ||||
| 		/// </summary> | ||||
|         public void Connect(string dbPath = "") | ||||
|         { | ||||
| 	        if(!string.IsNullOrEmpty(dbPath)) _dbPath = dbPath; | ||||
| 	        _connection = new SQLiteConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); | ||||
|         } | ||||
| 
 | ||||
|         public bool Exists => File.Exists(_dbPath); | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// 部署默认的 Database | ||||
| 		/// 将内置在 StreamingAssets 中的数据库文件部署到 PersistentDataPath 中 | ||||
| 		/// </summary> | ||||
| 		/// <returns></returns> | ||||
|         public void DeployEmbeddedDB(string embeddedPath, Action<bool> onComplete) | ||||
|         { | ||||
| 	        string from = Path.Combine(Application.streamingAssetsPath, embeddedPath); | ||||
| 
 | ||||
| 	        var uwr = new UnityWebRequest(from); | ||||
| 	        uwr.SendWebRequest().completed += ao => | ||||
| 	        { | ||||
| 		        var success = false; | ||||
| 		        try | ||||
| 		        { | ||||
| 			        if ( uwr.result == UnityWebRequest.Result.Success) | ||||
| 			        { | ||||
| 				        File.WriteAllBytes(_dbPath, uwr.downloadHandler.data); | ||||
| 				        success = true; | ||||
| 			        } | ||||
| 		        } | ||||
| 		        catch (Exception e) | ||||
| 		        { | ||||
| 			        Debug.LogError(e); | ||||
| 		        } | ||||
| 		        onComplete?.Invoke(success); | ||||
| 	        }; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 		/// <summary> | ||||
| 		/// 获取表格 | ||||
| 		/// </summary> | ||||
| 		/// <typeparam name="T"></typeparam> | ||||
| 		/// <returns></returns> | ||||
|         public IEnumerable<T> CreatOrLoadTable<T>() where T: new() | ||||
|         { | ||||
| 	       _connection.CreateTable<T>(); | ||||
| 	       return  _connection.Table<T>(); | ||||
|         } | ||||
| 		 | ||||
|         public List<T> GetTableList<T>()  where T: new() | ||||
|         { | ||||
| 	        var tb = CreatOrLoadTable<T>(); | ||||
| 	        return tb?.ToList() ?? new List<T>(); | ||||
|         } | ||||
| 		 | ||||
| 		 | ||||
| 		 | ||||
|         public int Insert<T>(T value) | ||||
|         { | ||||
| 	        return _connection.Insert(value); | ||||
|         } | ||||
| 
 | ||||
|         public int InsertAll(IEnumerable data) | ||||
|         { | ||||
| 	        return _connection.InsertAll(data); | ||||
|         } | ||||
| 
 | ||||
|         public int Remove(object primaryKey) | ||||
|         { | ||||
| 	        return _connection.Delete(primaryKey); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public int Remove<T>(object primaryKey) | ||||
|         { | ||||
| 	        return _connection.Delete<T>(primaryKey); | ||||
|         } | ||||
| 
 | ||||
|         public int UpdateAll(IEnumerable data) | ||||
|         { | ||||
| 	        return _connection.UpdateAll(data); | ||||
|         } | ||||
| 
 | ||||
|         public T Get<T>(object primaryKey) where T : new() | ||||
|         { | ||||
| 	        return _connection.Get<T>(primaryKey); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public T Find<T>(Expression<Func<T, bool>> predicate) where T : new() | ||||
|         { | ||||
| 	        return _connection.Find<T>(predicate); | ||||
|         } | ||||
| 
 | ||||
|         private bool _onCmdExecution = false; | ||||
|         /// <summary> | ||||
|         /// 执行 SQL 语句 | ||||
|         /// </summary> | ||||
|         /// <param name="query"></param> | ||||
|         /// <param name="onExecutionComplete"></param> | ||||
|         /// <param name="args"></param> | ||||
|         /// <returns></returns> | ||||
|         public int Execute(string query, Action<double> onExecutionComplete = null,  params object[] args) | ||||
|         { | ||||
| 	        if (_onCmdExecution) return -1; | ||||
| 	         | ||||
| 	        SQLiteConnection.TimeExecutionHandler del = null; | ||||
| 	        del = (t1, t2) => | ||||
| 	        { | ||||
| 		        _onCmdExecution = false; | ||||
| 		        _connection.TimeExecutionEvent -= del; | ||||
| 		        onExecutionComplete?.Invoke(t2.TotalSeconds); | ||||
| 	        }; | ||||
| 	        _connection.TimeExecutionEvent += del; | ||||
| 	        _onCmdExecution = true; | ||||
| 	        return _connection.Execute(query, args); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|      | ||||
|      | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 230a328bcc91476ea24d47a0ecbdf441 | ||||
| timeCreated: 1706926988 | ||||
|  | @ -0,0 +1,54 @@ | |||
| 
 | ||||
| 
 | ||||
| using SQLite4Unity3d; | ||||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|     using System.Collections.Generic; | ||||
|      | ||||
|      | ||||
|      | ||||
|     public class GuruDB | ||||
|     { | ||||
| 
 | ||||
| 
 | ||||
|         private static GuruDB _instance; | ||||
|         public static GuruDB Instance | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if(_instance == null) _instance = new GuruDB(); | ||||
|                 return _instance; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public int dbVersion = 1; | ||||
|         public string dbName = "guru"; | ||||
|         private DBService _dbService; | ||||
|         internal DBService Service => _dbService; | ||||
|         public bool IsDebug { get; private set; } = false; | ||||
|         public bool IsReady { get; private set; } | ||||
| 
 | ||||
|         public GuruDB() | ||||
|         { | ||||
| #if UNITY_EDITOR | ||||
|             IsDebug = true; | ||||
| #endif | ||||
|             _dbService = DBService.Open(dbName, IsDebug); | ||||
| 
 | ||||
|             IsReady = _dbService != null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public interface IDBItem | ||||
|     { | ||||
|         [PrimaryKey] | ||||
|         string id { get; set; } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 1c3e8542bfad4731b0c54abf3afe7cf3 | ||||
| timeCreated: 1706923244 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: efa1c022e13449558b8d4d620fae4e69 | ||||
| timeCreated: 1706927602 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 84ef5b243482457ab96d4c376fec23d6 | ||||
| timeCreated: 1706949578 | ||||
|  | @ -0,0 +1,7 @@ | |||
| namespace Guru | ||||
| { | ||||
|     public interface IJsonData | ||||
|     { | ||||
|          | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 68451c547db14e748cb50b5e773c6f00 | ||||
| timeCreated: 1706949587 | ||||
|  | @ -0,0 +1,62 @@ | |||
| 
 | ||||
| using System; | ||||
| using UnityEngine; | ||||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|     using Newtonsoft.Json; | ||||
| 
 | ||||
|      | ||||
|      | ||||
|     public static class JsonDataHelper | ||||
|     { | ||||
|         public static Action<Exception> ExceptionHandler; | ||||
| 
 | ||||
|          | ||||
|         public static string ToJsonString(object obj) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 return JsonConvert.SerializeObject(obj); | ||||
|             } | ||||
|             catch (Exception e) | ||||
|             { | ||||
|                 ExceptionHandler?.Invoke(e); | ||||
|                 Debug.LogError(e); | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static bool Parse<T>(string json, out T result) | ||||
|         { | ||||
|             bool success = false; | ||||
|             result = default(T); | ||||
|              | ||||
|             if(string.IsNullOrEmpty(json)) return false; | ||||
|              | ||||
|             try | ||||
|             { | ||||
|                 result =  JsonConvert.DeserializeObject<T>(json); | ||||
|                 success = true; | ||||
|             } | ||||
|             catch (Exception e) | ||||
|             { | ||||
|                 ExceptionHandler?.Invoke(e); | ||||
|                 Debug.LogError(e); | ||||
|             } | ||||
| 
 | ||||
|             return success; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         public static string ToJson(this IJsonData  obj) | ||||
|         { | ||||
|             return ToJsonString((object)obj); | ||||
|         } | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: ce50b36309b34a41b8bb844efbba1a17 | ||||
| timeCreated: 1706949612 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 043ca2bcb28a45fba7edf46e5819a53b | ||||
| timeCreated: 1706923871 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: e4425fb6790b4f16b10a6ac9545f087c | ||||
| timeCreated: 1706925944 | ||||
|  | @ -0,0 +1,48 @@ | |||
| using System; | ||||
| 
 | ||||
| namespace Guru | ||||
| { | ||||
|      | ||||
|     public partial class PropertyDatabase | ||||
|     { | ||||
|          | ||||
|          | ||||
|          | ||||
|          | ||||
|          | ||||
|          | ||||
|          | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     [Serializable] | ||||
|     public class PropertyEntity | ||||
|     { | ||||
|         const string TableName = "properties"; | ||||
|         // internal const string dbKey = "key"; | ||||
|         // internal const string dbValue = "value"; | ||||
|         // internal const string dbGroup = "gp"; | ||||
|         // internal const string dbUsage = "usage"; | ||||
|         // internal const string dbTag = "tag"; | ||||
|         // internal const string dbUpdateAt = "upt"; | ||||
| 
 | ||||
|         public string key; | ||||
|         public string value; | ||||
|         public string gp = PropertyKey.DefaultGroup; | ||||
|         public int usage = PropertyKey.UsageGeneral; | ||||
|         public string tag; | ||||
|         public string upt; | ||||
|         public int updateAt = 0; | ||||
|          | ||||
|          | ||||
|         | ||||
|          | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 357ec7a04d0a454086a09bd2f2fcfe79 | ||||
| timeCreated: 1706926072 | ||||
|  | @ -0,0 +1,59 @@ | |||
| namespace Guru | ||||
| { | ||||
|     public struct PropertyKey | ||||
|     { | ||||
|         public const int UsageGeneral = 0; | ||||
|         public const int UsageSettings = 1; | ||||
|         public const string DefaultGroup = "guru"; | ||||
| 
 | ||||
|         public string name { get; private set; } | ||||
|         public string key { get; private set; } | ||||
|         public string group { get; private set; } | ||||
|         public string tag { get; private set; } | ||||
|         public int usage { get; private set; } | ||||
|          | ||||
|         public static PropertyKey General(string name, string group = DefaultGroup, string tag = "") | ||||
|         { | ||||
|             return new PropertyKey | ||||
|             { | ||||
|                 name = name, | ||||
|                 key = $"{group}@{name}", | ||||
|                 group = group, | ||||
|                 tag = tag, | ||||
|                 usage = UsageGeneral | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         public static PropertyKey Setting(string name, string group = DefaultGroup, string tag = "") | ||||
|         { | ||||
|             return new PropertyKey | ||||
|             { | ||||
|                 name = name, | ||||
|                 key = $"{group}@{name}", | ||||
|                 group = group, | ||||
|                 tag = tag, | ||||
|                 usage = UsageSettings | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         /// 判断两个属性 Key 是否相等 | ||||
|         /// </summary> | ||||
|         /// <param name="other"></param> | ||||
|         /// <returns></returns> | ||||
|         public  bool Equals(PropertyKey other) | ||||
|         { | ||||
|             return name == other.name &&  | ||||
|                    key == other.key &&  | ||||
|                    group == other.group &&  | ||||
|                    tag == other.tag && | ||||
|                    usage == other.usage; | ||||
|         } | ||||
| 
 | ||||
|         public override string ToString() | ||||
|         { | ||||
|             return $"#{tag}#[{key}]({usage})"; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 5b1517229c974c46ac85c3c8f8b3b330 | ||||
| timeCreated: 1706924244 | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: d50e6dd9a1324036af9e950f5ec489ed | ||||
| timeCreated: 1706925986 | ||||
|  | @ -0,0 +1,19 @@ | |||
| namespace Guru | ||||
| { | ||||
|     public interface IPropertyStorage | ||||
|     { | ||||
|         void SetDouble(PropertyKey key, double value); | ||||
|         void SetInt(PropertyKey key, int value); | ||||
|         void SetBool(PropertyKey key, bool value); | ||||
|         void SetString(PropertyKey key, string value); | ||||
|         double? GetDouble(PropertyKey key, double? defaultValue); | ||||
|         int? GetInt(PropertyKey key, int? defaultValue); | ||||
|         bool? GetBool(PropertyKey key, bool? defaultValue); | ||||
|         string GetString(PropertyKey key, string defaultValue); | ||||
|          | ||||
|         void remove(PropertyKey key); | ||||
|         void removeAllWithTag(string tag); | ||||
|          | ||||
|          | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 761fa4520c9746b5b14e27078be64003 | ||||
| timeCreated: 1706923906 | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 36d6b6e33eeef40509c8647e2d7c555d | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 4475ec4c51944461ba8479536dfcda61 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: f288c6262551743be968c8c546dd065e | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: c1aacb5a55fc94e4da727839f3ea4d24 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 8a0518887aa4d45798babfb2249cdd6d | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,27 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 035fff0fb0daa454f882f263a44d8a71 | ||||
| PluginImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   iconMap: {} | ||||
|   executionOrder: {} | ||||
|   defineConstraints: [] | ||||
|   isPreloaded: 0 | ||||
|   isOverridable: 0 | ||||
|   isExplicitlyReferenced: 0 | ||||
|   validateReferences: 1 | ||||
|   platformData: | ||||
|   - first: | ||||
|       Any:  | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: {} | ||||
|   - first: | ||||
|       Editor: Editor | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         DefaultValueInitialized: true | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: a8bf69720c6da40ff9d7ada1a6e52198 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,27 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: fbfa6b2ad32f74cb9ba69b1613f0ac8b | ||||
| PluginImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   iconMap: {} | ||||
|   executionOrder: {} | ||||
|   defineConstraints: [] | ||||
|   isPreloaded: 0 | ||||
|   isOverridable: 0 | ||||
|   isExplicitlyReferenced: 0 | ||||
|   validateReferences: 1 | ||||
|   platformData: | ||||
|   - first: | ||||
|       Any:  | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: {} | ||||
|   - first: | ||||
|       Editor: Editor | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         DefaultValueInitialized: true | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: c0bc4319db8fc4e87b92d8849e10effa | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,27 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 533c36573354e4e82a0871a3ae353c64 | ||||
| PluginImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   iconMap: {} | ||||
|   executionOrder: {} | ||||
|   defineConstraints: [] | ||||
|   isPreloaded: 0 | ||||
|   isOverridable: 0 | ||||
|   isExplicitlyReferenced: 0 | ||||
|   validateReferences: 1 | ||||
|   platformData: | ||||
|   - first: | ||||
|       Any:  | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: {} | ||||
|   - first: | ||||
|       Editor: Editor | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         DefaultValueInitialized: true | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 87d29778372204305920a77087f262a4 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,52 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 25884d71c32604d7987793cbcbe801ac | ||||
| PluginImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   iconMap: {} | ||||
|   executionOrder: {} | ||||
|   defineConstraints: [] | ||||
|   isPreloaded: 0 | ||||
|   isOverridable: 0 | ||||
|   isExplicitlyReferenced: 0 | ||||
|   validateReferences: 1 | ||||
|   platformData: | ||||
|   - first: | ||||
|       Any:  | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: {} | ||||
|   - first: | ||||
|       Editor: Editor | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: x86_64 | ||||
|         DefaultValueInitialized: true | ||||
|   - first: | ||||
|       Standalone: Linux64 | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: | ||||
|         CPU: x86_64 | ||||
|   - first: | ||||
|       Standalone: OSXUniversal | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: | ||||
|         CPU: x86_64 | ||||
|   - first: | ||||
|       Standalone: Win | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: None | ||||
|   - first: | ||||
|       Standalone: Win64 | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: | ||||
|         CPU: x86_64 | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
|  | @ -0,0 +1,8 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: f7a5439d4fb9e4828937865037857668 | ||||
| folderAsset: yes | ||||
| DefaultImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,52 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 8f6a8ec41de2b4f569e23b20011eec8c | ||||
| PluginImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   iconMap: {} | ||||
|   executionOrder: {} | ||||
|   defineConstraints: [] | ||||
|   isPreloaded: 0 | ||||
|   isOverridable: 0 | ||||
|   isExplicitlyReferenced: 0 | ||||
|   validateReferences: 1 | ||||
|   platformData: | ||||
|   - first: | ||||
|       Any:  | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: {} | ||||
|   - first: | ||||
|       Editor: Editor | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: x86 | ||||
|         DefaultValueInitialized: true | ||||
|   - first: | ||||
|       Standalone: Linux64 | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: None | ||||
|   - first: | ||||
|       Standalone: OSXUniversal | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: x86 | ||||
|   - first: | ||||
|       Standalone: Win | ||||
|     second: | ||||
|       enabled: 1 | ||||
|       settings: | ||||
|         CPU: x86 | ||||
|   - first: | ||||
|       Standalone: Win64 | ||||
|     second: | ||||
|       enabled: 0 | ||||
|       settings: | ||||
|         CPU: None | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,11 @@ | |||
| fileFormatVersion: 2 | ||||
| guid: 26aa670e0ee4a47a8bb1d3dfcbec8533 | ||||
| MonoImporter: | ||||
|   externalObjects: {} | ||||
|   serializedVersion: 2 | ||||
|   defaultReferences: [] | ||||
|   executionOrder: 0 | ||||
|   icon: {instanceID: 0} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
		Loading…
	
		Reference in New Issue