WwiseReconcileCommandlet.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. /*******************************************************************************
  2. The content of this file includes portions of the proprietary AUDIOKINETIC Wwise
  3. Technology released in source code form as part of the game integration package.
  4. The content of this file may not be used without valid licenses to the
  5. AUDIOKINETIC Wwise Technology.
  6. Note that the use of the game engine is subject to the Unreal(R) Engine End User
  7. License Agreement at https://www.unrealengine.com/en-US/eula/unreal
  8. License Usage
  9. Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use
  10. this file in accordance with the end user license agreement provided with the
  11. software or, alternatively, in accordance with the terms contained
  12. in a written agreement between you and Audiokinetic Inc.
  13. Copyright (c) 2023 Audiokinetic Inc.
  14. *******************************************************************************/
  15. #include "Wwise/WwiseReconcileCommandlet.h"
  16. #include "AkAudioEvent.h"
  17. #include "AkAudioType.h"
  18. #include "AkAuxBus.h"
  19. #include "AkEffectShareSet.h"
  20. #include "AkInitBank.h"
  21. #include "AkSettings.h"
  22. #include "AkStateValue.h"
  23. #include "AkSwitchValue.h"
  24. #include "AkTrigger.h"
  25. #include "AkUnrealAssetDataHelper.h"
  26. #include "AssetRegistry/AssetRegistryModule.h"
  27. #include "AssetToolsModule.h"
  28. #include "FileHelpers.h"
  29. #include "ObjectTools.h"
  30. #include "AkAssetFactories.h"
  31. #include "PackageTools.h"
  32. #include "Wwise/Stats/Reconcile.h"
  33. #include "Wwise/WwiseReconcile.h"
  34. #include "Wwise/WwiseProjectDatabase.h"
  35. static constexpr auto ModesParam = TEXT("modes");
  36. static constexpr auto CreateOption = TEXT("create");
  37. static constexpr auto UpdateOption = TEXT("update");
  38. static constexpr auto DeleteOption = TEXT("delete");
  39. static constexpr auto AllOption = TEXT("all");
  40. static constexpr auto HelpOption = TEXT("help");
  41. UWwiseReconcileCommandlet::UWwiseReconcileCommandlet()
  42. {
  43. IsClient = false;
  44. IsEditor = true;
  45. IsServer = false;
  46. LogToConsole = true;
  47. HelpDescription = TEXT("Commandlet to generate Wwise SoundBanks.");
  48. HelpParamNames.Add(ModesParam);
  49. HelpParamDescriptions.Add(FString::Format(TEXT("Comma separated list of operations to perform on assets.\n"
  50. "{0}: Create Unreal assets from the Generated SoundBanks\n"
  51. "{1}: Update existing Unreal assets. This updates the asset name as well as its metadata.\n"
  52. "{2}: Delete Unreal assets that no longer exist in the Generated SoundBanks\n"
  53. "{3}: Fully reconcile Unreal assets"),
  54. {CreateOption, UpdateOption, DeleteOption, AllOption}));
  55. HelpParamNames.Add("?, help");
  56. HelpParamDescriptions.Add(TEXT("Display help"));
  57. HelpUsage = FString::Format(TEXT("<Editor.exe> <path_to_uproject> -run=WwiseReconcileCommandlet -modes={0},{1},{2},{3}"), {CreateOption, DeleteOption, UpdateOption, AllOption});
  58. }
  59. int32 UWwiseReconcileCommandlet::Main(const FString& Params)
  60. {
  61. int32 Result = 0;
  62. TMap<FString, FString> ParsedParams;
  63. ParseCommandLine(*Params, CmdTokens, CmdSwitches, ParsedParams);
  64. if( Params.Contains(TEXT("?")) || Params.Contains(HelpOption) )
  65. {
  66. PrintHelp();
  67. return Result;
  68. }
  69. EWwiseReconcileOperationFlags ReconcileOperationFlags = EWwiseReconcileOperationFlags::None;
  70. if (ParsedParams.Contains(ModesParam))
  71. {
  72. FString ModeStr = ParsedParams.FindRef("modes");
  73. TArray<FString> Modes;
  74. if (ModeStr.Contains(TEXT(",")))
  75. {
  76. ModeStr.ParseIntoArray(Modes, TEXT(","), true);
  77. }
  78. else
  79. {
  80. Modes.Add(ModeStr);
  81. }
  82. if (Modes.Num() == 0)
  83. {
  84. PrintHelp();
  85. Result = -1;
  86. return Result;
  87. }
  88. if (Modes.Contains(CreateOption))
  89. {
  90. ReconcileOperationFlags |= EWwiseReconcileOperationFlags::Create;
  91. }
  92. if (Modes.Contains(UpdateOption))
  93. {
  94. ReconcileOperationFlags |= EWwiseReconcileOperationFlags::UpdateExisting;
  95. }
  96. if (Modes.Contains(DeleteOption))
  97. {
  98. ReconcileOperationFlags |= EWwiseReconcileOperationFlags::Delete;
  99. }
  100. if (Modes.Contains(AllOption))
  101. {
  102. ReconcileOperationFlags |= EWwiseReconcileOperationFlags::All;
  103. }
  104. }
  105. if (ReconcileOperationFlags == EWwiseReconcileOperationFlags::None)
  106. {
  107. UE_LOG(LogWwiseReconcile, Error, TEXT("No Reconcile mode specified"))
  108. PrintHelp();
  109. Result = -1;
  110. return Result;
  111. }
  112. GetAllAssets();
  113. GetAssetChanges(ReconcileOperationFlags);
  114. for (const auto& Asset : AssetsToCreate)
  115. {
  116. UE_LOG(LogWwiseReconcile, Verbose, TEXT("New Asset %s will be created."), *Asset.WwiseAnyRef.GetName().ToString());
  117. }
  118. for (const auto& Asset: AssetsToUpdate)
  119. {
  120. UE_LOG(LogWwiseReconcile, Verbose, TEXT("Asset %s will be updated."), *Asset.GetFullName());
  121. }
  122. for (const auto& Asset: AssetsToDelete)
  123. {
  124. UE_LOG(LogWwiseReconcile, Verbose, TEXT("Asset %s of type %s will be deleted."), *Asset.GetFullName(), *AkUnrealAssetDataHelper::GetAssetClassName(Asset).ToString());
  125. }
  126. int NumAssetsToReconcile = AssetsToCreate.Num() + AssetsToUpdate.Num() + AssetsToDelete.Num();
  127. if (NumAssetsToReconcile > 0)
  128. {
  129. UE_LOG(LogWwiseReconcile, Display, TEXT("Reconciling %d Wwise Asset(s)..."), NumAssetsToReconcile)
  130. if (!ReconcileAssets())
  131. {
  132. Result = -1;
  133. UE_LOG(LogWwiseReconcile, Error, TEXT("Failed to reconcile assets. Check the log for details"));
  134. }
  135. }
  136. else
  137. {
  138. UE_LOG(LogWwiseReconcile, Display, TEXT("No Wwise Assets to Reconcile..."))
  139. }
  140. UE_LOG(LogWwiseReconcile, Display, TEXT("Finished reconciling Wwise Assets..."))
  141. return Result;
  142. }
  143. void UWwiseReconcileCommandlet::PrintHelp()
  144. {
  145. UE_LOG(LogWwiseReconcile, Display, TEXT("%s"), *HelpDescription);
  146. UE_LOG(LogWwiseReconcile, Display, TEXT("Usage: %s"), *HelpUsage);
  147. UE_LOG(LogWwiseReconcile, Display, TEXT("Parameters:"));
  148. for (int32 i = 0; i < HelpParamNames.Num(); ++i)
  149. {
  150. UE_LOG(LogWwiseReconcile, Display, TEXT("\t- %s: %s"), *HelpParamNames[i], *HelpParamDescriptions[i]);
  151. }
  152. UE_LOG(LogWwiseReconcile, Display, TEXT("For more information, see %s"), *HelpWebLink);
  153. }
  154. void UWwiseReconcileCommandlet::GetAllAssets()
  155. {
  156. AssetRegistryModule = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
  157. "AssetRegistry");
  158. AssetToolsModule = &FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
  159. if (!AssetToolsModule)
  160. {
  161. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not load the AssetTools Module"));
  162. return;
  163. }
  164. if (!AssetRegistryModule)
  165. {
  166. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not load the AssetRegistry Module"));
  167. return;
  168. }
  169. #if UE_5_1_OR_LATER
  170. AssetRegistryModule->Get().GetAssetsByClass(UAkAudioType::StaticClass()->GetClassPathName(), Assets, true);
  171. #else
  172. AssetRegistryModule->Get().GetAssetsByClass(UAkAudioType::StaticClass()->GetFName(), Assets, true);
  173. #endif
  174. GuidAssetMap.Empty();
  175. for (const FAssetData& AssetData : Assets)
  176. {
  177. if (UAkAudioType* AkAudioAsset = Cast<UAkAudioType>(AssetData.GetAsset()))
  178. {
  179. auto AssetGuid = AkAudioAsset->GetWwiseGuid();
  180. // Exclude the Init bank, and invalid assets
  181. if (AssetGuid.IsValid())
  182. {
  183. GuidAssetMap.Add(AssetGuid, AssetData);
  184. }
  185. else if (!AkUnrealAssetDataHelper::AssetOfType<UAkInitBank>(AssetData))
  186. {
  187. InvalidAssets.Add(AssetData);
  188. }
  189. }
  190. }
  191. ProjectDatabase = FWwiseProjectDatabase::Get();
  192. if (UNLIKELY(!ProjectDatabase))
  193. {
  194. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not load project database"));
  195. }
  196. ProjectDatabase->UpdateDataStructure();
  197. GuidWwiseMetadataMap.Empty();
  198. }
  199. void UWwiseReconcileCommandlet::GetAssetChanges(EWwiseReconcileOperationFlags OperationFlags)
  200. {
  201. ProjectDatabase = FWwiseProjectDatabase::Get();
  202. ProjectDatabase->UpdateDataStructure();
  203. if (UNLIKELY(!ProjectDatabase || !ProjectDatabase->IsProjectDatabaseParsed()))
  204. {
  205. UE_LOG(LogWwiseReconcile, Error, TEXT("No data loaded from Wwise project database"));
  206. return;
  207. }
  208. FWwiseDataStructureScopeLock DataStructure(*ProjectDatabase);
  209. TSet<FGuid> FoundAssets;
  210. // Check to make sure there are no issues getting data for the CurrentPlatform
  211. if (DataStructure.GetSoundBanks().Num() == 0)
  212. {
  213. FName PlatformName = DataStructure.GetCurrentPlatform().GetPlatformName();
  214. UE_LOG(LogWwiseReconcile, Error, TEXT("No data loaded from Wwise project database for the curent platform %s"), *PlatformName.ToString());
  215. return;
  216. }
  217. if (DataStructure.GetCurrentPlatformData()->Guids.Num() == 0)
  218. {
  219. FName PlatformName = DataStructure.GetCurrentPlatform().GetPlatformName();
  220. UE_LOG(LogWwiseReconcile, Error, TEXT("No data loaded from Wwise project database for the curent platform %s"), *PlatformName.ToString());
  221. return;
  222. }
  223. for (const auto& WwiseRef : DataStructure.GetCurrentPlatformData()->Guids)
  224. {
  225. const FWwiseAnyRef WwiseRefValue = WwiseRef.Value;
  226. EWwiseRefType RefType = WwiseRefValue.GetType();
  227. UClass* RefClass = GetUClassFromWwiseRefType(RefType);
  228. FGuid RefGuid = WwiseRefValue.GetGuid();
  229. if (!RefClass || FoundAssets.Contains(RefGuid))
  230. {
  231. continue;
  232. }
  233. FAssetData Asset = GuidAssetMap.FindRef(WwiseRefValue.GetGuid());
  234. if (!Asset.IsValid())
  235. {
  236. if (RefClass && EnumHasAnyFlags(OperationFlags, EWwiseReconcileOperationFlags::Create))
  237. {
  238. AssetsToCreate.Add({ WwiseRefValue });
  239. }
  240. }
  241. else if (EnumHasAnyFlags(OperationFlags, EWwiseReconcileOperationFlags::UpdateExisting))
  242. {
  243. UAkAudioType* AkAudioAsset = Cast<UAkAudioType>(Asset.GetAsset());
  244. if (RefClass && AkAudioAsset)
  245. {
  246. // Ignore SoundBanks
  247. if (RefType != EWwiseRefType::SoundBanksInfo)
  248. {
  249. FName AssetName = AkUnrealAssetDataHelper::GetAssetDefaultName(&WwiseRefValue);
  250. if (AkAudioAsset->IsAssetOutOfDate(WwiseRefValue))
  251. {
  252. AssetsToUpdate.Add(Asset);
  253. }
  254. if (AkAudioAsset->GetName() != AssetName.ToString())
  255. {
  256. AssetsToRename.Add(Asset);
  257. }
  258. }
  259. }
  260. }
  261. FoundAssets.Add(RefGuid);
  262. GuidAssetMap.Remove(RefGuid);
  263. }
  264. if (EnumHasAnyFlags(OperationFlags, EWwiseReconcileOperationFlags::Delete))
  265. {
  266. for (const auto& GuidAsset : GuidAssetMap)
  267. {
  268. if(!FoundAssets.Contains(GuidAsset.Key))
  269. {
  270. AssetsToDelete.Add(GuidAsset.Value);
  271. }
  272. }
  273. for (const auto& Asset : InvalidAssets)
  274. {
  275. UAkAudioType* AkAudioAsset = Cast<UAkAudioType>(Asset.GetAsset());
  276. if (!AkAudioAsset->ObjectIsInSoundBanks())
  277. {
  278. AssetsToDelete.Add(Asset);
  279. }
  280. }
  281. GuidAssetMap.Empty();
  282. InvalidAssets.Empty();
  283. }
  284. if (EnumHasAnyFlags(OperationFlags, EWwiseReconcileOperationFlags::UpdateExisting))
  285. {
  286. for (const auto& Asset : InvalidAssets)
  287. {
  288. UAkAudioType* AkAudioAsset = Cast<UAkAudioType>(Asset.GetAsset());
  289. if (AkAudioAsset->ObjectIsInSoundBanks())
  290. {
  291. AssetsToUpdate.Add(Asset);
  292. AssetsToRename.Add(Asset);
  293. }
  294. }
  295. }
  296. }
  297. bool UWwiseReconcileCommandlet::ReconcileAssets()
  298. {
  299. bool Succeeded = true;
  300. if (AssetsToCreate.Num() != 0 && CreateAssets().Num() == 0)
  301. {
  302. UE_LOG(LogWwiseReconcile, Warning, TEXT("No New AkAudioType assets created"));
  303. Succeeded = false;
  304. }
  305. if (AssetsToUpdate.Num() != 0 && UpdateExistingAssets().Num() == 0)
  306. {
  307. UE_LOG(LogWwiseReconcile, Warning, TEXT("Failed to consolidate existing AkAudioType assets"));
  308. Succeeded = false;
  309. }
  310. if (AssetsToDelete.Num() != 0 && DeleteAssets() <= 0)
  311. {
  312. UE_LOG(LogWwiseReconcile, Warning, TEXT("Failed to delete outdated AkAudioType assets"))
  313. Succeeded = false;
  314. }
  315. return Succeeded;
  316. }
  317. TArray<FAssetData> UWwiseReconcileCommandlet::CreateAssets()
  318. {
  319. check(IsInGameThread());
  320. TArray<UPackage*> PackagesToSave;
  321. TArray<FAssetData> NewAssets;
  322. for (const auto& Asset : AssetsToCreate)
  323. {
  324. const FWwiseAnyRef WwiseRef = Asset.WwiseAnyRef;
  325. FName AssetName = AkUnrealAssetDataHelper::GetAssetDefaultName(&WwiseRef);
  326. FString AssetPackagePath = AkUnrealAssetDataHelper::GetAssetDefaultPackagePath(&WwiseRef);
  327. AssetPackagePath = UPackageTools::SanitizePackageName(AssetPackagePath);
  328. UClass* NewAssetClass = GetUClassFromWwiseRefType(WwiseRef.GetType());
  329. if(!NewAssetClass)
  330. {
  331. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not determine which type of asset to create for '%s' in '%s'."), *AssetName.ToString(), *AssetPackagePath);
  332. continue;
  333. }
  334. UE_LOG(LogWwiseReconcile, Verbose, TEXT("Creating new asset '%s' in '%s'."), *AssetName.ToString(), *AssetPackagePath);
  335. UAkAudioType* NewAkAudioObject = Cast<UAkAudioType>(
  336. AssetToolsModule->Get().CreateAsset(AssetName.ToString(), AssetPackagePath, NewAssetClass, nullptr));
  337. if (!NewAkAudioObject)
  338. {
  339. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not save asset %s"), *AssetName.ToString());
  340. continue;
  341. }
  342. NewAkAudioObject->FillInfo(WwiseRef);
  343. NewAssets.Add(FAssetData(NewAkAudioObject));
  344. UE_LOG(LogWwiseReconcile, Verbose, TEXT("Created asset %s"), *AssetName.ToString());
  345. PackagesToSave.Add(NewAkAudioObject->GetPackage());
  346. }
  347. AssetsToCreate.Empty();
  348. if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, false))
  349. {
  350. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not save packages"));
  351. return {};
  352. }
  353. return NewAssets;
  354. }
  355. TArray<FAssetData> UWwiseReconcileCommandlet::UpdateExistingAssets()
  356. {
  357. check(IsInGameThread());
  358. TArray<FAssetRenameData> AssetsToRenameData;
  359. TArray<UPackage*> PackagesToSave;
  360. TArray<FAssetData> UpdatedAssets;
  361. for (const auto& AssetData: AssetsToUpdate)
  362. {
  363. if (auto AkAudioAsset = Cast<UAkAudioType>(AssetData.GetAsset()))
  364. {
  365. AkAudioAsset->FillInfo();
  366. FAssetData NewAssetData = FAssetData(AkAudioAsset);
  367. PackagesToSave.Add(NewAssetData.GetPackage());
  368. UpdatedAssets.Add(NewAssetData);
  369. AkAudioAsset->MarkPackageDirty();
  370. }
  371. }
  372. for (const auto& AssetData: AssetsToRename)
  373. {
  374. if (auto AkAudioAsset = Cast<UAkAudioType>(AssetData.GetAsset()))
  375. {
  376. FName NewAssetName = AkAudioAsset->GetAssetDefaultName();
  377. FAssetRenameData AssetRenameData(AssetData.GetAsset(), AssetData.PackagePath.ToString(), NewAssetName.ToString());
  378. AssetsToRenameData.Add(AssetRenameData);
  379. }
  380. }
  381. AssetsToUpdate.Empty();
  382. AssetsToRename.Empty();
  383. if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, false))
  384. {
  385. UE_LOG(LogWwiseReconcile, Error, TEXT("Failed to save updated Wwise assets."))
  386. return {};
  387. }
  388. if (AssetsToRenameData.Num() > 0 && !AssetToolsModule->Get().RenameAssets(AssetsToRenameData))
  389. {
  390. UE_LOG(LogWwiseReconcile, Error, TEXT("Failed to rename updated Wwise assets."))
  391. }
  392. return UpdatedAssets;
  393. }
  394. int32 UWwiseReconcileCommandlet::DeleteAssets()
  395. {
  396. check(IsInGameThread());
  397. if (AssetsToDelete.Num() == 0)
  398. {
  399. return 0;
  400. }
  401. TArray<UObject*> ObjectsToDelete;
  402. for (const auto& Asset : AssetsToDelete)
  403. {
  404. ObjectsToDelete.Add(Asset.GetAsset());
  405. }
  406. int32 NumDeletedObjects = ObjectTools::ForceDeleteObjects(ObjectsToDelete, false);
  407. if (NumDeletedObjects != ObjectsToDelete.Num())
  408. {
  409. UE_LOG(LogWwiseReconcile, Error, TEXT("Could not delete assets. Verify that none of the assets are still being referenced."))
  410. }
  411. AssetsToDelete.Empty();
  412. return NumDeletedObjects;
  413. }
  414. UClass* UWwiseReconcileCommandlet::GetUClassFromWwiseRefType(EWwiseRefType RefType)
  415. {
  416. switch (RefType)
  417. {
  418. case EWwiseRefType::Event:
  419. return UAkAudioEvent::StaticClass();
  420. case EWwiseRefType::AuxBus:
  421. return UAkAuxBus::StaticClass();
  422. case EWwiseRefType::AcousticTexture:
  423. return UAkAcousticTexture::StaticClass();
  424. case EWwiseRefType::State:
  425. return UAkStateValue::StaticClass();
  426. case EWwiseRefType::Switch:
  427. return UAkSwitchValue::StaticClass();
  428. case EWwiseRefType::GameParameter:
  429. return UAkRtpc::StaticClass();
  430. case EWwiseRefType::Trigger:
  431. return UAkTrigger::StaticClass();
  432. case EWwiseRefType::PluginShareSet:
  433. return UAkEffectShareSet::StaticClass();
  434. case EWwiseRefType::None:
  435. return nullptr;
  436. default:
  437. return nullptr;
  438. }
  439. }