NLDClient-yudde/ProjectNLD/Assets/Editor/Common/BuildLauncher.cs

571 lines
20 KiB
C#
Raw Normal View History

2025-06-11 16:12:05 +08:00
using AutoBuild.Helper;
using Cysharp.Threading.Tasks;
using HybridCLR.Editor;
2023-10-19 15:00:19 +08:00
using HybridCLR.Editor.Commands;
2025-06-11 16:12:05 +08:00
using HybridCLR.Editor.HotUpdate;
using PhxhSDK.AOT.VersionUpdate;
2025-06-11 16:12:05 +08:00
using System;
2024-05-15 17:15:25 +08:00
using System.Collections.Generic;
2024-08-21 17:45:27 +08:00
using System.IO;
2025-06-11 16:12:05 +08:00
using UnityEditor;
using UnityEngine;
using BuildTarget = UnityEditor.BuildTarget;
using Debug = UnityEngine.Debug;
2025-05-22 19:00:34 +08:00
using Directory = System.IO.Directory;
2025-06-23 13:44:52 +08:00
using YamlDotNet.Serialization;
2025-09-04 14:41:40 +08:00
using Obfuz4HybridCLR;
2023-10-19 15:00:19 +08:00
2024-05-15 17:15:25 +08:00
/// <summary>
/// Jenkins打包
/// </summary>
2023-10-19 15:00:19 +08:00
internal class BatchBuild
{
2024-04-12 18:06:06 +08:00
/// <summary>
/// 批处理更新设置
/// jenkins打包新包时使用该方法
/// </summary>
public static void ApplyPlayerSetting()
{
2024-05-15 17:15:25 +08:00
YooAssetBuildBase.Init();
2023-10-19 15:00:19 +08:00
}
2024-11-19 14:12:28 +08:00
public static string _oldDefines;
2024-05-15 17:15:25 +08:00
2024-09-26 16:35:41 +08:00
/// <summary>
2024-09-27 17:11:55 +08:00
/// 只在DEBUG下有
2024-09-26 16:35:41 +08:00
/// </summary>
2024-09-27 17:11:55 +08:00
private const string DEBUG = "DEBUG";
2025-05-13 15:27:38 +08:00
2025-06-12 14:17:31 +08:00
/// <summary>
/// dev上传地址
/// </summary>
private const string Dev_PostUrl = "http://dev_openapi.kedrgame.com/api/v1/";
2025-06-23 13:44:52 +08:00
2025-06-12 14:17:31 +08:00
/// <summary>
/// Pre上传地址
/// </summary>
private const string Pre_PostUrl = "http://pre_openapi.kedrgame.com/api/v1/";
2025-06-23 13:44:52 +08:00
2025-08-22 13:15:44 +08:00
/// <summary>
/// 海外Release上传地址
/// </summary>
private const string Oversea_Release_PostUrl = "http://intl_openapi.kedrgame.com/api/v1/";
2025-06-12 14:17:31 +08:00
/// <summary>
/// Release上传地址
/// </summary>
private const string Release_PostUrl = "http://openapi.kedrgame.com/api/v1/";
2025-06-24 13:11:59 +08:00
2024-11-19 14:12:28 +08:00
public static void _RevertScriptDefines()
{
var currentTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
PlayerSettings.SetScriptingDefineSymbolsForGroup(currentTargetGroup, _oldDefines);
}
/// <summary>
2024-05-15 17:15:25 +08:00
/// 批处理构建Resources和Player
/// jenkins打包新包时使用该方法
/// </summary>
public static void BuildContentAndPlayer()
2025-06-23 13:57:08 +08:00
{
try
{
InitChannelConfigFromYaml();
bool enableHotUpdate = YooAssetBuildBase.Init();
string bundleVersionCode = GetBundleVersionCode();
// 写入版本信息到Resources/LocalInfo.json
UpdateLocalInfo info = new()
{
workSpace = YooAssetBuildBase.workSpace,
accessKeyId = YooAssetBuildBase.appConfigAccessKeyId,
accessKeySecret = YooAssetBuildBase.appConfigAccessKeySecret,
bucketName = YooAssetBuildBase.appConfigBucketName,
configOriginalUrl = YooAssetBuildBase.appConfigURL,
//httpUrl = YooAssetBuildBase.httpURL,
httpAccessKey = YooAssetBuildBase.httpAccessKey,
2025-05-26 18:25:17 +08:00
httpKey = YooAssetBuildBase.httpKey,
2024-09-25 13:51:04 +08:00
version = YooAssetBuildBase.appVersion,
2025-01-07 19:44:51 +08:00
isDev = YooAssetBuildBase.isDev,
2025-08-22 13:15:44 +08:00
isOversea = YooAssetBuildBase.isOversea,
2025-05-13 15:27:38 +08:00
buildTicks = DateTime.Now.Ticks,
buildCode = bundleVersionCode,
2024-09-25 13:51:04 +08:00
};
2024-08-12 12:52:59 +08:00
PackDataInst.CreateResourceJson(info);
2025-05-13 15:27:38 +08:00
BuildTargetGroup currentTargetGroup =
BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(currentTargetGroup);
2024-08-12 12:52:59 +08:00
// 写入打包信息到Yoo_Build_Cache
#if DEVELOPMENT_BUILD || DEBUG
2025-06-23 13:44:52 +08:00
ExtBuildInfoHelper.CreateFirstExtBuildInfo(info.buildTicks, info.workSpace, info.version, info.buildCode,
true, defines);
#else
ExtBuildInfoHelper.CreateFirstExtBuildInfo(info.buildTicks, info.workSpace, info.version, false, defines);
#endif
2024-05-15 17:15:25 +08:00
2024-01-24 13:18:47 +08:00
BuildLauncher.SetEnableHotUpdate(enableHotUpdate);
if (enableHotUpdate)
BuildLauncher.BuildHotUpdateDlls();
2024-05-15 17:15:25 +08:00
YooAssetBuildBase.BuildContentAndPlayer();
2025-05-13 15:27:38 +08:00
2024-09-02 13:32:23 +08:00
// 写入打包信息到Yoo_Build_Cache
ExtBuildInfoHelper.CreateVertifyBuildInfo(info.workSpace, info.version);
2024-01-24 13:18:47 +08:00
// 还原宏
_RevertScriptDefines();
2024-08-21 17:45:27 +08:00
#region 打整包时缓存当前版本的AotDll
2025-05-13 15:27:38 +08:00
2024-12-30 13:27:51 +08:00
string aotDllStorePath = GetAotDllStorePath(info.workSpace, info.version);
2024-08-21 17:45:27 +08:00
string aotDllPath = SettingsUtil.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
DirectoryInfo dir = new(aotDllPath);
2024-08-22 14:10:32 +08:00
if (Directory.Exists(aotDllStorePath))
2024-08-22 14:36:11 +08:00
Directory.Delete(aotDllStorePath, true);
2024-08-22 14:10:32 +08:00
Directory.CreateDirectory(aotDllStorePath);
2024-08-21 17:45:27 +08:00
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
file.CopyTo(Path.Combine(aotDllStorePath, file.Name), false);
}
2025-05-13 15:27:38 +08:00
#endregion
2025-05-22 19:00:34 +08:00
2025-06-12 19:21:16 +08:00
PostLoginKey(info.workSpace, info.buildTicks, info.version); // 上传Aot loginKey
2023-11-23 14:41:25 +08:00
}
2024-01-24 13:18:47 +08:00
catch (Exception e)
{
2024-01-24 15:51:08 +08:00
//Build Exception用于在日志文件中定位此次打包的异常
2024-01-24 13:18:47 +08:00
Debug.LogError($"[Build Exception]:{e.Message} \r\n {e.StackTrace}");
throw;
}
2023-10-19 15:00:19 +08:00
}
/// <summary>
/// 批处理增量更新资源
/// jenkins打包增量更新时使用该方法
2023-10-19 15:00:19 +08:00
/// </summary>
public static void UpdatePreviousBuild()
{
2024-01-24 15:51:08 +08:00
try
{
2025-06-24 13:11:59 +08:00
InitChannelConfigFromYaml();
2025-05-13 15:27:38 +08:00
2024-05-15 17:15:25 +08:00
var enableHotUpdate = YooAssetBuildBase.Init();
2025-05-13 15:27:38 +08:00
2024-09-02 13:32:23 +08:00
ExtBuildInfoHelper.VertifyExitBuildInfo(YooAssetBuildBase.workSpace, YooAssetBuildBase.appVersion);
2025-05-13 15:27:38 +08:00
2024-01-24 15:51:08 +08:00
if (enableHotUpdate)
2024-08-21 19:06:36 +08:00
{
2024-08-27 19:32:49 +08:00
Debug.Log("开始检查AOT版本");
2024-12-30 13:27:51 +08:00
if (CheckAotOutDate(YooAssetBuildBase.workSpace, YooAssetBuildBase.appVersion))
2024-08-27 19:32:49 +08:00
{
Debug.Log("AOT版本检查通过");
2024-08-22 14:10:32 +08:00
BuildLauncher.BuildHotUpdateDlls();
2024-08-27 19:32:49 +08:00
}
else
{
Debug.LogError("AOT版本不匹配请重新打AOT包");
}
}
else
{
Debug.Log("未开启热更");
2024-08-21 19:06:36 +08:00
}
2025-05-13 15:27:38 +08:00
BuildTargetGroup currentTargetGroup =
BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(currentTargetGroup);
2024-08-13 15:28:35 +08:00
// 写入打包信息到Yoo_Build_Cache
#if DEVELOPMENT_BUILD || DEBUG
2025-05-13 15:27:38 +08:00
ExtBuildInfoHelper.UpdateExtBuildInfo(YooAssetBuildBase.workSpace, YooAssetBuildBase.appVersion, true,
defines);
#else
ExtBuildInfoHelper.UpdateExtBuildInfo(YooAssetBuildBase.workSpace, YooAssetBuildBase.appVersion, false, defines);
#endif
2024-05-15 17:15:25 +08:00
YooAssetBuildBase.BuildOnlyResUpdate();
2025-05-13 15:27:38 +08:00
2024-09-02 13:32:23 +08:00
ExtBuildInfoHelper.CreateVertifyBuildInfo(YooAssetBuildBase.workSpace, YooAssetBuildBase.appVersion);
2024-05-15 17:15:25 +08:00
_RevertScriptDefines();
2024-01-24 15:51:08 +08:00
}
catch (Exception e)
{
//Build Exception用于在日志文件中定位此次打包的异常
Debug.LogError($"[Build Exception]:{e.Message} \r\n {e.StackTrace}");
throw;
}
2023-10-19 15:00:19 +08:00
}
2024-08-21 17:45:27 +08:00
/// <summary>
/// jenkins检查AOT裁剪代码
/// </summary>
/// <param name="version"></param>
/// <returns></returns>
2024-12-30 13:27:51 +08:00
public static bool CheckAotOutDate(string workSpace, string version)
2024-08-21 17:45:27 +08:00
{
Debug.Log($"开始检查aot版本:{version}");
2024-12-30 13:27:51 +08:00
string stripedDllPath = GetAotDllStorePath(workSpace, version);
2024-08-21 17:45:27 +08:00
// 先编译dll
2025-09-04 14:41:40 +08:00
// CompileDllCommand.CompileDllActiveBuildTarget();
PrebuildCommandExt.CompileAndObfuscateDll(); // 编译混淆dll
2024-08-21 19:06:36 +08:00
// 再检查编译出来的dll是否满足以前的Aotdll
2025-07-11 15:45:30 +08:00
MissingMetadataChecker checker = new(stripedDllPath, SettingsUtil.HotUpdateAssemblyNamesIncludePreserved);
2024-08-21 17:45:27 +08:00
string hotUpdateDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(EditorUserBuildSettings.activeBuildTarget);
foreach (string dll in SettingsUtil.HotUpdateAssemblyFilesExcludePreserved)
{
string dllPath = $"{hotUpdateDir}/{dll}";
bool notAnyMissing = checker.Check(dllPath);
if (!notAnyMissing)
2025-07-11 13:11:38 +08:00
throw new Exception($"AOT缺失Dll({dllPath})请重新打AOT包");
2024-08-21 17:45:27 +08:00
}
return true;
}
/// <summary>
/// 获取缓存的AOT Dll路径
/// </summary>
/// <param name="version"></param>
2024-12-30 13:27:51 +08:00
public static string GetAotDllStorePath(string workSpace, string version)
2024-08-21 17:45:27 +08:00
{
2025-01-10 14:36:17 +08:00
return Application.dataPath + $"/../Yoo_Build_Cache/{workSpace}/{version}/CacheAOTDll";
2024-08-21 17:45:27 +08:00
}
2025-05-13 15:27:38 +08:00
public static string GetBundleVersionCode()
{
#if UNITY_IOS
return PlayerSettings.iOS.buildNumber;
#elif UNITY_ANDROID
return PlayerSettings.Android.bundleVersionCode.ToString();
#else
return "";
#endif
}
2025-05-22 15:49:25 +08:00
/// <summary>
/// 上传登录key
/// </summary>
2025-06-12 19:21:16 +08:00
public static void PostLoginKey(string workSpace, long buildTicks, string version)
2025-05-22 15:49:25 +08:00
{
2025-06-04 15:58:31 +08:00
string key = $"{workSpace}_{version}_{buildTicks}";
string loginKey = PhxhSDK.Utils.LoginKeyMD5(key);
string platform = string.Empty; // 0-安卓1-IOS
#if UNITY_ANDROID
platform = "0";
#elif UNITY_IOS
platform = "1";
#endif
2025-05-22 15:49:25 +08:00
Dictionary<string, string> headData = PostHeader.PostHeaderInfo(new Dictionary<string, string>
2025-05-22 19:00:34 +08:00
{
2025-06-04 15:58:31 +08:00
{ "plaintext", key },
2025-05-22 19:00:34 +08:00
{ "tag", loginKey },
{ "platform", platform },
2025-05-22 19:00:34 +08:00
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() }
});
LoginKeyData loginKeyData = new()
{
2025-06-04 15:58:31 +08:00
plaintext = key,
2025-05-22 19:00:34 +08:00
tag = loginKey,
platform = platform,
2025-05-22 19:00:34 +08:00
};
2025-05-22 15:49:25 +08:00
2025-05-22 17:00:48 +08:00
try
{
2025-06-11 16:12:05 +08:00
if (PackDataInst.Inst.localInfo.isDev)
2025-06-12 14:53:22 +08:00
{
2025-08-22 13:15:44 +08:00
if (PackDataInst.Inst.localInfo.isOversea)
HttpHelper.PostAsync($"{Oversea_Release_PostUrl}version", loginKeyData, headData).Forget(); // 暂时海外dev也使用Release
else
HttpHelper.PostAsync($"{Dev_PostUrl}version", loginKeyData, headData).Forget();
2025-06-12 14:53:22 +08:00
}
2025-06-11 16:12:05 +08:00
else
{
2025-08-22 13:15:44 +08:00
if (PackDataInst.Inst.localInfo.isOversea)
HttpHelper.PostAsync($"{Oversea_Release_PostUrl}version", loginKeyData, headData).Forget();
else
{
HttpHelper.PostAsync($"{Pre_PostUrl}version", loginKeyData, headData).Forget();
HttpHelper.PostAsync($"{Release_PostUrl}version", loginKeyData, headData).Forget();
}
2025-06-11 16:12:05 +08:00
}
}
catch (Exception e)
2025-05-22 17:00:48 +08:00
{
Debug.LogError($"[PostLoginKey Exception]:{e.Message} \r\n {e.StackTrace}");
throw;
}
2025-05-22 15:49:25 +08:00
}
2025-05-22 16:52:45 +08:00
2025-06-12 19:21:16 +08:00
/// <summary>
/// 自动发布debug loginKey
/// </summary>
public static void PostDebugSendKey()
{
string platform = string.Empty; // 0-安卓1-IOS
#if UNITY_ANDROID
platform = "0";
#elif UNITY_IOS
platform = "1";
#endif
2025-06-12 19:21:16 +08:00
Dictionary<string, string> dicHeadData = PostHeader.PostHeaderInfo(new Dictionary<string, string>
{
{ "platform", platform },
2025-06-12 19:21:16 +08:00
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() }
});
SendLoginKeyData sendLoginKeyData = new()
{
platform = platform,
};
HttpHelper.PostAsync($"{Dev_PostUrl}sendVersion", sendLoginKeyData, dicHeadData).Forget();
2025-06-12 19:21:16 +08:00
}
2025-05-22 19:00:34 +08:00
private class LoginKeyData
{
2025-06-04 15:58:31 +08:00
public string plaintext;
2025-05-22 19:00:34 +08:00
public string tag;
public string platform;
}
private class SendLoginKeyData
{
public string platform;
2025-05-22 19:00:34 +08:00
}
private class LoginKeyResData
{
public int code;
public string message;
public object data;
}
2025-06-23 13:44:52 +08:00
private class ChannelConfig
{
public List<string> Define_list { get; set; }
2025-08-22 13:15:44 +08:00
2025-06-23 13:44:52 +08:00
public string PackageName { get; set; }
2025-06-27 12:33:37 +08:00
2025-08-22 13:15:44 +08:00
public bool IsOversea { get; set; }
2025-06-27 12:33:37 +08:00
public int BuildCode { get; set; }
2025-06-23 14:23:38 +08:00
public string ConfigAccessKeyId { get; set; }
public string ConfigAccessKeySecret { get; set; }
public string ConfigBucketName { get; set; }
public string ConfigURL { get; set; }
2025-06-23 14:31:20 +08:00
public string AppAccessKey { get; set; }
public string AppKey { get; set; }
2025-06-23 14:23:38 +08:00
2025-06-23 13:44:52 +08:00
public CrashSightConfig CrashSight_Android { get; set; }
public CrashSightConfig CrashSight_iOS { get; set; }
}
private class CrashSightConfig
{
[YamlMember(Alias = "id")] public string Id { get; set; }
[YamlMember(Alias = "key")] public string Key { get; set; }
}
2025-06-23 14:23:38 +08:00
2025-06-23 13:44:52 +08:00
private const string ChannelSettingYaml = "/../ChannelSetting.yaml";
/// <summary>
/// 从YAML文件初始化渠道配置
/// </summary>
private static void InitChannelConfigFromYaml()
{
var hasYooAsset = false;
List<string> appendDefineList = new();
// 判断是否为开发模式
if (EditorUserBuildSettings.development)
{
appendDefineList.Add(DEBUG);
Debug.Log("追加宏定义:" + DEBUG);
}
string yamlPath = Application.dataPath + ChannelSettingYaml;
if (File.Exists(yamlPath))
{
try
{
// 忽略 YAML 中多余的字段
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
string yamlContent = File.ReadAllText(yamlPath);
var config = deserializer.Deserialize<ChannelConfig>(yamlContent);
// 处理宏定义
if (config.Define_list != null)
{
foreach (string define in config.Define_list)
{
string cleanDefine = define.Trim();
if (!string.IsNullOrEmpty(cleanDefine))
{
appendDefineList.Add(cleanDefine);
Debug.Log("追加宏定义:" + cleanDefine);
}
}
}
// 处理包名
if (!string.IsNullOrEmpty(config.PackageName))
{
Debug.Log("设置新包名:" + config.PackageName);
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
{
PlayerSettings.applicationIdentifier = config.PackageName;
}
}
2025-08-22 13:15:44 +08:00
YooAssetBuildBase.isOversea = config.IsOversea;
2025-06-27 12:33:37 +08:00
// 处理BuildCode
if (config.BuildCode != 0)
{
Debug.Log("设置BuildCode:" + config.BuildCode);
#if UNITY_IOS
PlayerSettings.iOS.buildNumber = config.BuildCode.ToString();
#elif UNITY_ANDROID
PlayerSettings.Android.bundleVersionCode = config.BuildCode;
#endif
}
2025-06-23 14:23:38 +08:00
// 处理AppConfig上传id
if (config.ConfigAccessKeyId != null)
{
YooAssetBuildBase.appConfigAccessKeyId = config.ConfigAccessKeyId;
Debug.Log($"设置AppConfig上传id:{YooAssetBuildBase.appConfigAccessKeyId}");
}
// 处理AppConfig上传key
if (config.ConfigAccessKeySecret != null)
{
YooAssetBuildBase.appConfigAccessKeySecret = config.ConfigAccessKeySecret;
Debug.Log($"设置AppConfig上传key:{YooAssetBuildBase.appConfigAccessKeySecret}");
}
// 处理AppConfig上传桶名
if (config.ConfigBucketName != null)
{
YooAssetBuildBase.appConfigBucketName = config.ConfigBucketName;
Debug.Log($"设置AppConfig上传桶名:{YooAssetBuildBase.appConfigBucketName}");
}
// 处理AppConfig上传url
if (config.ConfigURL != null)
{
YooAssetBuildBase.appConfigURL = config.ConfigURL;
Debug.Log($"设置AppConfig上传url:{YooAssetBuildBase.appConfigURL}");
}
2025-06-23 14:31:20 +08:00
// 处理http请求AccessKey
if (config.AppAccessKey != null)
{
YooAssetBuildBase.httpAccessKey = config.AppAccessKey;
Debug.Log($"设置http请求id:{YooAssetBuildBase.httpAccessKey}");
}
// 处理http请求AppKey
if (config.AppKey != null)
{
YooAssetBuildBase.httpKey = config.AppKey;
Debug.Log($"设置http请求id:{YooAssetBuildBase.httpKey}");
}
2025-06-23 13:44:52 +08:00
// 处理CrashSight_Android配置
if (config.CrashSight_Android != null &&
EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
{
var crashSightData = new PhxhSDK.Data.CrashSightData
{
id = config.CrashSight_Android.Id,
key = config.CrashSight_Android.Key
};
var crashSightPath = "Assets/Resources/CrashSightData.json";
var json = JsonUtility.ToJson(crashSightData);
File.WriteAllText(crashSightPath, json);
Debug.Log("CrashSight配置已保存:" + crashSightData.id + "|" + crashSightData.key);
}
// 处理CrashSight_Android配置
if (config.CrashSight_iOS != null &&
EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS)
{
var crashSightData = new PhxhSDK.Data.CrashSightData
{
id = config.CrashSight_iOS.Id,
key = config.CrashSight_iOS.Key
};
var crashSightPath = "Assets/Resources/CrashSightData.json";
var json = JsonUtility.ToJson(crashSightData);
File.WriteAllText(crashSightPath, json);
Debug.Log("CrashSight配置已保存:" + crashSightData.id + "|" + crashSightData.key);
}
// 更新宏定义
var currentTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
var oldDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(currentTargetGroup);
_oldDefines = oldDefines;
if (appendDefineList.Count > 0)
{
var newDefineList = new List<string>();
newDefineList.AddRange(oldDefines.Split(';'));
for (var i = 0; i < newDefineList.Count; i++)
{
var define = newDefineList[i];
if (string.IsNullOrEmpty(define))
continue;
if (define == "USE_YOO")
{
hasYooAsset = true;
}
if (appendDefineList.Contains(define))
appendDefineList.Remove(define);
}
newDefineList.AddRange(appendDefineList);
PlayerSettings.SetScriptingDefineSymbolsForGroup(currentTargetGroup,
string.Join(";", newDefineList));
}
// 关闭Unity闪屏
PlayerSettings.SplashScreen.showUnityLogo = false;
}
catch (Exception e)
{
Debug.LogError($"解析YAML配置文件失败: {e.Message}\n{e.StackTrace}");
throw;
}
}
else
{
Debug.LogError($"找不到渠道配置文件: {yamlPath}");
}
}
2024-11-21 18:56:50 +08:00
}