using System.Collections.Generic;
using System;
using System.Reflection;
using System.IO;
using System.Linq;
using System.Text;
using TinaX;
using TinaX.IO;
using TinaX.Utils;
using TinaX.VFSKit;
using TinaX.VFSKit.Const;
using TinaX.VFSKitInternal;
using TinaX.VFSKitInternal.Utils;
using TinaXEditor.Utils;
using TinaXEditor.VFSKit.Pipeline;
using TinaXEditor.VFSKit.Const;
using TinaXEditor.VFSKit.Utils;
using TinaXEditor.VFSKitInternal;
using UnityEditor;
using UnityEngine;
namespace TinaXEditor.VFSKit
{
///
/// VFS Assets Builder
///
public class VFSBuilder : IVFSBuilder
{
public VFSConfigModel Config { get; private set; }
///
/// Show tips GUI in editor..
///
public bool EnableTipsGUI { get; set; } = false;
public bool CopyToStreamingAssetsFolder { get; set; } = false;
public bool StrictMode { get; set; } = false;
public bool ClearAssetBundleSignBeforeBuild { get; set; } = false;
public bool ClearAssetBundleSignAfterBuild { get; set; } = false;
///
/// Force Rebuild | 强制重构建
///
public bool ForceRebuild { get; set; } = false;
public bool ClearOutputFolder { get; set; } = false;
//private HashSet
private List asset_hash_book = new List(); //记录的是unity工程里原始的asset,而不是打包后的ab
private Dictionary> dict_asset_hash_book = new Dictionary>(); //和上面一样存储的是原始asset, 不过这里是针对扩展组的,key的组名
///
/// 按照组,把每个组中包含的AssetBundle名存下来
///
private Dictionary> mDict_Group_AssetBundleNames = new Dictionary>(); //按照组,把每个组中包含的AssetBundle名存下来
private ProfileRecord curProfile;
private string mProfileName;
private bool mDevelopMode;
private AssetBundleManifest mAssetBundleManifest;
private BuilderPipeline mPipeline;
private bool mUsePipeline = false;
private EditorBuildInfo mEditorBuildInfo;
public VFSBuilder()
{
}
public VFSBuilder(VFSConfigModel config)
{
Config = config;
}
public IVFSBuilder SetConfig(VFSConfigModel config)
{
Config = config;
return this;
}
public IVFSBuilder UseProfile(string profileName)
{
mProfileName = profileName;
curProfile = VFSManagerEditor.GetProfileRecord(profileName);
mDevelopMode = XCoreEditor.IsXProfileDevelopMode(mProfileName);
return this;
}
public IVFSBuilder UsePipeline(BuilderPipeline pipeline)
{
mUsePipeline = true;
mPipeline = pipeline;
return this;
}
public IVFSBuilder UseAutoPipeline()
{
var interface_type = typeof(IBuildHandler);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes().Where(t => t.GetInterfaces().Contains(interface_type) && t != typeof(TinaXEditor.VFSKit.Pipeline.Builtin.BuilderPipelineHead) && t != typeof(TinaXEditor.VFSKit.Pipeline.Builtin.BuilderPipelineLast)))
.ToArray();
Dictionary dict_priority = new Dictionary();
//查找优先级
foreach(var type in types)
{
var priority_attr = type.GetCustomAttribute(true);
if(priority_attr == null)
{
dict_priority.Add(type, 100);
}
else
{
dict_priority.Add(type, priority_attr.Priority);
}
}
List list_types = new List(types);
list_types.Sort((x, y) => dict_priority[x].CompareTo(dict_priority[y]));
var pipeline = new BuilderPipeline();
foreach (var type in list_types)
{
pipeline.AddLast(Activator.CreateInstance(type) as IBuildHandler);
}
this.mUsePipeline = true;
this.mPipeline = pipeline;
return this;
}
public void RefreshAssetBundleSign()
{
/*
* Unity目前提供的API对应的打包方法是:
* 1. 给全局的文件加上assetbundle标记
* 2. 整体打包
*/
//获取到所有组的文件白名单目录
refreshProfileInfos();
if (EnableTipsGUI) EditorUtility.DisplayProgressBar("VFS Builder", "Handle AssetBundle signs ...", 0f);
var _whiteLists_folder = VFSManagerEditor.GetAllFolderPaths();
string ab_extension = Config.AssetBundleFileExtension; //没有点开头的后缀名
if (ab_extension.StartsWith("."))
ab_extension = ab_extension.Substring(1, ab_extension.Length - 1);
ab_extension = ab_extension.ToLower();
List _whiteLists_folder_list_temp = new List();
foreach(var path in _whiteLists_folder)
{
if (!IsAssetFolderExists(path))
{
Debug.LogError("[TinaX][VFS]Folder path in vfs config is not exists: \"" + path + "\"");
}
else
{
if (path.EndsWith("/"))
_whiteLists_folder_list_temp.Add(path.Substring(0, path.Length - 1));
else
_whiteLists_folder_list_temp.Add(path);
}
}
var _whitelist_folder_temp = _whiteLists_folder_list_temp.ToArray();
string[] guids = AssetDatabase.FindAssets("", _whitelist_folder_temp);
string[] asset_paths = VFSManagerEditor.GetAllWithelistAssetsPaths();
List asset_guids = new List();
foreach(var item in asset_paths)
{
var myguid = AssetDatabase.AssetPathToGUID(item);
if (!myguid.IsNullOrEmpty())
asset_guids.Add(myguid);
}
if (asset_guids.Count > 0)
ArrayUtil.Combine(ref guids, asset_guids.ToArray());
ArrayUtil.RemoveDuplicationElements(ref guids);
asset_paths = null;
asset_guids = null;
asset_hash_book.Clear();
dict_asset_hash_book.Clear();
int counter = 0;
int counter_t = 0;
int totalLength = guids.Length;
foreach (var guid in guids)
{
string cur_asset_path = AssetDatabase.GUIDToAssetPath(guid);
if (VFSManagerEditor.QueryAsset(cur_asset_path, Config, out AssetsStatusQueryResult result, true))
{
//查询到了信息,但是并不是所有情况都需要设置assetbundle记录,
bool sign_flag = true;
if (result.DevType == FolderBuildDevelopType.editor_only)
sign_flag = false; //该资源仅在编辑器下被加载,不参与打包。
if(result.DevType == FolderBuildDevelopType.develop_mode_only)
{
if (!mDevelopMode)
sign_flag = false; //资源应该仅在develop模式下使用,但是当前Profile的设置并不是develop
}
if (result.ExtensionGroup)
if (curProfile.IsDisabledGroup(result.GroupName))
sign_flag = false;
if (sign_flag)
{
var importer = AssetImporter.GetAtPath(cur_asset_path);
if (!XPath.IsFolder(cur_asset_path) && !result.AssetBundleFileNameWithoutExtension.IsNullOrEmpty())
{
string assetBundleName_without_extension = result.AssetBundleFileNameWithoutExtension;
void InvokePipeline(BuilderPipelineContext ctx)
{
if(ctx != null && ctx.Handler != null)
{
bool b = ctx.Handler.BeforeSetAssetBundleSign(ref assetBundleName_without_extension, ref result);
if(b && ctx.Next != null)
{
InvokePipeline(ctx.Next);
}
}
}
if (mUsePipeline)
{
InvokePipeline(mPipeline.First);
}
//正式设置AssetBundle
importer.SetAssetBundleNameAndVariant(assetBundleName_without_extension, ab_extension);
//记录Asset原始hash(用于补丁版本检索)
if (result.ExtensionGroup)
{
//记录到字典里
if (!dict_asset_hash_book.ContainsKey(result.GroupName))
dict_asset_hash_book.Add(result.GroupName, new List());
dict_asset_hash_book[result.GroupName].Add(new FilesHashBook.FileHash() { p = cur_asset_path, h = XFile.GetMD5(cur_asset_path, true) });
}
else
{
asset_hash_book.Add(new FilesHashBook.FileHash() { p = cur_asset_path, h = XFile.GetMD5(cur_asset_path, true) });
}
//记录AssetBundle名
string final_ab_name = assetBundleName_without_extension + "." + ab_extension;
if (!mDict_Group_AssetBundleNames.ContainsKey(result.GroupName))
mDict_Group_AssetBundleNames.Add(result.GroupName, new List());
if (!mDict_Group_AssetBundleNames[result.GroupName].Contains(final_ab_name))
mDict_Group_AssetBundleNames[result.GroupName].Add(final_ab_name);
}
}
}
if (EnableTipsGUI)
{
counter++;
if(totalLength < 100)
{
EditorUtility.DisplayProgressBar("VFS Builder", $"Handle AssetBundle signs : {counter} / {totalLength}", counter / totalLength);
}
else
{
counter_t++;
if(counter_t > 50)
{
counter_t = 0;
EditorUtility.DisplayProgressBar($"VFS Builder", "Handle AssetBundle signs : {counter} / {totalLength}", counter / totalLength);
}
}
}
}
AssetDatabase.SaveAssets();
if (EnableTipsGUI) EditorUtility.ClearProgressBar();
}
public void BuildAssetBundle(XRuntimePlatform platform, AssetCompressType compressType)
{
this.BuildAssetBundle(platform, compressType, out _, out _);
}
public void BuildAssetBundle(XRuntimePlatform platform, AssetCompressType compressType, out string output_folder, out string temp_output_folder)
{
var buildTarget = XPlatformEditorUtil.GetBuildTarget(platform);
var target_name = XPlatformUtil.GetNameText(platform);
output_folder = Path.Combine(VFSEditorConst.PROJECT_VFS_SOURCE_PACKAGES_ROOT_PATH, target_name);
string build_output_folder = Path.Combine(output_folder, "build_temp");
temp_output_folder = build_output_folder;
if (ClearOutputFolder)
XDirectory.DeleteIfExists(output_folder, true);
XDirectory.CreateIfNotExists(output_folder);
XDirectory.CreateIfNotExists(temp_output_folder);
//压缩方法
BuildAssetBundleOptions build_opt = BuildAssetBundleOptions.None;
switch (compressType)
{
default:
case AssetCompressType.LZ4:
build_opt = BuildAssetBundleOptions.ChunkBasedCompression;
break;
case AssetCompressType.LZMA:
build_opt = BuildAssetBundleOptions.None;
break;
case AssetCompressType.None:
build_opt = BuildAssetBundleOptions.UncompressedAssetBundle;
break;
}
//强制重新构建
if (ForceRebuild)
build_opt = build_opt | BuildAssetBundleOptions.ForceRebuildAssetBundle;
//严格模式
if (StrictMode)
build_opt = build_opt | BuildAssetBundleOptions.StrictMode;
//Hash保持一致
build_opt = build_opt | BuildAssetBundleOptions.DeterministicAssetBundle;
//叫Unity来打ab包
BuildPipeline.BuildAssetBundles(build_output_folder, build_opt, buildTarget);
//打包完了,把所有打包得到的AssetBundle文件记录下来
string ab_extension = Config.AssetBundleFileExtension;
if (!ab_extension.StartsWith("."))
ab_extension = "." + ab_extension;
string[] files = Directory.GetFiles(build_output_folder, $"*{ab_extension}", SearchOption.AllDirectories);
#region temp下的目录完全hash
//var hashBook = new FilesHashBook();
//List temp_hash_list = new List();
//int build_output_folder_len = build_output_folder.Length + 1;
//foreach (var file in files)
//{
// string pure_path = file.Substring(build_output_folder_len, file.Length - build_output_folder_len);
// if (pure_path.IndexOf("\\") != -1)
// pure_path = pure_path.Replace("\\", "/");
// temp_hash_list.Add(new FilesHashBook.FileHash()
// {
// p = pure_path,
// h = XFile.GetMD5(file, true)
// });
//}
//hashBook.Files = temp_hash_list.ToArray();
//string hashBook_path = Path.Combine(build_output_folder, VFSConst.ABsHashFileName);
//XFile.DeleteIfExists(hashBook_path);
//string hashBook_json = JsonUtility.ToJson(hashBook);
//File.WriteAllText(hashBook_path, hashBook_json, Encoding.UTF8);
#endregion
}
public void Build(XRuntimePlatform platform, AssetCompressType compressType)
{
Debug.Log("[TinaX.VFS] Start build assets...");
if (ClearAssetBundleSignBeforeBuild)
VFSEditorUtil.RemoveAllAssetbundleSigns();
Debug.Log(" handle assetbundle signs...");
RefreshAssetBundleSign();
Debug.Log(" build assetbundles by unity editor.");
string packages_root_path; //VFS存放相关文件的根目录
string output_temp_path; //Unity自身打包后直接输出的目录
BuildAssetBundle(platform, compressType, out packages_root_path, out output_temp_path);
#region 加载Build得到的AssetbundleManifest文件
string abmanifest_file_name = Path.GetFileName(output_temp_path);
string abmanifest_path = Path.Combine(output_temp_path, abmanifest_file_name);
AssetBundle ab_mainifest = AssetBundle.LoadFromFile(abmanifest_path);
mAssetBundleManifest = ab_mainifest.LoadAsset("AssetBundleManifest");
#endregion
HandleVFSFiles(packages_root_path, output_temp_path, platform);
SaveAssetHashFiles(Path.Combine(packages_root_path, VFSEditorConst.PROJECT_VFS_FILE_FOLDER_DATA));
MakeEditorBuildInfo(packages_root_path);
MakeBuildInfo(packages_root_path);
MakeVFSConfig(packages_root_path,Config);
if (CopyToStreamingAssetsFolder)
CopyToStreamingAssets(packages_root_path,XPlatformUtil.GetNameText(platform));
if (ClearAssetBundleSignAfterBuild)
VFSEditorUtil.RemoveAllAssetbundleSigns();
Debug.Log("[TinaX.VFS] Build Finished.");
}
///
///
///
/// 存放一系列VFS目录的根目录
/// Unity的Build结果输出目录
public void HandleVFSFiles(string root_path, string build_root_path,XRuntimePlatform platform)
{
mEditorBuildInfo = new EditorBuildInfo();
List groups = VFSManagerEditor.GetGroups();
string remote_files_root_path = Path.Combine(root_path, VFSEditorConst.PROJECT_VFS_FILE_FOLDER_REMOTE);
string local_files_root_path = Path.Combine(root_path, VFSEditorConst.PROJECT_VFS_FILES_FOLDER_MAIN);
string source_packages_extension_root_folder = Path.Combine(root_path, VFSEditorConst.PROJECT_VFS_FILES_FOLDER_EXTENSION);
XDirectory.DeleteIfExists(remote_files_root_path, true);
XDirectory.DeleteIfExists(local_files_root_path, true);
XDirectory.DeleteIfExists(source_packages_extension_root_folder, true);
foreach (var group in groups)
{
//扩展组的判断
if (group.ExtensionGroup)
{
#region 处理扩展组
string extension_group_root_path = Path.Combine(source_packages_extension_root_folder, group.GroupName.ToLower());
bool moved = CopyAssetBundleFilesByGroup(group, build_root_path, extension_group_root_path);
if (moved)
{
//manifest
group.MakeVFSManifest(root_path, mDict_Group_AssetBundleNames[group.GroupName], ref mAssetBundleManifest);
//给独立的组生成一份hash
group.MakeAssetBundleFilesHash(root_path, extension_group_root_path, mDict_Group_AssetBundleNames[group.GroupName]);
SaveExtensionGroupInfo(extension_group_root_path, group.GroupName, platform, group.ExtensionGroup_MainPackageVersionLimit,Config.AssetBundleFileExtension);
//保存Options
group.SaveGroupOptionFile(root_path);
}
#endregion
//登记
if (curProfile.IsDisabledGroup(group.GroupName))
mEditorBuildInfo.list_disable_extension.Add(group.GroupName);
else
{
mEditorBuildInfo.list_total_extension.Add(group.GroupName);
if (group.HandleMode == GroupHandleMode.LocalOrRemote)
{
if (curProfile.TryGetGroupLocation(group.GroupName, out var location))
{
if (location == ProfileRecord.E_GroupAssetsLocation.Local)
mEditorBuildInfo.list_extension_local.Add(group.GroupName);
else if (location == ProfileRecord.E_GroupAssetsLocation.Remote)
mEditorBuildInfo.list_extension_remote.Add(group.GroupName);
}
else
mEditorBuildInfo.list_extension_local.Add(group.GroupName);
}
else if (group.HandleMode == GroupHandleMode.RemoteOnly)
mEditorBuildInfo.list_extension_remote.Add(group.GroupName);
else
mEditorBuildInfo.list_extension_local.Add(group.GroupName);
}
}
else
{
bool moveToRemote = false;
bool moveToLocal = false;
if (group.HandleMode == GroupHandleMode.LocalOrRemote)
{
if (curProfile.TryGetGroupLocation(group.GroupName, out var location))
{
if (location == ProfileRecord.E_GroupAssetsLocation.Remote)
moveToRemote = true;
else
moveToLocal = true;
}
else
moveToLocal = true;
}
else if (group.HandleMode == GroupHandleMode.RemoteOnly)
moveToRemote = true;
else if (group.HandleMode == GroupHandleMode.LocalOnly || group.HandleMode == GroupHandleMode.LocalAndUpdatable)
moveToLocal = true;
//登记
mEditorBuildInfo.list_total_main_package.Add(group.GroupName);
if (moveToRemote)
{
//登记
mEditorBuildInfo.list_main_package_remote.Add(group.GroupName);
XDirectory.CreateIfNotExists(remote_files_root_path);
CopyAssetBundleFilesByGroup(group, build_root_path, remote_files_root_path);
//上一步copy的时候,开发者自定义的pipeline可以改变文件,所以制作assetbundle的hash信息的时候,必须使用上一步处理后的结果
group.MakeAssetBundleFilesHash(root_path, remote_files_root_path, mDict_Group_AssetBundleNames[group.GroupName]);
}
else if (moveToLocal)
{
//登记
mEditorBuildInfo.list_main_package_local.Add(group.GroupName);
XDirectory.CreateIfNotExists(local_files_root_path);
CopyAssetBundleFilesByGroup(group, build_root_path, local_files_root_path);
//上一步copy的时候,开发者自定义的pipeline可以改变文件,所以制作assetbundle的hash信息的时候,必须使用上一步处理后的结果
group.MakeAssetBundleFilesHash(root_path, local_files_root_path, mDict_Group_AssetBundleNames[group.GroupName]);
}
group.MakeVFSManifest(root_path, mDict_Group_AssetBundleNames[group.GroupName], ref mAssetBundleManifest);
}
}
}
private void SaveAssetHashFiles(string data_path)
{
XDirectory.CreateIfNotExists(data_path);
//mainPackage
string mainPackageHashPath = Path.Combine(data_path, VFSConst.AssetsHashFileName);
var main_obj = new FilesHashBook();
main_obj.Files = asset_hash_book.ToArray();
XConfig.SaveJson(main_obj, mainPackageHashPath, AssetLoadType.SystemIO);
string extGroupHashFolderPath = Path.Combine(data_path, VFSConst.ExtensionGroupAssetsHashFolderName);
XDirectory.DeleteIfExists(extGroupHashFolderPath,true);
Directory.CreateDirectory(extGroupHashFolderPath);
//各个扩展组
foreach(var item in dict_asset_hash_book)
{
string g_path = Path.Combine(extGroupHashFolderPath, item.Key + ".json");
var obj = new FilesHashBook();
obj.Files = item.Value.ToArray();
XConfig.SaveJson(obj, g_path, AssetLoadType.SystemIO);
}
}
///
///
///
/// root_path下面应该就是"vfs_root","vfs_data"之类的文件
public void CopyToStreamingAssets(string root_path,string platform_name)
{
Debug.Log(" copy To \"StreamingAssets\"");
VFSEditorUtil.CopyToStreamingAssets(root_path, platform_name);
}
private void SaveExtensionGroupInfo(string group_path, string group_name, XRuntimePlatform platform , long mainPackageVersionLimit, string ab_ext_name)
{
string file_path = VFSUtil.GetExtensionGroup_GroupInfo_Path_InGroupPath(group_path);
var obj = new ExtensionGroupInfo
{
Platform = platform,
GroupName = group_name,
MainPackageVersionLimit = mainPackageVersionLimit,
AssetBundleExtension = ab_ext_name
};
XConfig.SaveJson(obj, file_path, AssetLoadType.SystemIO);
}
private void refreshProfileInfos()
{
if (curProfile == null)
curProfile = VFSManagerEditor.GetProfileRecord(XCoreEditor.GetCurrentActiveXProfileName());
}
private bool IsAssetFolderExists(string path)
{
if (!path.StartsWith("Assets/"))
return false;
return System.IO.Directory.Exists(System.IO.Path.GetFullPath(path));
}
///
/// 不要处理的后缀
///
private readonly string[] DontHandle_Ext =
{
".cs",
".dll",
".so",
".exe",
".apk",
".ipa"
};
private bool ExtCanHandle(string path)//如果后缀名可以被处理,返回true
{
var path_ext_name = System.IO.Path.GetExtension(path).ToLower();
return !DontHandle_Ext.Any(item => item == path_ext_name.ToLower());
}
///
/// 把构建好的AssetBundle文件根据组进行复制分类
///
///
///
///
///
private bool CopyAssetBundleFilesByGroup(VFSEditorGroup group, string build_ab_path, string target_root_path)
{
bool moved = false;
int counter = 0;
int counter_t = 0;
int total_len = mDict_Group_AssetBundleNames[group.GroupName].Count;
if (EnableTipsGUI && total_len > 100)
{
EditorUtility.DisplayProgressBar("Classifying files", $"Classifying assetbundle files by group \"{group.GroupName}\"", 0.5f);
}
foreach (var assetbundle_name in mDict_Group_AssetBundleNames[group.GroupName])
{
if (EnableTipsGUI)
{
counter++;
counter_t++;
if (total_len > 100)
{
if (counter_t > 50)
{
counter_t = 0;
EditorUtility.DisplayProgressBar("Classifying files", $"Classifying assetbundle files by group \"{group.GroupName}\"\n{counter} / {total_len}", counter/total_len);
}
}
}
string assetbundle_path = Path.Combine(build_ab_path, assetbundle_name);
if (File.Exists(assetbundle_path))
{
string target_path = Path.Combine(target_root_path, assetbundle_name);
XFile.DeleteIfExists(target_path);
XDirectory.CreateIfNotExists(Path.GetDirectoryName(target_path));
File.Copy(assetbundle_path, target_path);
if (mUsePipeline)
{
FileStream fileStream = new FileStream(target_path, FileMode.Open, FileAccess.ReadWrite);
void InvokePipline(BuilderPipelineContext ctx)
{
if (ctx.Handler != null)
{
bool b = ctx.Handler.BeforeAssetBundleFileSavedByGroup(ref group, assetbundle_name, ref fileStream);
if (b && ctx.Next != null)
{
InvokePipline(ctx.Next);
}
}
}
if (mPipeline.First != null)
{
InvokePipline(mPipeline.First);
}
fileStream.Close();
fileStream.Dispose();
}
moved = true;
}
}
if (EnableTipsGUI)
EditorUtility.ClearProgressBar();
return moved;
}
///
/// 保存BuildInfo
///
///
private void MakeBuildInfo(string packages_root_path)
{
var binfo = new BuildInfo();
binfo.BuildID = System.Guid.NewGuid().ToString();
//save main package
string main_path = VFSUtil.GetMainPackage_BuildInfo_Path(packages_root_path);
XFile.DeleteIfExists(main_path);
XDirectory.CreateIfNotExists(Path.GetDirectoryName(main_path));
XConfig.SaveJson(binfo, main_path, AssetLoadType.SystemIO);
//groups
if(mEditorBuildInfo.list_total_extension != null && mEditorBuildInfo.list_total_extension.Count > 0)
{
foreach (var group in mEditorBuildInfo.list_total_extension)
{
string target_path = VFSUtil.GetExtensionGroup_BuildInfo_Path(packages_root_path, group);
string group_root_path = VFSUtil.GetExtensionGroupFolder(packages_root_path, group);
if (Directory.Exists(group_root_path))
{
XFile.DeleteIfExists(target_path);
XDirectory.CreateIfNotExists(Path.GetDirectoryName(target_path));
XConfig.SaveJson(binfo, target_path, AssetLoadType.SystemIO);
}
}
}
}
///
/// 保存EditorBuildInfo
///
///
private void MakeEditorBuildInfo(string package_root_path)
{
mEditorBuildInfo.build_profile_name = curProfile.ProfileName;
mEditorBuildInfo.ReadySave();
string path = VFSEditorUtil.Get_EditorBuildInfoPath(package_root_path);
XFile.DeleteIfExists(path);
XDirectory.CreateIfNotExists(Path.GetDirectoryName(path));
XConfig.SaveJson(mEditorBuildInfo, path, AssetLoadType.SystemIO);
}
private void MakeVFSConfig(string packages_root_path,VFSConfigModel config)
{
string config_path = VFSUtil.GetVFSConfigFilePath_InPackages(packages_root_path);
string json = JsonUtility.ToJson(config);
var json_obj = JsonUtility.FromJson(json);
if (json_obj.Groups != null)
{
List options = new List(json_obj.Groups);
for(int i = options.Count -1; i >= 0; i--)
{
if(options[i].ExtensionGroup)
{
options.RemoveAt(i);
continue;
}
}
json_obj.Groups = options.ToArray();
}
XConfig.SaveJson(json_obj, config_path, AssetLoadType.SystemIO);
}
}
}