Error registering Game Feature Plugin in cooked build

We have a situation where we are trying to load a Game Feature Plugin from an external pak file which is cooked as DLC and that we manual mount ourselves. The pak file mounts correctly (can be seen when using the PakList command) and we can iterate over the files in that pak so that all seems to be in order (output added at the end).

The problem comes when we try to register the GFP with the subsystem. After running “LoadGameFeaturePlugin file:D:\Projects\ARPGDebug_Native\Windows\ActionRPG\Modio\ExampleFeature_Windows\ExampleFeature.uplugin” the resulting state machine is in the ErrorRegistering state. Looking into it we found that this is being caused by the Game Feature Data not being able to be loaded because the package can’t be found. Following it down, the point of failure originates in FPakFile::FindLocationFromIndex() [IPlatformFilePak.cpp:7871] where it fails the first check (if(!FullPath.StartsWith(MountPoint))) because the FullPath is a relative file path (../../Modio/ExampleFeature_Windows/Content/ExampleFeature) but the mount point is a game path (/ExampleFeature/). The registration code checks for a game path for the Game Feature Data (/ExampleFeature/ExampleFeature.ExampleFeature) but it gets converted during the process of checking if it exists in FPackageResourceManagerFile::IteratePossibleFiles() [PackageResourceManagerFile.cpp:127].

We are using the result of

Plugin->GetMountedAssetPath()

for the mount point of the pak. We’ve tried other mount points, including an absolute path, but anything else seems to result in the pak not mounting correctly. Interestingly the mount point for the game pak does seem to be relative (../../../), which is why it seems to work for that, but we haven’t been able to find a solution that works. At this point we are unsure of what we could be doing wrong and any help would be appreciated.

// Output from mounting pak file
LogModioUGC: Verbose: UGC `ExampleFeature` contains 1 pak files within D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content.
LogModioUGC: VeryVerbose: Attempting to mount UGC pak file D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak at /ExampleFeature/
LogModioUGC: VeryVerbose: Pakfile D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak mounted
LogModioUGC: VeryVerbose: Pakfile D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak : AssetRegistry.bin
LogModioUGC: VeryVerbose: Pakfile D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak : Content/ExampleFeature.uasset
LogModioUGC: VeryVerbose: Pakfile D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak : Content/ExampleFeature.uexp

[Attachment Removed]

This one is tricky for me to answer because it’s easy to get lost in what system uses what type of path- My language may fail me.

For the purpose of me Repo’ing this and anyone reading this ticket i used this process detailed here to make the DLC https://dev.epicgames.com/community/learning/knowledge-base/44nr/creating-platform-agnostic-dlcs-in-unreal-engine

Let’s start with the solution that should get you moving forward, but also, I think it’s the best way to explain what’s going on.

//We want to mount the PAK file, and we will use the Embedded Mount point from cooking. PakFilePath is the absolute System path in this case. This is the last time we will use the Absolute System Path. 
IPakFile* PakFile = FCoreDelegates::MountPak.Execute(PakFilePath, 0);
 
...
 
//Next we need to find the Project Relative Path to use for the Game Feature Subsystem for the .uPlugin - This does not relate to the Absolute path we used before. In the case of me using Lyra i know where those things will be found. I'm using ProjectPluginsDir as it will return the Reletive path for all my Plugins, that should look something like "../../../Lyra/Plugins/"
    const FString DescriptorPath = FPaths::ProjectPluginsDir() / TEXT("GameFeatures") / PluginName  / (PluginName + TEXT(".uplugin"));
 
...
 
//Next, we need to convert this string into a file: URL. This should look something like "file:../../../MyProject/Plugins/GameFeatures/ExampleFeature/ExampleFeature.uplugin" - This is a Project Relative File path. 
const FString PluginURL = UGameFeaturesSubsystem::GetPluginURL_FileProtocol(DescriptorPath);
 
//Then we can let the GFP system handle Registration and Activation of the GFP 
GFS.LoadAndActivateGameFeaturePlugin(PluginURL, ... )
 
 

Give something like that a go and see if it works for you!

You can test with Console Commands by using

Mount <AbsoluteSystemPath>

LoadGameFeaturePlugin file:<Project Relative File Path>

So for me, that looks like

Mount D:\Builds\DLCExampleLyra-Windows.pak

LoadGameFeaturePlugin file:../../../Lyra/Plugins/GameFeatures/DLCExample/DLCExample.uplugin

[Attachment Removed]

Hey Troy,

just to fully understand the flow here, could you provide us with a call stack that leads into FPakFile::FindLocationFromIndex?

In general it’s very possible there’s a bug in the GFP loading when using the file:// url, since Fortnite/UEFN is the major user of GFPs internally and they mostly user bundle:// urls and acquire content through the installbundlemanager.

Regardling the mount points, there are two mount points in play here, and they can be a source of confusion.

First the pak file mount point. This is the relative path that is prepended to the path of any files inside the pak file.

This is a relative path as seen from the game’s executable.

It is usually the common prefix for all files in the pak file. So if only include content files from your plugin it should point to your plugins Content/ directory and any paths inside the pak file should be relative to that.

Since your paklist shows AssetRegistry.bin and files with a relative Content/… path your pak mount point should most likely look like this:

../../../ActionRPG/Modio/ExampleFeature_Windows/The pak mount point is also stored inside the pak file and does not necessarily need to be passed in from your game code.

It should be listed by UnrealPak.exe -List; however, that log line has been removed in more recent versions of UnrealPak.

However, since it looks like you’re placing your plugins in a different directory than from where it was packaged from.

This means that either the internal mountpoint in the pak needs to be overriden and the packagename path needs to be overriden, or both can be kept at the original location. They must be consistent.

If all of this works correctly you should be able to find the files using the IFileManager API, e.g. through a path like

../../../GameName/Plugins/PluginName/AssetRegistry.binor for your custom paths

../../../ActionRPG/Modio/ExampleFeature_Windows/AssetRegistry.bin

The second mount point is the packagepath or package name mount point.

This is a translation from the virtual filesystem to the virtual package names used in game/editor to reference assets.

For a regular plugin, this mount point should most likely look like this:

../../../ProjectName/Plugins/PluginName/Content/ -> /Pluginname/For your custom paths:

../../../ActionRPG/Modio/ExampleFeature_Windows/Content -> /ExampleFeature/You can use the PackageName.DumpMountPoints command to check your packagepath mount points.

With all that out of the way, it’s quite possible the GFP loading path has some assumptions here that break this flow.

I remember that loading a GFP from disk assumed the pak file was already mounted, so it’s likely on you to mount the pak first with the correct pak mount point before calling into LoadGameFeaturePlugin.

I think the pluginmanager was supposed to add the package name mount point through FPluginManager::MountPluginFromExternalSource

so you could try putting a breakpoint there and see if it gets hit and if yes what paths are used.

You should be able to run with -LogCmds=“LogPluginManager Verbose,LogPakFile Verbose” to get the pak file mount points printed to the log.

You mentioned you use Plugin->GetMountedAssetPath() for the mount point. This function will return the PackageName root of the plugin, so essentially /PluginName/. This would be correct to construct the path if you are adding the package name mount point, but is not correct to use for the pak file. For the pak file you would want to use the relative path to the content folder as noted above.

I think I’ve shared this with your team before, but the KB article Primer: Loading Content and Pak Files at Runtime also goes into detail on the different mount points and the troubleshooting options.

Kind Regards,

Sebastian

[Attachment Removed]

> ‘/ExampleFeature/’ -> ‘/ExampleFeature/Content/’

From my understanding that would never be a valid root path / mount point.

The expectation is that the root paths map directly to filesystem paths.

There might be some obscure way in which ‘/ExampleFeature/Content/’ gets resolved to an actual filesystem path (it could technically be a legal path on Linux/Unix), but from my understanding, this should never yield any useful result on Windows.

I’ve double checked with the system owner, and he agrees.

> PlatformPakFile->Mount(“D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak”, 4, “/ExampleFeature/”))

Same thing here, the expectation for the pak mount mount call is that the mount path points to an actual path on disk, in almost all cases as a relative path from the game’s binary. So you should definitely change that accordingly.

In the console command version I’ve explicitly added a sanity check to catch these cases:

PackageName.RegisterMountPoint /ExamplePlugin/ /ExamplePlugin/Content
LogPackageName: Error: PackageName.RegisterMountPoint: Invalid ContentPath, should not start with '/'! Example: '../../../ProjectName/Content/'

Overall, could you try changing these accordingly and see if it makes a difference?

I think you might have hit a special case where the “wrong” paths still somewhat worked, but you should have been using relative paths (or absolute paths on windows) for these pak mount points all along.

Best,

Sebastian

[Attachment Removed]

Thanks for the update!

I think your first point looks like something we would like to track as a bug of feature improvement. Could you let me know which function you would need access to and what delegate triggers the workaround?

There are definitely rough edges around unloading content, so we would like to improve that.

> I’m unsure what you want us to update with the call to Mount. As is that call results in this message “LogPackageName: FPackageName: Mount point added: ‘../../Modio/ExampleFeature_Windows/Content/’ mounted to ‘/ExampleFeature/’” which seems correct to me based on looking at the other mount points listed in the output of Packname.DumpMountPoints*.*

I’m not sure we’re talking about the same mount point or function call.

When calling FPakPlatformFile::Mount, the pak file is not supposed to add any package mount points. This only happens from FPackageName::RegisterMountPoint which is not called by the pak platformfile.

The package name logic should happen in the PluginManager or manually from your side.

The pak file code itself only cares about the pak mount point.

So the correct mount point to pass to FPakPlatformFile::Mount is ../../Modio/ExampleFeature_Windows/.

Usually you would not even need to pass this mount point at all, because it also saved inside the pak file when it is created. However that only works if your plugins are in the same location when creating the pak file as when you are mounting them.

I think right now your package name mount point is correct, but the pak mount point for the pak is wrong.

This would result in the type of error you listed in the original post.

When loading a package from the pak the following would happen (assuming you are not using IOStore):

  1. Package name gets resolved to filesystem path via the package name mount points and we are looking for a virtual filesystem file: /ExampleFeature/MyDataAsset -> ../../Modio/ExampleFeature_Windows/Content/MyDataAsset.uasset
  2. This file is not present on the actual filesystem, so the lookup goes into the pak system, which tries to use the wrong pak file mount point of /ExampleFeature/MyDataAsset to look up the file with the relative path of Content/MyDataAsset.uasset inside the pak file
  3. This causes the error that you originally posted

To verify this, could you run the “PakList” console command directly after mounting the pak file?

This should print the mount point for the pak file to the output log and it’s the quickest way to verify which mount point you used.

Cheers,

Sebastian

[Attachment Removed]

Thanks for checking. Your observations align with what I expected, the mount path for the pak file is wrong. At this stage in the lookup no package paths should be involved anymore and the pak file only resolves using relative (or absolute) mount points to the filesystem.

Please pass the pak mount point ../../Modio/ExampleFeature_Windows/ into the PakPlatformFile->Mount() call and that should hopefully make this work.

As for the AssetRegistry Issues:

The first thing you mentioned actually seems like the correct approach. The AssetRegistry itself is supposed to react to changes in the root paths, so it internally relies on the Core Callbacks (FPackageName::OnContentPathDismounted) to tell it whenever there were changes to these root paths.

Telling the AssetRegistry a ContentPath has been dismounted without triggering the actual removal of the mount point via FPackageName::UnRegisterMountPoint first would not make sense.

This flow should work with a single MountPoint for /ExampleFeature/ and should not require the extra second package name mount point that you were using.

To remove assets, you can try calling ScanPathsSynchronous() with bForceReScan=true after unmounting the pak files. The re-scan starts by removing all known assets within the scanned path, effectively dropping them from that folder.

As long as there are no actual assets mounted to the path, this would also mean the scan that follows will not find anything and you end up with those assets deleted from the ARState.

In some other cases this behavior can be considered a bug. I.e. it will delete the AR entries for assets within an IOStore container, with no way that the scan can rediscover these assets.

If you are only using pak files, however, this should work in your favor.

I’ll check with the developers to find out what the correct way to do this is, because I agree there’s no obvious API to handle this properly and selectively.

Kind Regards,

Sebastian

[Attachment Removed]

Hi Jack,

Thanks for the info. I haven’t had a chance to try this yet but I should have the time shortly. I’ll let you know what I find

[Attachment Removed]

This didn’t seem to really change anything. It looks like the code above is just a way to dynamically construct the path fed to the load call and doesn’t address the underlying issue which is the Mount point is a Game Path and the file path being loaded is a Relative Path

[Attachment Removed]

Hi Sebastian,

I’ve attached a call stack that leads to the issue occurring.

Is there any way to check what the internal mount point is? They are being loaded from a directory but we do that with our UGC plugins and they don’t seem to have any issue with this but it would be useful to check, just in case.

Checking the mount points for the plugin we have the following:

‘/ExampleFeature/’ -> ‘/ExampleFeature/Content/’

‘/ExampleFeature/’ -> ‘../../Modio/ExampleFeature_Windows/Content/’

We have two mount points because of some quirk of unloading pak files. I’ve tried without the first mount point and it doesn’t seem to make a difference but I thought I’d include it just in case.

We do manually mount the plugin (though MountPluginFromExternalSource is private so we do it indirectly through MountNewlyCreatedPlugin) and the pak file before attempting to load the Game Feature Plugin. For clarity the mount call looks like this when the arguments are expanded:

PlatformPakFile->Mount(“D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak”, 4, “/ExampleFeature/”))

This is the same code path that loads our other UGC plugins which do get loaded just fine. The only problem we are having with it is when a GFP is looking for it’s Game Feature Data (at least so far)

[Attachment Removed]

For some reason the attachment seems to have not uploaded properly so I’ve just added it again

[Attachment Removed]

To elaborate a bit more the /ExampleFeature/Content/ mount point is there to help facilitate unloading the AssetRegistryState. We do this because some of the necessary functions are private but there is a delegate that gets called when a pak file is unmounted if the mount point matches specific conditions. So we just add a second mount point to satisfy that condition. We do also have a relative path mounted (‘/ExampleFeature/’ -> ‘../../Modio/ExampleFeature_Windows/Content/’) and just to make sure I have tested without that extra mount point with no changes.

I’m unsure what you want us to update with the call to Mount. As is that call results in this message “LogPackageName: FPackageName: Mount point added: ‘../../Modio/ExampleFeature_Windows/Content/’ mounted to ‘/ExampleFeature/’” which seems correct to me based on looking at the other mount points listed in the output of Packname.DumpMountPoints.

I guess it could be useful to get an example of what you would expect to be a correct call to mount and what that would result in just as a sanity check.

[Attachment Removed]

PakList shows me this entry D:/Projects/ARPGDebug_Native/Windows/ActionRPG/Modio/ExampleFeature_Windows/Content/Paks/Windows/ExampleFeatureActionRPG-Windows.pak Mounted to /ExampleFeature/

Stepping through the call stack this is what I see:

  1. FPackageName::DoesPackageExist() is called trying to load from the virtual system ie /ExampleFeature/MyDataAsset
  2. FPackageResourceManagerFile::IteratePossibleFiles() mutates the package path from virtual to relative /ExampleFeature/MyDataAsset -> ../../Modio/ExampleFeature_Windows/Content/MyDataAsset.uasset
  3. FPakPlatformFile::FileExists() then checks the pak files first with FPakPlatformFile::FindFileInPakFiles() using the relative path
  4. This calls FPakFile::Find() on each pak file. This is what ultimately calls FPakFile::FindLocationFromIndex() which is checking if the relative path starts with the virtual mount point /ExampleFeature/

You made a note of IOStore. This current example is not using IOStore but we will want to make this work with it enabled so if there are any differences that would be useful to know too (thought I have tried this with IOStore enabled and get the same result).

Regarding the unloading I checked with the team and it looks like the problem is twofold. The bigger issue is UAssetRegistryImpl::OnContentPathDismounted() is a private member of a private class and IAssetRegistry doesn’t expose it as part of it’s interface. We use FPackageName::UnRegisterMountPoint() which calls FLongPackagePathsSingleton::RemoveMountPoint() and, after doing some checks that eventually calls UAssetRegistryImpl::OnContentPathDismounted().

The second limitation is that in order to cleanly remove assets from the Asset Registry at runtime, we have to remove assets from a base directory because there’s no method to reverse the process of appending an AssetRegistryState. This means that all content needs to be in a self-contained directory so that we can remove it en masse.

[Attachment Removed]