Files
2026-07-10 10:24:29 +08:00

1428 lines
53 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
using UnityEditor.Build.Player;
//using Newtonsoft.Json;
using System.Runtime.CompilerServices;
using System.Linq;
using UnityEditor.Build.Content;
//using Base;
#if UNITY_EDITOR
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
#endif
public class HybridCLRBuild
{
static string sPlatform = "pc";
static Dictionary<string, string> m_mapResNameDic = new Dictionary<string, string>();
static List<AssetBundleBuild> maps = new List<AssetBundleBuild>();
static List<string> paths = new List<string>();
static List<string> files = new List<string>();
static Dictionary<string, string> m_dicFilelist = new Dictionary<string, string>();
static string m_strListFileMD5;
static string m_strDllFileMD5;
static string m_strResFileMD5;
static string m_strOldVersion = "";
static string m_aesPassword = "#@pOrhgf2026091211325890vciu%c-X";
#if ENV_INTRANET
public static string UpdatePath = $"\\\\{GetLocal()}\\g\\App\\CDNRes\\public\\{BuildSettings.GameResVersion}\\";
#elif ENV_EXTRANET || ENV_PRODUCTION
public static string UpdatePath = Application.dataPath + "/../../../CDNRes/public/"+ BuildSettings.GameResVersion + "/";
#else
public static string UpdatePath = $"../XDevNode/Data/";//XWorld/
#endif
public static string GetUpdatePath()
{
return UpdatePath;
}
static string AppDataPath
{
get { return Application.dataPath.ToLower(); }//Assets目录
}
public static byte[] ChangeBytes(byte[] src, byte xor = 0xb4)
{
for (int i = 0; i < src.Length; i++)
{
src[i] = (byte)(src[i] ^ xor);
}
return src;
}
private static bool IsUnityAssetBundleData(byte[] data)
{
if (data == null || data.Length < 5)
return false;
return data[0] == (byte)'U'
&& data[1] == (byte)'n'
&& data[2] == (byte)'i'
&& data[3] == (byte)'t'
&& data[4] == (byte)'y';
}
private static string GetDataHead(byte[] data)
{
if (data == null || data.Length == 0)
return "<empty>";
int len = Math.Min(16, data.Length);
StringBuilder sb = new StringBuilder(len * 3);
for (int i = 0; i < len; i++)
{
byte b = data[i];
sb.Append(b >= 32 && b <= 126 ? (char)b : '.');
}
sb.Append(" / ");
for (int i = 0; i < len; i++)
{
if (i > 0)
sb.Append(' ');
sb.Append(data[i].ToString("X2"));
}
return sb.ToString();
}
public static void ClearDir(string path, bool bDeleteSelf = false)
{
if (!Directory.Exists(path))
return;
if (!bDeleteSelf)
{
System.IO.DirectoryInfo di = new DirectoryInfo(path);
foreach (FileInfo file in di.EnumerateFiles())
{
file.Delete();
}
foreach (DirectoryInfo dir in di.EnumerateDirectories())
{
dir.Delete(true);
}
}
else
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
path = path + ".meta";
File.Delete(path);
}
}
}
static string packageVer = "G7";
//static URLConfig uconfig;
public static string GetLocal()
{
//if (uconfig == null)
//{
// string configPath = Application.streamingAssetsPath + "/config.json";
// string configJson = File.ReadAllText(configPath);
// uconfig = JsonConvert.DeserializeObject<URLConfig>(configJson);
//}
//if (uconfig != null)
//{
// return uconfig?.local;
//}
return "127.0.0.1";
}
[MenuItem("HybridCLR/测试IP", false, 2001)]
public static void TestDebug()
{
Debug.Log($"更新地址:{GetUpdatePath()}");
}
//需要导出的aot文件,运行的时候会全部更新
public static List<string> AOTMetaAssemblyNames { get; } = new List<string>()
{
"mscorlib.dll",
"System.dll",
"System.Core.dll",
"ZString.dll",//解决泛型问题
"System.Runtime.CompilerServices.Unsafe.dll",
"UnityEngine.AndroidJNIModule.dll"
//"UnityEngine.UnityWebRequestModule.dll",
};
[MenuItem("HybridCLR/GenAll(+xlua)", false, 2000)]
public static void GenLuaAndHybridCLRAll()
{
//xlua wrap文件
CSObjectWrapEditor.Generator.GenAll();
//hybricCLR 生成
PrebuildCommand.GenerateAll();
//CSObjectWrapEditor.Generator.ClearAll();
}
[MenuItem("HybridCLR/更新dllPC", false, 2001)]
public static void BuildDllUpdatePC()
{
BuildHotFix(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64, "Client/Client.exe");
}
[MenuItem("HybridCLR/更新dllAndroid", false, 2002)]
public static void BuildDllUpdateAndroid()
{
BuildHotFix(BuildTargetGroup.Android, BuildTarget.Android);
}
//[MenuItem("HybridCLR/更新dlliOS", false, 2003)]
//public static void BuildDllUpdateiOS()
//{
// BuildHotFix(BuildTargetGroup.iOS, BuildTarget.iOS);
//}
[MenuItem("HybridCLR/更新dlliOS", false, 2003)]
public static void BuildDllUpdateiOS()
{
BuildHotFix(BuildTargetGroup.iOS, BuildTarget.iOS);
}
[MenuItem("HybridCLR/更新dllWebGL", false, 2004)]
public static void BuildDllUpdateWebGL()
{
BuildHotFix(BuildTargetGroup.WebGL, BuildTarget.WebGL);
}
[MenuItem("打包/生成底包/微信小游戏底包", false, 1030)]
public static void BuildWX()
{//彻底更新
//xlua wrap文件
//CSObjectWrapEditor.Generator.GenAll();
//hybricCLR 生成
//PrebuildCommand.GenerateAll();
//微信小游戏
Debug.Log($"BuildTarget : {EditorUserBuildSettings.activeBuildTarget}");
WeChatWASM.WXProjectConf config = WeChatWASM.WXConvertCore.config.ProjectConf;
Debug.Log($"WXPConfig projectName : {config.projectName}");
Debug.Log($"WXPConfig Appid : {config.Appid}");
Debug.Log($"WXPConfig CDN : {config.CDN}");
Debug.Log($"WXPConfig assetLoadType : {config.assetLoadType}");
Debug.Log($"WXPConfig compressDataPackage : {config.compressDataPackage}");
Debug.Log($"WXPConfig VideoUrl : {config.VideoUrl}");
Debug.Log($"WXPConfig Orientation : {config.Orientation}");
WeChatWASM.CompileOptions options = WeChatWASM.WXConvertCore.config.CompileOptions;
Debug.Log($"WXOp DevelopBuild : {options.DevelopBuild}");
Debug.Log($"WXOp AutoProfile : {options.AutoProfile}");
Debug.Log($"WXOp ScriptOnly : {options.ScriptOnly}");
Debug.Log($"WXOp Il2CppOptimizeSize : {options.Il2CppOptimizeSize}");
Debug.Log($"WXOp profilingFuncs : {options.profilingFuncs}");
Debug.Log($"WXOp ProfilingMemory : {options.ProfilingMemory}");
Debug.Log($"WXOp DeleteStreamingAssets : {options.DeleteStreamingAssets}");
Debug.Log($"WXOp CleanBuild : {options.CleanBuild}");
Debug.Log($"WXOp CustomNodePath : {options.CustomNodePath}");
Debug.Log($"WXOp Webgl2 : {options.Webgl2}");
Debug.Log($"WXOp fbslim : {options.fbslim}");
WeChatWASM.WXConvertCore.DoExport();
//生成底包版本号base.version
string basever = md5file($"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}/webgl/Build/webgl.data");
basever = $"var ver=\"{basever}\"";
File.WriteAllText($"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}/webgl/ver.js", basever);
//复制底包内容到服务器上
string dst = GetUpdatePath();// "\\\\192.168.9.231\\g\\App\\G7\\";
dst = dst.Substring(0, dst.Length - 16);
string[] args = Environment.GetCommandLineArgs();
string dstpath = GetCommandLineValue(args, "-dstpath");
if (dstpath != null && dstpath != "")
{
dst = dstpath;
}
bool bCopyHtml = false;
if (!File.Exists($"{dst}index.html"))
{
bCopyHtml = true;
}
string src = $"{WeChatWASM.WXConvertCore.config.ProjectConf.DST}\\webgl\\";
src = src.Replace("\\", "/");
//删除原文件
string[] names = Directory.GetFiles(dst);
foreach (string file in names)
{
if (file.Contains("index.html"))
continue;
File.Delete(file);
}
//复制
names = Directory.GetFiles(src, "*.*", SearchOption.AllDirectories);
foreach (string file in names)
{
if (file.Contains("StreamingAssets.unity3d") || (!bCopyHtml && file.Contains("index.html")))
continue;
string dstfile = file.Replace(src, "");
dstfile = $"{dst}{dstfile}";
if (!Directory.Exists(Path.GetDirectoryName(dstfile)))
Directory.CreateDirectory(Path.GetDirectoryName(dstfile));
if (File.Exists(dstfile))
File.Delete(dstfile);
File.Copy(file, dstfile, true);
}
}
public static void BuildHotFix(BuildTargetGroup targetGroup, BuildTarget target, string targetName = "", bool bUpdateDll = true)
{
//bool bLuaPackage = false;
string[] EncryptFile = { "a.unity3d", "h.unity3d","xhotfix/base.unity3d", "StreamingAssets.unity3d", "bs.unity3d" };//
if (EditorUserBuildSettings.activeBuildTarget != target)
{
EditorUserBuildSettings.SwitchActiveBuildTarget(targetGroup, target);
}
if (target == BuildTarget.Android)
{
sPlatform = "android";
}
else if (target == BuildTarget.iOS)
{
sPlatform = "ios";
}
else if (target == BuildTarget.WebGL)
{
sPlatform = "webgl";
//bLuaPackage = true;
}
else
{
sPlatform = "pc";
}
maps.Clear();
//基础更新(大厅/基础资源)产物输出到仓库 CDN/XWorld/<platform>/
//由 Server/Cdn.Runner 以 --root <仓库>/CDN 提供,客户端经 DevDiscovery 拿 CDN 根后从 <根>/XWorld/<platform>/ 增量下载。
//CWD = Unity 工程根 Client/,故 ../CDN = 仓库根 CDN)。产物已是 md5 命名,匹配 XResLoader.LoadManifest("XWorld")。
UpdatePath = $"../CDN/XWorld/{sPlatform}/";
//if (bUpdateDll)
//{//EncryptFile剔除其中的aot.unity,现在一般不更新aot.all,只打到底包
// EncryptFile = EncryptFile.Where(x => !x.Equals("aot.unity3d")).ToArray();
//}
string outPath = $"{AppDataPath}/StreamingAssets/";//这个不要直接清 还有其他文件
string hotfixDir = $"{outPath}xhotfix";
string aotDir = $"{outPath}xaot";
ClearDir(hotfixDir);
ClearDir(aotDir);
if (!Directory.Exists(outPath))
Directory.CreateDirectory(outPath);
if (!Directory.Exists(hotfixDir))
Directory.CreateDirectory(hotfixDir);
if (!Directory.Exists(aotDir))
Directory.CreateDirectory(aotDir);
foreach (var f in EncryptFile)
{//unity需要删除旧文件,不然这个会不更新,还是用老的加密ab文件,后面就会重复加密。
string file = $"{AppDataPath}/StreamingAssets/{f}";
if (File.Exists(file))
{
File.Delete(file);
}
file = $"{AppDataPath}/StreamingAssets/{f}.manifest";
if (File.Exists(file))
{
File.Delete(file);
}
}
//生成一次临时包,以便正确生成dll
//生成xlua的wrap代码
CSObjectWrapEditor.Generator.GenAll();
//runtime 编译
ScriptCompilationSettings scriptCompilationSettings = default;
scriptCompilationSettings.group = targetGroup;
scriptCompilationSettings.target = target;
PlayerSettings.stripEngineCode = true;
PlayerSettings.allowUnsafeCode = true;
string BasePath = System.Environment.CurrentDirectory;
PlayerSettings.SetScriptingBackend(targetGroup, ScriptingImplementation.IL2CPP);
ScriptCompilationResult scriptCompilationResult = PlayerBuildInterface.CompilePlayerScripts(scriptCompilationSettings, $"{BasePath}/client/");
//string[] SCENES = FindHotfixScenes();
//BuildPipeline.BuildPlayer(SCENES, targetName, target, BuildOptions.None);
//打入更新的资源,dll
string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target);
if (!File.Exists($"{aotAssembliesSrcDir}/mscorlib.dll"))
{//aot dll 检查
StripAOTDllCommand.GenerateStripedAOTDlls();
AssetDatabase.Refresh();
}
//HybridCLR dll update
BuildAssetsCommand.BuildAndCopyABAOTHotUpdateDlls();
AssetDatabase.Refresh();
//lua 打包
BuildLuaAB("bs.unity3d");
PackDllFile("h.unity3d", $"{AppDataPath}/StreamingAssets/xhotfix/");
//if (!bUpdateDll)
//PackDllFile("a.unity3d", $"{AppDataPath}/StreamingAssets/xaot/");
//打包基础代码入口资源,这个为挂载GameLauncher脚本的预设
AddBuildMap($"xhotfix/base.unity3d", "*.*", $"Assets/base/Prefab");
AssetDatabase.Refresh();
//渲染shader ab
//shader也跟下面的资源一起打吧
//PackAllPathFile("game_shader.unity3d", $"Assets/Game/Shader", "Shadervariants");
AddBuildMap("game_shader.unity3d", "*.*", "Assets/Game/Shader");
//资源 ab
string[] HotfixRes = { "Game/Art" };//热更资源目录, 按目录打资源
string namePre = $"Game_Art";
foreach (string respath in HotfixRes)
{
string tempPath = $"{AppDataPath}/{respath}/";
string buildPath = $"Assets/{respath}/";//路径必须以Assets开始
string[] dirs = Directory.GetDirectories(tempPath, "*", SearchOption.AllDirectories);
for (int i = 0; i < dirs.Length; i++)
{
string path = dirs[i];
if (path.Contains(".svn"))
continue;
string pathname = dirs[i].Replace(tempPath, string.Empty);
string name = pathname.Replace('\\', '_').Replace('/', '_');//Lua ab文件使用.替换目录分隔符
name = name.ToLower() + ".unity3d";
AddBuildMap($"{namePre}_{name}", "*.*", buildPath + pathname);
//AssetDatabase.Refresh();
}
AddBuildMap($"{namePre}_{respath}.unity3d", "*.*", $"Assets/{respath}/");
//AssetDatabase.Refresh();
}
//打ab包
BuildPipeline.BuildAssetBundles(outPath, maps.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression/*| BuildAssetBundleOptions.DisableWriteTypeTree*/, target);
//AssetDatabase.Refresh();
//改名StreamingAssets,加个后缀
if (File.Exists($"{outPath}StreamingAssets"))
{
File.Move($"{outPath}StreamingAssets", $"{outPath}StreamingAssets.unity3d");
}
//生成更新列表
//BuildFileIndex(sPlatform);//StreamingAssets 里的除了\\xaot\\ 和 \\xhotfix (dll目录)外所有文件
//添加dll更新列表
m_dicFilelist.Clear();
m_strDllFileMD5 = "";
m_strResFileMD5 = "";
//资源列表
BuildFileIndex("");//暂时不分平台
//dll列表
BuildDllsFileIndex("", bUpdateDll);
SaveUpdateVer("");
string update_xaot_dir = $"{GetUpdatePath()}xaot";
string update_xhotfix_dir = $"{GetUpdatePath()}xhotfix";
string update_dllfiles = $"{GetUpdatePath()}dllfiles.txt";
string update_files = $"{GetUpdatePath()}files.txt";
string update_ver = $"{GetUpdatePath()}updatever.txt";
//复制到目标地址
//先清理之前的
if (Directory.Exists(GetUpdatePath()))
{
ClearDir(update_xaot_dir);
ClearDir(update_xhotfix_dir);
//string [] files = Directory.GetFiles(GetUpdatePath(), "*.*");
//for (int i = 0; i < files.Length; i++)
//{
// if (files[i].Contains("basepack.bytes") || files[i].Contains("config.json"))
// continue;
// File.Delete(files[i]);
//}
}
string OldVerString = "";
if (File.Exists(update_ver))
{
OldVerString = File.ReadAllText(update_ver);
}
//CopyPath(outPath, GetUpdatePath());
//CopyPath($"{outPath}xaot", update_xaot_dir);
CopyPath($"{outPath}xhotfix", update_xhotfix_dir);//这里面有个base.unity3d
File.Copy($"{outPath}dllfiles.txt", update_dllfiles, true);
File.Copy($"{outPath}files.txt", $"{GetUpdatePath()}files.txt", true);
File.Copy($"{outPath}updatever.txt", update_ver, true);
int copyCount = CopyUpdateFilesFromIndex(outPath, GetUpdatePath());
Debug.Log($"Copy update resource files: {copyCount}");
//复制资源
//加密
byte[] w = System.Text.Encoding.UTF8.GetBytes($"{m_aesPassword}");
AESManager.Set(ChangeBytes(w));
foreach (string f in EncryptFile)
{
if (!File.Exists($"{AppDataPath}/StreamingAssets/{f}"))
continue;
//if (!f.Contains("bs.unity3d"))
//if (target != BuildTarget.WebGL)
{
byte[] srcdata = File.ReadAllBytes($"{AppDataPath}/StreamingAssets/{f}");
byte[] encryptdata = AESManager.Encrypt(srcdata);
//byte[] retdata = AESManager.Decrypt(encryptdata);
File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}", encryptdata);
//File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}_Encrypt", encryptdata);
}
string file = $"{GetUpdatePath()}{f}";
if (File.Exists(file))
{
File.Delete(file);
}
//File.Move($"{AppDataPath}/StreamingAssets/{f}_Encrypt", file);
File.Copy($"{AppDataPath}/StreamingAssets/{f}", file, true);
}
//xlua处理
//if (target == BuildTarget.WebGL && bLuaPackage)
//{
// File.Copy($"{outPath}bs.unity3d", $"{GetUpdatePath()}bs.unity3d", true);
//}
//以后修复
//GYPackager.BuildServerListMd5(target);
Debug.Log($"Dest Path : {GetUpdatePath()}");
//if (target == BuildTarget.WebGL)
{//给文件加上md5后缀,以便方便webgl小程序使用UnityWebRequest自动缓存
//先删除旧的
string finally_update_dllfiles;
string finally_update_files;
if (OldVerString != "")
{
string[] vers = OldVerString.Split(',');
vers[0] = GetFileMd5Name($"{GetUpdatePath()}dllfiles.txt",vers[0]);
vers[1] = GetFileMd5Name($"{GetUpdatePath()}files.txt",vers[1]);
//foreach (var ver in vers)
{
//删除旧的更新文件
if (File.Exists(vers[0]))
{
string dllString = File.ReadAllText(vers[0]);
foreach (string f in EncryptFile)
{
int pos = dllString.IndexOf(f);
if (pos != -1)
{
string md5 = dllString.Substring(pos + f.Length + 1, 8);
if (md5 != null && md5.Length == 8)
{
string ab = GetFileMd5Name($"{GetUpdatePath()}{f}", GetFixedMd5(md5));
if (File.Exists(ab))
File.Delete(ab);
}
}
}
//删除旧的dllfiles.txt文件
File.Delete(vers[0]);
}
if (File.Exists(vers[1]))
{
IEnumerable<string> lines = File.ReadLines(vers[1]);
foreach (var line in lines)
{
string[] items = line.Split(",");
if (items.Length > 2)
{
string path = items[0].Substring(1);//路径去掉左边的‘{’符号
if (items[1] != null && items[1].Length == 8)//第二个是md5码
{
string ab = GetFileMd5Name($"{GetUpdatePath()}{path}",GetFixedMd5(items[1]));
if (File.Exists(ab))
File.Delete(ab);
}
}
}
//删除旧的dllfiles.txt文件
File.Delete(vers[1]);
}
}
}
foreach (var file in m_dicFilelist)
{
string destFile = GetFileMd5Name($"{GetUpdatePath()}{file.Key}", GetFixedMd5(file.Value));
string srcFile = $"{GetUpdatePath()}{file.Key}";
if (!File.Exists(srcFile))
continue;
if (!File.Exists(destFile))
File.Move(srcFile, destFile);
}
//新的文件MD5 m_strDllFileMD5
finally_update_dllfiles = GetFileMd5Name($"{GetUpdatePath()}dllfiles.txt", GetFixedMd5(m_strDllFileMD5));
if(!File.Exists(finally_update_dllfiles))
File.Move(update_dllfiles, finally_update_dllfiles);
finally_update_files = GetFileMd5Name($"{GetUpdatePath()}files.txt", GetFixedMd5(m_strResFileMD5));
if (!File.Exists(finally_update_files))
File.Move(update_files, finally_update_files);
}
//可靠产出最终 updatever.txt(明文):它是 XResLoader.LoadManifest("XWorld") 的入口文件,须与
//files_<resmd5>.txt / dllfiles_<dllmd5>.txt 配套。中途的 File.Copy 在重定向到 CDN 后偶发不落盘,
//故在所有改名之后用已算出的 md5 显式重写一遍,保证存在且写在最后不被后续步骤清掉。
if (!string.IsNullOrEmpty(m_strDllFileMD5) && !string.IsNullOrEmpty(m_strResFileMD5))
{
Directory.CreateDirectory(GetUpdatePath());
File.WriteAllText($"{GetUpdatePath()}updatever.txt", $"{m_strDllFileMD5},{m_strResFileMD5}");
Debug.Log($"updatever.txt => {GetUpdatePath()}updatever.txt ({m_strDllFileMD5},{m_strResFileMD5})");
}
else
{
Debug.LogError("updatever.txt 未生成:dll/res 清单 md5 为空(SaveUpdateVer 可能提前返回)");
}
//清理outPath
//。。。
Debug.Log($"构建完成{DateTime.Now.ToString()}");
//清理中间打包目录
string temppath = $"{AppDataPath}/a";
ClearDir(temppath, true);
temppath = $"{AppDataPath}/h";
ClearDir(temppath, true);
//Lua
string luapath = $"{AppDataPath}/l";
ClearDir(luapath, true);
//xlua相关
//CSObjectWrapEditor.Generator.ClearAll();
}
static int CopyUpdateFilesFromIndex(string srcRoot, string dstRoot)
{
if (string.IsNullOrEmpty(srcRoot) || string.IsNullOrEmpty(dstRoot))
return 0;
if (!srcRoot.EndsWith("/") && !srcRoot.EndsWith("\\"))
srcRoot += "/";
if (!dstRoot.EndsWith("/") && !dstRoot.EndsWith("\\"))
dstRoot += "/";
int count = 0;
foreach (var item in m_dicFilelist)
{
string relativePath = item.Key;
if (string.IsNullOrEmpty(relativePath))
continue;
relativePath = relativePath.Replace('\\', '/');
if (relativePath.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)
|| relativePath.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase)
|| relativePath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string srcFile = Path.Combine(srcRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(srcFile))
{
Debug.LogWarning($"Update file listed but missing: {srcFile}");
continue;
}
string dstFile = Path.Combine(dstRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
string dstDir = Path.GetDirectoryName(dstFile);
if (!Directory.Exists(dstDir))
Directory.CreateDirectory(dstDir);
File.Copy(srcFile, dstFile, true);
count++;
}
return count;
}
static void CopyPath(string srcPath, string outPath)
{
if (!Directory.Exists(outPath))
{
Directory.CreateDirectory(outPath);
}
string[] luaPaths = { srcPath };//AppDataPath + "/../../Scripts_c/" };
for (int i = 0; i < luaPaths.Length; i++)
{
paths.Clear(); files.Clear();
string luaDataPath = luaPaths[i].ToLower();
Recursive(luaDataPath);
int n = 0;
foreach (string f in files)
{
if (f.EndsWith(".meta") || f.EndsWith(".manifest")) continue;
string newfile = f.Replace(luaDataPath, "");
string newpath = outPath + newfile;
string path = Path.GetDirectoryName(newpath);
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
if (File.Exists(newpath))
{
File.Delete(newpath);
}
File.Copy(f, newpath, true);
UpdateProgress(n++, files.Count, newpath);
}
}
EditorUtility.ClearProgressBar();
}
/// <summary>
/// 遍历目录及其子目录
/// </summary>
static void Recursive(string path)
{
if (path.Contains("\\xaot\\") || path.Contains("\\xhotfix\\")
|| path.Contains("/xaot/") || path.Contains("/xhotfix/"))
{//忽略
return;
}
string[] names = Directory.GetFiles(path);
string[] dirs = Directory.GetDirectories(path);
foreach (string filename in names)
{
string ext = Path.GetExtension(filename);
if (ext.Equals(".meta")) continue;
files.Add(filename.Replace('\\', '/'));
}
foreach (string dir in dirs)
{
if (dir.IndexOf(".svn") >= 0)
{
continue;
}
paths.Add(dir.Replace('\\', '/'));
Recursive(dir);
}
}
private static string[] FindHotfixScenes()
{//只导出基于热更的基础场景
List<string> LauncherSceneNames = new List<string>();
LauncherSceneNames.Insert(0, "Assets/Launcher/Launcher_Hotfix.unity");
return LauncherSceneNames.ToArray();
}
static void AddBuildMap(string bundleName, string pattern, string path, bool bDirPack = true)
{
var files = Array.FindAll(Directory.GetFiles(path, pattern), x => !x.EndsWith(".meta"));
if (files.Length == 0) return;
for (int i = 0; i < files.Length; i++)
{
files[i] = files[i].Replace('\\', '/');
}
if (bDirPack)
{
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = bundleName;
build.assetNames = files;
maps.Add(build);
Debug.LogWarning($"AddBuildMap(dir) : {build.assetBundleName}({files.Length})");
}
else
{
string[] singlefile;
string abname = bundleName.Substring(0, bundleName.IndexOf(".unity3d"));
abname += "_";
for (int i = 0; i < files.Length; i++)
{
AssetBundleBuild build = new AssetBundleBuild();
singlefile = new string[] { files[i] };
build.assetBundleName = abname + Path.GetFileNameWithoutExtension(files[i]).ToLower() + ".unity3d";// bundleName;
build.assetNames = singlefile;
maps.Add(build);
Debug.LogWarning($"AddBuildMap : {build.assetBundleName}({files.Length})");
}
}
}
public static bool PackAotFile(BuildTargetGroup targetGroup, BuildTarget target, string bundleName, string dllpath)
{
maps.Clear();
paths.Clear();
files.Clear();
if (target == BuildTarget.Android)
{
sPlatform = "android";
}
else if (target == BuildTarget.iOS)
{
sPlatform = "ios";
}
else if (target == BuildTarget.WebGL)
{
sPlatform = "webgl";
}
else
{
sPlatform = "pc";
}
UpdatePath = $"../CDN/XWorld/{sPlatform}/";
ScriptCompilationSettings scriptCompilationSettings = default;
scriptCompilationSettings.group = targetGroup;
scriptCompilationSettings.target = target;
PlayerSettings.stripEngineCode = true;
PlayerSettings.allowUnsafeCode = true;
string BasePath = System.Environment.CurrentDirectory;
PlayerSettings.SetScriptingBackend(targetGroup, ScriptingImplementation.IL2CPP);
var inputPath = $"{BasePath}/client/";
var outputPath = $"{BasePath}/clientObfuz/";
//打aot不需要这个
//ScriptCompilationResult scriptCompilationResult = PlayerBuildInterface.CompilePlayerScripts(scriptCompilationSettings, inputPath);
//AssetDatabase.Refresh();
string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target);
if (!File.Exists($"{aotAssembliesSrcDir}/mscorlib.dll"))
{//aot dll 检查
StripAOTDllCommand.GenerateStripedAOTDlls();
AssetDatabase.Refresh();
}
HybridCLR.Editor.BuildAssetsCommand.CopyAOTAssembliesToStreamingAssets();
AssetDatabase.Refresh();
PackDllFile(bundleName, dllpath);
string outPath = $"{AppDataPath}/StreamingAssets/";
string bundlePath = $"{outPath}{bundleName}";
if (File.Exists(bundlePath))
File.Delete(bundlePath);
if (File.Exists($"{bundlePath}.manifest"))
File.Delete($"{bundlePath}.manifest");
var manifest = BuildPipeline.BuildAssetBundles(outPath, maps.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression/*| BuildAssetBundleOptions.DisableWriteTypeTree*/, target);
if (manifest == null)
{
Debug.LogError("BuildAssetBundles 返回 null!原因通常是:1.资源重复打包 2.资源路径不存在 3.有残留的.meta错误。请查看控制台前一条报错。");
return false;
}
AssetDatabase.Refresh();
//string outBundleName = $"{outPath}/{bundlePath}";
if (!File.Exists(bundlePath))
{
Debug.LogError($"BuildAssetBundles 失败!{bundlePath} 不存在。请查看控制台前一条报错。");
return false;
}
Debug.Log($"BuildAssetBundles {bundlePath}成功");
//encrypt
string f = "a.unity3d";
byte[] w = System.Text.Encoding.UTF8.GetBytes($"{m_aesPassword}");
AESManager.Set(ChangeBytes(w));
byte[] srcdata = File.ReadAllBytes($"{AppDataPath}/StreamingAssets/{f}");
if (!IsUnityAssetBundleData(srcdata))
{
Debug.LogError($"{f} before encrypt is not a Unity AssetBundle. head:{GetDataHead(srcdata)}");
return false;
}
byte[] encryptdata = AESManager.Encrypt(srcdata);
byte[] retdata = AESManager.Decrypt(encryptdata);
if (!IsUnityAssetBundleData(retdata))
{
Debug.LogError($"{f} encrypt/decrypt check failed. head:{GetDataHead(retdata)}");
return false;
}
File.WriteAllBytes($"{AppDataPath}/StreamingAssets/{f}", encryptdata);
//清理临时目录
string temppath = $"{AppDataPath}/aot";
if (Directory.Exists(temppath))
{
Directory.Delete(temppath, true);
temppath = temppath + ".meta";
File.Delete(temppath);
}
temppath = $"{AppDataPath}/StreamingAssets/res";
if (Directory.Exists(temppath))
{
Directory.Delete(temppath, true);
temppath = temppath + ".meta";
File.Delete(temppath);
}
temppath = $"{AppDataPath}/StreamingAssets/xaot";
if (Directory.Exists(temppath))
{
Directory.Delete(temppath, true);
temppath = temppath + ".meta";
File.Delete(temppath);
}
temppath = $"{AppDataPath}/hotfix";
if (Directory.Exists(temppath))
{
Directory.Delete(temppath, true);
temppath = temppath + ".meta";
File.Delete(temppath);
}
SaveMd5UpdateFiles();
return true;
}
static void SaveMd5UpdateFiles()
{
//处理生成带md5文件
Debug.Log($"处理生成带md5文件");
string dllmd5 = File.ReadAllText($"{GetUpdatePath()}updatever.txt");
if (dllmd5 != m_strOldVersion)
{
Debug.Log($"处理生成带md5文件有更新");
}
else
{
Debug.Log($"处理生成带md5文件没有有更新");
}
{//有改变
string new_dllfiles = $"{GetUpdatePath()}dllfiles_{dllmd5}.txt";
File.Copy($"{AppDataPath}/StreamingAssets/dllfiles.txt", $"{GetUpdatePath()}dllfiles_{dllmd5}.txt", true);
string[] dllString = File.ReadAllLines(new_dllfiles);
//检查原version文件,删除旧的dllfiles.txt_md5文件和加密的dll文件
if (m_strOldVersion.Length >= 8)
{
string old_dllfiles = $"{GetUpdatePath()}dllfiles_{m_strOldVersion}.txt";
if (File.Exists(old_dllfiles))
{
//查找原来的文件,删除
string[] dlls = File.ReadAllLines(old_dllfiles);
//比较dlls和dllString的不同,删除旧的
var filesToDelete = dlls.Except(dllString);
foreach (string f in filesToDelete)
{
string trimmedString = f.Trim('{', '}');
string[] line = trimmedString.Split(',');
string fn = GetFileMd5Name(line[0], line[1]);
string file = $"{GetUpdatePath()}{fn}";
if (File.Exists(file))
{
File.Delete(file);
}
}
File.Delete(old_dllfiles);
}
}
//生成新的带md5文件
foreach (string f in dllString)
{
string trimmedString = f.Trim('{', '}');
string[] line = trimmedString.Split(',');
string fn = GetFileMd5Name(line[0], line[1]);
string srcfile = $"{GetUpdatePath()}{line[0]}";
string destfile = $"{GetUpdatePath()}{fn}";
if (!File.Exists(destfile))
{
File.Copy(srcfile, destfile, true);
}
}
}
/*/生成版本文件(代码md5,YooAssets版本号,资源版本号)
string updateShowVer = File.ReadAllText($"{GetUpdatePath()}updateShowVer.txt");
string serverinfo_file = $"{GetUpdatePath()}serverinfo.txt";
ServerConfig serverConfig = new ServerConfig();
if (BuildSettings.GameEnvironment == EBaseGameEnv.Local)
{
serverConfig.cdn = "http://192.168.130.30/cdn/";
serverConfig.resver = BuildSettings.GameResVersion; //"10000101";//先测试用101
serverConfig.resver_update = updateShowVer;//updateShowVer.txt
serverConfig.codemd5 = dllmd5;//updatever.txt
serverConfig.yooasset_ver = $"{yoopack2md5},{yoopack3md5}";//PatchManifest_Pack2.version 和PatchManifest_Pack3.version
serverConfig.login = "172.25.50.43:3001";
serverConfig.login_backup = "172.25.50.43:3001";
serverConfig.notice = "http://192.168.130.30/cdn/";
serverConfig.date = "2025082610220612";
serverConfig.gateway = new string[]{
"114.207.53.71",
"114.207.53.72",
};
}
else
{
serverConfig.cdn = "http://fish.funturegame.com:8081/download/cdn/";
serverConfig.resver = BuildSettings.GameResVersion;// "10000000";
serverConfig.resver_update = updateShowVer;//updateShowVer.txt
serverConfig.codemd5 = dllmd5;//updatever.txt
serverConfig.yooasset_ver = $"{yoopack2md5},{yoopack3md5}";//PatchManifest_Pack2.version 和PatchManifest_Pack3.version
serverConfig.login = "fish.funturegame.com:3001";
serverConfig.login_backup = "114.207.53.82:3001";
serverConfig.notice = "https://version_t1.funturio.com/";
serverConfig.date = "2025082610220612";
serverConfig.gateway = new string[]{
"114.207.53.71",
"114.207.53.72",
};
serverConfig.ext = new Dictionary<string, string>(){
{"xconsole","100"},
{"logserver","http://fish.funturegame.com:8089/api/v1/logs/batch"}
};
}
string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(serverConfig);
File.WriteAllText(serverinfo_file, jsonString);
//*/
}
static void PackDllFile(string bundleName, string dllpath)
{
//string resPath = $"{AppDataPath}/StreamingAssets/xhotfix/";
List<string> files = new List<string>();
files.AddRange(Directory.GetFiles(dllpath));
//files.AddRange(Directory.GetFiles($"{AppDataPath}/StreamingAssets/xaot/"));
//int nPos = resPath.IndexOf("assets/StreamingAssets/", StringComparison.OrdinalIgnoreCase);
int nPos = bundleName.IndexOf('.');
//加密
List<string> DllFiles = new List<string>();
string dirname = bundleName.Substring(0, nPos);
string temppath = $"{AppDataPath}/{dirname}";
if (!Directory.Exists(temppath))
{
Directory.CreateDirectory(temppath);
}
else
{
ClearDir(temppath);
}
//byte[] buffer, aesdata;
foreach (var file in files)
{
if (file.EndsWith(".dll"))
{
//buffer = File.ReadAllBytes(file);
//aesdata = buffer;// AESManager.Encrypt(buffer);
string name = Path.GetFileNameWithoutExtension(file);//file.Replace(".dll", ".bytes");
string filepath = $"{temppath}/{name}.bytes";
//File.WriteAllBytes(filepath, aesdata);
File.Move(file, filepath);
name = $"assets/{dirname}/{name}.bytes";
DllFiles.Add(name);
}
}
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = bundleName;
build.assetNames = DllFiles.ToArray();// filelist;
maps.Add(build);
}
static void BuildLuaAB(string bundleName)
{
string luapath = $"{Application.dataPath}/l/";
if(Directory.Exists(luapath))
ClearDir(luapath);
//Assets下的lua目录
CopyScriptsFile($"{Application.dataPath}/lua/", luapath, "bytes");
AssetDatabase.Refresh();//新增文件需要刷新
var files = Directory.GetFiles($"Assets/l", "*.bytes", SearchOption.AllDirectories);
for (int i = 0; i < files.Length; i++)
{
files[i] = files[i].Replace('\\', '/');
}
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = bundleName;
build.assetNames = files;
maps.Add(build);
}
static void CopyScriptsFile(string srcPath, string outPath, string fixname)
{
if (!Directory.Exists(outPath))
{
Directory.CreateDirectory(outPath);
}
string[] luaPaths = { srcPath };//AppDataPath + "/../../Scripts_c/" };
for (int i = 0; i < luaPaths.Length; i++)
{
paths.Clear(); files.Clear();
string luaDataPath = luaPaths[i].ToLower();
Recursive(luaDataPath);
int n = 0;
foreach (string f in files)
{
if (f.EndsWith(".meta")) continue;
string newfile = f.Replace(luaDataPath, "");
string newpath;
if (newfile.IndexOf(".lua") < 0)//转2进制文件
{
newpath = outPath + newfile + ".bytes";
}
else
{
newpath = outPath + newfile.Replace(".lua", "." + fixname);
}
newpath = newpath.ToLower();
string path = Path.GetDirectoryName(newpath);
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
if (File.Exists(newpath))
{
File.Delete(newpath);
}
File.Copy(f, newpath, true);
UpdateProgress(n++, files.Count, newpath);
}
}
EditorUtility.ClearProgressBar();
}
static void UpdateProgress(int progress, int progressMax, string desc)
{
string title = "Processing...[" + progress + " - " + progressMax + "]";
float value = (float)progress / (float)progressMax;
EditorUtility.DisplayProgressBar(title, desc, value);
}
static void BuildDllsFileIndex(string dir = "", bool bUpdateDll = true)
{
List<string> files = new List<string>();
string resPath = $"{AppDataPath}/StreamingAssets/{dir}/";
///----------------------创建文件列表-----------------------
string newFilePath = resPath + "dllfiles.txt";
if (File.Exists(newFilePath)) File.Delete(newFilePath);
if (!Directory.Exists($"{resPath}xaot/"))
Directory.CreateDirectory($"{resPath}xaot/");
if (!Directory.Exists($"{resPath}xhotfix/"))
Directory.CreateDirectory($"{resPath}xhotfix/");
//files.AddRange(Directory.GetFiles($"{resPath}xaot/"));
files.AddRange(Directory.GetFiles($"{resPath}xhotfix/"));
FileStream fs = new FileStream(newFilePath, FileMode.CreateNew);
StreamWriter sw = new StreamWriter(fs);
foreach (var dllfile in files)
{
if (!dllfile.EndsWith(".dll") && !dllfile.EndsWith(".unity3d"))
continue;
long size = 0;
string md5 = GetFixedMd5(md5file(dllfile, out size));
string value = dllfile.Replace(resPath, string.Empty);
sw.WriteLine($"{{{value},{md5},{size}}}");
m_dicFilelist[value] = md5;
}
//记录基础 ab
string[] BaseABFile = { "a.unity3d", "h.unity3d"};//, "bs.unity3d", "StreamingAssets"
foreach (var f in BaseABFile)
{
if (f == "a.unity3d" && bUpdateDll)
continue;
string file = $"{AppDataPath}/StreamingAssets/{f}";
if (File.Exists(file))
{
long size = 0;
string md5 = GetFixedMd5(md5file(file, out size));
string value = file.Replace($"{AppDataPath}/StreamingAssets/", string.Empty);
sw.WriteLine($"{{{value},{md5},{size}}}");
m_dicFilelist[value] = md5;
}
}
sw.Close(); fs.Close();
}
static void BuildFileIndex(string dir = "")
{
string resPath = $"{AppDataPath}/StreamingAssets/{dir}/";
///----------------------创建文件列表-----------------------
string newFilePath = resPath + "files.txt";
if (File.Exists(newFilePath)) File.Delete(newFilePath);
paths.Clear(); files.Clear();
Recursive(resPath);//此处去掉了dll热更相关的目录
FileStream fs = new FileStream(newFilePath, FileMode.CreateNew);
StreamWriter sw = new StreamWriter(fs);
for (int i = 0; i < files.Count; i++)
{
string file = files[i];
if (file.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
continue;
string ext = Path.GetExtension(file);
if (file.EndsWith(".meta") || file.Contains(".DS_Store") || file.EndsWith(".manifest")) continue;
long size = 0;
string md5 = GetFixedMd5(md5file(file, out size));
string value = file.Replace(resPath, string.Empty);
//if (!value.Contains("/") && value != "StreamingAssets.unity3d" && value != "bs.unity3d")//去除根路径文件
if(value == "h.unity3d")//剔除热更
continue;
sw.WriteLine($"{{{value},{md5},{size}}}");
m_dicFilelist[value] = md5;
}
sw.Close(); fs.Close();
//版本文件ver.txt
//string vermd5 = md5file(newFilePath);
//File.WriteAllText($"{resPath}/updatever.txt", vermd5);
}
static void SaveUpdateVer(string dir = "")
{
string resPath = $"{AppDataPath}/StreamingAssets/{dir}/";
string resfilelist = $"{resPath}files.txt";
string dllfilelist = $"{resPath}dllfiles.txt";
if (!File.Exists(dllfilelist) || !File.Exists(resfilelist))
{
UnityEngine.Debug.LogError("需要先生成更新列表文件");
return;//需要先
}
string resmd5 = md5file(resfilelist);
string dllmd5 = md5file(dllfilelist);
m_strResFileMD5 = GetFixedMd5(resmd5);
m_strDllFileMD5 = GetFixedMd5(dllmd5);
File.WriteAllText($"{resPath}/updatever.txt", $"{m_strDllFileMD5},{m_strResFileMD5}");
}
public static string md5file(string file)
{
long size = 0;
return md5file(file, out size);
}
public static string md5file(string file, out long size)
{
try
{
FileStream fs = new FileStream(file, FileMode.Open);
System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(fs);
size = fs.Length;
fs.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
catch (Exception ex)
{
throw new Exception("md5file() fail, error:" + ex.Message);
}
}
public static string GetPlatformString(BuildTarget target)
{
string sPlatform;
if (target == BuildTarget.Android)
{
sPlatform = "android";
}
else if (target == BuildTarget.iOS)
{
sPlatform = "ios";
}
else if (target == BuildTarget.WebGL)
{
sPlatform = "webgl";
}
else
{
sPlatform = "pc";
}
return sPlatform;
}
private static string GetVersionResDir(BuildTarget target)
{
string dir;
dir = Application.dataPath + $"/../../../CDNRes/public/G7/{GetPlatformString(target)}/";
//if (BuildSettings.GameEnvironment == EGameEnvType.Intranet)
//{
// dir = $"\\\\{GetLocal()}\\g\\App\\CDNRes\\public\\{BuildSettings.GameResVersion}\\{GetPlatformString(target)}\\";
//}
//else
//{
// dir = Application.dataPath + $"/../../../CDNRes/public/{BuildSettings.GameResVersion}/{GetPlatformString(target)}/";
//}
return dir;
}
public static void BuildServerListMd5(BuildTarget target, string packageName = "", string OutputPackageDir = "")
{
string dir = GetVersionResDir(target);
if (!Directory.Exists(dir))
{
Debug.LogError($"目录不存在:{dir}");
return;
}
StringBuilder sb = new StringBuilder();
//将三个包的Hash
for (int i = 1; i <= 3; i++)
{
string packName = $"Pack{i}";
string currentDir = dir;
if (packName == packageName)
{
currentDir = OutputPackageDir;
}
string verFile = $"{currentDir}PatchManifest_{packName}.version";
if (File.Exists(verFile))
{
string verName = File.ReadAllText(verFile);
string hashFile = $"{currentDir}PatchManifest_{packName}_{verName}.hash";
sb.AppendLine(File.ReadAllText(hashFile));
}
}
string package = $"{dir}{packageVer}";
if (File.Exists(package))
{
sb.AppendLine(File.ReadAllText(package));
}
string updatever = $"{dir}updatever.txt";
if (File.Exists(updatever))
{
sb.AppendLine(File.ReadAllText(updatever));
}
Debug.Log($"源hash:\n{sb}");
string md5 = Packager.md5(sb.ToString());
string md5File = $"{dir}server_res_list_md5.txt";
if (File.Exists(md5File))
{
File.Delete(md5File);
}
byte[] bytes = Encoding.UTF8.GetBytes(md5);
using (FileStream fs = File.Create(md5File))
{
fs.Write(bytes, 0, bytes.Length);
fs.Flush();
fs.Close();
}
Debug.Log($"md5生成成功:{md5} {md5File}");
}
static void PackAllPathFile(string bundleName, string path, string nopath = null)
{
paths.Clear(); files.Clear();
Recursive(path);
string[] filelist = files.ToArray();
List<string> total = new List<string>(filelist);
foreach (var file in filelist)
{
if (!m_mapResNameDic.ContainsKey(file))
{
m_mapResNameDic[file] = "1";
}
}
//依赖文件
foreach (var file in filelist)
{
List<string> depends = GetAllDependencies(file);
foreach (var fd in depends)
{
if (fd.EndsWith(".hlsl"))
continue;
if (!m_mapResNameDic.ContainsKey(fd))
{
m_mapResNameDic[fd] = "1";
total.Add(fd);
}
}
}
if (nopath != null)
{
for (int i = total.Count - 1; i >= 0; i--)
{
var file = total[i];
if (file.Contains(nopath))
{
string[] pathParts = file.Replace('\\', '/').Split('/');
foreach (var p in pathParts)
{
if (string.Equals(p, nopath, StringComparison.OrdinalIgnoreCase))//if (p == nopath)
{
//删除不合理的
total.RemoveAt(i);
break;
}
}
}
}
}
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = bundleName;
build.assetNames = total.ToArray();// filelist;
maps.Add(build);
UnityEngine.Debug.Log($"ab:{bundleName} has {filelist.Length} file");
}
static private List<string> GetAllDependencies(string mainAssetPath)
{
List<string> result = new List<string>();
string[] depends = AssetDatabase.GetDependencies(mainAssetPath, true);
foreach (string assetPath in depends)
{
// 注意:排除主资源对象
if (assetPath != mainAssetPath)
result.Add(assetPath);
}
return result;
}
static string GetFixedMd5(string md5)
{
return md5.Substring(0, 8);//只取8位
}
static string GetFileMd5Name(string filename, string md5)
{
int pos = filename.LastIndexOf('.');
if (pos != -1)
{
return $"{filename.Substring(0, pos)}_{md5}{filename.Substring(pos)}";
}
else
return $"{filename}_{md5}";
}
public static string GetCommandLineValue(string[] array, string key)
{
foreach (var item in array)
{
if (item.Contains(key))
{
string value = item.Split('=')[1];
return value;
}
}
return "";
}
}