提交模块化内容
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eb32bc6ddeb6a54281dd51759c715a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
# ApplePay
|
||||
|
||||
2025 年 05 月 24 日 V1.0.0 提交初始版本 applepay 模块,该模块功能依赖通用模块,网络模块和数据模块,请一起拉取
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f92f9691c788c414a82e03722722293e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10ce64fc727c4edeb97e73ad29ef5eab
|
||||
timeCreated: 1747906541
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2da583c874f4b13acc3eb54eaf5a428
|
||||
timeCreated: 1747996053
|
||||
@@ -0,0 +1,43 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SGModule.ApplePay
|
||||
{
|
||||
public class ApplePayData
|
||||
{
|
||||
[JsonProperty("innerOrderId")]
|
||||
public string innerOrderId;
|
||||
[JsonProperty("amount")]
|
||||
public int amount;
|
||||
[JsonProperty("sku")]
|
||||
public string sku;
|
||||
[JsonProperty("currency")]
|
||||
public string currency = "USD";
|
||||
[JsonProperty("shopName")]
|
||||
public string shopName;
|
||||
[JsonProperty("type")]
|
||||
public string type;
|
||||
|
||||
}
|
||||
|
||||
public class AppleCheckData
|
||||
{
|
||||
[JsonProperty("signedPayload")]
|
||||
public string signedPayload;
|
||||
[JsonProperty("innerOrderId")]
|
||||
public string innerOrderId;
|
||||
}
|
||||
|
||||
public class AppleSubscribeData
|
||||
{
|
||||
[JsonProperty("signedPayload")]
|
||||
public string signedPayload;
|
||||
[JsonProperty("sku")]
|
||||
public string sku;
|
||||
[JsonProperty("currency")]
|
||||
public string currency;
|
||||
[JsonProperty("amount")]
|
||||
public int amount;
|
||||
[JsonProperty("expires_time")]
|
||||
public long expires_time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9dcbc72e1537449aaea5b71d67e7bf85
|
||||
timeCreated: 1747996070
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69666ded8fdc4244a0d43d25f7831bca
|
||||
timeCreated: 1747906570
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace SGModule.ApplePay
|
||||
{
|
||||
public enum ApplePayBackType
|
||||
{
|
||||
/// <summary>
|
||||
///创建支付订单
|
||||
/// </summary>
|
||||
Create,
|
||||
/// <summary>
|
||||
///验证支付订单
|
||||
/// </summary>
|
||||
Check,
|
||||
/// <summary>
|
||||
///取消支付订单
|
||||
/// </summary>
|
||||
Cancel,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e83ae38d5d0e4880836c55d708e7083c
|
||||
timeCreated: 1747995062
|
||||
@@ -0,0 +1,661 @@
|
||||
using SGModule.Common.Base;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SGModule.Common.Helper;
|
||||
using SGModule.NetKit;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
||||
#if UNITY_IOS && UNITY_IAP
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using DG.Tweening;
|
||||
using SGModule.Common.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using SGModule.Net;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Purchasing;
|
||||
using UnityEngine.Purchasing.Extension;
|
||||
|
||||
namespace SGModule.ApplePay
|
||||
{
|
||||
public class ApplePayManager : SingletonMonoBehaviour<ApplePayManager>, IDetailedStoreListener
|
||||
{
|
||||
private IStoreController _storeController;
|
||||
private static IExtensionProvider _extensionProvider;
|
||||
private static IAppleExtensions _appleExtension;
|
||||
|
||||
private Action<string> _failedCallback;
|
||||
|
||||
private Action<ApplePayBackType, AppleResponseData> _successCallback;
|
||||
|
||||
private Dictionary<string, (string id, string type)> _products = new Dictionary<string, (string, string)>();
|
||||
|
||||
private ApplePayData _payData;
|
||||
|
||||
private string _packageName;
|
||||
|
||||
private float _lastPayAttemptTime;
|
||||
|
||||
|
||||
public void SendDebugToServer(string error, string stackTrace) {
|
||||
|
||||
// ErrorLogKit.Send("debug", error, stackTrace,SuperApplication.Instance.attribution);
|
||||
}
|
||||
|
||||
private Dictionary<string, ProductConfig> _productConfigs = new Dictionary<string, ProductConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// 通过 ProductConfig 数组初始化商品
|
||||
/// </summary>
|
||||
public void InitProduct(List<ProductConfig> configs, string packageName, Action<ApplePayBackType, AppleResponseData> successCallback)
|
||||
{
|
||||
var module = StandardPurchasingModule.Instance();
|
||||
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
|
||||
|
||||
_successCallback = successCallback;
|
||||
_packageName = packageName;
|
||||
SendDebugToServer("[Apple pay] InitProduct 开始------", Environment.StackTrace);
|
||||
try
|
||||
{
|
||||
foreach (var config in configs)
|
||||
{
|
||||
// Debug.Log( $"[Apple pay] InitProduct ------{config.sku} , {config.type} ");
|
||||
if (Enum.TryParse<UnityEngine.Purchasing.ProductType>(config.type, true, out var productType))
|
||||
{
|
||||
_productConfigs.Add(config.sku, config);
|
||||
|
||||
builder.AddProduct(config.sku, productType);
|
||||
}
|
||||
else
|
||||
{
|
||||
SendDebugToServer($"无法解析 ProductType: {config.type}", Environment.StackTrace);
|
||||
Debug.LogError($"无法解析 ProductType: {config.type}");
|
||||
// 可根据需要处理默认类型或跳过该商品
|
||||
}
|
||||
}
|
||||
|
||||
SendDebugToServer("[Apple pay] InitProduct 结束------", Environment.StackTrace);
|
||||
UnityPurchasing.Initialize(this, builder);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SendDebugToServer($" 初始化商品 失败 InitProduct: {e.Message}", Environment.StackTrace);
|
||||
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
|
||||
Debug.Log( $"[Apple pay] InitProduct -----2 ");
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void SetPayData(string sku)
|
||||
{
|
||||
var payData = GetPayData(sku);
|
||||
_payData = payData;
|
||||
|
||||
}
|
||||
|
||||
private ApplePayData GetPayData(string sku)
|
||||
{
|
||||
if (_productConfigs.TryGetValue(sku, out var data))
|
||||
{
|
||||
Debug.Log( $"[Apple pay] InitProduct ------{data.sku} , {data.type} ");
|
||||
return new ApplePayData
|
||||
{
|
||||
sku = sku,
|
||||
currency = "USD",
|
||||
amount = (int)Math.Round(data.price * 100),
|
||||
};
|
||||
}
|
||||
Debug.LogError($" set _payData error------_payData is null ");
|
||||
return null;
|
||||
}
|
||||
|
||||
private Coroutine _payDataCheckCoroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 启动_payData超时检测协程
|
||||
/// </summary>
|
||||
private void StartPayDataTimeoutCheck()
|
||||
{
|
||||
if (_payDataCheckCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_payDataCheckCoroutine);
|
||||
}
|
||||
_payDataCheckCoroutine = StartCoroutine(PayDataTimeoutCheck());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测_payData是否超时的协程
|
||||
/// </summary>
|
||||
private IEnumerator PayDataTimeoutCheck()
|
||||
{
|
||||
float startTime = Time.time;
|
||||
float timeout = 60f; // 1分钟超时
|
||||
|
||||
while (Time.time - startTime < timeout)
|
||||
{
|
||||
// 如果_payData已经为null,停止检测
|
||||
if (_payData == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 1分钟后检查,如果_payData仍然不为null,则设为null
|
||||
if (_payData != null)
|
||||
{
|
||||
_payData = null;
|
||||
Debug.Log("[Apple Pay] _payData超时,已自动设为null");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 购买接口
|
||||
/// </summary>
|
||||
/// <param name="payData">购买需要的字段存放</param>
|
||||
/// <param name="successCallback">成功回调(ApplePayBackType为订单返回的类型:具体看 ApplePayBackType 枚举),作用:用来打点</param>
|
||||
/// <param name="failedCallback">失败回调 (string为失败原因)</param>
|
||||
public void Purchase(string sku, Action<ApplePayBackType, AppleResponseData> successCallback, Action<string> failedCallback)
|
||||
{
|
||||
Debug.Log( $"[Apple Pay] Purchase: {JsonConvert.SerializeObject(_payData)}");
|
||||
|
||||
SendDebugToServer("[Apple pay] 购买 Purchase ------", Environment.StackTrace);
|
||||
|
||||
if (Time.time - _lastPayAttemptTime < 5)
|
||||
{
|
||||
failedCallback?.Invoke("Clicks are too frequent");
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPayAttemptTime = Time.time;
|
||||
Debug.Log( $"[Apple Pay] Purchase-00---------{JsonConvert.SerializeObject(_payData)}");
|
||||
if (_payData != null)
|
||||
{
|
||||
// 启动超时检测
|
||||
StartPayDataTimeoutCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log( $"[Apple Pay] Purchase-1---------");
|
||||
|
||||
if (!IsInitialized())
|
||||
{
|
||||
failedCallback?.Invoke("Not Initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log( $"[Apple Pay] Purchase-2---------");
|
||||
|
||||
var product = _storeController.products.WithID(sku);
|
||||
if (product == null || !product.availableToPurchase)
|
||||
{
|
||||
failedCallback?.Invoke("Either is not found or is not available for purchase");
|
||||
return;
|
||||
}
|
||||
|
||||
_successCallback = successCallback;
|
||||
_failedCallback = failedCallback;
|
||||
|
||||
SetPayData(sku);
|
||||
|
||||
_storeController.InitiatePurchase(product);
|
||||
|
||||
Debug.Log( $"[Apple Pay] Purchase-3---------");
|
||||
|
||||
if (_payData != null)
|
||||
{
|
||||
var payData = new ApplePayData
|
||||
{
|
||||
sku = sku,
|
||||
currency = "USD",
|
||||
amount = _payData.amount,
|
||||
};
|
||||
|
||||
ApplePayNet.ApplePayCreate<ApplePayData>(payData, (response) =>
|
||||
{
|
||||
Debug.Log( $"[Apple Pay] Purchase-4---------");
|
||||
|
||||
if (response.IsSuccess)
|
||||
{
|
||||
_successCallback?.Invoke(ApplePayBackType.Create, null);
|
||||
}
|
||||
|
||||
if (_payData != null) _payData.innerOrderId = response.Data.innerOrderId;
|
||||
SendDebugToServer($"[Apple Pay] Purchase-5-------创建内部ID innerOrderId--{_payData.innerOrderId}", Environment.StackTrace);
|
||||
|
||||
Debug.Log( $"[Apple Pay] Purchase-5-------innerOrderId--{_payData.innerOrderId}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IOS恢复内购
|
||||
/// 会在删除应用后,第一次安装是自动恢复
|
||||
/// </summary>
|
||||
/// <param name="restoreCallback">恢复回调</param>
|
||||
public void AppleRestore(Action<bool, string> restoreCallback)
|
||||
{
|
||||
if (!IsInitialized())
|
||||
{
|
||||
Debug.LogWarning("[ApplePay] IAppleExtensions 未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log( "[ApplePay] 用户手动恢复购买");
|
||||
_appleExtension.RestoreTransactions((success, error) =>
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
Debug.Log( "[Apple Pay] Restore Transactions 成功");
|
||||
// 这里会触发 ProcessPurchase 回调
|
||||
restoreCallback(true, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[Apple Pay] Restore Transactions 失败: {error}");
|
||||
// 可以提示用户重试
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#region 购买成功回调
|
||||
|
||||
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
|
||||
{
|
||||
|
||||
SendDebugToServer($"[Apple pay] 购买成功回调 Purchase ---{JsonConvert.SerializeObject(purchaseEvent.purchasedProduct)}---", Environment.StackTrace);
|
||||
|
||||
var product = purchaseEvent.purchasedProduct;
|
||||
var productType = product.definition.type;
|
||||
var sku = product.definition.id;
|
||||
var receipt = product.receipt;
|
||||
|
||||
|
||||
Debug.Log(
|
||||
$" ProcessPurchase 1 Purchase: {JsonConvert.SerializeObject(purchaseEvent.purchasedProduct)}");
|
||||
|
||||
if (purchaseEvent is not { purchasedProduct: not null })
|
||||
{
|
||||
Debug.LogError("[Apple Pay] ProcessPurchase 2 : purchaseEvent 或 purchasedProduct 为 null");
|
||||
return PurchaseProcessingResult.Complete;
|
||||
}
|
||||
|
||||
Debug.Log( $"购买商品类型: {productType}, SKU: {sku}");
|
||||
if (productType == ProductType.Subscription)
|
||||
{
|
||||
SendDebugToServer($"[Apple pay] 购买商品类型 Purchase ---{productType}-{sku}--", Environment.StackTrace);
|
||||
|
||||
// 服务器验证
|
||||
UploadReceiptForValidation(sku, product.transactionID, isSuccess =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
// 验证成功后的逻辑
|
||||
// 验证成功,完成购买
|
||||
_storeController.ConfirmPendingPurchase(product);
|
||||
}
|
||||
});
|
||||
|
||||
return PurchaseProcessingResult.Pending;
|
||||
}
|
||||
|
||||
if (productType == ProductType.NonConsumable)
|
||||
{
|
||||
Debug.Log(
|
||||
$" 识别到非消耗性商品 ----{DataMgr.ApplePayTransactionID.Value.Contains(product.transactionID)}");
|
||||
if (DataMgr.ApplePayTransactionID.Value.Contains(product.transactionID))
|
||||
{
|
||||
return PurchaseProcessingResult.Complete;
|
||||
}
|
||||
|
||||
DataMgr.ApplePayTransactionID.Value.Add(product.transactionID);
|
||||
}
|
||||
|
||||
// 普通商品直接处理
|
||||
HandlePurchaseSuccess(product.transactionID, isSuccess =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
// 验证成功,完成购买
|
||||
_storeController.ConfirmPendingPurchase(product);
|
||||
}
|
||||
});
|
||||
|
||||
return PurchaseProcessingResult.Pending;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支付测试代码
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="sku"></param>
|
||||
/// <param name="transactionID"></param>
|
||||
public void ApplePayTest(ProductType type, string sku, string transactionID, Action<ApplePayBackType, AppleResponseData> successCallback)
|
||||
{
|
||||
|
||||
SetPayData(sku);
|
||||
Debug.Log( $"[ApplePay] 测试支付 type: {type}, sku: {sku}, transactionID: {transactionID}" );
|
||||
|
||||
_successCallback = successCallback;
|
||||
|
||||
if (type == ProductType.Subscription)
|
||||
{
|
||||
// 服务器验证
|
||||
UploadReceiptForValidation(sku, transactionID, isSuccess =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
Debug.Log( "订阅商品验证成功");
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProductType.NonConsumable)
|
||||
{
|
||||
Debug.Log(
|
||||
$" 识别到非消耗性商品 ----{DataMgr.ApplePayTransactionID.Value.Contains(transactionID)}");
|
||||
if (DataMgr.ApplePayTransactionID.Value.Contains(transactionID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DataMgr.ApplePayTransactionID.Value.Add(transactionID);
|
||||
}
|
||||
|
||||
// 普通商品直接处理
|
||||
HandlePurchaseSuccess(transactionID, isSuccess =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
Debug.Log( "普通商品验证成功");
|
||||
}
|
||||
},true);
|
||||
}
|
||||
|
||||
//服务器验证
|
||||
private void UploadReceiptForValidation(string sku, string transactionID, Action<bool> onValidationComplete)
|
||||
{
|
||||
Debug.Log( "识别到订阅商品,准备进行订阅验证");
|
||||
SendDebugToServer($"[Apple pay] 识别到订阅商品,准备进行订阅验证", Environment.StackTrace);
|
||||
|
||||
var productConfig = GetPayData(sku);
|
||||
if (productConfig != null)
|
||||
{
|
||||
Debug.Log( $"订阅商品ID: {sku} transactionID: {transactionID} _payData.amount: {productConfig.amount}");
|
||||
|
||||
SendDebugToServer($"订阅商品ID: {sku} transactionID: {transactionID} _payData.amount: {productConfig.amount}", Environment.StackTrace);
|
||||
|
||||
ApplePayNet.AppleSubscribeCheck<AppleSubscribeData>(
|
||||
transactionID,
|
||||
sku,
|
||||
productConfig.amount,
|
||||
_packageName,
|
||||
response =>
|
||||
{
|
||||
bool isSuccess = response.IsSuccess;
|
||||
|
||||
// 在这里返回验证结果
|
||||
onValidationComplete?.Invoke(isSuccess);
|
||||
_payData = null;
|
||||
if (isSuccess)
|
||||
{
|
||||
Debug.Log( "订阅验证成功");
|
||||
SendDebugToServer($"订阅验证成功", Environment.StackTrace);
|
||||
|
||||
_successCallback?.Invoke(ApplePayBackType.Check, new AppleResponseData()
|
||||
{
|
||||
expires_time = response.Data.expires_time,
|
||||
sku = sku
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[Apple Pay] 订阅验证失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支付成功后的本地处理(订阅和普通商品共用)
|
||||
/// </summary>
|
||||
private void HandlePurchaseSuccess(string transactionID, Action<bool> onValidationComplete, bool isTest = false)
|
||||
{
|
||||
var payDataJson = GetApplePayData();
|
||||
Debug.Log( $" HandlePurchaseSuccess payDataJson: {JsonConvert.SerializeObject(payDataJson)}");
|
||||
|
||||
var statusDictionary = JsonConvert.DeserializeObject<Dictionary<string, ApplePayData>>(payDataJson)
|
||||
?? new Dictionary<string, ApplePayData>();
|
||||
|
||||
SendDebugToServer($"普通商品 HandlePurchaseSuccess: {transactionID}", Environment.StackTrace);
|
||||
|
||||
if (!statusDictionary.ContainsKey(transactionID))
|
||||
{
|
||||
Debug.Log( $"记录新交易 transactionID: {transactionID}");
|
||||
Debug.Log( $"-----_payData: {JsonConvert.SerializeObject(_payData)}" );
|
||||
if (_payData == null || string.IsNullOrWhiteSpace(_payData.innerOrderId))
|
||||
{
|
||||
Debug.LogWarning("[Apple Pay] _payData 为空或 innerOrderId 无效");
|
||||
return;
|
||||
}
|
||||
|
||||
statusDictionary.Add(transactionID, _payData);
|
||||
SaveApplePayData(statusDictionary);
|
||||
_payData = null;
|
||||
}
|
||||
|
||||
if (statusDictionary.TryGetValue(transactionID, out var cValue))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(cValue.innerOrderId) || isTest)
|
||||
{
|
||||
ApplePaySuccess(transactionID, onValidationComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveApplePayData(Dictionary<string, ApplePayData> payData)
|
||||
{
|
||||
// 保存更新后的数据
|
||||
string json = JsonConvert.SerializeObject(payData);
|
||||
PlayerPrefs.SetString("SGModule_apple_pay_data", json);
|
||||
}
|
||||
|
||||
private string GetApplePayData()
|
||||
{
|
||||
string json = PlayerPrefs.GetString("SGModule_apple_pay_data", "");
|
||||
// GetString if (json == "")
|
||||
// {
|
||||
// json = DataWrapper.ApplePayData;
|
||||
// }
|
||||
return json;
|
||||
}
|
||||
|
||||
private void ApplePaySuccess(string transactionID, Action<bool> onValidationComplete)
|
||||
{
|
||||
Debug.Log( "apple 支付 StartCoroutine------");
|
||||
StartCoroutine(ProcessPayData(transactionID, onValidationComplete));
|
||||
}
|
||||
|
||||
private IEnumerator ProcessPayData(string orderId, Action<bool> onValidationComplete)
|
||||
{
|
||||
// 发起请求
|
||||
ApplePayRequest(orderId, onValidationComplete);
|
||||
|
||||
yield return null; // 等待本次请求完成
|
||||
}
|
||||
|
||||
private void ApplePayRequest(string transactionID, Action<bool> onValidationComplete)
|
||||
{
|
||||
var payDataJson = GetApplePayData();
|
||||
Debug.Log( $"ApplePayRequest 1 payDataJson: {JsonConvert.SerializeObject(payDataJson)}");
|
||||
SendDebugToServer($"验单 普通商品: {transactionID}", Environment.StackTrace);
|
||||
|
||||
var statusDictionary = JsonConvert.DeserializeObject<Dictionary<string, ApplePayData>>(payDataJson);
|
||||
|
||||
if (statusDictionary.ContainsKey(transactionID))
|
||||
{
|
||||
Debug.Log( $"ApplePayRequest 2 transactionID: {transactionID}");
|
||||
|
||||
var data = statusDictionary[transactionID];
|
||||
|
||||
ApplePayNet.ApplePayCheck<ApplePayData>(transactionID, data.innerOrderId, _packageName, (response) =>
|
||||
{
|
||||
Debug.Log(
|
||||
$"ApplePayRequest 3 response.IsSuccess: {JsonConvert.SerializeObject(response)}");
|
||||
|
||||
onValidationComplete?.Invoke(response.IsSuccess);
|
||||
|
||||
if (response.IsSuccess)
|
||||
{
|
||||
_successCallback(ApplePayBackType.Check, null);
|
||||
|
||||
statusDictionary.Remove(transactionID);
|
||||
// 保存更新后的数据
|
||||
SaveApplePayData(statusDictionary);
|
||||
}
|
||||
|
||||
if (!new List<int>() { 1021, 1026, 1027, 1028 }.Contains(response.Code))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
statusDictionary.Remove(transactionID);
|
||||
// 保存更新后的数据
|
||||
SaveApplePayData(statusDictionary);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 初始化
|
||||
|
||||
private bool IsInitialized()
|
||||
{
|
||||
Debug.Log( "[barry] check IsInitialized======:");
|
||||
return _storeController != null && _extensionProvider != null;
|
||||
}
|
||||
|
||||
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
|
||||
{
|
||||
_storeController = controller;
|
||||
_extensionProvider = extensions;
|
||||
_appleExtension = extensions.GetExtension<IAppleExtensions>();
|
||||
}
|
||||
|
||||
public void OnInitializeFailed(InitializationFailureReason error)
|
||||
{
|
||||
Debug.Log( "[barry] OnInitializeFailed1 Reason:" + error);
|
||||
// throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OnInitializeFailed(InitializationFailureReason error, string message)
|
||||
{
|
||||
Debug.Log( "[barry] OnInitializeFailed2 Reason:" + error);
|
||||
Debug.Log( "[barry] OnInitializeFailed2 message:" + message);
|
||||
// throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 购买失败回调
|
||||
|
||||
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
|
||||
{
|
||||
Debug.Log( " OnPurchaseFailed===1==:");
|
||||
HandleOnPurchaseFail(failureReason);
|
||||
}
|
||||
|
||||
|
||||
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
|
||||
{
|
||||
Debug.Log( "OnPurchaseFailed===2==:");
|
||||
HandleOnPurchaseFail(failureDescription.reason);
|
||||
}
|
||||
|
||||
private void HandleOnPurchaseFail(PurchaseFailureReason failureReason)
|
||||
{
|
||||
Debug.Log( $" HandleOnPurchaseFail 1 ");
|
||||
|
||||
if (_payData == null) return;
|
||||
|
||||
Debug.Log( $" HandleOnPurchaseFail 2 ");
|
||||
|
||||
ApplePayNet.ApplePayCancel<ApplePayData>(_payData, (response) =>
|
||||
{
|
||||
string msg = ToFriendlyString(failureReason);
|
||||
Debug.Log( "[barry] HandleOnPurchaseFail:" + response.IsSuccess + " reason: " + msg);
|
||||
_failedCallback?.Invoke(msg);
|
||||
_payData = null;
|
||||
|
||||
_successCallback?.Invoke(ApplePayBackType.Cancel,null);
|
||||
});
|
||||
}
|
||||
|
||||
private static string ToFriendlyString(PurchaseFailureReason reason)
|
||||
{
|
||||
Debug.Log( $"ToFriendlyString 1 reason==={reason}");
|
||||
|
||||
return reason switch
|
||||
{
|
||||
PurchaseFailureReason.ProductUnavailable => "商品不可用",
|
||||
PurchaseFailureReason.PurchasingUnavailable => "购买功能不可用",
|
||||
PurchaseFailureReason.ExistingPurchasePending => "已有未完成的购买",
|
||||
PurchaseFailureReason.SignatureInvalid => "签名验证失败",
|
||||
PurchaseFailureReason.UserCancelled => "用户取消",
|
||||
PurchaseFailureReason.PaymentDeclined => "支付被拒绝",
|
||||
PurchaseFailureReason.DuplicateTransaction => "重复的交易ID",
|
||||
PurchaseFailureReason.Unknown => "未知错误",
|
||||
_ => $"未识别的错误: {(int)reason}"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
#else
|
||||
namespace SGModule.ApplePay {
|
||||
public class ApplePayManager: SingletonMonoBehaviour<ApplePayManager>{
|
||||
public void StartPay() {
|
||||
}
|
||||
|
||||
public string GetApplePayName(string key)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
public void Purchase(ApplePayData payData, Action<ApplePayBackType> successCallback,
|
||||
Action<string> failedCallback) {
|
||||
var msg = "Apple Pay: Purchase Failed, No plugin is installed.";
|
||||
Log.Info("[Apple IOS]",msg);
|
||||
failedCallback?.Invoke(msg);
|
||||
}
|
||||
|
||||
public void AppleRestore(Action<bool, string> restoreCallback)
|
||||
{
|
||||
|
||||
}
|
||||
// public void InitProduct(List<ProductConfig>configs, string packageName)
|
||||
// {
|
||||
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
public class AppleResponseData
|
||||
{
|
||||
[JsonProperty("expires_time")]
|
||||
public long expires_time;
|
||||
[JsonProperty("sku")]
|
||||
public string sku;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8478e5310ad64f23a3805cd8a3fa024f
|
||||
timeCreated: 1747907338
|
||||
@@ -0,0 +1,95 @@
|
||||
using SGModule.Common.Helper;
|
||||
using SGModule.Net;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace SGModule.ApplePay
|
||||
{
|
||||
public static class ApplePayNet
|
||||
{
|
||||
#region IOSPay
|
||||
/// <summary>
|
||||
/// ios支付创建订单
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="onCompleted"></param>
|
||||
public static void ApplePayCreate<T>(ApplePayData data, UnityAction<ResponseData<T>> onCompleted = null)
|
||||
{
|
||||
NetKit.NetKit.Instance.Post<T>("shop/applePayCreate", data,
|
||||
response => { onCompleted?.Invoke(response); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ios支付取消订单
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="onCompleted"></param>
|
||||
public static void ApplePayCancel<T>(ApplePayData data, UnityAction<ResponseData<T>> onCompleted = null)
|
||||
{
|
||||
NetKit.NetKit.Instance.Post<T>("shop/applePayCancel", data,
|
||||
response => { onCompleted?.Invoke(response); });
|
||||
}
|
||||
public static void ApplePaySubscriptionHistory<T>(UnityAction<ResponseData<T>> onCompleted = null)
|
||||
{
|
||||
NetKit.NetKit.Instance.Post<T>("shop/subscribeHistory", null,
|
||||
response => { onCompleted?.Invoke(response); });
|
||||
}
|
||||
// public static void ApplePaySubscriptionCheck<T>(HistoryObject obj,UnityAction<ResponseData<T>> onCompleted = null)
|
||||
// {
|
||||
// NetKit.NetKit.Instance.Post<T>("shop/subscribeCheck", obj,
|
||||
// response => { onCompleted?.Invoke(response); });
|
||||
// }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ios支付验证订单
|
||||
/// </summary>
|
||||
/// <param name="transactionID">apple支付返回的订单id</param>
|
||||
/// <param name="innerOrderId">内部订单id,创建订单时返回的</param>
|
||||
/// <param name="key">包名</param>
|
||||
/// <param name="onCompleted">回调</param>
|
||||
public static void ApplePayCheck<T>(string transactionID, string innerOrderId,
|
||||
string key, UnityAction<ResponseData<T>> onCompleted = null)
|
||||
{
|
||||
var test = new AppleCheckData
|
||||
{
|
||||
signedPayload = Common.Helper.Cryptor.Encrypt(transactionID, key),
|
||||
innerOrderId = innerOrderId
|
||||
};
|
||||
|
||||
NetKit.NetKit.Instance.Post<T>("shop/applePayCheck", test,
|
||||
response => { onCompleted?.Invoke(response); });
|
||||
}
|
||||
/// <summary>
|
||||
/// ios支付订阅
|
||||
/// </summary>
|
||||
/// <param name="transactionID">apple支付返回的订单id</param>
|
||||
/// <param name="sku">商品id</param>
|
||||
/// <param name="amount">价格</param>
|
||||
/// <param name="key">包名</param>
|
||||
/// <param name="onCompleted">回调</param>
|
||||
public static void AppleSubscribeCheck<T>(string transactionID, string sku, int amount,
|
||||
string key, UnityAction<ResponseData<T>> onCompleted = null)
|
||||
{
|
||||
var test = new AppleSubscribeData
|
||||
{
|
||||
signedPayload = Common.Helper.Cryptor.Encrypt(transactionID, key),
|
||||
sku = sku,
|
||||
amount = amount,
|
||||
currency = "USD"
|
||||
};
|
||||
Log.Info("[Apple pay]", $" AppleSubscribeCheck 开始...{sku}");
|
||||
|
||||
NetKit.NetKit.Instance.Post<T>(
|
||||
"shop/appleSubscribe",
|
||||
test,
|
||||
response =>
|
||||
{
|
||||
Log.Info("[Apple pay]", $"AppleSubscribeCheck 结束...{response.IsSuccess}");
|
||||
onCompleted?.Invoke(response);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 760f78942ee949a9af90bc8ac6710be7
|
||||
timeCreated: 1747996461
|
||||
@@ -0,0 +1,22 @@
|
||||
#if UNITY_IOS && UNITY_IAP
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Purchasing;
|
||||
|
||||
namespace SGModule.ApplePay
|
||||
{
|
||||
[System.Serializable]
|
||||
public class ProductConfig
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string name;
|
||||
[JsonProperty("sku")]
|
||||
public string sku;
|
||||
[JsonProperty("price")]
|
||||
public float price;
|
||||
[JsonProperty("type")]
|
||||
public string type;
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bab80a35d16a48e6856d995e175b82d3
|
||||
timeCreated: 1747911023
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d7663ebef8b88644a509c798b452728
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,138 @@
|
||||
# 🧱 Common 公共模块
|
||||
|
||||
------
|
||||
|
||||
## 📖 概述
|
||||
|
||||
Common 模块是项目中最基础、最核心的模块,包含通用工具类、配置文件、扩展方法及基础系统组件。**所有其他模块都应依赖此模块**,务必在拉取其他模块前先行安装。
|
||||
|
||||
------
|
||||
|
||||
## 📁 功能总览
|
||||
|
||||
- ✅ 项目配置管理
|
||||
- ✅ 单例模式基类(支持非Mono与Mono行为)
|
||||
- ✅ 常用工具类(加解密、日志、时间、设备信息)
|
||||
- ✅ 扩展方法(数组、字符串、枚举等)
|
||||
- ✅ 编辑器工具(自动生成配置文件)
|
||||
- ✅ GM工具相关支持
|
||||
|
||||
------
|
||||
|
||||
## ⚙️ 核心系统详解
|
||||
|
||||
### 🧩 配置文件系统(GameConfig, NetworkConfig)
|
||||
|
||||
📌 通过菜单栏 `SwhiteGames > Create GameConfig` 可在 `Resources/` 目录下自动创建 `GameConfig` 文件,包含以下配置字段:
|
||||
|
||||
| 字段名 | 含义说明 |
|
||||
| ------------- | ----------------------------------------- |
|
||||
| `PackageName` | 游戏包名 |
|
||||
| `IsRelease` | 是否为正式发布包(关联宏 `GAME_RELEASE`) |
|
||||
| `EnabledLog` | 是否启用日志打印 |
|
||||
|
||||
📦 获取配置示例:
|
||||
|
||||
```csharp
|
||||
var enabledLog = ConfigManager.GameConfig?.enabledLog;
|
||||
var packageName = ConfigManager.GameConfig?.packageName;
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
|
||||
📌 通过菜单栏 `SwhiteGames > Create NetworkConfig` 可在 `Resources/` 目录下自动创建 `NetworkConfig` 文件,包含以下配置字段:
|
||||
|
||||
| 字段名 | 含义说明 |
|
||||
| ------------- |----------|
|
||||
| `showNetworkLog` | 是否启用日志打印 |
|
||||
| `debugHost` | 测试环境Host |
|
||||
| `releaseHost` | 正式环境Host |
|
||||
| `connectionMode` | 连接模式 |
|
||||
|
||||
📦 获取配置示例:
|
||||
|
||||
```csharp
|
||||
var connectionMode = ConfigManager.NetworkConfig?.connectionMode;
|
||||
var showNetworkLog = ConfigManager.NetworkConfig?.showNetworkLog;
|
||||
```
|
||||
|
||||
------
|
||||
### 🔁 单例系统
|
||||
|
||||
- **SingletonMonoBehaviour**:MonoBehaviour 单例,自动创建 GameObject,跨场景不销毁
|
||||
|
||||
```csharp
|
||||
// MonoBehaviour
|
||||
public class AudioManager : SingletonMonoBehaviour<AudioManager> { ... }
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
### 🛠️ 工具类一览(Helper)
|
||||
|
||||
| 类名 | 功能说明 |
|
||||
| ----------------- | ------------------------------------------- |
|
||||
| `Log` | ✅日志打印(开发调试利器) |
|
||||
| `Cryptor` | 🔐 加密解密支持(对称加密) |
|
||||
| `TimeHelper` | 🕒 时间戳转换与格式化处理 |
|
||||
| `DeviceHelper` | 📱 获取设备唯一标识 |
|
||||
| `RandomHelper` | 🎲 权重随机、随机打乱等 |
|
||||
| `SerializeHelper` | 🔄 JSON 序列化 / 反序列化(可使用As<T>()替换) |
|
||||
| `MD5Helper` | 🔑 快速生成MD5签名 |
|
||||
| `Base64Helper` | 🧬 Base64编解码 |
|
||||
|
||||
------
|
||||
|
||||
### 📌 日志打印统一用法
|
||||
|
||||
统一使用 `Log` 工具类进行日志输出,支持彩色分级,正式环境中可自动关闭日志输出。
|
||||
|
||||
```csharp
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
Log.Info("标签", "信息内容"); // ✅绿色输出
|
||||
Log.Warning("标签", "警告内容"); // ⚠️橙色输出
|
||||
Log.Error("标签", "错误内容"); // ❌红色输出
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
### ✨ 扩展方法一览
|
||||
|
||||
- **EnumExtensions**:获取枚举描述 `GameState.Ready.GetDescription()`
|
||||
- **StringExtensions**:空值检查 `str.IsNullOrWhiteSpace()`
|
||||
- **ObjectExtensions**:类型安全转换 `"123".As<int>()`
|
||||
- **ListExtensions**:列表随机元素 `items.Random()`
|
||||
- **ArrayExtensions**:数组相关操作
|
||||
|
||||
------
|
||||
|
||||
## 🧠 最佳实践
|
||||
|
||||
| 模块 | 建议做法 |
|
||||
| ---------- | ------------------------------------------------------------ |
|
||||
| 管理器类 | Mono 类继承 `SingletonMonoBehaviour` |
|
||||
| 日志打印 | 全局使用 `Log` 统一管理,按模块分类 |
|
||||
| 类型转换 | 使用 `.As<T>()` 替代传统强转,异常更少 |
|
||||
| 加密数据 | 所有敏感数据使用 `Cryptor` 加密后存储 |
|
||||
| 时间处理 | 使用 `TimeHelper` 保证时区一致性 |
|
||||
| 随机操作 | 使用 `RandomHelper` 替代 `UnityEngine.Random` |
|
||||
| 数据键管理 | 通过 `KeyRegistry` 注册和使用,避免硬编码 |
|
||||
|
||||
------
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- ❗ `SingletonMonoBehaviour` 不要手动放入场景,会自动创建
|
||||
- 🔐 加密密钥需妥善保管,避免泄露
|
||||
- 🌏 时间处理请注意本地时区与 UTC 的转换
|
||||
- 🎲 请勿使用 `UnityEngine.Random`,统一使用 `RandomHelper`
|
||||
|
||||
------
|
||||
|
||||
## 💡 扩展建议
|
||||
|
||||
- 🧩 新增扩展方法时,请分类放入 `Extensions/` 并补充完整 XML 注释
|
||||
- 🧰 新增工具类保持静态类设计,考虑线程安全
|
||||
- 📖 键系统支持从配置文件中加载,并支持按模块分组管理
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a5954894fd434e4f8726239c65e8497
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1255dc405c83ca54ba0fe153fde0fd3c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7910bd51ff93d347891de376c8c6d3c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.IO;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SwhiteGames.Editor {
|
||||
public class GameConfigCreator : EditorWindow {
|
||||
public string packName = "";
|
||||
public bool enabledLog = true;
|
||||
public bool hardwareAcceleration = true;
|
||||
|
||||
private string _configFileName = "GameConfig";
|
||||
|
||||
private void OnGUI() {
|
||||
GUILayout.Label("GameConfig Settings", EditorStyles.boldLabel);
|
||||
_configFileName = EditorGUILayout.TextField("Character Name", _configFileName);
|
||||
|
||||
packName = EditorGUILayout.TextField(string.IsNullOrEmpty(packName) ? "请输入包名..." : "包名",
|
||||
packName);
|
||||
|
||||
enabledLog = EditorGUILayout.Toggle("开启日志", enabledLog);
|
||||
|
||||
#if UNITY_ANDROID
|
||||
hardwareAcceleration = EditorGUILayout.Toggle("硬件加速", hardwareAcceleration);
|
||||
#endif
|
||||
if (GUILayout.Button("Create")) {
|
||||
if (string.IsNullOrEmpty(packName)) {
|
||||
EditorUtility.DisplayDialog("请填写正确配置",
|
||||
$"packName {!string.IsNullOrEmpty(packName)}",
|
||||
"确定");
|
||||
return;
|
||||
}
|
||||
|
||||
CreateGameConfig();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("SwhiteGames/Create GameConfig")]
|
||||
public static void ShowWindow() {
|
||||
GetWindow<GameConfigCreator>("Create GameConfig");
|
||||
}
|
||||
|
||||
private void CreateGameConfig() {
|
||||
// 确保 Resources 文件夹存在
|
||||
var resourcesPath = Path.Combine("Assets", "Resources");
|
||||
if (!Directory.Exists(resourcesPath)) {
|
||||
Directory.CreateDirectory(resourcesPath);
|
||||
AssetDatabase.Refresh(); // 刷新资源数据库
|
||||
|
||||
Log.Common.Info("Resources 文件夹已创建。");
|
||||
}
|
||||
|
||||
// 检查同名文件
|
||||
var assetPath = Path.Combine(resourcesPath, _configFileName + ".asset");
|
||||
if (File.Exists(assetPath)) {
|
||||
// 如果同名文件已存在,弹出提示并结束逻辑
|
||||
EditorUtility.DisplayDialog("文件已存在", $"配置文件 '{_configFileName}' 已存在于 Resources 文件夹中。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建并设置 config 实例
|
||||
var gameConfig = CreateInstance<GameConfig>();
|
||||
gameConfig.packageName = packName;
|
||||
gameConfig.enabledLog = enabledLog;
|
||||
gameConfig.hardwareAcceleration = hardwareAcceleration;
|
||||
|
||||
// 将 ScriptableObject 实例保存到 Resources 文件夹中
|
||||
AssetDatabase.CreateAsset(gameConfig, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Log.Common.Info("GameConfig 配置文件已成功创建在 " + assetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88d46a9d0224446fb77e9864ebe4d4f5
|
||||
timeCreated: 1731660781
|
||||
@@ -0,0 +1,39 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SwhiteGames.Editor {
|
||||
[CustomEditor(typeof(GameConfig))]
|
||||
public class GameConfigEditor : UnityEditor.Editor {
|
||||
public override void OnInspectorGUI() {
|
||||
DrawDefaultInspector(); // 可选:绘制默认 Inspector
|
||||
|
||||
var gameConfig = (GameConfig) target;
|
||||
|
||||
// 包名
|
||||
gameConfig.packageName = EditorGUILayout.TextField("包名", gameConfig.packageName);
|
||||
|
||||
// isRelease 选项
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
|
||||
EditorGUILayout.Toggle(new GUIContent("isRelease", "是否为发布版本, 由玩家设置中脚本定义符 GAME_RELEASE 决定"),
|
||||
gameConfig.isRelease);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
// 启用日志(随 isRelease 联动)
|
||||
EditorGUI.BeginDisabledGroup(gameConfig.isRelease);
|
||||
gameConfig.enabledLog =
|
||||
EditorGUILayout.Toggle(new GUIContent("启用日志", "发布版本默认不可启动日志"), gameConfig.enabledLog);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
#if UNITY_ANDROID
|
||||
// 硬件加速
|
||||
gameConfig.hardwareAcceleration = EditorGUILayout.Toggle("硬件加速", gameConfig.hardwareAcceleration);
|
||||
#endif
|
||||
|
||||
// 标记对象已修改,确保更改能保存
|
||||
if (GUI.changed) {
|
||||
EditorUtility.SetDirty(gameConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d35d44cfe6b42dd8abc4fcee72c69c8
|
||||
timeCreated: 1743589873
|
||||
@@ -0,0 +1,94 @@
|
||||
#if UNITY_ANDROID && UNITY_EDITOR
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using SGModule.Common;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEditor.Android;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SwhiteGames.Editor {
|
||||
public class HardwareAccelerationManager : IPostGenerateGradleAndroidProject {
|
||||
public int callbackOrder => 99;
|
||||
|
||||
private static bool EnableHardwareAcceleration => ConfigManager.GameConfig.hardwareAcceleration;
|
||||
|
||||
public void OnPostGenerateGradleAndroidProject(string basePath) {
|
||||
var manifestPath = GetManifestPath(basePath);
|
||||
if (!File.Exists(manifestPath)) {
|
||||
Log.Warning("HardwareAcceleration", $"Manifest not found at {manifestPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var manifest = new AndroidManifestHelper(manifestPath);
|
||||
|
||||
var changed = manifest.SetHardwareAccelerated(EnableHardwareAcceleration);
|
||||
|
||||
if (changed) {
|
||||
manifest.Save();
|
||||
Log.Info("HardwareAcceleration", $"Updated manifest with hardwareAccelerated={EnableHardwareAcceleration}");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetManifestPath(string basePath) {
|
||||
return Path.Combine(basePath, "src/main/AndroidManifest.xml");
|
||||
}
|
||||
}
|
||||
|
||||
internal class AndroidManifestHelper {
|
||||
private readonly XmlDocument _doc;
|
||||
private readonly XmlNamespaceManager _nsMgr;
|
||||
private const string AndroidNs = "http://schemas.android.com/apk/res/android";
|
||||
private readonly string _manifestPath;
|
||||
|
||||
public AndroidManifestHelper(string path) {
|
||||
_manifestPath = path;
|
||||
_doc = new XmlDocument();
|
||||
_doc.Load(path);
|
||||
|
||||
_nsMgr = new XmlNamespaceManager(_doc.NameTable);
|
||||
_nsMgr.AddNamespace("android", AndroidNs);
|
||||
}
|
||||
|
||||
public bool SetHardwareAccelerated(bool enabled) {
|
||||
var activity = GetMainActivity();
|
||||
if (activity == null) {
|
||||
Debug.LogWarning("[HardwareAcceleration] Could not find main activity");
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetHardwareAccelerated(activity, enabled);
|
||||
}
|
||||
|
||||
private bool SetHardwareAccelerated(XmlNode activity, bool enabled) {
|
||||
var value = enabled ? "true" : "false";
|
||||
var attr = (activity as XmlElement)?.GetAttributeNode("hardwareAccelerated", AndroidNs);
|
||||
|
||||
if (attr == null) {
|
||||
attr = _doc.CreateAttribute("android", "hardwareAccelerated", AndroidNs);
|
||||
activity.Attributes?.Append(attr);
|
||||
attr.Value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attr.Value != value) {
|
||||
attr.Value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private XmlNode GetMainActivity() {
|
||||
return _doc.SelectSingleNode(
|
||||
"/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " +
|
||||
"intent-filter/category/@android:name='android.intent.category.LAUNCHER']",
|
||||
_nsMgr);
|
||||
}
|
||||
|
||||
public void Save() {
|
||||
_doc.Save(_manifestPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dffc1d0f59e04033a4372c6e86992176
|
||||
timeCreated: 1753164058
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.IO;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Editor {
|
||||
public class NetworkConfigCreator : EditorWindow {
|
||||
private const string DefaultHost = "sandbox-api.swhitegames.tech";
|
||||
private const string ConfigFileName = "NetworkConfig";
|
||||
|
||||
public bool showNetworkLog = true;
|
||||
public string debugHost = DefaultHost;
|
||||
public string releaseHost = "";
|
||||
|
||||
public ConnectionMode model = ConnectionMode.Http;
|
||||
|
||||
private void OnGUI() {
|
||||
showNetworkLog = EditorGUILayout.Toggle("显示日志", showNetworkLog);
|
||||
|
||||
debugHost = EditorGUILayout.TextField("Debug Host:", debugHost);
|
||||
releaseHost = EditorGUILayout.TextField("Release Host:", releaseHost);
|
||||
|
||||
model = (ConnectionMode) EditorGUILayout.EnumPopup("连接模式:", model);
|
||||
|
||||
|
||||
if (GUILayout.Button("Create NetworkConfig")) {
|
||||
if (string.IsNullOrEmpty(debugHost) || string.IsNullOrEmpty(releaseHost)) {
|
||||
EditorUtility.DisplayDialog("请填写正确配置",
|
||||
$"Debug Host {(string.IsNullOrEmpty(debugHost) ? "不存在" : "存在")}, Release Host {(string.IsNullOrEmpty(releaseHost) ? "不存在" : "存在")}",
|
||||
"确定");
|
||||
return;
|
||||
}
|
||||
|
||||
CreateNetworkConfig();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("SwhiteGames/Create NetworkConfig")]
|
||||
public static void ShowWindow() {
|
||||
GetWindow<NetworkConfigCreator>("Create NetworkConfig");
|
||||
}
|
||||
|
||||
private void CreateNetworkConfig() {
|
||||
// 确保 Resources 文件夹存在
|
||||
var resourcesPath = Path.Combine("Assets", "Resources");
|
||||
if (!Directory.Exists(resourcesPath)) {
|
||||
Directory.CreateDirectory(resourcesPath);
|
||||
AssetDatabase.Refresh(); // 刷新资源数据库
|
||||
Log.Common.Info("Resources 文件夹已创建。");
|
||||
}
|
||||
|
||||
// 检查同名文件
|
||||
var assetPath = Path.Combine(resourcesPath, ConfigFileName + ".asset");
|
||||
if (File.Exists(assetPath)) {
|
||||
// 如果同名文件已存在,弹出提示并结束逻辑
|
||||
EditorUtility.DisplayDialog("文件已存在", $"配置文件 '{ConfigFileName}' 已存在于 Resources 文件夹中。", "确定");
|
||||
return;
|
||||
}
|
||||
// 检查同名文件
|
||||
// string assetPath = Path.Combine(resourcesPath, _configFileName + ".asset");
|
||||
// string uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); // 生成唯一路径
|
||||
|
||||
// 创建并设置 config 实例
|
||||
var networkConfig = CreateInstance<NetworkConfig>();
|
||||
networkConfig.showNetworkLog = showNetworkLog;
|
||||
networkConfig.debugHost = debugHost;
|
||||
networkConfig.releaseHost = releaseHost;
|
||||
networkConfig.connectionMode = model;
|
||||
|
||||
// 将 ScriptableObject 实例保存到 Resources 文件夹中
|
||||
AssetDatabase.CreateAsset(networkConfig, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Log.Common.Info("NetworkConfig 配置文件已成功创建在 " + assetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 534fe38ef6ad460493d6cb0a852a7fb5
|
||||
timeCreated: 1731480153
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 306dc076af5dda247803b0ad907f4968
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c18f1f3b9ea744f5b3d4a66286b726c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
// 保存数据到 Keychain
|
||||
bool SaveToKeychain(const char* key, const char* value) {
|
||||
NSString* keyString = [NSString stringWithUTF8String:key];
|
||||
NSString* valueString = [NSString stringWithUTF8String:value];
|
||||
|
||||
NSData* valueData = [valueString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSDictionary* query = @{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrAccount : keyString,
|
||||
};
|
||||
|
||||
SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
|
||||
NSDictionary* attributes = @{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrAccount : keyString,
|
||||
(__bridge id)kSecValueData : valueData,
|
||||
};
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
|
||||
return status == errSecSuccess;
|
||||
}
|
||||
|
||||
// 从 Keychain 中获取数据
|
||||
const char* GetFromKeychain(const char* key) {
|
||||
NSString* keyString = [NSString stringWithUTF8String:key];
|
||||
|
||||
NSDictionary* query = @{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrAccount : keyString,
|
||||
(__bridge id)kSecReturnData : @YES,
|
||||
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
|
||||
};
|
||||
|
||||
CFTypeRef result = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
|
||||
if (status == errSecSuccess) {
|
||||
NSData* valueData = (__bridge_transfer NSData*)result;
|
||||
NSString* valueString = [[NSString alloc] initWithData:valueData encoding:NSUTF8StringEncoding];
|
||||
return strdup([valueString UTF8String]);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 删除 Keychain 中的数据
|
||||
bool DeleteFromKeychain(const char* key) {
|
||||
NSString* keyString = [NSString stringWithUTF8String:key];
|
||||
|
||||
NSDictionary* query = @{
|
||||
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrAccount : keyString,
|
||||
};
|
||||
|
||||
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
return status == errSecSuccess;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bc4268c12974c68bd77c36016e019ed
|
||||
timeCreated: 1736750208
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53a17396f989d874b9b047cca52f77f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f92c00a864347c945bec347302a061e0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Common.Base {
|
||||
public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour {
|
||||
private static T _instance;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化完成标记,有需要的地方可以使用
|
||||
/// </summary>
|
||||
protected static bool IsInitComplete;
|
||||
|
||||
// 存储所有单例的父对象
|
||||
private static GameObject _singletonParent;
|
||||
|
||||
public static T Instance {
|
||||
get {
|
||||
if (_instance == null) {
|
||||
// 尝试找到已存在的实例
|
||||
_instance = FindObjectOfType<T>();
|
||||
|
||||
// 如果没有找到,则创建一个新的实例
|
||||
if (_instance == null) {
|
||||
// 确保所有单例对象放到同一个父对象下
|
||||
if (_singletonParent == null) {
|
||||
_singletonParent = GameObject.Find("Singletons") ?? new GameObject("Singletons");
|
||||
DontDestroyOnLoad(_singletonParent); // 不在场景切换时销毁
|
||||
}
|
||||
|
||||
var singletonObject = new GameObject(typeof(T).Name);
|
||||
singletonObject.transform.SetParent(_singletonParent.transform); // 设置父对象
|
||||
_instance = singletonObject.AddComponent<T>();
|
||||
}
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保在场景中只存在一个实例
|
||||
protected virtual void Awake() {
|
||||
if (_instance == null) {
|
||||
_instance = this as T;
|
||||
}
|
||||
else if (_instance != this) {
|
||||
Log.Common.Warning($"An instance of {typeof(T)} already exists! Destroying this instance.");
|
||||
Destroy(gameObject); // 如果有其他实例,销毁当前对象
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy() {
|
||||
if (_instance == this) {
|
||||
_instance = null; // 清除引用
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a32d260789150d449d0313a73dcc5b1
|
||||
timeCreated: 1728986962
|
||||
@@ -0,0 +1,30 @@
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Common {
|
||||
public static class ConfigManager {
|
||||
private const string GameConfigFilePath = "GameConfig";
|
||||
private const string NetworkConfigFilePath = "NetworkConfig";
|
||||
|
||||
static ConfigManager() {
|
||||
GameConfig = Resources.Load<GameConfig>(GameConfigFilePath);
|
||||
if (GameConfig == null) {
|
||||
Log.Common.Error($"加载失败:未找到配置文件 {GameConfigFilePath},请确保它放在 Resources 文件夹下并命名正确");
|
||||
}
|
||||
|
||||
NetworkConfig = Resources.Load<NetworkConfig>(NetworkConfigFilePath);
|
||||
if (NetworkConfig == null) {
|
||||
Log.Common.Error($"加载失败:未找到配置文件 {NetworkConfigFilePath},请确保它放在 Resources 文件夹下并命名正确");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static GameConfig GameConfig {
|
||||
get;
|
||||
}
|
||||
|
||||
public static NetworkConfig NetworkConfig {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8639bb4e64f34c3db2c6537cfdac4974
|
||||
timeCreated: 1731316175
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f468ddf1f1fc08446865eb4fb41b74aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using SGModule.Common.Helper;
|
||||
using SGModule.Common.Interface;
|
||||
|
||||
namespace SGModule.Common.Extensions {
|
||||
public static class ArrayExtensions {
|
||||
public static T Random<T>(this T[] items, T fallback = default) where T : class, IWeighted {
|
||||
if (items == null || items.Length == 0) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return RandomHelper.RandomByWeight(items, fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea3b1bd83aa74285b19f87301391b906
|
||||
timeCreated: 1749197978
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SGModule.Common.Extensions {
|
||||
public static class EnumExtensions {
|
||||
public static string GetDescription(this Enum value) {
|
||||
var field = value.GetType().GetField(value.ToString());
|
||||
var attribute = (DescriptionAttribute) Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
|
||||
return attribute?.Description ?? value.ToString(); // 如果没有描述,就使用枚举名称
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fb5c1462c4f411bb8c08fec74c0bb60
|
||||
timeCreated: 1731403821
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
using SGModule.Common.Interface;
|
||||
|
||||
namespace SGModule.Common.Extensions {
|
||||
public static class ListExtensions {
|
||||
public static T Random<T>(this IEnumerable<T> items, T fallback = default) where T : class, IWeighted {
|
||||
if (items == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var list = items as IList<T> ?? items.ToList();
|
||||
|
||||
if (list.Count == 0) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return RandomHelper.RandomByWeight(list, fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f42389145cf940a0b4202dbd516228c4
|
||||
timeCreated: 1749198436
|
||||
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
namespace SGModule.Common.Extensions {
|
||||
/// <summary>
|
||||
/// 提供通用对象类型转换的扩展方法,支持数值类型、字符串、枚举、集合、时间等多种类型间的智能转换。
|
||||
/// </summary>
|
||||
public static class ObjectExtensions {
|
||||
/// <summary>
|
||||
/// 将对象强制转换为目标类型 <typeparamref name="T" />,转换失败时返回默认值。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <param name="value">待转换的对象</param>
|
||||
/// <param name="defaultValue">转换失败时返回的默认值</param>
|
||||
/// <param name="format">可选格式字符串(如用于时间或数值格式化)</param>
|
||||
/// <param name="formatProvider">格式化提供者,默认为当前区域设置</param>
|
||||
/// <returns>转换后的目标类型值,或 <paramref name="defaultValue" />。</returns>
|
||||
/// <example>
|
||||
/// 字符串到基本类型
|
||||
/// <code>
|
||||
/// "123".As<int>(); // 输出 123
|
||||
/// "45.67".As<float>(); // 输出 45.67f
|
||||
/// "true".As<bool>(); // 输出 true
|
||||
/// "2023-01-01".As<DateTime>(); // 输出 2023-01-01
|
||||
/// "".As<int>(); // 输出 0
|
||||
/// ((string)null).As<int>(); // 输出 0
|
||||
/// "abc".As<int>(); // 输出 0(无法转换)
|
||||
/// "1.23E+5".As<float>(); // 输出 123000f
|
||||
/// .....
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static T As<T>(this object value, T defaultValue = default, string format = null, IFormatProvider formatProvider = null) {
|
||||
return (T) As(value, typeof(T), defaultValue, format, formatProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象转换为指定的目标类型。
|
||||
/// 支持基础类型转换、字符串与 JSON 互转、枚举解析、集合映射、Base64 处理等。
|
||||
/// </summary>
|
||||
/// <param name="value">要转换的对象</param>
|
||||
/// <param name="targetType">目标类型</param>
|
||||
/// <param name="defaultValue">转换失败时的返回值</param>
|
||||
/// <param name="format">可选的格式字符串(用于时间、数字格式化)</param>
|
||||
/// <param name="formatProvider">格式提供者(区域信息),默认为当前区域</param>
|
||||
/// <returns>转换结果或默认值</returns>
|
||||
public static object As(this object value, Type targetType, object defaultValue = null, string format = null, IFormatProvider formatProvider = null) {
|
||||
if (value == null || value == DBNull.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
formatProvider ??= CultureInfo.CurrentCulture;
|
||||
|
||||
try {
|
||||
// 1. 类型兼容直接返回
|
||||
if (targetType.IsInstanceOfType(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 2. 数值类型之间转换(int, float, decimal 等)
|
||||
if (value.IsNumericType() && targetType.IsNumericType()) {
|
||||
return Convert.ChangeType(value, targetType, formatProvider);
|
||||
}
|
||||
|
||||
// 3. 字符串 ⇄ 字节数组
|
||||
if (targetType == typeof(byte[]) && value is string str) {
|
||||
return Encoding.UTF8.GetBytes(str);
|
||||
}
|
||||
|
||||
if (targetType == typeof(string) && value is byte[] bytes) {
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
// 4. 转为字符串
|
||||
if (targetType == typeof(string)) {
|
||||
if (value is string s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (value.IsNumericType()) {
|
||||
return string.Format(formatProvider, format ?? "{0}", value);
|
||||
}
|
||||
|
||||
if (value is DateTime dt) {
|
||||
return string.IsNullOrEmpty(format) ? dt.ToString(formatProvider) : dt.ToString(format, formatProvider);
|
||||
}
|
||||
|
||||
try {
|
||||
return JsonConvert.SerializeObject(value);
|
||||
}
|
||||
catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 从字符串解析为目标类型
|
||||
if (value is string strValue) {
|
||||
if (string.IsNullOrWhiteSpace(strValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 字符串 → 枚举
|
||||
if (targetType.IsEnum) {
|
||||
try {
|
||||
return Enum.Parse(targetType, strValue, true);
|
||||
}
|
||||
catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串 → 数值
|
||||
if (targetType.IsNumericType()) {
|
||||
return Convert.ChangeType(strValue, targetType, formatProvider);
|
||||
}
|
||||
|
||||
// 字符串 → 时间
|
||||
if (targetType == typeof(DateTime)) {
|
||||
return string.IsNullOrEmpty(format)
|
||||
? DateTime.Parse(strValue, formatProvider)
|
||||
: DateTime.ParseExact(strValue, format, formatProvider);
|
||||
}
|
||||
|
||||
// 字符串 → 自定义类(JSON)
|
||||
if (targetType.IsClass && targetType != typeof(string)) {
|
||||
try {
|
||||
return JsonConvert.DeserializeObject(strValue, targetType);
|
||||
}
|
||||
catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 集合类型转换(如 List<string> -> List<int>)
|
||||
if (targetType.IsGenericListOrArray() && value is IEnumerable enumerable) {
|
||||
var elementType = targetType.GetElementType() ?? targetType.GetGenericArguments()[0];
|
||||
var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
|
||||
|
||||
foreach (var item in enumerable) {
|
||||
var converted = typeof(ObjectExtensions)
|
||||
.GetMethod("As", new[] { typeof(object), typeof(Type), typeof(object), typeof(string), typeof(IFormatProvider) })
|
||||
?.Invoke(null, new[] { item, elementType, null, format, formatProvider });
|
||||
|
||||
list.Add(converted);
|
||||
}
|
||||
|
||||
return targetType.IsArray ? list.ToArray(elementType) : list;
|
||||
}
|
||||
|
||||
// 7. 尝试通过 TypeConverter 转换(主要处理结构体、基础类型)
|
||||
if (targetType.IsValueType || targetType == typeof(string)) {
|
||||
var strVal = value.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(strVal)) {
|
||||
var converter = TypeDescriptor.GetConverter(targetType);
|
||||
if (converter.CanConvertFrom(typeof(string))) {
|
||||
return converter.ConvertFrom(null, formatProvider as CultureInfo, strVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Fallback:通过 JSON 转换
|
||||
return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), targetType);
|
||||
}
|
||||
catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
#region 🔧 辅助方法
|
||||
|
||||
private static bool IsNumericType(this Type type) {
|
||||
return Type.GetTypeCode(type) switch {
|
||||
TypeCode.Byte or TypeCode.SByte or TypeCode.UInt16 or TypeCode.UInt32 or
|
||||
TypeCode.UInt64 or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64 or
|
||||
TypeCode.Decimal or TypeCode.Double or TypeCode.Single => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsNumericType(this object value) {
|
||||
return value?.GetType().IsNumericType() ?? false;
|
||||
}
|
||||
|
||||
private static bool IsGenericListOrArray(this Type type) {
|
||||
return type != null && (type.IsArray ||
|
||||
(type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>)
|
||||
|| type.GetGenericTypeDefinition() == typeof(IList<>))));
|
||||
}
|
||||
|
||||
private static Array ToArray(this IList list, Type elementType) {
|
||||
var array = Array.CreateInstance(elementType, list.Count);
|
||||
list.CopyTo(array, 0);
|
||||
return array;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class ObjectExtensionsTest {
|
||||
public static void AsTestRun() {
|
||||
TestCase("int 到 int", 123.As<int>(), 123);
|
||||
TestCase("float 到 int", 123.1f.As<int>(), 123);
|
||||
TestCase("double 到 decimal", 99.99.As<decimal>(), 99.99m);
|
||||
TestCase("int 到 long", 100.As<long>(), 100L);
|
||||
|
||||
TestCase("字符串 到 int", "123".As<int>(), 123);
|
||||
TestCase("字符串 到 float", "45.67".As<float>(), 45.67f);
|
||||
TestCase("字符串 到 bool", "true".As<bool>(), true);
|
||||
TestCase("字符串 到 DateTime", "2023-01-01".As<DateTime>(), new DateTime(2023, 1, 1));
|
||||
TestCase("空字符串 到 int", "".As<int>(), 0);
|
||||
TestCase("null字符串 到 int", ((string) null).As<int>(), 0);
|
||||
TestCase("非法字符串 到 int", "abc".As<int>(), 0);
|
||||
TestCase("科学计数法字符串 到 float", "1.23E+5".As<float>(), 123000f);
|
||||
|
||||
TestCase("int 到 字符串", 42.As<string>(), "42");
|
||||
TestCase("float 到 字符串(en-US)", 3.14159f.As<string>(formatProvider: new CultureInfo("en-US")), "3.14159");
|
||||
TestCase("float 到 字符串(de-DE)", 3.14159f.As<string>(formatProvider: new CultureInfo("de-DE")), "3,14159");
|
||||
TestCase("DateTime 到 字符串(格式化)", new DateTime(2024, 5, 1).As<string>(format: "yyyy-MM-dd"), "2024-05-01");
|
||||
|
||||
TestCase("null 到 int", ((object) null).As<int>(), 0);
|
||||
TestCase("null 到 string", ((object) null).As<string>(), null);
|
||||
TestCase("DBNull 到 int", DBNull.Value.As<int>(), 0);
|
||||
|
||||
TestCase("枚举 到 string", Color.Red.As<string>(), "Red");
|
||||
TestCase("字符串 到 枚举", "Green".As<Color>(), Color.Green);
|
||||
|
||||
var json = "{\"Name\":\"Alice\",\"Age\":30}";
|
||||
TestCase("JSON 字符串 到 Person", json.As<Person>().Name, "Alice");
|
||||
TestCase("对象 到 JSON 字符串", new Person { Name = "Alice", Age = 30 }.As<string>(), JsonConvert.SerializeObject(new Person { Name = "Alice", Age = 30 }));
|
||||
|
||||
TestCase("字符串数组 到 List<int>", new[] { "1", "2", "3" }.As<List<int>>(), new List<int> { 1, 2, 3 });
|
||||
TestCase("List<float> 到 float[]", new List<float> { 1.1f, 2.2f }.As<float[]>(), new[] { 1.1f, 2.2f });
|
||||
TestCase("空集合 到 int[]", new string[] { }.As<int[]>(), new int[] { });
|
||||
|
||||
var str = "hello";
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
TestCase("byte[] 到 base64 string", bytes.As<string>(), str);
|
||||
TestCase("base64 string 到 byte[]", BitConverter.ToString(str.As<byte[]>()), BitConverter.ToString(bytes));
|
||||
|
||||
TestCase("字符串小数(德语) 到 float", "1,23".As<float>(formatProvider: new CultureInfo("de-DE")), 1.23f);
|
||||
TestCase("字符串小数(英语) 到 float", "1.23".As<float>(formatProvider: new CultureInfo("en-US")), 1.23f);
|
||||
|
||||
TestCase("非法 JSON 到 Person(失败返回 null)", "{invalid}".As<Person>(), null);
|
||||
TestCase("不能转的对象 到 int", new Person { Name = "A" }.As<int>(), 0);
|
||||
TestCase("不能转的对象 到 string(fallback 到 JsonConvert)", new Person { Name = "A", Age = 20 }.As<string>(), JsonConvert.SerializeObject(new Person { Name = "A", Age = 20 }));
|
||||
}
|
||||
|
||||
private static void TestCase<T>(string description, T actual, T expected) {
|
||||
var isSuccess = CommonUtils.DeepEquals(actual, expected);
|
||||
|
||||
var actualStr = FormatValue(actual);
|
||||
var expectedStr = FormatValue(expected);
|
||||
var msg = $"{description} => 结果: {actualStr}, 期望: {expectedStr}";
|
||||
if (isSuccess) {
|
||||
Log.Info("As<T>()", msg);
|
||||
}
|
||||
else {
|
||||
Log.Warning("As<T>()", msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatValue<T>(T value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (value is byte[] byteArray) {
|
||||
return $"[{string.Join(", ", byteArray)}]";
|
||||
}
|
||||
|
||||
if (value is int[] intArray) {
|
||||
return $"[{string.Join(", ", intArray)}]";
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private enum Color {
|
||||
Red,
|
||||
Green,
|
||||
Blue
|
||||
}
|
||||
|
||||
private class Person {
|
||||
public string Name {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int Age {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6571543817d44e79cbd4f48a4d3616a
|
||||
timeCreated: 1749716272
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SGModule.Common.Extensions {
|
||||
public static class StringExtensions {
|
||||
public static bool IsNullOrWhiteSpace(this string str) {
|
||||
return string.IsNullOrWhiteSpace(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 562d871c5a8d49db9d78fb2921475549
|
||||
timeCreated: 1731309251
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 483f1710e0c24e53a773d154310180e8
|
||||
timeCreated: 1731657933
|
||||
@@ -0,0 +1,367 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SGModule.Common;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
public class GMTool : SingletonMonoBehaviour<GMTool> {
|
||||
private readonly int buttonHeight = 50;
|
||||
private readonly int fontSize = 35; // 字体大小
|
||||
|
||||
private readonly Dictionary<string, string> inputFields = new();
|
||||
|
||||
private readonly List<GMToolItem> items = new();
|
||||
private readonly int spacing = 10; // 控件之间的间距
|
||||
private GUIStyle buttonStyle;
|
||||
private int currentX; // 当前 X 坐标
|
||||
private int currentY; // 当前 Y 坐标
|
||||
private GUIStyle inputFieldStyle;
|
||||
private GUIStyle labelStyle;
|
||||
|
||||
private bool toggleDisplay; // 控制显示与隐藏
|
||||
private int windowWidth; // 屏幕宽度
|
||||
|
||||
|
||||
protected override void Awake() {
|
||||
base.Awake();
|
||||
if (!ConfigManager.GameConfig.isRelease) {
|
||||
IsInitComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
if (!IsInitComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化样式
|
||||
if (buttonStyle == null || labelStyle == null || inputFieldStyle == null) {
|
||||
InitializeStyles();
|
||||
}
|
||||
|
||||
// 初始化布局
|
||||
StartLayout();
|
||||
|
||||
// 显示开关按钮
|
||||
AddButton(toggleDisplay ? "关闭 GM 工具" : "打开 GM 工具", () => {
|
||||
toggleDisplay = !toggleDisplay;
|
||||
});
|
||||
|
||||
if (!toggleDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 添加控件示例
|
||||
AddLabel("GM 工具");
|
||||
// AddButton("初始化网络模块", () =>
|
||||
// {
|
||||
// var gameConfig = ConfigManager.GetInstance.GetGameConfig();
|
||||
// NetworkKit.Instance.InitData(gameConfig.packageName, gameConfig.isRelease);
|
||||
// });
|
||||
// AddButton("登录", () =>
|
||||
// {
|
||||
// NetworkKit.Instance.LoginRequest("Test", false, (arg0, model) =>
|
||||
// {
|
||||
// Debug.LogError($"登录结果 {arg0}");
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
// AddButton($"加载配置模块初始化", () =>
|
||||
// {
|
||||
// ConfigLoader.Instance.Init(NetworkKit.Instance.GetLoginModel().setting,
|
||||
// NetworkKit.Instance.GetLoginModel().cdn_url, new List<ConfigModel>()
|
||||
// {
|
||||
// new CommonModel("Common"),
|
||||
// new PrizeWheelDataModel("PrizeWheelData"),
|
||||
// },
|
||||
// state =>
|
||||
// {
|
||||
// Debug.LogError($"配置加载状态{state}");
|
||||
// },
|
||||
// (errorName, message) =>
|
||||
// {
|
||||
// Debug.LogError($"配置解析错误 {errorName} 错误信息:{message}");
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// AddButton($"读取Common配置", () =>
|
||||
// {
|
||||
// var model = ConfigLoader.Instance.GetConfig<CommonModel>();
|
||||
// Debug.LogError(model);
|
||||
// });
|
||||
// AddButton($"读取PrizeWheelDataModel配置", () =>
|
||||
// {
|
||||
// var model = ConfigLoader.Instance.GetConfig<PrizeWheelDataModel>();
|
||||
// Debug.LogError(model.DataList.Count);
|
||||
// });
|
||||
// AddButton($"测试解析为自定义配置", () =>
|
||||
// {
|
||||
// Debug.LogError(ConfigLoader.Instance.ParesPersonalizedConfig(new PrizeWheelDataModel("PrizeWheelData"), "prize"));
|
||||
// });
|
||||
// AddButton($"读取自定义配置", () =>
|
||||
// {
|
||||
// var personalizedConfig = ConfigLoader.Instance.GetPersonalizedConfig<PrizeWheelDataModel>("prize");
|
||||
// Debug.LogError(personalizedConfig.DataList.Count);
|
||||
// });
|
||||
|
||||
|
||||
UpdateItems();
|
||||
|
||||
|
||||
// AddButton("退出游戏", Application.Quit);
|
||||
}
|
||||
|
||||
private void InitializeStyles() {
|
||||
buttonStyle = new GUIStyle(GUI.skin.button) {
|
||||
fontSize = fontSize,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = Color.green }
|
||||
};
|
||||
|
||||
labelStyle = new GUIStyle(GUI.skin.label) {
|
||||
fontSize = fontSize,
|
||||
normal = { textColor = Color.red }
|
||||
};
|
||||
|
||||
inputFieldStyle = new GUIStyle(GUI.skin.textField) {
|
||||
fontSize = fontSize,
|
||||
alignment = TextAnchor.MiddleLeft
|
||||
};
|
||||
}
|
||||
|
||||
private void StartLayout() {
|
||||
currentX = spacing;
|
||||
currentY = spacing;
|
||||
windowWidth = Screen.width;
|
||||
}
|
||||
|
||||
private void AddButton(string text, Action onClick) {
|
||||
// 计算按钮宽度,基于标题内容自适应
|
||||
var buttonWidth = Mathf.Max(150, Mathf.CeilToInt(buttonStyle.CalcSize(new GUIContent(text)).x) + 20);
|
||||
|
||||
// 换行检查
|
||||
if (currentX + buttonWidth + spacing > windowWidth) {
|
||||
currentX = spacing;
|
||||
currentY += buttonHeight + spacing;
|
||||
}
|
||||
|
||||
// 绘制按钮
|
||||
if (GUI.Button(new Rect(currentX, currentY, buttonWidth, buttonHeight), text, buttonStyle)) {
|
||||
onClick?.Invoke();
|
||||
}
|
||||
|
||||
// 更新下一个控件的位置
|
||||
currentX += buttonWidth + spacing;
|
||||
}
|
||||
|
||||
private void AddLabel(string text) {
|
||||
// // 计算标签宽度和高度
|
||||
// Vector2 size = labelStyle.CalcSize(new GUIContent(text));
|
||||
// int labelWidth = Mathf.CeilToInt(size.x);
|
||||
// int labelHeight = Mathf.CeilToInt(size.y);
|
||||
//
|
||||
// // 换行检查
|
||||
// if (currentX + labelWidth + spacing > windowWidth)
|
||||
// {
|
||||
// currentX = spacing;
|
||||
// currentY += buttonHeight + spacing;
|
||||
// }
|
||||
//
|
||||
// // 绘制标签
|
||||
// GUI.Label(new Rect(currentX, currentY, labelWidth, labelHeight), text, labelStyle);
|
||||
//
|
||||
// // 更新下一个控件的位置
|
||||
// currentX += labelWidth + spacing;
|
||||
|
||||
// 启用自动换行 可用
|
||||
labelStyle.wordWrap = true;
|
||||
|
||||
var size = labelStyle.CalcSize(new GUIContent(text));
|
||||
var labelWidth = Mathf.CeilToInt(size.x);
|
||||
var labelHeight = Mathf.CeilToInt(size.y);
|
||||
|
||||
// 计算标签宽度和高度
|
||||
var maxWidth = windowWidth - spacing * 2; // 标签最大宽度
|
||||
// float labelWidth = maxWidth; // 长文本直接占满一行
|
||||
var needNewRow = labelWidth / maxWidth > 0.6f;
|
||||
if (needNewRow) {
|
||||
labelWidth = maxWidth;
|
||||
labelHeight = (int) labelStyle.CalcHeight(new GUIContent(text), labelWidth);
|
||||
}
|
||||
|
||||
// 换行逻辑:如果当前行剩余宽度不足,提前换行
|
||||
if (currentX + labelWidth + spacing > windowWidth) {
|
||||
currentX = spacing; // 回到左侧
|
||||
currentY += buttonHeight + spacing; // 换到下一行
|
||||
}
|
||||
|
||||
// 绘制标签
|
||||
GUI.Label(new Rect(currentX, currentY, labelWidth, labelHeight), text, labelStyle);
|
||||
|
||||
// 更新位置:占满一整行,因此重置到新行
|
||||
if (needNewRow) {
|
||||
currentX = spacing;
|
||||
currentY += labelHeight + spacing;
|
||||
}
|
||||
else {
|
||||
currentX += labelWidth + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInputField(string key, string placeholder, string buttonText, Action<string> onSubmit) {
|
||||
var inputFieldWidth = Mathf.Max(200, Mathf.CeilToInt(inputFieldStyle.CalcSize(new GUIContent(placeholder)).x) + 20);
|
||||
var buttonWidth = Mathf.Max(100, Mathf.CeilToInt(buttonStyle.CalcSize(new GUIContent(buttonText)).x) + 20);
|
||||
|
||||
if (currentX + inputFieldWidth + buttonWidth + spacing * 2 > windowWidth) {
|
||||
currentX = spacing;
|
||||
currentY += buttonHeight + spacing;
|
||||
}
|
||||
|
||||
if (!inputFields.TryGetValue(key, out var inputField)) {
|
||||
inputFields.Add(key, "");
|
||||
}
|
||||
|
||||
|
||||
inputField = GUI.TextField(new Rect(currentX, currentY, inputFieldWidth, buttonHeight), inputField, inputFieldStyle);
|
||||
currentX += inputFieldWidth + spacing;
|
||||
|
||||
if (GUI.Button(new Rect(currentX, currentY, buttonWidth, buttonHeight), buttonText, buttonStyle)) {
|
||||
onSubmit?.Invoke(inputField);
|
||||
}
|
||||
|
||||
inputFields[key] = inputField;
|
||||
|
||||
currentX += buttonWidth + spacing;
|
||||
}
|
||||
|
||||
private void AddSeparator(string text = "") {
|
||||
// 换行以确保分割线占据整行
|
||||
currentX = spacing;
|
||||
currentY += buttonHeight + spacing;
|
||||
|
||||
// // 分割线高度和宽度
|
||||
// int separatorHeight = Mathf.CeilToInt(fontSize * 1.5f); // 动态设置高度
|
||||
// int separatorWidth = windowWidth - spacing * 2;
|
||||
//
|
||||
// // 计算文字尺寸
|
||||
// Vector2 textSize = labelStyle.CalcSize(new GUIContent(text));
|
||||
// int textWidth = Mathf.CeilToInt(textSize.x);
|
||||
//
|
||||
// // 绘制分割线
|
||||
// Rect lineRect = new Rect(currentX, currentY + separatorHeight / 2, separatorWidth, 1);
|
||||
// GUI.Box(lineRect, GUIContent.none);
|
||||
//
|
||||
//
|
||||
// // 绘制文字
|
||||
// if (!string.IsNullOrEmpty(text))
|
||||
// {
|
||||
// GUI.Label(new Rect((windowWidth - textWidth) / 2, currentY, textWidth, separatorHeight), text, labelStyle);
|
||||
// }
|
||||
|
||||
// 分割线宽度和高度
|
||||
var separatorWidth = windowWidth - spacing * 2;
|
||||
var separatorHeight = Mathf.CeilToInt(fontSize * 1.5f); // 分割线高度(文字高度)
|
||||
var lineHeight = Mathf.CeilToInt(fontSize * 0.1f); // 线条高度
|
||||
|
||||
if (string.IsNullOrEmpty(text)) {
|
||||
// 如果没有文字,绘制整行分割线
|
||||
DrawLine(new Rect(currentX, currentY + separatorHeight / 2 - lineHeight / 2, separatorWidth, lineHeight));
|
||||
}
|
||||
else {
|
||||
// 计算文字尺寸
|
||||
var textSize = labelStyle.CalcSize(new GUIContent(text));
|
||||
var textWidth = Mathf.CeilToInt(textSize.x);
|
||||
|
||||
// 计算分割线两侧长度
|
||||
var lineWidth = (separatorWidth - textWidth - spacing * 2) / 2;
|
||||
|
||||
// 绘制左侧线条
|
||||
DrawLine(new Rect(currentX, currentY + separatorHeight / 2 - lineHeight / 2, lineWidth, lineHeight));
|
||||
|
||||
// 绘制文字
|
||||
GUI.Label(new Rect(currentX + lineWidth + spacing, currentY, textWidth, separatorHeight), text, labelStyle);
|
||||
|
||||
// 绘制右侧线条
|
||||
DrawLine(new Rect(currentX + lineWidth + textWidth + spacing * 2, currentY + separatorHeight / 2 - lineHeight / 2, lineWidth,
|
||||
lineHeight));
|
||||
}
|
||||
|
||||
// 更新位置
|
||||
currentY += separatorHeight + spacing;
|
||||
}
|
||||
|
||||
private void DrawLine(Rect rect) {
|
||||
// 设置线条颜色
|
||||
var originalColor = GUI.color;
|
||||
GUI.color = Color.white; // 可以改成其他颜色
|
||||
|
||||
// 绘制线条
|
||||
GUI.DrawTexture(rect, Texture2D.whiteTexture);
|
||||
|
||||
// 恢复原始颜色
|
||||
GUI.color = originalColor;
|
||||
}
|
||||
|
||||
public void AddItem(GMToolItem item) {
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
private void UpdateItems() {
|
||||
foreach (var item in items) {
|
||||
switch (item.Type) {
|
||||
case GUIType.Label:
|
||||
AddLabel(item.TextFunc.Invoke());
|
||||
break;
|
||||
case GUIType.Button:
|
||||
AddButton(item.TextFunc.Invoke(), () => {
|
||||
item.OnClick?.Invoke(null);
|
||||
});
|
||||
break;
|
||||
case GUIType.InputField:
|
||||
AddInputField(item.Key, item.Placeholder, item.TextFunc.Invoke(), s => {
|
||||
item.OnClick?.Invoke(s);
|
||||
});
|
||||
break;
|
||||
case GUIType.Separator:
|
||||
AddSeparator(item.TextFunc?.Invoke());
|
||||
break;
|
||||
default:
|
||||
Log.Common.Error($"Unsupported GUIType: {item.Type}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GUIType {
|
||||
Label,
|
||||
Button,
|
||||
InputField,
|
||||
Separator // 分割线类型
|
||||
}
|
||||
|
||||
public class GMToolItem {
|
||||
public string Key;
|
||||
public Action<string> OnClick;
|
||||
public string Placeholder;
|
||||
public Func<string> TextFunc;
|
||||
public GUIType Type;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="type">控件类型</param>
|
||||
/// <param name="textFunc">显示的文本,传入委托可以动态变化</param>
|
||||
/// <param name="onClick">点击委托,只有Button与InputField有效,Button可忽略字符串参数,InputField输入的内容会传入</param>
|
||||
/// <param name="key">InputField 需要用到的key,记得保持唯一性,如果重复有可能会导致重复的输入框同步一样的内容</param>
|
||||
/// <param name="placeholder">提示占位文本</param>
|
||||
public GMToolItem(GUIType type, Func<string> textFunc, Action<string> onClick = null, string key = "", string placeholder = "") {
|
||||
Type = type;
|
||||
TextFunc = textFunc;
|
||||
OnClick = onClick;
|
||||
Key = key;
|
||||
Placeholder = placeholder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d06d4a3e6b703474f8fd425530b85b87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8ac58d9307eb1c45849414a44d923ca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class Base64Helper {
|
||||
public static string Encode(string source) {
|
||||
return Base64Encode(Encoding.UTF8, source);
|
||||
}
|
||||
|
||||
public static string Decode(string result) {
|
||||
return Base64Decode(Encoding.UTF8, result);
|
||||
}
|
||||
|
||||
private static string Base64Encode(Encoding encoding, string source) {
|
||||
var bytes = encoding.GetBytes(source);
|
||||
var encode = Convert.ToBase64String(bytes);
|
||||
return encode;
|
||||
}
|
||||
|
||||
private static string Base64Decode(Encoding encoding, string result) {
|
||||
var bytes = Convert.FromBase64String(result);
|
||||
var decode = encoding.GetString(bytes);
|
||||
return decode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0078c84c6f3f4d0ba79a2ef151bf0545
|
||||
timeCreated: 1731309598
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
// 混合通用方法工具类(放置一些通用的静态函数)
|
||||
public static class CommonUtils {
|
||||
/// <summary>
|
||||
/// 深度比较两个对象是否相等,支持复杂结构(如 List、Dictionary、嵌套对象)
|
||||
/// </summary>
|
||||
public static bool DeepEquals<T>(T a, T b) {
|
||||
if (a == null && b == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return JToken.DeepEquals(JToken.FromObject(a), JToken.FromObject(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb253d0247f54d55b83685a0e7c51b70
|
||||
timeCreated: 1749455481
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Text;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public class Cryptor {
|
||||
public static string Encrypt(string data, string key) {
|
||||
var keyMD5 = MD5Helper.MD5String1(key);
|
||||
var str = Base64Helper.Encode(data + keyMD5);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
for (int i = 0, j = bytes.Length - 1; i < j; i += 1, j -= 1) {
|
||||
if (i % 2 == 0) {
|
||||
(bytes[i], bytes[j]) = (bytes[j], bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var loginData = Encoding.UTF8.GetString(bytes);
|
||||
return loginData;
|
||||
}
|
||||
|
||||
public static string Decrypt(string data, string key) {
|
||||
var bytes = Encoding.UTF8.GetBytes(data);
|
||||
for (int i = 0, j = bytes.Length - 1; i < j; i += 1, j -= 1) {
|
||||
if (i % 2 == 0) {
|
||||
(bytes[i], bytes[j]) = (bytes[j], bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var str = Encoding.UTF8.GetString(bytes);
|
||||
var str1 = Base64Helper.Decode(str);
|
||||
var keyMD5 = MD5Helper.MD5String1(key);
|
||||
var result = str1.Replace(keyMD5, string.Empty);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 957dffab93b34167b72cdfd2b0e709fe
|
||||
timeCreated: 1731309567
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class DeviceHelper {
|
||||
private const string Idfv = "IDFV";
|
||||
|
||||
public static string GetDeviceID(string deviceName = "TestUser001") {
|
||||
#if UNITY_IOS && !UNITY_EDITOR
|
||||
deviceName = Keychain.Get(Idfv);
|
||||
if (string.IsNullOrEmpty(deviceName))
|
||||
{
|
||||
deviceName = SystemInfo.deviceUniqueIdentifier;
|
||||
Keychain.Save(Idfv, deviceName);
|
||||
}
|
||||
|
||||
return deviceName;
|
||||
#elif UNITY_EDITOR
|
||||
return deviceName;
|
||||
#endif
|
||||
return SystemInfo.deviceUniqueIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 121d8e1769529b440bc85f03a485ec10
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class Log {
|
||||
// 预定义模块实例
|
||||
public static readonly ModuleLogger Common = new("Common");
|
||||
public static readonly ModuleLogger Net = new("Net", ConfigManager.NetworkConfig?.showNetworkLog ?? false);
|
||||
public static readonly ModuleLogger MarkdownKit = new("MarkdownKit");
|
||||
public static readonly ModuleLogger ApplePay = new("ApplePay");
|
||||
public static readonly ModuleLogger ConfigLoader = new("ConfigLoader");
|
||||
public static readonly ModuleLogger NetKit = new("NetKit");
|
||||
public static readonly ModuleLogger DataStorage = new("DataStorage");
|
||||
public static readonly ModuleLogger IAP = new("IAP");
|
||||
|
||||
private static bool IsEnabled => ConfigManager.GameConfig?.enabledLog ?? false;
|
||||
|
||||
public static void Info(string label, string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
InternalLog(LogType.Log, label, msg, LogColors.Info, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Warning(string label, string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
InternalLog(LogType.Warning, label, msg, LogColors.Warning, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Error(string label, string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
InternalLog(LogType.Error, label, msg, LogColors.Error, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Exception(string label, Exception ex, bool showStack = true) {
|
||||
if (!IsEnabled || ex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var msg = $"Exception: {ex.Message}";
|
||||
if (showStack && !string.IsNullOrEmpty(ex.StackTrace)) {
|
||||
msg += $"\n{ex.StackTrace}";
|
||||
}
|
||||
|
||||
InternalLog(LogType.Error, label, msg, LogColors.Exception, showStack);
|
||||
}
|
||||
|
||||
internal static void InternalLog(LogType type, string label, string msg, string color, bool showStack) {
|
||||
var formattedMsg = $"<color={color}>[{label}]</color> {msg}";
|
||||
|
||||
var original = Application.GetStackTraceLogType(type);
|
||||
if (!showStack) {
|
||||
Application.SetStackTraceLogType(type, StackTraceLogType.None);
|
||||
}
|
||||
|
||||
Debug.unityLogger.Log(type, formattedMsg);
|
||||
|
||||
if (!showStack) {
|
||||
Application.SetStackTraceLogType(type, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ModuleLogger {
|
||||
private readonly bool _isEnabled;
|
||||
private readonly string _label;
|
||||
|
||||
public ModuleLogger(string label, bool isEnabled = true) {
|
||||
_label = label;
|
||||
_isEnabled = isEnabled;
|
||||
}
|
||||
|
||||
private bool IsEnabled => (ConfigManager.GameConfig?.enabledLog ?? false) && _isEnabled;
|
||||
|
||||
public void Info(string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
Log.InternalLog(LogType.Log, _label, msg, LogColors.Info, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public void Warning(string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
Log.InternalLog(LogType.Warning, _label, msg, LogColors.Warning, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public void Error(string msg, bool showStack = true) {
|
||||
if (IsEnabled) {
|
||||
Log.InternalLog(LogType.Error, _label, msg, LogColors.Error, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public void Exception(Exception ex, bool showStack = true) {
|
||||
if (!IsEnabled || ex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = $"Exception: {ex.Message}";
|
||||
if (showStack && !string.IsNullOrEmpty(ex.StackTrace)) {
|
||||
msg += $"\n{ex.StackTrace}";
|
||||
}
|
||||
|
||||
Log.InternalLog(LogType.Error, _label, msg, LogColors.Exception, showStack);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LogColors {
|
||||
public const string Info = "#4CAF50";
|
||||
public const string Warning = "#CC9A06";
|
||||
public const string Error = "#CC423B";
|
||||
public const string Exception = "red"; // 可选保留原文
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 451867446e034f4194f1319933be9cdd
|
||||
timeCreated: 1748248695
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public class MD5Helper {
|
||||
public static string GetFileMD5(string file) {
|
||||
try {
|
||||
var fs = new FileStream(file, FileMode.Open);
|
||||
MD5 md5 = new MD5CryptoServiceProvider();
|
||||
var retVal = md5.ComputeHash(fs);
|
||||
fs.Close();
|
||||
var sb = new StringBuilder();
|
||||
foreach (var str in retVal) {
|
||||
sb.Append(str.ToString("X2"));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new Exception("GetFileMD5 fail error: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串的MD5值
|
||||
/// </summary>
|
||||
public static string GetStringMD5(string str) {
|
||||
if (string.IsNullOrEmpty(str)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MD5 md5 = new MD5CryptoServiceProvider();
|
||||
var bytResult = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
|
||||
var strResult = BitConverter.ToString(bytResult);
|
||||
strResult = strResult.Replace("-", string.Empty);
|
||||
return strResult;
|
||||
}
|
||||
|
||||
public static string MD5String1(string text) {
|
||||
var buffer = Encoding.Default.GetBytes(text);
|
||||
var check = new MD5CryptoServiceProvider();
|
||||
var somme = check.ComputeHash(buffer);
|
||||
var result = new StringBuilder();
|
||||
foreach (var a in somme) {
|
||||
var value = a.ToString("X");
|
||||
if (a < 16) {
|
||||
result.Append(0);
|
||||
result.Append(value);
|
||||
}
|
||||
else {
|
||||
result.Append(value);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d8c3bafdc1d41ee87e8c72f54f42007
|
||||
timeCreated: 1731309631
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class ModuleVersion {
|
||||
public static void Show() {
|
||||
var path = Path.Combine(Application.dataPath, "../gupm.toml");
|
||||
|
||||
if (!File.Exists(path)) {
|
||||
Log.Warning("Submodules", $"找不到配置文件: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
var insideSubmodules = false;
|
||||
var submodules = new Dictionary<string, string>();
|
||||
var lines = File.ReadAllLines(path);
|
||||
|
||||
foreach (var rawLine in lines) {
|
||||
var line = rawLine.Trim();
|
||||
|
||||
// 进入 [Submodules] 区块
|
||||
if (!insideSubmodules) {
|
||||
if (line.Equals("[Submodules]", StringComparison.OrdinalIgnoreCase)) {
|
||||
insideSubmodules = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果遇到下一个区块就退出(可选)
|
||||
if (line.StartsWith("[") && line.EndsWith("]")) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 忽略空行和注释
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 使用 = 来切分
|
||||
var parts = line.Split(new[] { '=' }, 2);
|
||||
if (parts.Length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = parts[0].Trim();
|
||||
var value = parts[1].Trim().Trim('"');
|
||||
|
||||
submodules[key] = value;
|
||||
}
|
||||
|
||||
foreach (var kv in submodules) {
|
||||
Log.Info("Submodules", $"{kv.Key} : {kv.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b578d006bf7b47b8a7ec2d5b48a53c2b
|
||||
timeCreated: 1748947020
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Interface;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class RandomHelper {
|
||||
private static readonly Random _random = new();
|
||||
|
||||
public static T RandomByWeight<T>(IList<T> items, T fallback) where T : class, IWeighted {
|
||||
var totalWeight = items.Sum(item => item.Weight);
|
||||
if (totalWeight <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var roll = _random.Next(0, totalWeight);
|
||||
var cumulative = 0;
|
||||
|
||||
foreach (var item in items) {
|
||||
cumulative += item.Weight;
|
||||
if (roll < cumulative) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return items.Last();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45bcdda204c1483c84aaaf0d6ec0f251
|
||||
timeCreated: 1749198281
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class SerializeHelper {
|
||||
private static readonly JsonSerializerSettings _defaultUseJsonSettings = new() {
|
||||
Formatting = Formatting.None,
|
||||
DateFormatString = "yyyy/MM/dd HH:mm:ss"
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerSettings _jsonIndentedSettings = new() {
|
||||
Formatting = Formatting.Indented,
|
||||
DateFormatString = "yyyy/MM/dd HH:mm:ss"
|
||||
};
|
||||
|
||||
static SerializeHelper() {
|
||||
JsonConvert.DefaultSettings = () => _defaultUseJsonSettings;
|
||||
}
|
||||
|
||||
public static string ToJson(object obj) {
|
||||
return JsonConvert.SerializeObject(obj, _defaultUseJsonSettings);
|
||||
}
|
||||
|
||||
public static string ToJsonIndented(object obj) {
|
||||
return JsonConvert.SerializeObject(obj, _jsonIndentedSettings);
|
||||
}
|
||||
|
||||
public static string ToJson(object obj, Type type) {
|
||||
return JsonConvert.SerializeObject(obj, type, _defaultUseJsonSettings);
|
||||
}
|
||||
|
||||
public static string ToJson<T>(object obj) {
|
||||
return ToJson(obj, typeof(T));
|
||||
}
|
||||
|
||||
public static T ToObject<T>(string json) {
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9879900ede554789a516d519840ef577
|
||||
timeCreated: 1731311911
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
|
||||
namespace SGModule.Common.Helper {
|
||||
public static class TimeHelper {
|
||||
/// <summary>
|
||||
/// 将 Unix 时间戳转换为本地时间
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳(秒)</param>
|
||||
/// <returns>可读的本地时间字符串</returns>
|
||||
public static string ConvertToLocalTime(long timestamp) {
|
||||
// Unix 时间戳起始时间
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// 转换为本地时间
|
||||
var localTime = epoch.AddSeconds(timestamp).ToLocalTime();
|
||||
|
||||
return localTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Unix 时间戳转换为 UTC 时间
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳(秒)</param>
|
||||
/// <returns>可读的 UTC 时间字符串</returns>
|
||||
public static string ConvertToUTCTime(long timestamp) {
|
||||
// Unix 时间戳起始时间
|
||||
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// 转换为 UTC 时间
|
||||
var utcTime = epoch.AddSeconds(timestamp);
|
||||
|
||||
return utcTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
public static string FormatTime(int totalSeconds) {
|
||||
if (totalSeconds < 60) {
|
||||
return $"{totalSeconds}s"; // 小于60秒,直接显示秒
|
||||
}
|
||||
|
||||
if (totalSeconds < 3600) {
|
||||
var minutes = totalSeconds / 60;
|
||||
var seconds = totalSeconds % 60;
|
||||
return $"{minutes}m {seconds}s"; // 小于1小时,显示分秒
|
||||
}
|
||||
else {
|
||||
var hours = totalSeconds / 3600;
|
||||
var remainingSeconds = totalSeconds % 3600;
|
||||
var minutes = remainingSeconds / 60;
|
||||
var seconds = remainingSeconds % 60;
|
||||
return $"{hours}h {minutes}m {seconds}s"; // 超过1小时,显示时分秒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f65b3d4cbd7c456bb9373c261aef0932
|
||||
timeCreated: 1731662312
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8e5eb462e8045f19663f3e4bb29c5e6
|
||||
timeCreated: 1749198053
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SGModule.Common.Interface {
|
||||
public interface IWeighted {
|
||||
int Weight {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c91fe8993478411f8d6a81ce2074a336
|
||||
timeCreated: 1749198110
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26e8d993c96816f42bf326d832cd0f68
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
public static class Keychain {
|
||||
public static bool Save(string key, string value) {
|
||||
#if UNITY_IOS
|
||||
return SaveToKeychain(key, value);
|
||||
#endif
|
||||
Log.Common.Warning("Keychain is only supported on iOS.");
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string Get(string key) {
|
||||
#if UNITY_IOS
|
||||
var resultPtr = GetFromKeychain(key);
|
||||
if (resultPtr != IntPtr.Zero) {
|
||||
var result = Marshal.PtrToStringAuto(resultPtr);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
#endif
|
||||
Log.Common.Warning("Keychain is only supported on iOS.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool Delete(string key) {
|
||||
#if UNITY_IOS
|
||||
return DeleteFromKeychain(key);
|
||||
#endif
|
||||
Log.Common.Warning("Keychain is only supported on iOS.");
|
||||
return false;
|
||||
}
|
||||
#if UNITY_IOS
|
||||
|
||||
[DllImport("__Internal")]
|
||||
private static extern bool SaveToKeychain(string key, string value);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
private static extern IntPtr GetFromKeychain(string key);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
private static extern bool DeleteFromKeychain(string key);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acd6a7aa2034443549cb53d6ae963143
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d82ad3c27862f549a7cd0bc3f5b4e9a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
// [CreateAssetMenu(fileName = "GameConfig", menuName = "GameData/GameConfig")]
|
||||
public class GameConfig : ScriptableObject {
|
||||
[Header("全局配置")] [HideInInspector] public string packageName;
|
||||
|
||||
|
||||
#if GAME_RELEASE
|
||||
private const bool _isRelease = true;
|
||||
#else
|
||||
private const bool _isRelease = false;
|
||||
#endif
|
||||
|
||||
public bool isRelease => _isRelease;
|
||||
|
||||
private bool _enabledLog = true;
|
||||
|
||||
public bool enabledLog {
|
||||
get => !isRelease && _enabledLog;
|
||||
set => _enabledLog = !isRelease && value;
|
||||
}
|
||||
|
||||
[HideInInspector] public bool hardwareAcceleration = true;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c836612725633cc4093d7ac06e7d9ef1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
|
||||
// [CreateAssetMenu(fileName = "NetworkConfig", menuName = "GameData/NetworkConfig")]
|
||||
public class NetworkConfig : ScriptableObject {
|
||||
[Header("Network Config")] [Header("显示日志")]
|
||||
public bool showNetworkLog = true;
|
||||
|
||||
[Header("Url配置")] public string debugHost;
|
||||
|
||||
public string releaseHost;
|
||||
|
||||
[Header("连接模式")] public ConnectionMode connectionMode = ConnectionMode.Http;
|
||||
}
|
||||
|
||||
public enum ConnectionMode {
|
||||
Http, // 基于 HTTP 协议的链接
|
||||
WebSocket // 基于 WebSocket 协议的链接
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6161e194ecfb437f89a55349867a492e
|
||||
timeCreated: 1731317249
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1126f8fc6cfff6941a90361e0d733577
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
# ⚙️ ConfigLoader 配置加载模块
|
||||
|
||||
此模块负责配置文件的加载与检测,依赖于 `Common` 通用模块,请确保已安装该模块。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 📂 配置文件加载
|
||||
- 🔍 配置字段缺失检测
|
||||
- ❌ 配置字段冗余检测
|
||||
- ⚙️ 自动化插件安装
|
||||
- 📝 日志标准化输出
|
||||
|
||||
------
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
导入项目后,会弹出插件安装提示框。点击“确定”后,插件将自动安装;若点击“取消”,脚本重新编译时仍会再次弹出提示。
|
||||
|
||||
- 插件安装需要一定时间,请耐心等待。安装完成后会有弹窗提示。
|
||||
- 安装完成后,会自动生成配置文件夹 `Configs`,请将游戏配置文件复制到此文件夹中。
|
||||
- **请勿修改配置文件名**。如需替换配置文件,请先删除旧文件,确保文件夹中每个配置只有一份。
|
||||
- 游戏首次运行时,会自动将该文件夹复制到游戏的数据存储位置。
|
||||
|
||||
------
|
||||
|
||||
## 🚀 使用指南
|
||||
|
||||
在登录成功后,调用 `ConfigLoader` 的 `Init` 方法传入所需参数,具体参数请查看方法注释。
|
||||
|
||||
> 注意:
|
||||
>
|
||||
> - **List 类型**的配置需继承 `ConfigModel<T, T0>`,不再需要之前的 `IDataList` 接口。
|
||||
> - **非 List 类型**配置继承 `ConfigModel<T>`。
|
||||
|
||||
### 初始化示例
|
||||
|
||||
```csharp
|
||||
var loginModel = NetworkKit.Instance.GetLoginModel();
|
||||
ConfigLoader.Instance.Init(
|
||||
loginModel.setting,
|
||||
loginModel.cdn_url,
|
||||
new List<ConfigModel> {
|
||||
new CommonModel("Common"),
|
||||
new SignDailyRewardModel("SignDailyReward"),
|
||||
new TurntableModel("turntable"),
|
||||
// 其它配置...
|
||||
},
|
||||
state => {
|
||||
Debug.Log($"配置加载状态: {state}");
|
||||
},
|
||||
(errorName, message) => {
|
||||
Debug.LogError($"配置解析错误: {errorName},错误信息:{message}");
|
||||
});
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## 🔍 读取配置示例
|
||||
|
||||
```csharp
|
||||
// 读取基础配置
|
||||
var model = ConfigLoader.Instance.GetConfig<CommonModel>();
|
||||
|
||||
// 解析自定义配置
|
||||
ConfigLoader.Instance.ParesPersonalizedConfig(new PrizeWheelDataModel("PrizeWheelData"), "prize");
|
||||
|
||||
// 获取自定义配置
|
||||
var personalizedConfig = ConfigLoader.Instance.GetPersonalizedConfig<PrizeWheelDataModel>("prize");
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b25e03cedceda0f4db9aebc261a5bf0a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c34977ebbc9d4440826289429a2adc3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c03491d9ce1584a408c18056b93f91d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,188 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
#if USE_ADDRESSABLES
|
||||
using UnityEditor.AddressableAssets;
|
||||
#endif
|
||||
|
||||
namespace SGModule.Editor {
|
||||
[InitializeOnLoad]
|
||||
public static class AddressablesManager {
|
||||
private const string AddressablesPackageName = "com.unity.addressables";
|
||||
private const string AddressablesSymbol = "USE_ADDRESSABLES";
|
||||
private static AddRequest _addRequest;
|
||||
|
||||
// 安装标记,避免重复弹窗
|
||||
private static bool _isInstallingAddressables;
|
||||
|
||||
static AddressablesManager() {
|
||||
EditorApplication.projectChanged += OnProjectChanged; // 监听项目变更
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
|
||||
private static string DefaultConfigsPath => "Assets/Configs"; // 默认的 Addressables 配置路径
|
||||
|
||||
private static void CheckAndSetupAddressables() {
|
||||
if (IsAddressablesInstalled()) {
|
||||
Log.Info("ConfigLoader", "Addressables 已安装,正在初始化...");
|
||||
|
||||
AddScriptingDefineSymbol(AddressablesSymbol);
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
EnsureAddressablesConfigured();
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
RemoveScriptingDefineSymbol(AddressablesSymbol);
|
||||
if (!_isInstallingAddressables) {
|
||||
PromptToInstallAddressables();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAddressablesInstalled() {
|
||||
return PackageInfo.GetAllRegisteredPackages().Any(package => package.name == AddressablesPackageName);
|
||||
}
|
||||
|
||||
private static void PromptToInstallAddressables() {
|
||||
var install = EditorUtility.DisplayDialog(
|
||||
"Addressables 未安装",
|
||||
"当前插件依赖 Addressables 功能,请安装以确保正常使用。\n\n是否立即安装 Addressables?",
|
||||
"安装",
|
||||
"取消");
|
||||
|
||||
if (install) {
|
||||
InstallAddressables();
|
||||
}
|
||||
else {
|
||||
Log.Warning("ConfigLoader", "用户取消安装 Addressables,部分功能可能无法正常使用!");
|
||||
}
|
||||
}
|
||||
|
||||
private static void InstallAddressables() {
|
||||
if (_isInstallingAddressables) {
|
||||
return; // 防止重复安装
|
||||
}
|
||||
|
||||
Log.Info("ConfigLoader", "开始安装 Addressables...");
|
||||
|
||||
_isInstallingAddressables = true; // 标记正在安装,避免重复弹窗
|
||||
_addRequest = Client.Add(AddressablesPackageName);
|
||||
|
||||
EditorApplication.update += MonitorAddRequest;
|
||||
}
|
||||
|
||||
private static void MonitorAddRequest() {
|
||||
if (_addRequest == null || !_addRequest.IsCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorApplication.update -= MonitorAddRequest;
|
||||
|
||||
if (_addRequest.Status == StatusCode.Success) {
|
||||
Log.Info("ConfigLoader", "Addressables 安装成功!");
|
||||
|
||||
EditorUtility.DisplayDialog("安装完成", "Addressables 安装成功,请稍候等待 Unity 刷新。", "确定");
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
else if (_addRequest.Status >= StatusCode.Failure) {
|
||||
Log.Error("ConfigLoader", $"安装 Addressables 失败:{_addRequest.Error.message}");
|
||||
|
||||
EditorUtility.DisplayDialog("安装失败", $"安装 Addressables 失败:{_addRequest.Error.message}", "确定");
|
||||
}
|
||||
|
||||
_isInstallingAddressables = false; // 重置标记
|
||||
}
|
||||
|
||||
private static void AddScriptingDefineSymbol(string symbol) {
|
||||
var symbols = new List<string>(PlayerSettings
|
||||
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
|
||||
if (symbols.Contains(symbol)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
symbols.Add(symbol);
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
|
||||
string.Join(";", symbols));
|
||||
Log.Info("ConfigLoader", $"已添加脚本定义符号: {symbol}");
|
||||
}
|
||||
|
||||
private static void RemoveScriptingDefineSymbol(string symbol) {
|
||||
var symbols = new List<string>(PlayerSettings
|
||||
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
|
||||
if (symbols.Contains(symbol)) {
|
||||
symbols.Remove(symbol);
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
|
||||
string.Join(";", symbols));
|
||||
|
||||
Log.Info("ConfigLoader", $"已移除脚本定义符号: {symbol}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnProjectChanged() {
|
||||
Log.Info("ConfigLoader", "检测到项目变更,重新检查 Addressables 状态...");
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
private static void EnsureAddressablesConfigured() {
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
|
||||
if (settings == null) {
|
||||
Log.Info("ConfigLoader", "正在初始化 Addressables 设置...");
|
||||
|
||||
settings = AddressableAssetSettingsDefaultObject.GetSettings(true); // 自动创建配置
|
||||
}
|
||||
|
||||
// 确保默认的 Configs 文件夹存在并被添加到 Addressables
|
||||
var needRefresh = false;
|
||||
if (!Directory.Exists(DefaultConfigsPath)) {
|
||||
Directory.CreateDirectory(DefaultConfigsPath);
|
||||
Log.Info("ConfigLoader", $"已创建文件夹: {DefaultConfigsPath}");
|
||||
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
if (!IsFolderInAddressables(DefaultConfigsPath)) {
|
||||
var guid = AssetDatabase.AssetPathToGUID(DefaultConfigsPath);
|
||||
var group = settings.DefaultGroup;
|
||||
settings.CreateOrMoveEntry(guid, group)?.SetLabel("config", true, true);
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFolderInAddressables(string folderPath) {
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group)
|
||||
{
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (AssetDatabase.GUIDToAssetPath(entry.guid) == folderPath)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 383ac67f695b4c6e94ba988c9303cd5b
|
||||
timeCreated: 1733734050
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34c8928adac69ec44ac63b385ef330d1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53a14b320d93d7c439c59c8190a874ab
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,418 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using SGModule.Common;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Extensions;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace SGModule.ConfigLoader
|
||||
{
|
||||
public enum ConfigLoaderState
|
||||
{
|
||||
None,
|
||||
Failed,
|
||||
JsonEmptyError,
|
||||
Successful
|
||||
}
|
||||
|
||||
public class ConfigLoader : SingletonMonoBehaviour<ConfigLoader>
|
||||
{
|
||||
private const int MaxErrorCount = 6;
|
||||
private const string ConfigFileNameKey = "ConfigFileName";
|
||||
private readonly Dictionary<Type, object> _configData = new();
|
||||
private readonly Dictionary<string, ConfigModel> _userDefinedConfig = new();
|
||||
|
||||
private string _cdnConfigFileName;
|
||||
private string _cdnUrl;
|
||||
|
||||
private string _configJson;
|
||||
|
||||
private int _initConfigErrorCount;
|
||||
|
||||
private bool _isConfigLoaded;
|
||||
private Dictionary<string, object> _jsonDictionary = new();
|
||||
|
||||
// 暴露只读接口
|
||||
public IReadOnlyDictionary<string, object> JsonDictionary => _jsonDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化配置模块方法
|
||||
/// </summary>
|
||||
/// <param name="cdnConfigFileName">登录获得的配置文件名</param>
|
||||
/// <param name="cdnUrl">下载配置的地址</param>
|
||||
/// <param name="configs">需要初始化解析的配置</param>
|
||||
/// <param name="configLoadCallback">配置加载回调</param>
|
||||
/// <param name="parseErrorHandler">配置解析错误回调</param>
|
||||
public void Init(string cdnConfigFileName, string cdnUrl, List<ConfigModel> configs,
|
||||
UnityAction<ConfigLoaderState> configLoadCallback = null,
|
||||
UnityAction<string, string> parseErrorHandler = null)
|
||||
{
|
||||
_cdnConfigFileName = cdnConfigFileName;
|
||||
_cdnUrl = cdnUrl;
|
||||
|
||||
IsInitComplete = true;
|
||||
|
||||
CheckConfigFile(state =>
|
||||
{
|
||||
if (state == ConfigLoaderState.Successful)
|
||||
{
|
||||
InitConfigList(configs, parseErrorHandler);
|
||||
}
|
||||
|
||||
configLoadCallback?.Invoke(state);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="configs"></param>
|
||||
/// <param name="errorCallback"></param>
|
||||
private void InitConfigList(List<ConfigModel> configs, UnityAction<string, string> errorCallback = null)
|
||||
{
|
||||
foreach (var config in configs)
|
||||
{
|
||||
Pares(config, errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并加载配置文件
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
private void CheckConfigFile(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
if (!IsInitComplete)
|
||||
{
|
||||
Log.ConfigLoader.Warning("配置加载模块模块未初始化");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_initConfigErrorCount > MaxErrorCount)
|
||||
{
|
||||
callback?.Invoke(ConfigLoaderState.Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFirstLaunch())
|
||||
{
|
||||
FirstLaunchCopyConfig(callback); // 进行首次启动的操作
|
||||
return;
|
||||
}
|
||||
|
||||
bool needDownloadConfigFile = false;
|
||||
if ((!string.IsNullOrEmpty(_cdnConfigFileName)) && PlayerPrefs.GetString(ConfigFileNameKey, "") != _cdnConfigFileName) needDownloadConfigFile = true;
|
||||
//需要下载配置的话等待下载更新
|
||||
if (needDownloadConfigFile)
|
||||
{
|
||||
DownloadConfig(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadLocalConfig(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是首次打开游戏
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsFirstLaunch()
|
||||
{
|
||||
return !PlayerPrefs.HasKey("FirstLaunch");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置首次启动游戏
|
||||
/// </summary>
|
||||
private void SetFirstLaunch()
|
||||
{
|
||||
PlayerPrefs.SetInt("FirstLaunch", 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 首次启动游戏拷贝配置文件到PersistentData目录下
|
||||
/// </summary>
|
||||
private void FirstLaunchCopyConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
SetFirstLaunch();
|
||||
FileNetworkManager.Instance.CopyStreamingAssetsToPersistentDataPath(result =>
|
||||
{
|
||||
var isSuccess = false;
|
||||
if (result)
|
||||
{
|
||||
var names =
|
||||
FileNetworkManager.Instance.GetFileNamesFromPersistentDataPath(FileNetworkManager.FolderName);
|
||||
if (names.Length > 0)
|
||||
{
|
||||
PlayerPrefs.SetString(ConfigFileNameKey, names[0]); //项目当前文件中的配置版本
|
||||
isSuccess = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ConfigLoader.Error("拷贝文件失败 请检查Configs文件夹是否有配置文件");
|
||||
}
|
||||
|
||||
if (!isSuccess)
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
}
|
||||
|
||||
CheckConfigFile(callback);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载配置文件
|
||||
/// </summary>
|
||||
private void DownloadConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
FileNetworkManager.Instance.ReadData($"{_cdnUrl}/config/{_cdnConfigFileName}", result =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
PlayerPrefs.SetString(ConfigFileNameKey, _cdnConfigFileName); //更新当前配置文件文件名
|
||||
FileNetworkManager.Instance.WriteToPersistentData(FileNetworkManager.Instance.GetConfigFOlderPath(),
|
||||
_cdnConfigFileName, result);
|
||||
ReloadConfig(result, callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
Log.ConfigLoader.Error("下载配置文件失败");
|
||||
ReadLocalConfig(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取本地配置文件
|
||||
/// </summary>
|
||||
private void ReadLocalConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
|
||||
var path = Path.Combine(FileNetworkManager.Instance.GetConfigFOlderPath(), savedCfgName);
|
||||
FileNetworkManager.Instance.ReadData(path, result =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
ReloadConfig(result, callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
Log.ConfigLoader.Error("读取本地数据异常");
|
||||
|
||||
|
||||
//读取本地配置文件失败,重新从默认位置拷贝原始配置文件到配置文件夹下
|
||||
FirstLaunchCopyConfig(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有新的配置
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool HasNewConfig()
|
||||
{
|
||||
if (!PlayerPrefs.HasKey(ConfigFileNameKey))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
|
||||
var needDownloadConfigFile = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_cdnConfigFileName))
|
||||
{
|
||||
//如果本地Player Prefs里没有保存配置文件名
|
||||
if (string.IsNullOrEmpty(savedCfgName))
|
||||
{
|
||||
needDownloadConfigFile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//与CDN上的对比名称
|
||||
if (!savedCfgName.Equals(_cdnConfigFileName))
|
||||
{
|
||||
needDownloadConfigFile = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return needDownloadConfigFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新加载配置
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="callback"></param>
|
||||
private void ReloadConfig(string json, [NotNull] UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
ValidateConfigFile(json, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查json
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="callback"></param>
|
||||
private void ValidateConfigFile(string json, UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
if (json.IsNullOrWhiteSpace())
|
||||
{
|
||||
callback?.Invoke(ConfigLoaderState.JsonEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.StartsWith("{"))
|
||||
{
|
||||
var gameConfig = ConfigManager.GameConfig;
|
||||
json = Cryptor.Decrypt(json, gameConfig.packageName);
|
||||
}
|
||||
|
||||
_configJson = json;
|
||||
var dictionary = SerializeHelper.ToObject<Dictionary<string, object>>(_configJson);
|
||||
_jsonDictionary = dictionary;
|
||||
|
||||
_isConfigLoaded = true;
|
||||
callback?.Invoke(ConfigLoaderState.Successful);
|
||||
|
||||
|
||||
// ParseGameConfig(dictionary, callback);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T ParseNewConfig<T>(T config, UnityAction<string, string> errorCallback = null) where T : ConfigModel
|
||||
{
|
||||
if (!_isConfigLoaded)
|
||||
{
|
||||
Log.ConfigLoader.Warning("配置文件未加载完成");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (_configData.TryGetValue(config.GetType(), out var obj))
|
||||
{
|
||||
Log.ConfigLoader.Warning("当前配置已经成功解析了");
|
||||
return obj as T;
|
||||
}
|
||||
|
||||
return Pares(config, errorCallback) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <param name="addToConfigData">是否保存起来</param>
|
||||
/// <returns></returns>
|
||||
private ConfigModel Pares(ConfigModel config, UnityAction<string, string> errorCallback = null,
|
||||
bool addToConfigData = true)
|
||||
{
|
||||
var configModel = config.Parse(JsonDictionary);
|
||||
if (configModel != null)
|
||||
{
|
||||
if (addToConfigData)
|
||||
{
|
||||
var type = configModel.GetType();
|
||||
// if (!_configData.ContainsKey(type))
|
||||
// {
|
||||
// _configData.Add(type, configModel);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogWarning($"请注意,重复解析配置文件{type}");
|
||||
// _configData[type] = configModel;
|
||||
// }
|
||||
|
||||
_configData[type] = configModel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCallback?.Invoke(config.GetType().Name, "解析异常");
|
||||
Log.ConfigLoader.Warning("解析异常");
|
||||
}
|
||||
|
||||
return configModel as ConfigModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析自定义名称配置文件,将配置解析为自定义名称的配置文件并保存
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="configName">数据结构类型</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <returns></returns>
|
||||
public bool ParesPersonalizedConfig(ConfigModel config, string configName,
|
||||
UnityAction<string, string> errorCallback = null)
|
||||
{
|
||||
if (_userDefinedConfig.ContainsKey(configName))
|
||||
{
|
||||
Log.ConfigLoader.Warning("存在相同名称的自定义配置");
|
||||
return false;
|
||||
}
|
||||
|
||||
var configModel = Pares(config, errorCallback, false);
|
||||
if (configModel != null)
|
||||
{
|
||||
_userDefinedConfig.Add(configName, configModel);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"解析配置重新异常{config} {configName}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取自定义配置数据
|
||||
/// </summary>
|
||||
/// <param name="configName">配置名称</param>
|
||||
/// <typeparam name="T">对应数据结构</typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetPersonalizedConfig<T>(string configName)
|
||||
{
|
||||
if (_userDefinedConfig.TryGetValue(configName, out var value))
|
||||
{
|
||||
if (value is T configModel)
|
||||
{
|
||||
return configModel;
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"有{configName}的自定义配置,但类型不对,期望类型是{typeof(T).Name} 现有类型{value.GetType().Name} ");
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public T GetConfig<T>()
|
||||
{
|
||||
return _configData.TryGetValue(typeof(T), out var value) ? (T)value : default;
|
||||
}
|
||||
|
||||
public void AddConfig(ConfigModel configModel)
|
||||
{
|
||||
var type = configModel.GetType();
|
||||
_configData[type] = configModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cda67e479db4e8fbd10667fba04b197
|
||||
timeCreated: 1731984367
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
#endif
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class FileNetworkManager : SingletonMonoBehaviour<FileNetworkManager> {
|
||||
public const string FolderName = "Configs";
|
||||
private const string FolderLabel = "config";
|
||||
private string _configFolderPath;
|
||||
|
||||
protected override void Awake() {
|
||||
base.Awake();
|
||||
|
||||
_configFolderPath = Path.Combine(Application.persistentDataPath, FolderName);
|
||||
}
|
||||
|
||||
public string GetConfigFOlderPath() {
|
||||
return _configFolderPath;
|
||||
}
|
||||
|
||||
// 示例解析文件列表方法(你可以根据实际需求实现)
|
||||
private List<string> ParseFileList(string data) {
|
||||
// 假设 data 是文件列表的文本,解析文件名
|
||||
// 根据实际情况解析,例如通过换行符分割
|
||||
return data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
}
|
||||
|
||||
public void CopyStreamingAssetsToPersistentDataPath(Action<bool> onComplete = null) {
|
||||
// 如果目标文件夹不存在,创建它
|
||||
if (!Directory.Exists(_configFolderPath)) {
|
||||
Directory.CreateDirectory(_configFolderPath);
|
||||
}
|
||||
|
||||
StartCoroutine(CopyFile(onComplete));
|
||||
}
|
||||
|
||||
private void HandleInitializationError() {
|
||||
// 检查文件是否存在
|
||||
var path = $"{Application.dataPath}/Library/com.unity.addressables/aa/Android/settings.json";
|
||||
if (!File.Exists(path)) {
|
||||
Log.ConfigLoader.Warning(
|
||||
$"Settings file not found at: {path}. Rebuilding Addressables may be required.");
|
||||
}
|
||||
|
||||
|
||||
// 提示用户或执行其他逻辑
|
||||
// 比如:显示弹窗或退出程序
|
||||
}
|
||||
|
||||
private IEnumerator CopyFile(Action<bool> onComplete = null) {
|
||||
#if USE_ADDRESSABLES
|
||||
var handle = Addressables.LoadResourceLocationsAsync(FolderLabel);
|
||||
|
||||
yield return handle;
|
||||
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded) {
|
||||
// 查找以 ".json" 结尾的文件
|
||||
var jsonLocation = handle.Result.FirstOrDefault(loc => loc.PrimaryKey.EndsWith(".txt"));
|
||||
if (jsonLocation != null) {
|
||||
var jsonFileName = Path.GetFileName(jsonLocation.PrimaryKey);
|
||||
// 加载 JSON 文件
|
||||
var textAssetAsync = Addressables.LoadAssetAsync<TextAsset>(jsonLocation);
|
||||
|
||||
yield return textAssetAsync;
|
||||
|
||||
if (textAssetAsync.Status == AsyncOperationStatus.Succeeded) {
|
||||
var jsonFile = textAssetAsync.Result;
|
||||
Log.ConfigLoader.Info($"Loaded JSON Name:{jsonFileName} content: " + jsonFile.text);
|
||||
|
||||
|
||||
var destFilePath = Path.Combine(_configFolderPath, jsonFileName);
|
||||
File.WriteAllBytes(destFilePath, jsonFile.bytes);
|
||||
onComplete?.Invoke(true);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to load JSON file");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("No JSON file found in folder");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to load folder resources");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
#else
|
||||
Log.Error( "没有 Addressables 插件,请检查 ");
|
||||
yield break;
|
||||
#endif
|
||||
}
|
||||
|
||||
private IEnumerator CopyFile(string sourceFile, string destFile, Action<bool> onComplete = null) {
|
||||
var filePath = "file://" + sourceFile;
|
||||
|
||||
using (var www = UnityWebRequest.Get(filePath)) {
|
||||
yield return www.SendWebRequest();
|
||||
|
||||
if (www.result == UnityWebRequest.Result.Success) {
|
||||
File.WriteAllBytes(destFile, www.downloadHandler.data);
|
||||
Log.ConfigLoader.Info($"File copied to {destFile}");
|
||||
onComplete?.Invoke(true);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to copy file: " + www.error);
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取或下载
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="callback"></param>
|
||||
public void ReadData(string path, Action<string> callback) {
|
||||
StartCoroutine(ReadDataEnumerator(path, callback));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取或下载
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <returns></returns>
|
||||
private IEnumerator ReadDataEnumerator(string path, Action<string> callback) {
|
||||
string fullPath;
|
||||
|
||||
// 判断是网络URL还是本地文件路径
|
||||
if (path.StartsWith("http://") || path.StartsWith("https://")) {
|
||||
fullPath = path; // 网络URL
|
||||
#if UNITY_EDITOR
|
||||
if (path.StartsWith("http://") && PlayerSettings.insecureHttpOption == InsecureHttpOption.NotAllowed) {
|
||||
Log.ConfigLoader.Error("发起了 HTTP 链接,但设置了不允许非Https连接,请检查设置!!!");
|
||||
|
||||
callback?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// 本地文件路径
|
||||
fullPath = "file://" + path;
|
||||
}
|
||||
|
||||
// 使用UnityWebRequest读取数据
|
||||
using (var webRequest = UnityWebRequest.Get(fullPath)) {
|
||||
yield return webRequest.SendWebRequest();
|
||||
|
||||
if (webRequest.result == UnityWebRequest.Result.ConnectionError ||
|
||||
webRequest.result == UnityWebRequest.Result.ProtocolError) {
|
||||
Log.ConfigLoader.Error($"Error while reading file: {webRequest.error} path: {fullPath}");
|
||||
|
||||
callback?.Invoke(null); // 返回null表示出错
|
||||
}
|
||||
else {
|
||||
var data = webRequest.downloadHandler.text;
|
||||
Log.ConfigLoader.Info($"Data read successfully:\n {data}");
|
||||
|
||||
callback?.Invoke(data); // 通过回调传出数据
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将内容写入指定文件,并删除原有文件。
|
||||
/// </summary>
|
||||
/// <param name="folderName">文件夹名称</param>
|
||||
/// <param name="fileName">文件名称</param>
|
||||
/// <param name="content">要写入的内容</param>
|
||||
public void WriteToPersistentData(string folderName, string fileName, string content) {
|
||||
// 获取持久化数据路径
|
||||
var folderPath = Path.Combine(Application.persistentDataPath, folderName);
|
||||
|
||||
// 如果文件夹不存在,则创建它
|
||||
if (!Directory.Exists(folderPath)) {
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
// 删除原有文件
|
||||
var existingFiles = Directory.GetFiles(folderPath);
|
||||
foreach (var file in existingFiles) {
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
// 完整文件路径
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
|
||||
// 写入新文件
|
||||
File.WriteAllText(filePath, content);
|
||||
|
||||
Log.ConfigLoader.Info($"File written to: {filePath}");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 提取指定目录下所有文件文件名
|
||||
/// </summary>
|
||||
/// <param name="subdirectory"></param>
|
||||
/// <returns></returns>
|
||||
public string[] GetFileNamesFromPersistentDataPath(string subdirectory) {
|
||||
var directoryPath = Path.Combine(Application.persistentDataPath, subdirectory);
|
||||
|
||||
if (Directory.Exists(directoryPath))
|
||||
// 获取目录中的所有文件名
|
||||
{
|
||||
return Directory.GetFiles(directoryPath)
|
||||
.Select(Path.GetFileName) // 只提取文件名
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"Directory does not exist: {directoryPath}");
|
||||
return Array.Empty<string>(); // 返回空数组
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35da25a8aaf547b8accde3b52c38a887
|
||||
timeCreated: 1731982304
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed771f8b80c42a24e8b44eb1aa5d12bd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb3fbe8a7d2b0ea4c822d6cefeee3708
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user