Compare commits
1 Commits
main
...
feature/In
| Author | SHA1 | Date |
|---|---|---|
|
|
3f10cf718e |
|
|
@ -114,6 +114,7 @@ namespace Guru
|
||||||
// 经济相关
|
// 经济相关
|
||||||
public static readonly string ParameterBalance = "balance"; // 用于余额
|
public static readonly string ParameterBalance = "balance"; // 用于余额
|
||||||
public static readonly string ParameterSku = "sku"; // sku
|
public static readonly string ParameterSku = "sku"; // sku
|
||||||
|
public static readonly string ParameterScene = "scene"; // sku
|
||||||
public static readonly string ParameterVirtualCurrencyName = "virtual_currency_name"; // 虚拟货币名称
|
public static readonly string ParameterVirtualCurrencyName = "virtual_currency_name"; // 虚拟货币名称
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,65 +72,59 @@ namespace Guru
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取虚拟货币
|
/// 获取道具/货币
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="currencyName"></param>
|
/// <param name="virtualCurrencyName"></param>
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="balance"></param>
|
|
||||||
/// <param name="method"></param>
|
/// <param name="method"></param>
|
||||||
/// <param name="levelName"></param>
|
/// <param name="balance"></param>
|
||||||
/// <param name="isIap"></param>
|
/// <param name="value"></param>
|
||||||
/// <param name="sku"></param>
|
/// <param name="methodDetails"></param>
|
||||||
/// <param name="scene"></param>
|
public static void EarnVirtualCurrency(string virtualCurrencyName, string method, int balance, int value,
|
||||||
public static void EarnVirtualCurrency(string currencyName, int value, int balance,
|
string methodDetails = "")
|
||||||
string method = "",
|
|
||||||
string levelName = "",
|
|
||||||
bool isIap = false,
|
|
||||||
string sku = "",
|
|
||||||
string scene = "")
|
|
||||||
{
|
{
|
||||||
if (isIap) method = "iap_buy";
|
|
||||||
var data = new Dictionary<string, dynamic>()
|
var data = new Dictionary<string, dynamic>()
|
||||||
{
|
{
|
||||||
{ ParameterVirtualCurrencyName, currencyName },
|
{ ParameterVirtualCurrencyName, virtualCurrencyName },
|
||||||
|
{ ParameterItemCategory, method },
|
||||||
|
{ ParameterItemName, methodDetails },
|
||||||
{ ParameterValue, value },
|
{ ParameterValue, value },
|
||||||
{ ParameterBalance, balance },
|
{ 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 });
|
LogEvent(EventEarnVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true });
|
||||||
|
|
||||||
// FB 上报收入点
|
// FB 上报收入点
|
||||||
FB.LogAppEvent(EventEarnVirtualCurrency, value, data);
|
// FB.LogAppEvent(EventEarnVirtualCurrency, value, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
public static void SpendVirtualCurrency(string currencyName, int value, int balance,
|
/// 消耗道具/货币
|
||||||
string method = "",
|
/// </summary>
|
||||||
string levelName = "",
|
/// <param name="contentId"></param>
|
||||||
string scene = "")
|
/// <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>()
|
var data = new Dictionary<string, dynamic>()
|
||||||
{
|
{
|
||||||
{ ParameterVirtualCurrencyName, currencyName },
|
{ ParameterVirtualCurrencyName, virtualCurrencyName },
|
||||||
{ ParameterValue, value },
|
{ ParameterValue, price },
|
||||||
{ ParameterBalance, balance },
|
{ ParameterBalance, balance },
|
||||||
{ ParameterLevelName, levelName },
|
{ ParameterItemName, contentId },
|
||||||
{ ParameterItemCategory, method },
|
{ ParameterItemCategory, contentType },
|
||||||
};
|
};
|
||||||
|
if (!string.IsNullOrEmpty(scene)) data[ParameterScene] = scene; // 获取的虚拟货币或者道具的场景
|
||||||
if (!string.IsNullOrEmpty(scene)) data[ParameterItemName] = scene; // 获取的虚拟货币或者道具的场景
|
|
||||||
|
|
||||||
LogEvent(EventSpendVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true });
|
LogEvent(EventSpendVirtualCurrency, data, new EventSetting() { EnableFirebaseAnalytics = true });
|
||||||
|
|
||||||
// FB 上报消费点
|
// FB 上报消费点
|
||||||
FB.LogAppEvent(EventSpendVirtualCurrency, value, data);
|
// FB.LogAppEvent(EventSpendVirtualCurrency, value, data);
|
||||||
|
|
||||||
// FB 上报消耗事件买量点
|
// FB 上报消耗事件买量点
|
||||||
FBSpentCredits(value, scene, method); // 点位信息有变化
|
FBSpentCredits(contentId, contentType, price); // 点位信息有变化
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -140,7 +134,7 @@ namespace Guru
|
||||||
/// <param name="amount"></param>
|
/// <param name="amount"></param>
|
||||||
/// <param name="contentId"></param>
|
/// <param name="contentId"></param>
|
||||||
/// <param name="contentType"></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,
|
FB.LogAppEvent(AppEventName.SpentCredits, amount,
|
||||||
new Dictionary<string, object>()
|
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