912 lines
36 KiB
C#
912 lines
36 KiB
C#
#if ENABLE_CCD
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
using Unity.Services.Ccd.Management;
|
|
using Unity.Services.Ccd.Management.Models;
|
|
using Unity.Services.Core;
|
|
using UnityEditor.AddressableAssets.Settings;
|
|
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
|
using UnityEngine;
|
|
using UnityEngine.AddressableAssets;
|
|
using UnityEngine.AddressableAssets.ResourceLocators;
|
|
|
|
namespace UnityEditor.AddressableAssets.Build
|
|
{
|
|
/// <summary>
|
|
/// CCD Events used for building Addressables content with CCD.
|
|
/// </summary>
|
|
public class CcdBuildEvents
|
|
{
|
|
static CcdBuildEvents s_Instance;
|
|
|
|
/// <summary>
|
|
/// The static instance of CcdBuildEvents.
|
|
/// </summary>
|
|
public static CcdBuildEvents Instance
|
|
{
|
|
get
|
|
{
|
|
if (s_Instance == null)
|
|
{
|
|
s_Instance = new CcdBuildEvents();
|
|
s_Instance.RegisterNewBuildEvents();
|
|
s_Instance.RegisterUpdateBuildEvents();
|
|
}
|
|
return s_Instance;
|
|
}
|
|
}
|
|
|
|
const string k_ContentStatePath = "addressables_content_state.bin";
|
|
|
|
internal void RegisterNewBuildEvents()
|
|
{
|
|
OnPreBuildEvents += s_Instance.VerifyBuildVersion;
|
|
OnPreBuildEvents += s_Instance.RefreshDataSources;
|
|
OnPreBuildEvents += s_Instance.VerifyTargetBucket;
|
|
OnPostBuildEvents += s_Instance.UploadContentState;
|
|
OnPostBuildEvents += s_Instance.UploadAndRelease;
|
|
}
|
|
|
|
internal void RegisterUpdateBuildEvents()
|
|
{
|
|
OnPreUpdateEvents += s_Instance.VerifyBuildVersion;
|
|
OnPreUpdateEvents += s_Instance.RefreshDataSources;
|
|
OnPreUpdateEvents += s_Instance.DownloadContentStateBin;
|
|
OnPreUpdateEvents += s_Instance.VerifyTargetBucket;
|
|
OnPostUpdateEvents += s_Instance.UploadContentState;
|
|
OnPostUpdateEvents += s_Instance.UploadAndRelease;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pre Addressables build event.
|
|
/// </summary>
|
|
public delegate Task<bool> PreEvent(AddressablesDataBuilderInput input);
|
|
|
|
/// <summary>
|
|
/// Pre new build events.
|
|
/// Default events:
|
|
/// <see cref="RefreshDataSources"/>
|
|
/// <see cref="VerifyTargetBucket"/>
|
|
/// </summary>
|
|
public static event PreEvent OnPreBuildEvents;
|
|
|
|
/// <summary>
|
|
/// Pre update build events.
|
|
/// Default events:
|
|
/// <see cref="RefreshDataSources"/>
|
|
/// <see cref="DownloadContentStateBin"/>
|
|
/// <see cref="VerifyTargetBucket"/>
|
|
/// </summary>
|
|
public static event PreEvent OnPreUpdateEvents;
|
|
|
|
/// <summary>
|
|
/// Post Addressables build event.
|
|
/// </summary>
|
|
public delegate Task<bool> PostEvent(AddressablesDataBuilderInput input,
|
|
AddressablesPlayerBuildResult result);
|
|
|
|
/// <summary>
|
|
/// Post new build events.
|
|
/// Default events:
|
|
/// <see cref="UploadContentState"/>
|
|
/// <see cref="UploadAndRelease"/>
|
|
/// </summary>
|
|
public static event PostEvent OnPostBuildEvents;
|
|
|
|
/// <summary>
|
|
/// Post update build events.
|
|
/// Default events:
|
|
/// <see cref="UploadContentState"/>
|
|
/// <see cref="UploadAndRelease"/>
|
|
/// </summary>
|
|
public static event PostEvent OnPostUpdateEvents;
|
|
|
|
internal async Task<bool> OnPreEvent(bool isUpdate, AddressablesDataBuilderInput input)
|
|
{
|
|
if (isUpdate)
|
|
{
|
|
return await InvokePreEvent(OnPreUpdateEvents, input);
|
|
}
|
|
return await InvokePreEvent(OnPreBuildEvents, input);
|
|
}
|
|
|
|
internal async Task<bool> InvokePreEvent(PreEvent events, AddressablesDataBuilderInput input)
|
|
{
|
|
if (events == null)
|
|
{
|
|
|
|
return true;
|
|
}
|
|
|
|
var total = events.GetInvocationList().Length;
|
|
for (var i = 0; i < total; i++)
|
|
{
|
|
var e = (PreEvent)events.GetInvocationList()[i];
|
|
var shouldContinue = await e.Invoke(input);
|
|
if (!shouldContinue)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> OnPostEvent(bool isUpdate, AddressablesDataBuilderInput input,
|
|
AddressablesPlayerBuildResult result)
|
|
{
|
|
if (isUpdate)
|
|
{
|
|
return await InvokePostEvent(OnPostUpdateEvents, input, result);
|
|
}
|
|
return await InvokePostEvent(OnPostBuildEvents, input, result);
|
|
}
|
|
|
|
internal async Task<bool> InvokePostEvent(PostEvent events, AddressablesDataBuilderInput input,
|
|
AddressablesPlayerBuildResult result)
|
|
{
|
|
if (events == null)
|
|
return true;
|
|
|
|
var total = events.GetInvocationList().Length;
|
|
for (var i = 0; i < total; i++)
|
|
{
|
|
var e = (PostEvent)events.GetInvocationList()[i];
|
|
var shouldContinue = await e.Invoke(input, result);
|
|
if (!shouldContinue)
|
|
{
|
|
// if a post-build step adds an error we have to log it manually
|
|
if (result != null && result.Error != "")
|
|
{
|
|
Debug.LogError(result.Error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepend an event to the pre new build events.
|
|
/// </summary>
|
|
/// <param name="newEvent">Pre build event</param>
|
|
public static void PrependPreBuildEvent(PreEvent newEvent)
|
|
{
|
|
Delegate[] oldEvents = OnPreBuildEvents?.GetInvocationList();
|
|
OnPreBuildEvents = newEvent;
|
|
if (oldEvents != null)
|
|
foreach (var t in oldEvents)
|
|
{
|
|
OnPreBuildEvents += (PreEvent)(t);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepend an event to the post new build events.
|
|
/// </summary>
|
|
/// <param name="newEvent">Post build event</param>
|
|
public static void PrependPostBuildEvent(PostEvent newEvent)
|
|
{
|
|
Delegate[] oldEvents = OnPostBuildEvents?.GetInvocationList();
|
|
OnPostBuildEvents = newEvent;
|
|
if (oldEvents != null)
|
|
foreach (var t in oldEvents)
|
|
{
|
|
OnPostBuildEvents += (PostEvent)(t);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepend an event to the pre update build events.
|
|
/// </summary>
|
|
/// <param name="newEvent">Pre build event</param>
|
|
public static void PrependPreUpdateEvent(PreEvent newEvent)
|
|
{
|
|
Delegate[] oldEvents = OnPreUpdateEvents?.GetInvocationList();
|
|
OnPreUpdateEvents = newEvent;
|
|
if (oldEvents != null)
|
|
foreach (var t in oldEvents)
|
|
{
|
|
OnPreUpdateEvents += (PreEvent)(t);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepend an event to the post update build events.
|
|
/// </summary>
|
|
/// <param name="newEvent">Post build event</param>
|
|
public static void PrependPostUpdateEvent(PostEvent newEvent)
|
|
{
|
|
Delegate[] oldEvents = OnPostUpdateEvents?.GetInvocationList();
|
|
OnPostUpdateEvents = newEvent;
|
|
if (oldEvents != null)
|
|
foreach (var t in oldEvents)
|
|
{
|
|
OnPostUpdateEvents += (PostEvent)(t);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> VerifyBuildVersion(AddressablesDataBuilderInput input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input.AddressableSettings.OverridePlayerVersion))
|
|
{
|
|
Addressables.LogWarning("<b>When using CCD it is recommended that you set a 'Player Version Override' in Addressables Settings.</b> You can have it use the Player build version by setting it to [UnityEditor.PlayerSettings.bundleVersion].");
|
|
Addressables.LogWarning("Documentation on how to disable this warning is available in the example DisableBuildWarnings.cs.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the CCD data source settings.
|
|
/// </summary>
|
|
/// <param name="input">Addressables data builder context</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> RefreshDataSources(AddressablesDataBuilderInput input)
|
|
{
|
|
try
|
|
{
|
|
var projectId = CloudProjectSettings.projectId;
|
|
await ProfileDataSourceSettings.UpdateCCDDataSourcesAsync(projectId, true);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
internal async Task<bool> LoopGroups(AddressableAssetSettings settings, Func<AddressableAssetSettings, AddressableAssetGroup, BundledAssetGroupSchema, Task<bool>> action)
|
|
{
|
|
var tasks = new List<Task<bool>>();
|
|
foreach (var group in settings.groups)
|
|
{
|
|
if (group == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var schema = group.GetSchema<BundledAssetGroupSchema>();
|
|
if (schema == null)
|
|
{
|
|
continue;
|
|
}
|
|
tasks.Add(action(settings, group, schema));
|
|
}
|
|
var results = await Task.WhenAll(tasks.ToArray());
|
|
foreach (var result in results)
|
|
{
|
|
// if any fail, all fail
|
|
if (result == false)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the targeted CCD bucket exists or create it.
|
|
/// </summary>
|
|
/// <param name="input">Addressables data builder context</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> VerifyTargetBucket(AddressablesDataBuilderInput input)
|
|
{
|
|
if (input.AddressableSettings == null)
|
|
{
|
|
string error;
|
|
if (EditorApplication.isUpdating)
|
|
error = "Addressable Asset Settings does not exist. EditorApplication.isUpdating was true.";
|
|
else if (EditorApplication.isCompiling)
|
|
error = "Addressable Asset Settings does not exist. EditorApplication.isCompiling was true.";
|
|
else
|
|
error = "Addressable Asset Settings does not exist. Failed to create.";
|
|
Debug.LogError(error);
|
|
return false;
|
|
}
|
|
|
|
if (!hasRemoteGroups(input.AddressableSettings))
|
|
{
|
|
Debug.LogWarning("No Addressable Asset Groups have been marked remote or the current profile is not using CCD.");
|
|
if (input.AddressableSettings.BuildRemoteCatalog)
|
|
{
|
|
Debug.LogWarning("A remote catalog will be built without any remote Asset Bundles.");
|
|
}
|
|
|
|
}
|
|
|
|
// Reclean directory before every build
|
|
if (Directory.Exists(AddressableAssetSettings.kCCDBuildDataPath))
|
|
{
|
|
Directory.Delete(AddressableAssetSettings.kCCDBuildDataPath, true);
|
|
}
|
|
|
|
return await LoopGroups(input.AddressableSettings, verifyTargetBucket);
|
|
}
|
|
|
|
internal bool hasRemoteGroups(AddressableAssetSettings settings)
|
|
{
|
|
foreach (var group in settings.groups)
|
|
{
|
|
if (group == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var schema = group.GetSchema<BundledAssetGroupSchema>();
|
|
if (schema == null)
|
|
{
|
|
continue;
|
|
}
|
|
var foundGroupType = GetGroupType(settings, schema);
|
|
if (isCCDGroup(foundGroupType))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
internal bool isCCDGroup(ProfileGroupType groupType)
|
|
{
|
|
|
|
if (groupType == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (IsUsingManager(groupType))
|
|
{
|
|
return true;
|
|
}
|
|
return groupType.GroupTypePrefix.StartsWith("CCD");
|
|
}
|
|
|
|
internal async Task<bool> verifyTargetBucket(AddressableAssetSettings settings, AddressableAssetGroup group, BundledAssetGroupSchema schema)
|
|
{
|
|
AddressableAssetSettings.NullifyBundleFileIds(group);
|
|
|
|
// if not using the manager try to lookup the bucket and verify it's not promotion only
|
|
var existingGroupType = GetDefaultGroupType(ProfileDataSourceSettings.GetSettings());
|
|
if (!IsUsingManager(settings, schema))
|
|
{
|
|
if (existingGroupType != null)
|
|
{
|
|
var promotionOnly = IsPromotionOnlyBucket(settings, schema);
|
|
if (promotionOnly)
|
|
{
|
|
Debug.LogError("Cannot upload to Promotion Only bucket.");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// CcdManagedData.ConfigState.Override means it has been overriden by the customer at build time
|
|
if (settings.m_CcdManagedData.State == CcdManagedData.ConfigState.Override)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Target bucket should only be verified if Automatic profile type
|
|
settings.m_CcdManagedData.EnvironmentId = ProfileDataSourceSettings.GetSettings().currentEnvironment.id;
|
|
settings.m_CcdManagedData.EnvironmentName = ProfileDataSourceSettings.GetSettings().currentEnvironment.name;
|
|
|
|
// existing automatic bucket loaded from cache
|
|
if (existingGroupType != null)
|
|
{
|
|
var promotionOnly = IsPromotionOnlyBucket(settings, schema);
|
|
if (promotionOnly)
|
|
{
|
|
Debug.LogError("Cannot upload to Promotion Only bucket.");
|
|
return false;
|
|
}
|
|
settings.m_CcdManagedData.BucketId = existingGroupType
|
|
.GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}").Value;
|
|
settings.m_CcdManagedData.Badge = existingGroupType
|
|
.GetVariableBySuffix($"{nameof(CcdBadge)}{nameof(CcdBadge.Name)}").Value;
|
|
}
|
|
// otherwise try to create
|
|
else
|
|
{
|
|
try
|
|
{
|
|
var api = CcdManagement.Instance;
|
|
var createdBucket = await CreateManagedBucket(api, ProfileDataSourceSettings.GetSettings().currentEnvironment.id,
|
|
EditorUserBuildSettings.activeBuildTarget.ToString());
|
|
if (createdBucket.Attributes.PromoteOnly)
|
|
{
|
|
Debug.LogError("Cannot upload to Promotion Only bucket.");
|
|
return false;
|
|
}
|
|
settings.m_CcdManagedData.BucketId = createdBucket.Id.ToString();
|
|
settings.m_CcdManagedData.Badge = "latest";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Download addressables_content_state.bin from the CCD managed bucket.
|
|
/// </summary>
|
|
/// <param name="input">Addressables data builder context</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> DownloadContentStateBin(AddressablesDataBuilderInput input)
|
|
{
|
|
if (!input.AddressableSettings.BuildRemoteCatalog)
|
|
{
|
|
Debug.LogWarning("Not downloading content state because 'Build Remote Catalog' is not checked in Addressable Asset Settings. This will disable content updates");
|
|
return true;
|
|
}
|
|
|
|
var settings = input.AddressableSettings;
|
|
var groupType = getRemoteCatalogGroupType(settings);
|
|
if (groupType == null)
|
|
{
|
|
Debug.LogError("Could not find remote catalog paths");
|
|
return false;
|
|
}
|
|
|
|
if (!this.isCCDGroup(groupType))
|
|
{
|
|
Debug.LogError("Content state could not be downloaded as the remote catalog is not targeting CCD");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
SetEnvironmentId(settings, groupType);
|
|
var bucketId = GetBucketId(settings, groupType);
|
|
if (bucketId == null)
|
|
{
|
|
Debug.LogError("Content state could not be downloaded as no bucket was specified. This is populated for managed profiles in the VerifyBucket event.");
|
|
return false;
|
|
}
|
|
var api = CcdManagement.Instance;
|
|
var ccdEntry = await GetEntryByPath(api, new Guid(bucketId), k_ContentStatePath);
|
|
if (ccdEntry != null)
|
|
{
|
|
var contentStream = await api.GetContentAsync(new EntryOptions(new Guid(bucketId), ccdEntry.Entryid));
|
|
|
|
var contentStatePath = Path.Combine(settings.GetContentStateBuildPath(), k_ContentStatePath);
|
|
if (!Directory.Exists(contentStatePath))
|
|
Directory.CreateDirectory(Path.GetDirectoryName(contentStatePath));
|
|
else if (File.Exists(contentStatePath))
|
|
File.Delete(contentStatePath);
|
|
|
|
using (var fileStream = File.Create(contentStatePath))
|
|
{
|
|
contentStream.CopyTo(fileStream);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Upload content to the CCD managed bucket and create a release.
|
|
/// </summary>
|
|
/// <param name="input">Addressables data builder context</param>
|
|
/// <param name="result">Addressables build result</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> UploadAndRelease(AddressablesDataBuilderInput input,
|
|
AddressablesPlayerBuildResult result)
|
|
{
|
|
// Verify files exist that need uploading
|
|
var foundRemoteContent = result.FileRegistry?.GetFilePaths()
|
|
.Any(path => path.StartsWith(AddressableAssetSettings.kCCDBuildDataPath)) == true;
|
|
|
|
if (!foundRemoteContent)
|
|
{
|
|
Debug.LogWarning(
|
|
"Skipping upload and release as no remote content was found to upload. Ensure you have at least one content group's 'Build & Load Path' set to Remote.");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
//Getting files
|
|
Debug.Log("Creating and uploading entries");
|
|
var startDirectory = new DirectoryInfo(AddressableAssetSettings.kCCDBuildDataPath);
|
|
var buildData = CreateData(startDirectory);
|
|
|
|
|
|
//Creating a release for each bucket
|
|
var defaultEnvironmentId = ProfileDataSourceSettings.GetSettings().currentEnvironment.id;
|
|
|
|
await UploadAndRelease(CcdManagement.Instance, input.AddressableSettings, defaultEnvironmentId, buildData);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private ProfileGroupType getRemoteCatalogGroupType(AddressableAssetSettings settings)
|
|
{
|
|
var buildPath = settings.RemoteCatalogBuildPath;
|
|
var loadPath = settings.RemoteCatalogLoadPath;
|
|
var groupType = GetGroupType(settings, buildPath, loadPath);
|
|
return groupType;
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Upload addressables_content_state.bin to the CCD managed bucket.
|
|
/// </summary>
|
|
/// <param name="input">Addressables data builder context</param>
|
|
/// <param name="result">Addressables build result</param>
|
|
/// <returns></returns>
|
|
public async Task<bool> UploadContentState(AddressablesDataBuilderInput input,
|
|
AddressablesPlayerBuildResult result)
|
|
{
|
|
|
|
if (!input.AddressableSettings.BuildRemoteCatalog)
|
|
{
|
|
Debug.LogWarning("Not uploading content state, because 'Build Remote Catalog' is not checked in Addressable Asset Settings. This will disable content updates");
|
|
return true;
|
|
}
|
|
|
|
var settings = input.AddressableSettings;
|
|
var groupType = getRemoteCatalogGroupType(settings);
|
|
if (groupType == null)
|
|
{
|
|
Debug.LogError("Could not find remote catalog paths");
|
|
return false;
|
|
}
|
|
|
|
if (!this.isCCDGroup(groupType))
|
|
{
|
|
Debug.LogError("Content state could not be uploaded as the remote catalog is not targeting CCD");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
SetEnvironmentId(settings, groupType);
|
|
var bucketId = GetBucketId(settings, groupType);
|
|
if (bucketId == null)
|
|
{
|
|
Debug.LogError("Content state could not be uploaded as no bucket was specified. This is populated for managed profiles in the VerifyBucket event.");
|
|
return false;
|
|
}
|
|
var api = CcdManagement.Instance;
|
|
|
|
var contentStatePath = Path.Combine(settings.GetContentStateBuildPath(), k_ContentStatePath);
|
|
if (!File.Exists(contentStatePath))
|
|
{
|
|
Debug.LogError($"Content state file is missing {contentStatePath}");
|
|
return false;
|
|
}
|
|
var contentHash = AddressableAssetUtility.GetMd5Hash(contentStatePath);
|
|
|
|
using (var stream = File.OpenRead(contentStatePath))
|
|
{
|
|
|
|
var entryModelOptions = new EntryModelOptions(k_ContentStatePath, contentHash, (int)stream.Length)
|
|
{
|
|
UpdateIfExists = true
|
|
};
|
|
var createdEntry = await api.CreateOrUpdateEntryByPathAsync(
|
|
new EntryByPathOptions(new Guid(bucketId), k_ContentStatePath),
|
|
entryModelOptions);
|
|
|
|
var uploadContentOptions = new UploadContentOptions(
|
|
new Guid(bucketId), createdEntry.Entryid, stream);
|
|
await api.UploadContentAsync(uploadContentOptions);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
internal bool IsPromotionOnlyBucket(AddressableAssetSettings settings, BundledAssetGroupSchema schema)
|
|
{
|
|
if (schema != null)
|
|
{
|
|
var foundGroupType = GetGroupType(settings, schema);
|
|
if (foundGroupType != null && foundGroupType.GroupTypePrefix.StartsWith("CCD"))
|
|
{
|
|
if (bool.Parse(foundGroupType.GetVariableBySuffix(nameof(CcdBucket.Attributes.PromoteOnly)).Value))
|
|
{
|
|
Debug.LogError("Cannot upload to Promotion Only bucket.");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|
|
return false;
|
|
}
|
|
|
|
internal ProfileGroupType GetGroupType(AddressableAssetSettings settings, BundledAssetGroupSchema schema)
|
|
{
|
|
|
|
return GetGroupType(settings, schema.BuildPath, schema.LoadPath);
|
|
}
|
|
|
|
internal ProfileGroupType GetGroupType(AddressableAssetSettings settings, ProfileValueReference buildPath, ProfileValueReference loadPath)
|
|
{
|
|
// we need the "unresolved" value since we're tring to match it to its original type
|
|
Debug.Log("Loading from active profile id " + settings.activeProfileId);
|
|
var buildPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, buildPath.Id);
|
|
var loadPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, loadPath.Id);
|
|
Debug.Log(loadPathValue);
|
|
if (buildPathValue == null || loadPathValue == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var groupType = new ProfileGroupType("temp");
|
|
groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, buildPathValue));
|
|
groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, loadPathValue));
|
|
return ProfileDataSourceSettings.GetSettings().FindGroupType(groupType);
|
|
}
|
|
|
|
|
|
internal bool IsUsingManager(AddressableAssetSettings settings, BundledAssetGroupSchema schema)
|
|
{
|
|
var groupType = GetGroupType(settings, schema);
|
|
return IsUsingManager(groupType);
|
|
}
|
|
|
|
internal bool IsUsingManager(ProfileGroupType groupType)
|
|
{
|
|
if (groupType == null)
|
|
{
|
|
return false;
|
|
}
|
|
return groupType.GroupTypePrefix == AddressableAssetSettings.CcdManagerGroupTypePrefix;
|
|
}
|
|
|
|
internal void SetEnvironmentId(AddressableAssetSettings settings, ProfileGroupType groupType)
|
|
{
|
|
string environmentId = null;
|
|
if (!IsUsingManager(groupType))
|
|
{
|
|
// if not using the manager load the bucketID from the group type
|
|
environmentId = groupType.GetVariableBySuffix($"{nameof(ProfileDataSourceSettings.Environment)}{nameof(ProfileDataSourceSettings.Environment.id)}").Value;
|
|
}
|
|
else if (settings.m_CcdManagedData != null)
|
|
{
|
|
environmentId = settings.m_CcdManagedData.EnvironmentId;
|
|
}
|
|
|
|
if (environmentId == null)
|
|
{
|
|
throw new Exception("unable to determine environment ID.");
|
|
}
|
|
|
|
CcdManagement.SetEnvironmentId(environmentId);
|
|
}
|
|
|
|
internal string GetBucketId(AddressableAssetSettings settings, ProfileGroupType groupType)
|
|
{
|
|
if (!IsUsingManager(groupType))
|
|
{
|
|
// if not using the manager load the bucketID from the group type
|
|
return groupType.GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}").Value;
|
|
}
|
|
if (settings.m_CcdManagedData != null)
|
|
{
|
|
return settings.m_CcdManagedData.BucketId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static ProfileGroupType GetDefaultGroupType(ProfileDataSourceSettings dataSourceSettings)
|
|
{
|
|
//Find existing bucketId
|
|
var groupTypes = dataSourceSettings.GetGroupTypesByPrefix(string.Join(
|
|
ProfileGroupType.k_PrefixSeparator.ToString(), "CCD", dataSourceSettings.currentEnvironment.projectGenesisId,
|
|
dataSourceSettings.currentEnvironment.id));
|
|
return groupTypes.FirstOrDefault(gt =>
|
|
gt.GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Name)}").Value ==
|
|
EditorUserBuildSettings.activeBuildTarget.ToString());
|
|
}
|
|
|
|
async Task<CcdBucket> CreateManagedBucket(ICcdManagementServiceSdk api, string envId, string bucketName)
|
|
{
|
|
CcdBucket ccdBucket;
|
|
try
|
|
{
|
|
CcdManagement.SetEnvironmentId(envId);
|
|
ccdBucket = await api.CreateBucketAsync(
|
|
new CreateBucketOptions(bucketName));
|
|
}
|
|
catch (CcdManagementException e)
|
|
{
|
|
if (e.ErrorCode == CcdManagementErrorCodes.AlreadyExists)
|
|
{
|
|
var buckets = await ProfileDataSourceSettings.GetAllBucketsAsync(envId);
|
|
ccdBucket = buckets.First(bucket =>
|
|
bucket.Value.Name == EditorUserBuildSettings.activeBuildTarget.ToString()).Value;
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
return ccdBucket;
|
|
}
|
|
|
|
async Task<CcdEntry> GetEntryByPath(ICcdManagementServiceSdk api, Guid bucketId, string path)
|
|
{
|
|
CcdEntry ccdEntry = null;
|
|
try
|
|
{
|
|
ccdEntry = await api.GetEntryByPathAsync(new EntryByPathOptions(bucketId, path));
|
|
}
|
|
catch (CcdManagementException e)
|
|
{
|
|
if (e.ErrorCode != CommonErrorCodes.NotFound)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
return ccdEntry;
|
|
}
|
|
|
|
CcdBuildDataFolder CreateData(DirectoryInfo startDirectory)
|
|
{
|
|
var buildDataFolder = new CcdBuildDataFolder
|
|
{
|
|
Name = AddressableAssetSettings.kCCDBuildDataPath,
|
|
Location = startDirectory.FullName
|
|
};
|
|
buildDataFolder.GetChildren(startDirectory);
|
|
return buildDataFolder;
|
|
}
|
|
|
|
int StartProgress(string description)
|
|
{
|
|
#if UNITY_2020_1_OR_NEWER
|
|
return Progress.Start("CCD", description, Progress.Options.Managed);
|
|
#else
|
|
Debug.Log(description);
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
void RemoveProgress(int progressId)
|
|
{
|
|
#if UNITY_2020_1_OR_NEWER
|
|
Progress.Remove(progressId);
|
|
#endif
|
|
}
|
|
|
|
void ReportProgress(int progressId, float progress, string message)
|
|
{
|
|
#if UNITY_2020_1_OR_NEWER
|
|
Progress.Report(progressId, progress, message);
|
|
#else
|
|
Debug.Log($"[{progress}] {message}");
|
|
#endif
|
|
}
|
|
|
|
// Do not use Addressable.Log in this method as it may not be executed on the main thread
|
|
async Task UploadAndRelease(ICcdManagementServiceSdk api, AddressableAssetSettings settings, string defaultEnvironmentId, CcdBuildDataFolder buildData)
|
|
{
|
|
var progressId = StartProgress("Upload and Release");
|
|
try
|
|
{
|
|
foreach (var env in buildData.Environments)
|
|
{
|
|
CcdManagement.SetEnvironmentId(env.Name);
|
|
|
|
if (env.Name == ProfileDataSourceSettings.MANAGED_ENVIRONMENT)
|
|
{
|
|
CcdManagement.SetEnvironmentId(defaultEnvironmentId);
|
|
}
|
|
|
|
foreach (var bucket in env.Buckets)
|
|
{
|
|
Guid bucketId;
|
|
var bucketIdString = bucket.Name == ProfileDataSourceSettings.MANAGED_BUCKET
|
|
? settings.m_CcdManagedData.BucketId
|
|
: bucket.Name;
|
|
if (String.IsNullOrEmpty(bucketIdString))
|
|
{
|
|
Debug.LogError($"Invalid bucket ID for {bucket.Name}");
|
|
continue;
|
|
}
|
|
bucketId = Guid.Parse(bucketIdString);
|
|
|
|
foreach (var badge in bucket.Badges)
|
|
{
|
|
if (badge.Name == ProfileDataSourceSettings.MANAGED_BADGE)
|
|
{
|
|
badge.Name = "latest";
|
|
}
|
|
var entries = new List<CcdReleaseEntryCreate>();
|
|
var total = badge.Files.Count();
|
|
for (var i = 0; i < total; i++)
|
|
{
|
|
var file = badge.Files[i];
|
|
var contentHash = AddressableAssetUtility.GetMd5Hash(file.FullName);
|
|
using (var stream = File.OpenRead(file.FullName))
|
|
{
|
|
var entryPath = file.Name;
|
|
var entryModelOptions = new EntryModelOptions(entryPath, contentHash, (int)stream.Length)
|
|
{
|
|
UpdateIfExists = true
|
|
};
|
|
ReportProgress(progressId, (i + 1) / total, $"Creating Entry {entryPath}");
|
|
var createdEntry = await api.CreateOrUpdateEntryByPathAsync(new EntryByPathOptions(bucketId, entryPath),
|
|
entryModelOptions).ConfigureAwait(false);
|
|
Debug.Log($"Created Entry {entryPath}");
|
|
|
|
ReportProgress(progressId, (i + 1) / total, $"Uploading Entry {entryPath}");
|
|
var uploadContentOptions = new UploadContentOptions(bucketId, createdEntry.Entryid, stream);
|
|
await api.UploadContentAsync(uploadContentOptions).ConfigureAwait(false);
|
|
|
|
ReportProgress(progressId, (i + 1) / total, $"Uploaded Entry {entryPath}");
|
|
entries.Add(new CcdReleaseEntryCreate(createdEntry.Entryid, createdEntry.CurrentVersionid));
|
|
}
|
|
}
|
|
|
|
// Add content_sate.bin to release if present
|
|
var contentStateEntry = await GetEntryByPath(api, bucketId, k_ContentStatePath);
|
|
if (contentStateEntry != null)
|
|
entries.Add(new CcdReleaseEntryCreate(contentStateEntry.Entryid, contentStateEntry.CurrentVersionid));
|
|
|
|
//Creating release
|
|
ReportProgress(progressId, total, "Creating release");
|
|
Debug.Log("Creating release.");
|
|
var release = await api.CreateReleaseAsync(new CreateReleaseOptions(bucketId)
|
|
{
|
|
Entries = entries,
|
|
Notes = $"Automated release created for {badge.Name}"
|
|
}).ConfigureAwait(false);
|
|
Debug.Log($"Release {release.Releaseid} created.");
|
|
|
|
//Don't update latest badge (as it always updates)
|
|
if (badge.Name != "latest")
|
|
{
|
|
ReportProgress(progressId, total, "Updating badge");
|
|
Debug.Log("Updating badge.");
|
|
var badgeRes = await api.AssignBadgeAsync(new AssignBadgeOptions(bucketId, badge.Name, release.Releaseid))
|
|
.ConfigureAwait(false);
|
|
Debug.Log($"Badge {badgeRes.Name} updated.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError(e.ToString());
|
|
}
|
|
finally
|
|
{
|
|
RemoveProgress(progressId);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
#endif
|
|
|