CARLA
Loading...
Searching...
No Matches
ContentPackManager.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
8#include "Carla.h"
12
13#include <util/ue-header-guard-begin.h>
14#include "AssetRegistry/AssetRegistryState.h"
15#include "AssetRegistry/IAssetRegistry.h"
16#include "Engine/Engine.h"
17#include "Engine/LevelStreaming.h"
18#include "Engine/World.h"
19#include "GenericPlatform/GenericPlatformProperties.h"
20#include "HAL/FileManager.h"
21#include "HAL/PlatformMisc.h"
22#include "HAL/PlatformProperties.h"
23#include "Interfaces/IPluginManager.h"
24#include "Misc/CommandLine.h"
25#include "Misc/CoreDelegates.h"
26#include "Misc/EngineVersion.h"
27#include "Misc/FileHelper.h"
28#include "Misc/PackageName.h"
29#include "Misc/Parse.h"
30#include "Misc/Paths.h"
31#include "UObject/Package.h"
32#include "UObject/UObjectGlobals.h"
33#include "UObject/UObjectIterator.h"
34#include <util/ue-header-guard-end.h>
35
36static const TCHAR *CARLA_PACK_MANIFEST_NAME = TEXT("carla-pack.json");
37
39{
40 if (GEngine == nullptr)
41 {
42 return nullptr;
43 }
44 return GEngine->GetEngineSubsystem<UCarlaContentPackManager>();
45}
46
47void UCarlaContentPackManager::Initialize(FSubsystemCollectionBase &Collection)
48{
49 Super::Initialize(Collection);
50
51 // Auto-discovery is for game processes only: the cooker must not pick up
52 // packs lying next to the project, and the full editor keeps its plugin
53 // set as configured (packs can still be mounted explicitly there).
54 if (IsRunningCommandlet() || GIsEditor)
55 {
56 UE_LOG(LogCarla, Log, TEXT("Content packs: auto-discovery skipped (editor/commandlet process)"));
57 return;
58 }
59 Discover();
60}
61
63{
64 Super::Deinitialize();
65}
66
67// =============================================================================
68// -- Discovery ----------------------------------------------------------------
69// =============================================================================
70
72{
73 TArray<FString> Dirs;
74 Dirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / TEXT("Packs")));
75
76 auto AppendList = [&Dirs](const FString &List)
77 {
78 TArray<FString> Parts;
79 List.ParseIntoArray(Parts, TEXT(";"), true);
80 for (FString &Part : Parts)
81 {
82 Part.TrimStartAndEndInline();
83 Part = Part.TrimQuotes();
84 if (!Part.IsEmpty())
85 {
86 Dirs.AddUnique(FPaths::ConvertRelativePathToFull(Part));
87 }
88 }
89 };
90
91 // -carla-packs=<dir>[;<dir>]; quote the value ("-carla-packs=\"/a b;/c\"") when
92 // a directory contains spaces, the token otherwise ends at the first space.
93 FString FromCommandLine;
94 if (FParse::Value(FCommandLine::Get(), TEXT("-carla-packs="), FromCommandLine))
95 {
96 AppendList(FromCommandLine);
97 }
98
99 const FString FromEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("CARLA_PACKS"));
100 if (!FromEnv.IsEmpty())
101 {
102 AppendList(FromEnv);
103 }
104
105 return Dirs;
106}
107
109{
110 if (bDiscovered)
111 {
112 return;
113 }
114 bDiscovered = true;
115
116 const FString Stamp = GetBaseReleaseStamp();
117 UE_LOG(LogCarla, Log, TEXT("Content packs: base release stamp '%s'"),
118 Stamp.IsEmpty() ? TEXT("<none: editor/dev build>") : *Stamp);
119
120 for (const FString &Dir : GetDiscoveryDirs())
121 {
122 if (!FPaths::DirectoryExists(Dir))
123 {
124 UE_LOG(LogCarla, Log, TEXT("Content packs: directory '%s' does not exist, skipping"), *Dir);
125 continue;
126 }
127
128 TArray<FString> Children;
129 IFileManager::Get().FindFiles(Children, *(Dir / TEXT("*")), false, true);
130 Children.Sort();
131 for (const FString &Child : Children)
132 {
133 const FString Manifest = Dir / Child / CARLA_PACK_MANIFEST_NAME;
134 if (!FPaths::FileExists(Manifest))
135 {
136 continue;
137 }
138 FString Error;
139 if (!Mount(Manifest, Error))
140 {
141 UE_LOG(LogCarla, Warning, TEXT("Content packs: '%s' not mounted: %s"), *Manifest, *Error);
142 }
143 }
144 }
145
146 // Packs the engine mounted itself (Plugins/<Pack>.upluginmanifest in a
147 // packaged build, or an enabled plugin in the editor): never mount them a
148 // second time, just register their manifest so maps and catalogs show up.
149 for (const TSharedRef<IPlugin> &Plugin : IPluginManager::Get().GetEnabledPluginsWithContent())
150 {
151 const FString Manifest = FPaths::ConvertRelativePathToFull(Plugin->GetBaseDir()) / CARLA_PACK_MANIFEST_NAME;
152 if (!Plugin->IsMounted() || !FPaths::FileExists(Manifest) || FindPack(Plugin->GetName()) != nullptr)
153 {
154 continue;
155 }
156 FString Error;
158 if (!FCarlaContentPackManifest::LoadFromFile(Manifest, Parsed, Error))
159 {
160 UE_LOG(LogCarla, Warning, TEXT("Content packs: engine-mounted plugin '%s' not adopted: %s"),
161 *Plugin->GetName(), *Error);
162 continue;
163 }
164 if (!Parsed.Name.Equals(Plugin->GetName(), ESearchCase::IgnoreCase))
165 {
166 UE_LOG(LogCarla, Warning,
167 TEXT("Content packs: engine-mounted plugin '%s' not adopted: manifest name '%s' does not match the plugin name"),
168 *Plugin->GetName(), *Parsed.Name);
169 continue;
170 }
171 if (!Mount(Manifest, Error))
172 {
173 UE_LOG(LogCarla, Warning, TEXT("Content packs: engine-mounted plugin '%s' not adopted: %s"),
174 *Plugin->GetName(), *Error);
175 }
176 }
177}
178
179// =============================================================================
180// -- Queries ------------------------------------------------------------------
181// =============================================================================
182
184{
185 for (const FCarlaContentPack &Pack : Packs)
186 {
187 if (Pack.GetName().Equals(Name, ESearchCase::IgnoreCase))
188 {
189 return &Pack;
190 }
191 }
192 return nullptr;
193}
194
195FString UCarlaContentPackManager::FindMapPackage(const FString &MapName) const
196{
197 for (const FCarlaContentPack &Pack : Packs)
198 {
199 if (!Pack.bMounted)
200 {
201 continue;
202 }
203 for (const FCarlaContentPackMap &Map : Pack.Manifest.Maps)
204 {
205 if (Map.Name.Equals(MapName, ESearchCase::IgnoreCase))
206 {
207 return Map.Package;
208 }
209 }
210 }
211 return FString();
212}
213
215{
216 TArray<FString> Names;
217 for (const FCarlaContentPack &Pack : Packs)
218 {
219 if (Pack.bMounted)
220 {
221 for (const FCarlaContentPackMap &Map : Pack.Manifest.Maps)
222 {
223 Names.AddUnique(Map.Name);
224 }
225 }
226 }
227 return Names;
228}
229
231{
232 TArray<FString> Dirs;
233 for (const FCarlaContentPack &Pack : Packs)
234 {
235 if (Pack.bMounted)
236 {
237 Dirs.Add(Pack.ContentDir);
238 }
239 }
240 return Dirs;
241}
242
243TArray<FString> UCarlaContentPackManager::FindPackCatalogFiles(const FString &RelativeName)
244{
245 TArray<FString> Files;
246 const UCarlaContentPackManager *Manager = Get();
247 if (Manager == nullptr)
248 {
249 return Files;
250 }
251 for (const FString &ContentDir : Manager->GetPackContentDirs())
252 {
253 const FString File = ContentDir / TEXT("Config") / RelativeName;
254 if (IFileManager::Get().FileExists(*File))
255 {
256 Files.Add(File);
257 }
258 }
259 return Files;
260}
261
263{
264 FString Stamp;
265 const FString StampFile = FPaths::ProjectDir() / TEXT("BaseRelease");
266 if (FPaths::FileExists(StampFile) && FFileHelper::LoadFileToString(Stamp, *StampFile))
267 {
268 Stamp.TrimStartAndEndInline();
269 }
270 return Stamp;
271}
272
273// =============================================================================
274// -- Mount --------------------------------------------------------------------
275// =============================================================================
276
278 const FCarlaContentPackManifest &Manifest,
279 FString &OutError) const
280{
281 const FString Stamp = GetBaseReleaseStamp();
282 if (Stamp.IsEmpty())
283 {
284 UE_LOG(LogCarla, Warning,
285 TEXT("Content pack '%s': this build has no BaseRelease stamp (editor/dev build); "
286 "skipping the base release check (pack was built for '%s')"),
287 *Manifest.Name, *Manifest.BaseRelease);
288 }
289 else if (Manifest.BaseRelease.IsEmpty())
290 {
291 UE_LOG(LogCarla, Warning,
292 TEXT("Content pack '%s': manifest has no base_release; this build is '%s'"),
293 *Manifest.Name, *Stamp);
294 }
295 else if (!Manifest.BaseRelease.Equals(Stamp, ESearchCase::IgnoreCase))
296 {
297 OutError = FString::Printf(
298 TEXT("pack '%s' was built against base release '%s' but this server is '%s'; "
299 "rebuild the pack against this release"),
300 *Manifest.Name, *Manifest.BaseRelease, *Stamp);
301 return false;
302 }
303
304 const FString ThisPlatform = FPlatformProperties::IniPlatformName();
305 // Win64 is the UAT name of the Windows platform
306 const FString PackPlatform = Manifest.Platform.Equals(TEXT("Win64"), ESearchCase::IgnoreCase)
307 ? FString(TEXT("Windows"))
308 : Manifest.Platform;
309 if (Manifest.Platform.IsEmpty())
310 {
311 UE_LOG(LogCarla, Warning,
312 TEXT("Content pack '%s': manifest has no platform; this server runs on '%s'"),
313 *Manifest.Name, *ThisPlatform);
314 }
315 else if (!PackPlatform.Equals(ThisPlatform, ESearchCase::IgnoreCase))
316 {
317 OutError = FString::Printf(
318 TEXT("pack '%s' was built for platform '%s' but this server runs on '%s'"),
319 *Manifest.Name, *Manifest.Platform, *ThisPlatform);
320 return false;
321 }
322
323 if (!Manifest.EngineVersion.IsEmpty())
324 {
325 const FEngineVersion &Current = FEngineVersion::Current();
326 const FString ThisEngine = FString::Printf(TEXT("%u.%u.%u"),
327 Current.GetMajor(), Current.GetMinor(), Current.GetPatch());
328 if (!Manifest.EngineVersion.Equals(ThisEngine))
329 {
330 UE_LOG(LogCarla, Warning,
331 TEXT("Content pack '%s': built with engine %s, this server runs %s"),
332 *Manifest.Name, *Manifest.EngineVersion, *ThisEngine);
333 }
334 }
335
336 return true;
337}
338
340{
341 Pack.MountedPaks.Reset();
342
343 // MountExplicitlyLoadedPlugin only registers the mount point: the pak loop
344 // in FPluginManager::ProcessEnabledPlugins runs for startup plugins only,
345 // so a pack mounted at runtime has to mount its own containers (as Game
346 // Feature Plugins do). The .utoc/.ucas siblings are picked up by the pak
347 // platform file from the .pak path.
348 if (!FPlatformProperties::RequiresCookedData())
349 {
350 // Editor process: loose assets under Content/ are served by the mount
351 // point itself, cooked containers cannot be used here.
352 return true;
353 }
354
355 // Containers are named by the stager (<DLCName><ProjectName>-<Platform>):
356 // glob, never assume a name. The .pak is the handle FPakPlatformFile mounts
357 // and it picks up the sibling .utoc/.ucas itself.
358 const FString PaksDir = Pack.ContentDir / TEXT("Paks") / FPlatformProperties::PlatformName();
359 TArray<FString> PakFiles;
360 IFileManager::Get().FindFiles(PakFiles, *(PaksDir / TEXT("*.pak")), true, false);
361 PakFiles.Sort();
362
363 if (PakFiles.Num() == 0)
364 {
365 TArray<FString> TocFiles;
366 IFileManager::Get().FindFiles(TocFiles, *(PaksDir / TEXT("*.utoc")), true, false);
367 if (TocFiles.Num() > 0)
368 {
369 OutError = FString::Printf(
370 TEXT("'%s' has %d .utoc container(s) but no .pak next to them; the stager always writes the "
371 ".pak stub, ship all three of .pak/.utoc/.ucas"), *PaksDir, TocFiles.Num());
372 return false;
373 }
374 UE_LOG(LogCarla, Warning,
375 TEXT("Content pack '%s': no .pak under '%s'; only loose files (if any) will be visible"),
376 *Pack.GetName(), *PaksDir);
377 return true;
378 }
379
380 if (!FCoreDelegates::MountPak.IsBound())
381 {
382 OutError = TEXT("pak mounting is unavailable in this process (FCoreDelegates::MountPak not bound)");
383 return false;
384 }
385
386 for (const FString &PakName : PakFiles)
387 {
388 const FString PakPath = PaksDir / PakName;
389 if (FCoreDelegates::MountPak.Execute(PakPath, 0) == nullptr)
390 {
391 OutError = FString::Printf(TEXT("failed to mount pak '%s'"), *PakPath);
392 UnmountPaks(Pack);
393 return false;
394 }
395 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': mounted pak '%s'"), *Pack.GetName(), *PakPath);
396 Pack.MountedPaks.Add(PakPath);
397 }
398 return true;
399}
400
402{
403 for (int32 i = Pack.MountedPaks.Num() - 1; i >= 0; --i)
404 {
405 const FString &PakPath = Pack.MountedPaks[i];
406 if (FCoreDelegates::OnUnmountPak.IsBound() && FCoreDelegates::OnUnmountPak.Execute(PakPath))
407 {
408 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': unmounted pak '%s'"), *Pack.GetName(), *PakPath);
409 }
410 else
411 {
412 UE_LOG(LogCarla, Warning, TEXT("Content pack '%s': failed to unmount pak '%s'"), *Pack.GetName(), *PakPath);
413 }
414 }
415 Pack.MountedPaks.Reset();
416}
417
419{
420 IPluginManager &PluginManager = IPluginManager::Get();
421 const FString &Name = Pack.GetName();
422
423 TSharedPtr<IPlugin> Existing = PluginManager.FindPlugin(Name);
424 if (Existing.IsValid())
425 {
426 const FString ExistingFile = FPaths::ConvertRelativePathToFull(Existing->GetDescriptorFileName());
427 if (!FPaths::IsSamePath(ExistingFile, Pack.PluginFile))
428 {
429 OutError = FString::Printf(
430 TEXT("pack name '%s' collides with the plugin at '%s'"), *Name, *ExistingFile);
431 return false;
432 }
433 if (Existing->IsEnabled() && FPackageName::MountPointExists(FString::Printf(TEXT("/%s/"), *Name)))
434 {
435 OutError = FString::Printf(TEXT("plugin '%s' is already mounted"), *Name);
436 return false;
437 }
438 }
439 bool bAddedNow = false;
440 if (!Existing.IsValid())
441 {
442 FText FailReason;
443 if (!PluginManager.AddToPluginsList(Pack.PluginFile, &FailReason))
444 {
445 OutError = FString::Printf(TEXT("cannot register plugin '%s': %s"),
446 *Pack.PluginFile, *FailReason.ToString());
447 return false;
448 }
449 bAddedNow = true;
450 }
451
452 if (!PluginManager.MountExplicitlyLoadedPlugin(Name))
453 {
454 OutError = FString::Printf(
455 TEXT("MountExplicitlyLoadedPlugin('%s') failed; the .uplugin must set "
456 "\"ExplicitlyLoaded\": true and \"CanContainContent\": true"), *Name);
457 if (bAddedNow)
458 {
459 // Otherwise the bad descriptor stays registered and a retry after
460 // fixing the .uplugin keeps failing until restart.
461 PluginManager.RemoveFromPluginsList(Pack.PluginFile);
462 }
463 return false;
464 }
465
466 const FString MountPoint = FString::Printf(TEXT("/%s/"), *Name);
467 if (!FPackageName::MountPointExists(MountPoint))
468 {
469 OutError = FString::Printf(TEXT("mount point '%s' was not registered"), *MountPoint);
470 return false;
471 }
472
473 TSharedPtr<IPlugin> Plugin = PluginManager.FindPlugin(Name);
474 if (Plugin.IsValid())
475 {
476 Pack.ContentDir = FPaths::ConvertRelativePathToFull(Plugin->GetContentDir());
477 }
478 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': mounted '%s' -> '%s'"),
479 *Name, *MountPoint, *Pack.ContentDir);
480 return true;
481}
482
484{
485 IAssetRegistry *AssetRegistry = IAssetRegistry::Get();
486 if (AssetRegistry == nullptr)
487 {
488 UE_LOG(LogCarla, Warning, TEXT("Content pack '%s': asset registry unavailable"), *Pack.GetName());
489 return;
490 }
491
492 const FString MountPoint = FString::Printf(TEXT("/%s/"), *Pack.GetName());
493 const FString StateFile = Pack.PackDir / TEXT("AssetRegistry.bin");
494
495 if (FPaths::FileExists(StateFile))
496 {
497 if (AssetRegistry->ActiveMountsRegisterAndLoadFilePathSynchronous(StateFile))
498 {
499 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': loaded asset registry '%s'"),
500 *Pack.GetName(), *StateFile);
501 return;
502 }
503 FAssetRegistryState State;
504 if (FAssetRegistryState::LoadFromDisk(*StateFile, FAssetRegistryLoadOptions(), State))
505 {
506 AssetRegistry->AppendState(State);
507 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': appended asset registry '%s'"),
508 *Pack.GetName(), *StateFile);
509 return;
510 }
511 UE_LOG(LogCarla, Warning,
512 TEXT("Content pack '%s': cannot load '%s', falling back to a synchronous scan of %s"),
513 *Pack.GetName(), *StateFile, *MountPoint);
514 }
515 else
516 {
517 UE_LOG(LogCarla, Warning,
518 TEXT("Content pack '%s': no AssetRegistry.bin, scanning %s synchronously"),
519 *Pack.GetName(), *MountPoint);
520 }
521
522 AssetRegistry->ScanPathsSynchronous({MountPoint}, true);
523}
524
526 const FString &Path,
527 FString &OutError,
528 const FCarlaContentPack **OutPack)
529{
530 if (OutPack != nullptr)
531 {
532 *OutPack = nullptr;
533 }
534 if (Path.IsEmpty())
535 {
536 OutError = TEXT("empty pack path");
537 return false;
538 }
539
540 FString ManifestPath = FPaths::ConvertRelativePathToFull(Path);
541 if (FPaths::DirectoryExists(ManifestPath))
542 {
543 ManifestPath = ManifestPath / CARLA_PACK_MANIFEST_NAME;
544 }
545 if (!FPaths::FileExists(ManifestPath))
546 {
547 OutError = FString::Printf(
548 TEXT("'%s' is not a content pack: no directory or manifest at '%s'"), *Path, *ManifestPath);
549 return false;
550 }
551
553 if (!FCarlaContentPackManifest::LoadFromFile(ManifestPath, Manifest, OutError))
554 {
555 return false;
556 }
557
558 // Reuse the slot of an unmounted pack of the same name (re-mount).
559 int32 Index = INDEX_NONE;
560 for (int32 i = 0; i < Packs.Num(); ++i)
561 {
562 if (Packs[i].GetName().Equals(Manifest.Name, ESearchCase::IgnoreCase))
563 {
564 if (Packs[i].bMounted)
565 {
566 OutError = FString::Printf(TEXT("pack '%s' is already mounted from '%s'"),
567 *Manifest.Name, *Packs[i].PackDir);
568 if (OutPack != nullptr)
569 {
570 *OutPack = &Packs[i];
571 }
572 return false;
573 }
574 Index = i;
575 break;
576 }
577 }
578
579 if (!CheckCompatibility(Manifest, OutError))
580 {
581 return false;
582 }
583
585 Pack.Manifest = Manifest;
586 Pack.PackDir = Manifest.PackDir;
587 Pack.PluginFile = Pack.PackDir / (Manifest.Name + TEXT(".uplugin"));
588 Pack.ContentDir = Pack.PackDir / TEXT("Content");
589
590 if (!FPaths::FileExists(Pack.PluginFile))
591 {
592 OutError = FString::Printf(TEXT("pack '%s' has no plugin descriptor '%s'"),
593 *Manifest.Name, *Pack.PluginFile);
594 return false;
595 }
596 if (!FPaths::DirectoryExists(Pack.ContentDir))
597 {
598 UE_LOG(LogCarla, Warning, TEXT("Content pack '%s': no Content directory at '%s'"),
599 *Manifest.Name, *Pack.ContentDir);
600 }
601
602 // A plugin the engine already mounted (Plugins/<Pack>.upluginmanifest at
603 // startup) is adopted, not mounted twice: its paks and mount point are
604 // live, only the manifest and the asset registry state are registered.
605 TSharedPtr<IPlugin> EnginePlugin = IPluginManager::Get().FindPlugin(Manifest.Name);
606 if (EnginePlugin.IsValid())
607 {
608 // Name collision is decided here, before any container is mounted.
609 const FString ExistingFile = FPaths::ConvertRelativePathToFull(EnginePlugin->GetDescriptorFileName());
610 if (!FPaths::IsSamePath(ExistingFile, Pack.PluginFile))
611 {
612 OutError = FString::Printf(
613 TEXT("pack name '%s' collides with the plugin at '%s'"), *Manifest.Name, *ExistingFile);
614 return false;
615 }
616 }
617 if (EnginePlugin.IsValid() && EnginePlugin->IsMounted())
618 {
619 Pack.bEngineMounted = true;
620 Pack.ContentDir = FPaths::ConvertRelativePathToFull(EnginePlugin->GetContentDir());
621 UE_LOG(LogCarla, Log, TEXT("Content pack '%s': plugin already mounted by the engine, adopting it"),
622 *Manifest.Name);
623 }
624 else
625 {
626 if (!MountPaks(Pack, OutError))
627 {
628 return false;
629 }
630 if (!MountPlugin(Pack, OutError))
631 {
632 UnmountPaks(Pack);
633 return false;
634 }
635 }
637
638 Pack.bMounted = true;
639 if (Index == INDEX_NONE)
640 {
641 Index = Packs.Add(MoveTemp(Pack));
642 }
643 else
644 {
645 Packs[Index] = MoveTemp(Pack);
646 }
647
648 const FCarlaContentPack &Mounted = Packs[Index];
650 UE_LOG(LogCarla, Log, TEXT("Content pack '%s' %s mounted from '%s' (%d maps: %s)"),
651 *Mounted.GetName(), *Mounted.Manifest.Version, *Mounted.PackDir,
652 Mounted.Manifest.Maps.Num(), *FString::Join(Mounted.Manifest.GetMapNames(), TEXT(", ")));
653 if (OutPack != nullptr)
654 {
655 *OutPack = &Mounted;
656 }
657 // The pack's catalogs are merged now, but the actor definitions of the
658 // running episode were built when its factories were registered. Bind what
659 // the pack added, so its blueprints are usable without a world load (the
660 // unmount path drops them again through ReleaseContentPack).
661 for (TObjectIterator<UActorDispatcher> DispatcherIt; DispatcherIt; ++DispatcherIt)
662 {
663 if (!IsValid(*DispatcherIt))
664 {
665 continue;
666 }
667 for (TObjectIterator<ACarlaActorFactory> FactoryIt; FactoryIt; ++FactoryIt)
668 {
669 if (IsValid(*FactoryIt))
670 {
671 DispatcherIt->BindNewDefinitions(**FactoryIt);
672 }
673 }
674 }
675
676 OnPackMounted.Broadcast(Mounted);
677 return true;
678}
679
680// =============================================================================
681// -- Unmount ------------------------------------------------------------------
682// =============================================================================
683
684/// Package name prefixes that belong to a pack: its mount point plus, for every
685/// project map it ships in place (/Game/Carla/Maps/Town12/Town12), the map
686/// package itself and its folder when that folder is the map's own
687/// (/Game/Carla/Maps/Town12/, where the cooked World Partition cells live).
689{
690 TArray<FString> Prefixes;
691 Prefixes.Add(FString::Printf(TEXT("/%s/"), *Pack.GetName()));
692 for (const FCarlaContentPackMap &Map : Pack.Manifest.Maps)
693 {
694 if (!Map.Package.StartsWith(TEXT("/Game/")))
695 {
696 continue;
697 }
698 Prefixes.Add(Map.Package);
699 Prefixes.Add(Map.Package + TEXT("/"));
700 const FString Folder = FPackageName::GetLongPackagePath(Map.Package);
701 if (FPackageName::GetShortName(Folder).Equals(Map.Name, ESearchCase::IgnoreCase))
702 {
703 Prefixes.Add(Folder + TEXT("/"));
704 }
705 }
706 return Prefixes;
707}
708
709static bool UCarlaContentPackManager_IsPackPackage(const TArray<FString> &Prefixes, const FString &PackageName)
710{
711 for (const FString &Prefix : Prefixes)
712 {
713 if (Prefix.EndsWith(TEXT("/")) ? PackageName.StartsWith(Prefix) : PackageName.Equals(Prefix))
714 {
715 return true;
716 }
717 }
718 return false;
719}
720
721bool UCarlaContentPackManager::IsPackInUse(const FCarlaContentPack &Pack, FString &OutReason) const
722{
723 const TArray<FString> Prefixes = UCarlaContentPackManager_PackagePrefixes(Pack);
724
725 if (GEngine != nullptr)
726 {
727 for (const FWorldContext &Context : GEngine->GetWorldContexts())
728 {
729 UWorld *World = Context.World();
730 if (World == nullptr)
731 {
732 continue;
733 }
734 const FString WorldPackage = World->GetOutermost()->GetName();
735 if (UCarlaContentPackManager_IsPackPackage(Prefixes, WorldPackage))
736 {
737 OutReason = FString::Printf(TEXT("its map '%s' is the current world"), *WorldPackage);
738 return true;
739 }
740 for (const ULevelStreaming *Streaming : World->GetStreamingLevels())
741 {
742 if (Streaming != nullptr &&
743 UCarlaContentPackManager_IsPackPackage(Prefixes, Streaming->GetWorldAssetPackageName()))
744 {
745 OutReason = FString::Printf(TEXT("its level '%s' is streamed into the current world"),
746 *Streaming->GetWorldAssetPackageName());
747 return true;
748 }
749 }
750 }
751 }
752
753 return false;
754}
755
756static TArray<FString> UCarlaContentPackManager_LoadedPackages(const TArray<FString> &Prefixes)
757{
758 TArray<FString> Alive;
759 for (TObjectIterator<UPackage> It; It; ++It)
760 {
761 const FString PackageName = It->GetName();
762 if (UCarlaContentPackManager_IsPackPackage(Prefixes, PackageName))
763 {
764 Alive.Add(PackageName);
765 if (Alive.Num() >= 5)
766 {
767 break;
768 }
769 }
770 }
771 return Alive;
772}
773
775{
776 const TArray<FString> Prefixes = UCarlaContentPackManager_PackagePrefixes(Pack);
777
778 // Nothing from the pack was ever loaded: no need for a full GC.
779 if (UCarlaContentPackManager_LoadedPackages(Prefixes).Num() == 0)
780 {
781 return false;
782 }
783
784 // Anything else still referencing pack content shows up as a live package
785 // under the mount point once unreferenced objects are gone.
786 CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true);
787
788 const TArray<FString> Alive = UCarlaContentPackManager_LoadedPackages(Prefixes);
789 if (Alive.Num() > 0)
790 {
791 OutReason = FString::Printf(TEXT("objects from the pack are still loaded (%s%s)"),
792 *FString::Join(Alive, TEXT(", ")), Alive.Num() >= 5 ? TEXT(", ...") : TEXT(""));
793 return true;
794 }
795 return false;
796}
797
798bool UCarlaContentPackManager::Unmount(const FString &Name, FString &OutError)
799{
800 int32 Index = INDEX_NONE;
801 for (int32 i = 0; i < Packs.Num(); ++i)
802 {
803 if (Packs[i].GetName().Equals(Name, ESearchCase::IgnoreCase))
804 {
805 Index = i;
806 break;
807 }
808 }
809 if (Index == INDEX_NONE)
810 {
811 OutError = FString::Printf(TEXT("unknown content pack '%s'"), *Name);
812 return false;
813 }
814 FCarlaContentPack &Pack = Packs[Index];
815 if (!Pack.bMounted)
816 {
817 OutError = FString::Printf(TEXT("content pack '%s' is not mounted"), *Name);
818 return false;
819 }
820
821 if (Pack.bEngineMounted)
822 {
823 OutError = FString::Printf(
824 TEXT("content pack '%s' was mounted by the engine at startup (Plugins/%s.upluginmanifest); "
825 "remove it from the Plugins directory and restart the server to unload it"),
826 *Pack.GetName(), *Pack.GetName());
827 return false;
828 }
829
830 // Cheap refusal first (state must not change on a refused unmount): the
831 // pack map or one of its levels is loaded.
832 FString Reason;
833 if (IsPackInUse(Pack, Reason))
834 {
835 OutError = FString::Printf(TEXT("cannot unmount content pack '%s': %s; load another map first"),
836 *Pack.GetName(), *Reason);
837 return false;
838 }
839
840 // Definition caches hold the pack's blueprint classes and meshes; drop
841 // them first, otherwise the garbage collection below keeps them alive.
842 {
843 const FString MountPoint = FString::Printf(TEXT("/%s/"), *Pack.GetName());
844 // Objects of an episode torn down by load_world stay reachable to
845 // TObjectIterator until the next garbage collection; their definitions
846 // may point at classes that are already gone. Only touch the ones that
847 // still belong to a live world.
848 const auto BelongsToLiveWorld = [](const UObject *Object)
849 {
850 const UWorld *World = Object->GetWorld();
851 return IsValid(World) && !World->bIsTearingDown;
852 };
853 for (TObjectIterator<ACarlaActorFactory> It; It; ++It)
854 {
855 if (IsValid(*It) && BelongsToLiveWorld(*It))
856 {
857 It->ReleaseContentPack(MountPoint);
858 }
859 }
860 for (TObjectIterator<UActorDispatcher> It; It; ++It)
861 {
862 if (IsValid(*It) && BelongsToLiveWorld(*It))
863 {
864 It->ReleaseContentPack(MountPoint);
865 }
866 }
867 }
868
869 if (ArePackObjectsAlive(Pack, Reason))
870 {
871 // Rare: something outside the current world still holds pack objects.
872 // The catalog entries were already dropped; a reload_world rebuilds them.
873 UE_LOG(LogCarla, Warning,
874 TEXT("Content pack '%s': unmount refused after its blueprint definitions were dropped; "
875 "reload_world restores them"), *Pack.GetName());
876 OutError = FString::Printf(TEXT("cannot unmount content pack '%s': %s; reload the world and retry"),
877 *Pack.GetName(), *Reason);
878 return false;
879 }
880
881 FText FailReason;
882 if (!IPluginManager::Get().UnmountExplicitlyLoadedPlugin(Pack.GetName(), &FailReason))
883 {
884 OutError = FString::Printf(TEXT("cannot unmount plugin '%s': %s"),
885 *Pack.GetName(), *FailReason.ToString());
886 return false;
887 }
888 UnmountPaks(Pack);
889 Pack.bMounted = false;
891
892 UE_LOG(LogCarla, Log, TEXT("Content pack '%s' unmounted"), *Pack.GetName());
893 OnPackUnmounted.Broadcast(Pack);
894 return true;
895}
static TArray< FString > UCarlaContentPackManager_LoadedPackages(const TArray< FString > &Prefixes)
static bool UCarlaContentPackManager_IsPackPackage(const TArray< FString > &Prefixes, const FString &PackageName)
static const TCHAR * CARLA_PACK_MANIFEST_NAME
static TArray< FString > UCarlaContentPackManager_PackagePrefixes(const FCarlaContentPack &Pack)
Package name prefixes that belong to a pack: its mount point plus, for every project map it ships in ...
static bool IsValid(const ACarlaWheeledVehicle *Vehicle)
Mounts CARLA content packs (content-only Unreal plugins cooked as DLC against a base release,...
bool MountPlugin(FCarlaContentPack &Pack, FString &OutError)
virtual void Initialize(FSubsystemCollectionBase &Collection) override
bool ArePackObjectsAlive(const FCarlaContentPack &Pack, FString &OutReason)
Objects under "/<Pack>/" survive a garbage collection (GC only runs when something from the pack was ...
bool Unmount(const FString &Name, FString &OutError)
Unmount a pack by name.
static UCarlaContentPackManager * Get()
virtual void Deinitialize() override
TArray< FString > GetPackContentDirs() const
Content dirs of all mounted packs, in mount order.
TArray< FString > GetPackMapNames() const
Map names declared by all mounted packs.
bool MountPaks(FCarlaContentPack &Pack, FString &OutError)
TArray< FCarlaContentPack > Packs
const FCarlaContentPack * FindPack(const FString &Name) const
bool Mount(const FString &Path, FString &OutError, const FCarlaContentPack **OutPack=nullptr)
Mount a pack.
FString FindMapPackage(const FString &MapName) const
Long package name of a map declared by a mounted pack, or empty.
void UnmountPaks(FCarlaContentPack &Pack)
FCarlaContentPackEvent OnPackMounted
void RegisterAssetRegistry(FCarlaContentPack &Pack)
static TArray< FString > FindPackCatalogFiles(const FString &RelativeName)
<ContentDir>/Config/<RelativeName> of every mounted pack where the file exists, in mount order (later...
static FString GetBaseReleaseStamp()
Base release stamp of this binary (<ProjectDir>/BaseRelease), empty in editor/dev builds.
bool CheckCompatibility(const FCarlaContentPackManifest &Manifest, FString &OutError) const
void Discover()
Mount every pack found in <ProjectDir>/Packs/*‍/carla-pack.json, the directories listed in -carla-pac...
TArray< FString > GetDiscoveryDirs() const
FCarlaContentPackEvent OnPackUnmounted
bool IsPackInUse(const FCarlaContentPack &Pack, FString &OutReason) const
The pack map or one of its levels is loaded in some world (no GC).
static void InvalidateMapCache()
Drop the cached .umap walk (content pack mounted/unmounted).
Parsed carla-pack.json.
static bool LoadFromFile(const FString &InManifestPath, FCarlaContentPackManifest &OutManifest, FString &OutError)
Read and parse <ManifestPath>. Fills ManifestPath/PackDir.
FString PackDir
Absolute path of the pack directory (parent of the manifest).
TArray< FCarlaContentPackMap > Maps
TArray< FString > GetMapNames() const
One map declared by a content pack manifest.
A content pack known to the manager (discovered or mounted explicitly).
FString ContentDir
Absolute path of the plugin content dir (<PackDir>/Content), the filesystem side of the "/<Name>/" mo...
FString PluginFile
Absolute path of <PackDir>/<Name>.uplugin.
bool bEngineMounted
True when the plugin was mounted by the engine at startup (a pack dropped under Plugins/ with a ....
const FString & GetName() const
FString PackDir
Absolute pack directory (contains carla-pack.json and <Name>.uplugin).
FCarlaContentPackManifest Manifest
TArray< FString > MountedPaks
Pak files mounted for this pack (cooked builds only).