Forest_Client/Forest/Assets/Scripts/Gameplay2/BuildManager.cs

673 lines
22 KiB
C#
Raw Normal View History

2024-07-10 19:06:07 +08:00
using System;
2024-07-16 15:58:21 +08:00
using PhxhSDK;
2024-07-09 14:59:12 +08:00
using UnityEngine;
2024-07-17 18:56:07 +08:00
using System.Linq;
2024-07-19 15:27:04 +08:00
using LC.Newtonsoft.Json;
using Framework.Constants;
2024-07-10 19:06:07 +08:00
using Sirenix.OdinInspector;
2024-07-16 18:53:34 +08:00
using Cysharp.Threading.Tasks;
2024-07-17 18:56:07 +08:00
using System.Collections.Generic;
2024-08-19 15:44:15 +08:00
using cfg.Build;
using PhxhSDK.Phxh;
2024-08-15 17:56:54 +08:00
using UnityEngine.AddressableAssets;
2024-07-09 14:59:12 +08:00
namespace Framework.Manager
{
2024-07-10 19:06:07 +08:00
/// <summary>
/// 解锁类型
/// </summary>
public enum UnlockType
2024-07-09 14:59:12 +08:00
{
2024-07-10 19:06:07 +08:00
[LabelText("按挂点解锁")] ForGroup,
2024-07-09 14:59:12 +08:00
2024-07-10 19:06:07 +08:00
[LabelText("按主题解锁")] ForThematic,
}
/// <summary>
/// 解锁条件类型
/// </summary>
public enum UnlockConditionType
{
[LabelText("关卡解锁")] Level,
[LabelText("金币解锁")] Coin,
}
[Serializable]
public class BuildData
{
2024-07-17 18:56:07 +08:00
[ReadOnly] public string buildID;
[LabelText("挂点数量")] public int nodeCount;
[LabelText("主题数量")] public int thematicCount;
2024-07-10 19:06:07 +08:00
[LabelText("解锁类型")] [SerializeField] public UnlockType unlockType;
[LabelText("解锁条件类型")] [SerializeField] public UnlockConditionType unlockConditionType;
[LabelText("解锁条件")] public List<UnlockInfo> unlockInfos;
2024-07-17 18:56:07 +08:00
[LabelText("挂点数据")] public List<BuildNode> NodeInfos;
2024-07-11 20:03:13 +08:00
2024-07-10 19:06:07 +08:00
[Serializable]
public class UnlockInfo
2024-07-09 14:59:12 +08:00
{
2024-07-10 19:06:07 +08:00
[LabelText("条件")] public int condition;
2024-07-11 20:03:13 +08:00
//主题 or 挂点
[LabelText("对应组")] public int conditionGroup;
2024-07-29 16:01:29 +08:00
[LabelText("前置组")] public int preGroup;
2024-07-09 14:59:12 +08:00
}
2024-07-11 20:03:13 +08:00
2024-07-19 16:36:38 +08:00
[Serializable]
2024-07-10 19:06:07 +08:00
public class BuildNode
2024-07-09 14:59:12 +08:00
{
2024-07-19 13:34:24 +08:00
[LabelText("挂点名称")] [ReadOnly] public string Name;
2024-07-10 19:06:07 +08:00
2024-07-17 18:56:07 +08:00
[HideInInspector] public List<string> Options;
[HideInInspector] public string IconPath;
2024-07-19 15:27:04 +08:00
[JsonConstructor]
public BuildNode(string name)
{
this.Name = name;
2024-07-19 16:36:38 +08:00
this.Options = new List<string>();
this.IconPath = string.Empty;
2024-07-19 15:27:04 +08:00
}
public BuildNode()
{
}
2024-07-09 14:59:12 +08:00
}
}
2024-07-16 15:58:21 +08:00
/// <summary>
/// 节点信息类
/// </summary>
public class NodeInfo
{
public string Name;
public Dictionary<string, OptionInfo> Options;
//按挂点解锁条件
public int Condition;
2024-07-15 20:00:39 +08:00
2024-07-29 16:01:29 +08:00
//前置解锁组
public string PreGroup;
2024-07-15 20:00:39 +08:00
public string IconPath;
public OptionInfo GetOptionInfo(string optionID = null)
{
if (optionID == null) optionID = "Option1";
return Options.GetValueOrDefault(optionID);
}
}
2024-07-16 15:58:21 +08:00
/// <summary>
/// 节点选项类
/// </summary>
public class OptionInfo
{
public string Name;
2024-07-19 17:18:17 +08:00
public string IconPath;
2024-07-16 15:04:13 +08:00
//按主题解锁条件
public int Condition;
2024-07-29 16:01:29 +08:00
//前置主题
public string PreThematic;
}
2024-07-09 14:59:12 +08:00
2024-08-19 15:44:15 +08:00
/// <summary>
/// 每个主题节点保存信息
/// </summary>
public class BuildInfo
{
public string BuildSceneID;
public Dictionary<string, string> ChooseNodeInfo;
public BuildInfo()
{
ChooseNodeInfo = new Dictionary<string, string>();
}
public BuildInfo(string buildId)
{
BuildSceneID = buildId;
ChooseNodeInfo = new Dictionary<string, string>();
}
}
/// <summary>
/// 玩家养成建造存盘数据
/// </summary>
public class UserBuildInfo
{
public int GuideGroup;
2024-08-19 15:44:15 +08:00
public string CurBuildId;
public Dictionary<string, BuildInfo> AllChooseNodeInfo;
public UserBuildInfo()
{
2024-08-19 15:44:15 +08:00
AllChooseNodeInfo = new Dictionary<string, BuildInfo>();
}
2024-08-19 15:44:15 +08:00
public UserBuildInfo(string curBuildId)
{
2024-08-19 15:44:15 +08:00
CurBuildId = curBuildId;
AllChooseNodeInfo = new Dictionary<string, BuildInfo>();
}
}
public class BuildManager
2024-07-09 14:59:12 +08:00
{
2024-08-19 17:25:46 +08:00
private const string UserBuildSaveKey = "UserBuildInfo";
private const string NodeName = "Node{0}";
private const string OptionName = "Option{0}";
private static BuildManager _instance;
public static BuildManager Instance
{
get
{
if (_instance == null)
{
_instance = new BuildManager();
}
return _instance;
}
}
2024-08-15 17:56:54 +08:00
/// <summary>
2024-08-19 17:25:46 +08:00
/// 玩家存盘选择信息
2024-08-15 17:56:54 +08:00
/// </summary>
public UserBuildInfo UserBuildInfo
{
get => _userBuildInfo;
private set => _userBuildInfo = value;
}
2024-08-19 17:25:46 +08:00
private UserBuildInfo _userBuildInfo;
2024-08-19 15:44:15 +08:00
/// <summary>
/// 当前主题玩家选择情况
/// </summary>
public BuildInfo BuildInfo
{
get => _curBuildInfo;
private set => _curBuildInfo = value;
}
2024-08-19 17:25:46 +08:00
private BuildInfo _curBuildInfo;
2024-07-19 17:59:28 +08:00
2024-08-19 17:25:46 +08:00
/// <summary>
/// 更换场景
/// </summary>
public bool ChangeBuildSceneID;
2024-07-17 18:56:07 +08:00
2024-08-19 17:25:46 +08:00
/// <summary>
/// 已达到的条件
/// </summary>
public int ReachCondition { get; private set; } = -1;
2024-07-16 18:53:34 +08:00
2024-08-19 17:25:46 +08:00
/// <summary>
/// 当前场景蓝图
/// </summary>
2024-07-18 20:01:43 +08:00
public Sprite CurBlueprint;
2024-08-19 17:25:46 +08:00
/// <summary>
/// 当前场景顶栏图片
/// </summary>
public Sprite CurTopBuildUI;
/// <summary>
/// Build场景相机
/// </summary>
2024-07-18 20:01:43 +08:00
public Camera CurBuildCamera;
2024-08-19 17:25:46 +08:00
/// <summary>
/// 当前场景节点信息
/// </summary>
private Dictionary<string, NodeInfo> _nodeInfos;
/// <summary>
///动态加载的图标
/// </summary>
private Dictionary<string, Sprite> _iconSprites;
/// <summary>
/// 已经加载的资源
/// </summary>
private readonly List<string> _loadAssets = new List<string>();
2024-07-16 15:58:21 +08:00
private BuildData _curBuildData;
2024-07-10 19:06:07 +08:00
private bool _isInit;
2024-08-20 13:19:06 +08:00
public bool BuildInGame;
2024-07-10 19:06:07 +08:00
2024-08-19 15:44:15 +08:00
public async UniTask LoadBuild(string buildID, int reachCondition)
{
InitForStorage();
var buildConfigId = !string.IsNullOrEmpty(buildID) ? buildID : _userBuildInfo.CurBuildId;
if (!TableManager.Instance.Tables.BuildConfig.DataMap.TryGetValue(buildConfigId, out var buildConfig))
{
DebugUtil.LogError("没有{0}的建造配置", buildConfigId);
return;
}
if (GuideMananger.Instance.IsGuiding && !string.IsNullOrEmpty(GuideMananger.Instance.PassLevelID))
{
var guideCondition = int.Parse(GuideMananger.Instance.PassLevelID.Substring("level".Length));
Instance.UpdateReachCondition(guideCondition);
}
else
{
UpdateReachCondition(reachCondition);
}
var buildData = await JsonHelper.LoadFromAddressable<BuildData>(buildConfig.BuildData);
2024-08-20 13:19:06 +08:00
2024-08-19 15:44:15 +08:00
await Init(buildData, true);
await Addressables.LoadSceneAsync(buildConfig.Path).ToUniTask();
_userBuildInfo.CurBuildId = buildConfigId;
StorageManager.Instance.SaveWithoutUpdate();
}
2024-08-19 17:25:46 +08:00
/// <summary>
/// 获取存盘信息
/// </summary>
private void InitForStorage()
{
if (_isInit) return;
_userBuildInfo = StorageManager.Instance.GetStorage<UserBuildInfo>(NormalConstants.UserBuildSaveKey);
_userBuildInfo ??= new UserBuildInfo();
if (string.IsNullOrEmpty(_userBuildInfo.CurBuildId))
{
_userBuildInfo.CurBuildId = TableManager.Instance.Tables.BuildConfig.DataList[0].ID;
DebugUtil.LogError("无存盘信息, 读默认表:{0}", _userBuildInfo.CurBuildId);
}
}
2024-08-19 15:44:15 +08:00
public async UniTask Init(BuildData buildData, bool inGame = false)
2024-07-09 14:59:12 +08:00
{
2024-08-15 17:56:54 +08:00
if (_isInit && !ChangeBuildSceneID) return;
2024-08-19 15:44:15 +08:00
2024-08-20 13:19:06 +08:00
DebugUtil.LogError("当前建造ID:{0}", buildData.buildID);
BuildInGame = inGame;
_curBuildData = buildData;
2024-08-19 17:25:46 +08:00
_nodeInfos = new Dictionary<string, NodeInfo>();
2024-07-16 18:53:34 +08:00
_iconSprites = new Dictionary<string, Sprite>();
InitNodesInfo();
2024-08-19 15:44:15 +08:00
InitUserBuildInfo();
InitCondition();
2024-07-18 20:01:43 +08:00
await InitIcon();
InitBlueprint();
2024-08-19 15:44:15 +08:00
2024-07-18 20:01:43 +08:00
if (inGame)
CurBuildCamera = CameraManager.Instance.UICamera;
2024-08-19 15:44:15 +08:00
_isInit = true;
}
/// <summary>
/// 初始化该Build场景的节点信息
/// </summary>
private void InitNodesInfo()
{
try
{
foreach (var node in _curBuildData.NodeInfos)
{
var nodeInfo = new NodeInfo
{
Name = node.Name,
Options = new Dictionary<string, OptionInfo>(),
IconPath = node.IconPath
};
2024-07-16 18:53:34 +08:00
foreach (var option in node.Options)
{
var optionInfo = new OptionInfo()
{
Name = option
};
2024-07-09 14:59:12 +08:00
nodeInfo.Options.Add(option, optionInfo);
}
2024-08-19 17:25:46 +08:00
_nodeInfos.Add(nodeInfo.Name, nodeInfo);
}
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitNodesInfo 初始化节点错误 :{0}", e);
}
}
2024-07-10 19:06:07 +08:00
2024-08-19 15:44:15 +08:00
/// <summary>
2024-08-19 17:25:46 +08:00
/// 初始化选择信息
/// </summary>
2024-08-19 15:44:15 +08:00
private void InitUserBuildInfo()
{
try
{
2024-08-20 13:19:06 +08:00
if (!BuildInGame)
2024-08-19 17:25:46 +08:00
{
StorageManager.Instance.Init();
2024-08-19 15:44:15 +08:00
_userBuildInfo = StorageManager.Instance.GetStorage<UserBuildInfo>(UserBuildSaveKey);
2024-08-19 17:25:46 +08:00
}
2024-08-19 15:44:15 +08:00
if (_userBuildInfo == null)
{
2024-08-19 15:44:15 +08:00
var buildId = _curBuildData.buildID;
_userBuildInfo = new UserBuildInfo(buildId);
}
2024-08-19 15:44:15 +08:00
var curBuildID = _curBuildData.buildID;
//获取节点存盘信息
if (!_userBuildInfo.AllChooseNodeInfo.TryGetValue(curBuildID, out var buildInfo))
{
2024-08-19 15:44:15 +08:00
_userBuildInfo.AllChooseNodeInfo.TryAdd(curBuildID, new BuildInfo(curBuildID));
}
2024-08-19 17:25:46 +08:00
foreach (var nodeInfo in _nodeInfos)
{
2024-08-19 15:44:15 +08:00
if (_userBuildInfo.AllChooseNodeInfo[curBuildID].ChooseNodeInfo
.TryGetValue(nodeInfo.Key, out var option))
{
continue;
}
2024-08-19 15:44:15 +08:00
_userBuildInfo.AllChooseNodeInfo[curBuildID].ChooseNodeInfo.TryAdd(nodeInfo.Key, "");
DebugUtil.LogError("当前场景:{0},添加了信息节点:{1},选项:{2}", curBuildID, nodeInfo.Key, "");
}
2024-07-09 14:59:12 +08:00
2024-08-19 15:44:15 +08:00
_curBuildInfo = _userBuildInfo.AllChooseNodeInfo[curBuildID];
2024-07-29 16:01:29 +08:00
//DebugUserChooseNode();
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitUserBuildInfo 初始玩家信息错误 :{0}", e);
2024-07-16 18:53:34 +08:00
}
2024-07-18 20:01:43 +08:00
}
2024-07-16 15:58:21 +08:00
/// <summary>
/// 初始化条件
/// </summary>
private void InitCondition()
{
try
{
switch (_curBuildData.unlockType)
{
case UnlockType.ForGroup:
{
foreach (var unlockInfo in _curBuildData.unlockInfos)
{
var nodeName = string.Format(NodeName, unlockInfo.conditionGroup);
2024-08-19 17:25:46 +08:00
if (_nodeInfos.TryGetValue(nodeName, out var nodeInfo))
{
nodeInfo.Condition = unlockInfo.condition;
2024-07-29 16:01:29 +08:00
var nextNode = string.Format(NodeName, unlockInfo.preGroup);
nodeInfo.PreGroup = nextNode;
//DebugUtil.LogError("挂点解锁:节点{0}的解锁条件是: {1}, 前置解锁组是: {2}", nodeInfo.Name, unlockInfo.condition, nextNode);
}
}
break;
}
case UnlockType.ForThematic:
{
foreach (var unlockInfo in _curBuildData.unlockInfos)
{
var optionName = string.Format(OptionName, unlockInfo.conditionGroup);
2024-08-19 17:25:46 +08:00
foreach (var nodeInfo in _nodeInfos.Values)
{
if (nodeInfo.Options.TryGetValue(optionName, out var optionInfo))
{
optionInfo.Condition = unlockInfo.condition;
2024-07-29 16:01:29 +08:00
var nextOption = string.Format(OptionName, unlockInfo.preGroup);
optionInfo.PreThematic = nextOption;
DebugUtil.LogError("主题解锁:节点{0}的选项{1}的解锁条件是:{2}, 前置解锁主题是: {3}", nodeInfo.Name,
optionInfo.Name,
unlockInfo.condition, nextOption);
}
}
}
break;
}
}
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitCondition 初始化条件错误 :{0}", e);
}
}
/// <summary>
/// 异步加载该建造物品图标
/// </summary>
private async UniTask InitIcon()
{
try
{
2024-08-19 17:25:46 +08:00
foreach (var nodeInfo in _nodeInfos.Values)
{
foreach (var optionInfo in nodeInfo.Options.Values)
{
2024-07-19 17:18:17 +08:00
optionInfo.IconPath = await InitOptionIcon(nodeInfo.IconPath, optionInfo.Name);
}
}
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitIcon 初始化图标数据错误 :{0}", e);
}
2024-07-15 20:00:39 +08:00
}
2024-07-16 15:04:13 +08:00
/// <summary>
/// 初始化加载选项图标
2024-07-16 15:04:13 +08:00
/// </summary>
2024-07-19 17:18:17 +08:00
private async UniTask<string> InitOptionIcon(string iconsPath, string optionName)
2024-07-15 20:00:39 +08:00
{
try
2024-07-15 20:00:39 +08:00
{
2024-07-19 17:18:17 +08:00
var path = string.Format(iconsPath, optionName);
var assetPath = path.Replace(Application.dataPath, "").Replace('\\', '/');
var sprite = await AssetManager.Instance.LoadAssetAsync<Sprite>(assetPath);
if (_iconSprites.TryAdd(path, sprite))
return path;
return null;
2024-07-16 15:04:13 +08:00
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitOptionIcon 加载选项图标错误, 路径: {0}, Error: {1}", iconsPath, e);
return null;
}
}
2024-07-16 15:04:13 +08:00
/// <summary>
2024-08-19 17:25:46 +08:00
/// 加载当前场景蓝图和对应UI
/// </summary>
private async void InitBlueprint()
{
try
{
2024-08-19 17:25:46 +08:00
var bluePath = string.Format(Constants.PathConstants.BuildBlueprint, _curBuildData.buildID);
CurBlueprint = await AssetManager.Instance.LoadAssetAsync<Sprite>(bluePath);
var buildUIPath = string.Format(PathConstants.BuildTopUIPath, _curBuildData.buildID);
CurTopBuildUI = await AssetManager.Instance.LoadAssetAsync<Sprite>(buildUIPath);
_loadAssets.Add(bluePath);
_loadAssets.Add(buildUIPath);
}
catch (Exception e)
{
DebugUtil.LogError("BuildManager.InitBlueprint 加载蓝图错误 :{0}", e);
}
2024-07-16 15:04:13 +08:00
}
2024-07-16 15:58:21 +08:00
/// <summary>
/// 获得选项Icon图标
/// </summary>
2024-07-16 15:04:13 +08:00
public Sprite GetOptionIcon(string nodeName, string optionName)
{
2024-08-19 17:25:46 +08:00
if (_nodeInfos.TryGetValue(nodeName, out var nodeInfo))
2024-07-16 15:04:13 +08:00
{
if (nodeInfo.Options.TryGetValue(optionName, out var optionInfo))
{
2024-07-19 17:18:17 +08:00
if (_iconSprites.TryGetValue(optionInfo.IconPath, out var sprite))
2024-07-16 15:04:13 +08:00
return sprite;
2024-07-15 20:00:39 +08:00
}
}
2024-07-16 15:04:13 +08:00
return null;
}
2024-07-16 15:58:21 +08:00
/// <summary>
/// 根据节点获得解锁条件
/// </summary>
2024-07-15 20:00:39 +08:00
public int GetCondition(string nodeName)
{
var condition = 0;
2024-08-19 17:25:46 +08:00
if (_nodeInfos.TryGetValue(nodeName, out var nodeInfo))
2024-07-15 20:00:39 +08:00
{
condition = nodeInfo.Condition;
}
return condition;
}
2024-07-10 19:06:07 +08:00
2024-08-19 17:25:46 +08:00
/// <summary>
/// 保存节点
/// </summary>
public void SaveNodeInfo(string node, string option)
{
2024-08-19 15:44:15 +08:00
if (_curBuildInfo.ChooseNodeInfo.TryGetValue(node, out var oldOption))
{
2024-08-19 15:44:15 +08:00
_curBuildInfo.ChooseNodeInfo[node] = option;
2024-08-19 17:25:46 +08:00
DebugUtil.LogY($"场景{_userBuildInfo.CurBuildId}中的节点{node}保存了{option}选择");
}
else
{
DebugUtil.LogWarning("玩家Build存档信息没有 {0} 节点信息,请检查初始化", node);
2024-08-19 15:44:15 +08:00
_curBuildInfo.ChooseNodeInfo.Add(node, option);
}
StorageManager.Instance.SaveWithoutUpdate();
}
/// <summary>
/// 获取下一个解锁节点
/// </summary>
public string GetNextLockNode()
2024-07-17 18:56:07 +08:00
{
string nodeName = null;
switch (_curBuildData.unlockType)
{
case UnlockType.ForGroup:
{
2024-08-19 17:25:46 +08:00
foreach (var node in _nodeInfos)
2024-07-17 18:56:07 +08:00
{
2024-08-19 15:44:15 +08:00
if (_curBuildInfo.ChooseNodeInfo.TryGetValue(node.Key, out var curNode) &&
_curBuildInfo.ChooseNodeInfo.TryGetValue(node.Value.PreGroup, out var preNode))
2024-07-17 18:56:07 +08:00
{
2024-07-29 16:01:29 +08:00
//当前节点位选择且前置节点已选择
if (string.IsNullOrEmpty(curNode) && !string.IsNullOrEmpty(preNode))
{
//DebugUtil.LogError("下一个节点是{0}", node.Key);
return node.Key;
}
2024-07-17 18:56:07 +08:00
}
}
2024-08-19 17:25:46 +08:00
var firstNode = _nodeInfos
.OrderBy(kv => kv.Value.Condition)
2024-07-29 16:01:29 +08:00
.FirstOrDefault();
2024-07-29 16:01:29 +08:00
return firstNode.Key;
2024-07-17 18:56:07 +08:00
}
//TODO 按主题解锁
default:
break;
}
2024-07-25 15:29:18 +08:00
//DebugUtil.LogError("得到最小的节点是{0}", nodeName);
2024-07-17 18:56:07 +08:00
return nodeName;
}
/// <summary>
/// 更新已经达到的条件
/// </summary>
public void UpdateReachCondition(int condition)
{
2024-07-19 17:59:28 +08:00
//TODO 分场景 、解锁类型、解锁条件
2024-08-19 17:25:46 +08:00
ReachCondition = condition;
2024-07-17 18:56:07 +08:00
}
/// <summary>
/// 更新本地节点选择
/// </summary>
2024-07-25 15:29:18 +08:00
public void SetBuildUserInfo(int guideGroupID)
{
_userBuildInfo.GuideGroup = guideGroupID;
2024-07-25 15:29:18 +08:00
StorageManager.Instance.SyncForce = true;
}
/// <summary>
/// 播放音效
/// </summary>
public void PlaySound()
{
2024-08-20 13:19:06 +08:00
if (BuildInGame)
AudioManager.Instance.PlaySound(AudioType.SOUND, "S_Btn",
new UnityAudio(false));
}
2024-07-29 16:01:29 +08:00
/// <summary>
/// Debug 清楚玩家所有选择
/// </summary>
public void ClearOption()
{
2024-08-19 17:25:46 +08:00
foreach (var node in _nodeInfos)
2024-07-29 16:01:29 +08:00
{
2024-08-19 15:44:15 +08:00
if (_curBuildInfo.ChooseNodeInfo.TryGetValue(node.Key, out var option))
_curBuildInfo.ChooseNodeInfo[node.Key] = "";
2024-07-29 16:01:29 +08:00
}
DebugUserChooseNode();
StorageManager.Instance.SyncForce = true;
}
private void DebugUserChooseNode()
{
2024-08-19 15:44:15 +08:00
foreach (var infos in BuildInfo.ChooseNodeInfo)
2024-07-29 16:01:29 +08:00
{
DebugUtil.LogError("节点 {0} 选择的的是 {1}", infos.Key, infos.Value);
2024-07-29 16:01:29 +08:00
}
}
2024-07-09 14:59:12 +08:00
public void Release()
{
2024-08-19 17:25:46 +08:00
foreach (var icon in _iconSprites)
{
AssetManager.Instance.Unload(icon.Key);
}
foreach (var ass in _loadAssets)
{
AssetManager.Instance.Unload(ass);
}
2024-07-09 14:59:12 +08:00
}
}
}