Compare commits

...

1 Commits

56 changed files with 4990 additions and 37 deletions

View File

@ -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"; // 虚拟货币名称
}
}

View File

@ -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>()

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 93bb1ade31b44cc9bb3dbad3051cbe78
timeCreated: 1706922645

View File

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f4820a46a3a4b02ac85a48a4e25014c
timeCreated: 1706922699

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e921121b06404396b164c6538e68c6aa
timeCreated: 1706922657

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54c27103f7594aa9bc337f5e076fb550
timeCreated: 1706959298

View File

@ -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";
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b4e29d06a7f4ddb860e08d3aa81610e
timeCreated: 1706945881

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21e2bced768541b5befa3b4397783424
timeCreated: 1706944831

View File

@ -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 : "";
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a1e83edde24c4a46bd8ce00aaf09f273
timeCreated: 1706945145

3
Runtime/GuruLibs.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c176d24ea24445f9ab67763aa8ef3f7c
timeCreated: 1706925964

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42b7e7382b1d45d39f1542ed00f4e890
timeCreated: 1706923167

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 230a328bcc91476ea24d47a0ecbdf441
timeCreated: 1706926988

View File

@ -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; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c3e8542bfad4731b0c54abf3afe7cf3
timeCreated: 1706923244

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: efa1c022e13449558b8d4d620fae4e69
timeCreated: 1706927602

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84ef5b243482457ab96d4c376fec23d6
timeCreated: 1706949578

View File

@ -0,0 +1,7 @@
namespace Guru
{
public interface IJsonData
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 68451c547db14e748cb50b5e773c6f00
timeCreated: 1706949587

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce50b36309b34a41b8bb844efbba1a17
timeCreated: 1706949612

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 043ca2bcb28a45fba7edf46e5819a53b
timeCreated: 1706923871

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e4425fb6790b4f16b10a6ac9545f087c
timeCreated: 1706925944

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 357ec7a04d0a454086a09bd2f2fcfe79
timeCreated: 1706926072

View File

@ -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})";
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5b1517229c974c46ac85c3c8f8b3b330
timeCreated: 1706924244

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d50e6dd9a1324036af9e950f5ec489ed
timeCreated: 1706925986

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 761fa4520c9746b5b14e27078be64003
timeCreated: 1706923906

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 36d6b6e33eeef40509c8647e2d7c555d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4475ec4c51944461ba8479536dfcda61
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f288c6262551743be968c8c546dd065e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c1aacb5a55fc94e4da727839f3ea4d24
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8a0518887aa4d45798babfb2249cdd6d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8bf69720c6da40ff9d7ada1a6e52198
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c0bc4319db8fc4e87b92d8849e10effa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 87d29778372204305920a77087f262a4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7a5439d4fb9e4828937865037857668
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26aa670e0ee4a47a8bb1d3dfcbec8533
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: