Mod dependencies and file relationships
Mods do not always stand alone. One mod can require another to work at all, prefer another for a better experience, or conflict outright with a third. Left unhandled, that shows up to players as mods that silently fail to load, sessions that crash from an incompatible pair, or a favorite mod that stops working with no explanation. CurseForge tracks these relationships for every mod file; making sense of them for your players is your game's job.
A mod file can declare that it needs another mod, that it prefers another mod, or that it must never be loaded alongside another mod. CurseForge carries that on every mod file and your game receives it in the dependencies array of FFile. This page covers that model, the file model that carries it, and how to pick the right file for the running platform. Everything here is cfcore-sdk-ue.
The SDK does not resolve dependencies for you
Currently the SDK delivers dependency data and does nothing with it. FFileDependency appears in exactly two places in the plugin: its declaration in api/models/file_dependency.h, and the dependencies field on FFile. No function in ICFCoreLibrary or ICFCoreApi reads relationType. The Install overloads install what you hand them: they do not walk dependencies, do not enqueue a required dependency, and do not block an install that conflicts with something already installed. A player who installs a mod with a RequiredDependency gets that mod alone, and the dependency stays missing until your code fetches and installs it.
The data itself arrives populated. FFile is deserialized by Unreal's reflection based JSON conversion, so dependencies is filled in wherever a full FFile arrives.
| Behaviour | Who owns it |
|---|---|
| Fetching a file's dependency list | SDK. FFile.dependencies is populated by GetFiles, MatchPlatformFiles, and FCFCoreMod.latestFiles from GetMods and GetMod |
| Reading an installed mod's dependencies | SDK. FInstalledMod.installedFile and latestUpdatedFile are full FFile, so no network call is needed |
| Deciding which dependencies to install | You |
| Ordering the installs | You. All three Install overloads take one mod per call |
| Blocking an incompatible pair | You |
| Load order at runtime | You compute it, the SDK persists it in FInstalledModProperties.loadOrder |
| Verifying the downloaded archive against a hash | SDK, during install |
| Verifying installed modules against server fingerprints | SDK, through ICFCoreLibrary::PerformModsValidation |
| Choosing the right file for the running platform | SDK, through MatchPlatformFiles and AssureClientModsUpdated |
At minimum, your game needs to implement:
- A resolver that expands
RequiredDependencyentries into an install set. - A conflict check in both directions: refuse a file whose
dependenciesname anIncompatiblemod the player already has, and scan every installed mod'sinstalledFile.dependenciesfor anIncompatibleentry pointing back at what you are installing.ICFCoreLibrary::GetInstalledModsis the baseline,GetSystemInstalledModsif your rules are machine wide. - A UI surface for
OptionalDependencyso the player opts in. - A cycle guard and a visited set. A dependency can declare dependencies, and two mods can name each other.
- An uninstall rule. Uninstalling a mod that another installed mod requires breaks that other mod, and the SDK will not warn you.
- A roll back path. Stop in flight installs with
ICFCoreLibrary::CancelInstallation, remove completed ones withUninstall.
The SDK must be initialized first, and every call here is asynchronous and calls back on the game's main thread. If you want resolution built into the SDK instead of into your game, raise it with cfforstudios@overwolf.com.
The file dependency model
Each entry in FFile.dependencies is one directed edge from the file you are looking at to another mod. The struct is BlueprintType with all three properties BlueprintReadOnly.
| Field | Type | Default | Meaning |
|---|---|---|---|
modId | int64 | 0 | The mod on the other end of the relation |
fileId | int64 | 0 | The specific file of that mod, when the relation names one |
relationType | ECFCoreFileRelationType | None | The kind of relation |
The meaning of a zero fileId is not stated by the SDK. Do not assume it means "latest file" and never forward a zero fileId into an install call. Install Mod Extended and the mod plus file Install overloads treat an FFile whose id is 0 as a request for the latest file, so a zero id silently installs whatever is newest rather than failing. Resolve modId with ICFCoreApi::GetMod and pick a file deliberately. See Installing and managing mods for the install parameters.
File relation types
The seven values in api/models/enums/file_relation_type.h. The last column is recommended policy, not SDK behaviour: the SDK does the same thing for every row, nothing.
| Value | Numeric | Recommended install policy |
|---|---|---|
None | 0 | Default value. Treat as unclassified, log it, neither install nor block |
EmbeddedLibrary | 1 | A shared library the mod expects to be present. Install before the mod that names it |
OptionalDependency | 2 | Show as an optional add on. Never install without the player's consent |
RequiredDependency | 3 | Install before the mod that names it. If it fails, cancel in flight installs, uninstall what completed, report the failure |
Tool | 4 | An external or companion tool, not a runtime requirement. Surface as information |
Incompatible | 5 | Refuse the install while the named mod is installed and enabled. Offer to disable or uninstall it first |
Include | 6 | Content bundled with the mod. Do not double install. Surface as information |
maxConcurrentInstallations defaults to 3, so parallel installs complete out of the order you started them in. Chain them on the installed callback if you need a strict order.
Walking the graph
- Fetch the file the player asked for, with its
dependenciesarray, and partition the array byrelationType. - Collect the
fileIdof everyRequiredDependencyinto a pending set. WherefileIdis0, resolve the mod first and choose a file. - Fetch all pending files in one
GetFilescall, not one call per dependency. - Repeat for each new file, tracking visited mod ids so a cycle cannot loop forever.
- Check collected
Incompatibleentries against the installed set, in both directions, and stop with a player facing message on a match. - Install the resolved set, dependencies before the mod that requires them.
The sample holds state across the asynchronous levels of the walk in four members: visited_mod_ids_, resolved_file_ids_, unpinned_mod_ids_, incompatible_mod_ids_.
#include <cfcore_context.h>
#include <api/cfcore_api.h>
#include <api/models/file_dependency.h>
#include <api/models/filters/get_files_filter.h>
using namespace cfcore;
void ATestGameMode::CollectDependencies(const FFile& file,
TSet<int64>& out_next_file_ids) {
for (const FFileDependency& dep : file.dependencies) {
switch (dep.relationType) {
case ECFCoreFileRelationType::RequiredDependency:
case ECFCoreFileRelationType::EmbeddedLibrary:
if (visited_mod_ids_.Contains(dep.modId)) break; // Cycle guard.
visited_mod_ids_.Add(dep.modId);
if (dep.fileId != 0) {
resolved_file_ids_.Add(dep.fileId);
out_next_file_ids.Add(dep.fileId);
} else {
// No file pinned. Resolve with GetMod and pick a file yourself.
unpinned_mod_ids_.AddUnique(dep.modId);
}
break;
case ECFCoreFileRelationType::Incompatible:
incompatible_mod_ids_.AddUnique(dep.modId);
break;
default: // Optional, Tool, Include, None, or a newer value.
break; // Surface it or log it. Never install it silently.
}
}
}
void ATestGameMode::FetchDependencyFiles(const TSet<int64>& file_ids) {
if (file_ids.Num() == 0) {
OnDependencyGraphComplete(); // Conflict check, then install.
return;
}
FCFCoreGetFilesFilter filter;
filter.fileIds = file_ids.Array();
CFCoreContext::GetInstance()->Api()->GetFiles(filter,
ICFCoreApi::FGetFilesDelegate::CreateLambda(
[this](const TOptional<TArray<FFile>>& opt_files,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet() || !opt_files.IsSet()) return;
TSet<int64> next_level;
for (const FFile& file : opt_files.GetValue()) {
if (!file.isAvailable) continue; // Cannot be installed.
CollectDependencies(file, next_level);
}
FetchDependencyFiles(next_level);
}));
}
Blueprint: fetch the same data with Get Files Info By Ids (pins FileIds, OnResults, OnError). The node takes a plain FileIds array where C++ takes an FCFCoreGetFilesFilter. Loop the dependencies array on each returned FFile struct, break each element to read modId, fileId and relationType, and switch on it. Install each resolved file with Install Mod Extended (pins InMod, InFile, InAdditionalParams, OnProgress, OnInstalled, OnError), waiting for OnInstalled before the next. Install Mod always takes the mod's latest file, so a resolver wants Install Mod Extended with a non-zero InFile id. Read the installed set with Get Installed Mods or Get Global Installed Mods, roll back with Cancel Mod Installation and Uninstall Mod.
A dependency is only satisfied when the installed mod's enabled is true and its status is usable: skip Pending, Invalid, Uploading and Uninstalled, and set a policy for Modified. ICFCoreLibrary::SyncWithServer (Blueprint: Synchronize Installed Mods with Server) is what marks an installed mod OutOfDate. To show the player what changed before they accept an optional dependency, call ICFCoreApi::GetModFileChangelog (Blueprint: Get Mod File Changelog).
The file model
FFile is the complete description of one downloadable revision of a mod. Your resolver, installer and validation code all read from it.
| Field | Type | Notes |
|---|---|---|
id | int64 | 0 is treated as uninstalled or non-existent |
gameId | int64 | The game this file belongs to |
modId | int64 | The mod this file belongs to |
isAvailable | bool | Check before you try to install |
displayName | FString | Human readable name for UI |
fileName | FString | The file name as published |
releaseType | ECFCoreFileReleaseType | None 0, Release 1, Beta 2, Alpha 3 |
fileStatus | ECFCoreFileStatus | Moderation and publishing status, see below |
hashes | TArray<FFileHash> | Download integrity, see below |
fileDate | FDateTime | Publish date |
fileLength | int64 | The size of the archive as uploaded, in bytes |
fileSizeOnDisk | int64 | Expected total size of the extracted mod. Compared against the summed directory size during quick validation |
downloadCount | int64 | Popularity signal for UI |
downloadUrl | FString | May be empty. Use ICFCoreApi::GetModFileDownloadUrl when it is |
gameVersions | TArray<FString> | Raw game version strings |
sortableGameVersions | TArray<FSortableGameVersion> | Structured versions, see below |
dependencies | TArray<FFileDependency> | The relation edges above |
ExposeAsAlternative | bool | Whether this file is exposed as an alternative. Capitalized unlike every other field on this struct |
parentProjectFileId | int64 | The parent project's file, for child files |
alternateFileId | int64 | An alternate file for this file |
isServerPack | bool | This file is itself a server pack |
serverPackFileId | int64 | The server pack file belonging to this file |
fileFingerprint | int64 | A fingerprint for the file as a whole. The SDK's validation path uses the per module fingerprints, not this |
modules | TArray<FFileModule> | The installable units inside the file, see below |
cookingInfo | FFileCookingInfo | Carries cookerVersion, for cloud cooking |
FSortableGameVersion gives a comparable form of the raw gameVersions strings: gameVersionName (FString), gameVersionPadded (FString), gameVersion (FString), gameVersionReleaseDate (FDateTime), gameVersionTypeId (int32).
File status values
ECFCoreFileStatus decides whether a file is publishable at all. Filter on it before adding a file to an install set. For what drives these values, see moderation.
| Value | Numeric | Value | Numeric |
|---|---|---|---|
None | 0 | Archived | 8 |
Processing | 1 | Testing | 9 |
ChangesRequired | 2 | Released | 10 |
UnderReview | 3 | ReadyForReview | 11 |
Approved | 4 | Deprecated | 12 |
Rejected | 5 | Baking | 13 |
MalwareDetected | 6 | AwaitingPublishing | 14 |
Deleted | 7 | FailedPublishing | 15 |
File indexes
FFileIndex is the lightweight summary in FCFCoreMod.latestFilesIndexes. Use it to pick a file without pulling every full FFile.
| Field | Type | Notes |
|---|---|---|
gameVersion | FString | The game version this entry targets |
fileId | int64 | The file to fetch with GetFiles |
fileName | FString | The published file name |
releaseType | ECFCoreFileReleaseType | None, Release, Beta, Alpha |
gameVersionTypeId | int32 | The version type, see the get version types API |
modLoader | ECFCoreModLoaderType | Defaults to Any |
latestFilesIndexes does not carry dependency data. FFileDependency lives only on the full FFile. To resolve dependencies from a mod object, use FCFCoreMod.latestFiles, which is an array of full FFile, or fetch the ids from latestFilesIndexes with GetFiles.
Mod loader types
ECFCoreModLoaderType appears on FFileIndex.modLoader and on FCFCoreSearchModsFilter.modLoaderType: Any 0, Forge 1, Cauldron 2, LiteLoader 3, Fabric 4.
These are Minecraft ecosystem loaders. For any other game Any is the value you will see, and the search filter serializer only sends modLoaderType when it differs from Any. Do not build a loader compatibility rule on this enum unless CurseForge has configured loaders for your game.
Child file types
api/models/enums/child_file_type.h declares ECFCoreChildFileType: None 0, ServerPack 1, Source 2.
This enum is in the public API surface but nothing in the current release references it. No struct field and no function parameter has this type, so do not build logic on it. The child file information you can act on today is the isServerPack, serverPackFileId, parentProjectFileId, alternateFileId and ExposeAsAlternative fields on FFile.
Hashes and hash algorithms
FFileHash has two fields: value (FString) and algo (ECFCoreHashAlgo), which is None 0, Sha1 1, Md5 2.
The SDK verifies the download during installation. The behaviour is not what a hashes array suggests:
- It takes the hash array from the file being installed, or from the delta diff record when the install is an update through a delta diff.
- An empty array logs a warning and continues. An unhashed file is not a failure.
- It uses
hashes[0]only. That entry'svalueis the expected hash and itsalgoselects the algorithm. Later entries are ignored. Md5andSha1are computed and compared case insensitively.- Any other
algo, includingNone, fails the install withDownloadedFileHasInvalidHash. A mismatch fails with the same code and logs both values.
Because only hashes[0] is used, do not write logic that assumes an algorithm sits at a given index. If you verify a file yourself, iterate the array and match on algo.
The delta diff record in step 1 comes from ICFCoreApi::GetModFileDeltaDiff(mod_id, new_file_id, old_file_id, delegate), which returns an FFileDeltaDiff carrying oldFileId, newFileId, status, downloadUrl, fileName, fileLength, fileSizeOnDisk and its own hashes array. This call is C++ only, there is no Blueprint node for it in the current release.
Modules and fingerprints
FFileModule has two fields: name (FString) and fingerprint (int64). A module is a path inside the installed mod, resolved against the mods root directory joined with FInstalledMod.pathOnDisk. It can be a single file or a directory. The SDK uses modules in three validation paths.
| Check | Trigger | What it compares |
|---|---|---|
| Modules exist | Quick validation inside the SDK | Every FFileModule.name under the mod's pathOnDisk must exist. A missing one sets the mod to EInstalledModStatus::Invalid with the reason "Missing module". Skipped when the file declares zero modules |
| Module fingerprints | ICFCoreLibrary::PerformModsValidation | For each module the SDK hashes the module's files with Murmur2, sorts the per file hashes numerically, concatenates them, hashes the result again, then compares against the server's FFileModule.fingerprint |
| File size on disk | Quick validation, gated on the modFileSizeValidation server side feature flag | Sums the size of the files in the mod directory against FFile.fileSizeOnDisk. A mismatch marks the mod Invalid with the reason "Corrupted mod files". Skipped when fileSizeOnDisk is 0 |
A directory module's fingerprint is computed over the files found recursively. Files listed in the ignoredDynamicModFiles setting are excluded from both the fingerprint and the size sum, so if your game writes save data or config next to a mod's assets, add those names or validation reports the mod invalid. Quick validation skips any mod already at Invalid, so a mod marked invalid once will not clear itself on a later pass.
ignoredDynamicModFiles entries must be lower case bare filenames with no directory part. The matcher lowercases the clean filename of each candidate before testing set membership, so an entry of SaveData.bin never matches and the exclusion silently does nothing. The default value is { "assetregistry.bin" }, which shows the expected form.
PerformModsValidation refetches the server side file records with GetFiles first, so it needs network access, and the checksum pass is proportional to mod size. Run it where a pause is acceptable, not mid match. A mod is set to Invalid when its mod id is unknown to the CurseForge servers, when it does not exist on local disk, or when it does not match the server side checksum.
// Feed PerformModsValidation the array from GetInstalledMods. Note that the
// C++ GetInstalledMods delegate takes only the array, with no error param.
library->PerformModsValidation(installed_mods,
ICFCoreLibrary::FPerformModsValidationDelegate::CreateLambda(
[](const TOptional<TArray<FInstalledMod>>& opt_invalid,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() || !opt_invalid.IsSet()) return;
for (const FInstalledMod& mod : opt_invalid.GetValue()) {
UE_LOG(LogTemp, Warning, TEXT("Invalid mod %lld"), mod.details.id);
}
}));
Blueprint: Perform Mods Validation (pins installed_mods, on_success, on_error), fed by Get Installed Mods. Invalid mods come back on on_success and the status is persisted, so Get Installed Mods afterwards reports them as invalid too. Validation is covered in full on Installing and managing mods.
Filtering files by platform
A file id is not platform neutral. A mod published for several platforms has a different file per platform, so a game server that hands its client a list of file ids has handed over ids that may belong to another platform.
| Call | Filter | Returns | Use it when |
|---|---|---|---|
ICFCoreApi::GetFiles | FCFCoreGetFilesFilter, one field: TArray<int64> fileIds | The files for exactly those ids | You already have the right ids for this platform, from latestFilesIndexes or a dependency's fileId |
ICFCoreApi::MatchPlatformFiles | FMatchPlatformFilesFilter, one field: TArray<int64> fileIds | The files matching each input id for the current platform | You received ids from elsewhere, above all from a game server the player is joining |
Both filter structs contain a single fileIds array and nothing else. There is no per platform, per release type or per game version field. Platform matching happens server side, keyed on how the SDK identifies the running platform, and is not something you parameterize. Four keys change how the platform is identified or which files come back.
| Key | Where | Effect |
|---|---|---|
isServer | FCFCoreSettings and the matching Project Settings entry | Set true on a game server so the platform is identified as a server platform, for example windows_server rather than windows |
isServerPcOnly | FCFCoreSettings and Project Settings | Relevant only when isServer is true. Set it when your server only supports PC, non-console, clients |
filterPcOnly | FCFCoreGetModsFilter | Applicable when isServer is true. True returns the latest server mod that has a corresponding Windows mod, false returns a server mod that has all supported platforms |
devModIds | FCFCoreGetModsFilter | Server only. Requests the development versions of the listed mods |
FCFCoreGetModsFilter is only reachable from the ICFCoreApi::GetMods overload that takes a filter, which is C++ only. Get Mods Info By Ids takes a plain modIds array and gives no access to filterPcOnly or devModIds. A Blueprint-only dedicated server integration cannot set them.
#include <api/models/filters/match_platform_files_filter.h>
FMatchPlatformFilesFilter filter;
filter.fileIds = server_file_ids;
// This delegate takes its TOptionals by value, unlike FGetFilesDelegate
// which takes them by const reference. Match it exactly or it will not bind.
CFCoreContext::GetInstance()->Api()->MatchPlatformFiles(filter,
ICFCoreApi::FMatchPlatformFilesDelegate::CreateLambda(
[](TOptional<TArray<FFile>> opt_files,
TOptional<FCFCoreApiResponseError> opt_err) {
if (opt_err.IsSet() || !opt_files.IsSet()) return;
for (const FFile& file : opt_files.GetValue()) {
// Correct ids for this platform. Their dependencies still need
// resolving: MatchPlatformFiles does not expand them.
}
}));
Blueprint: Match Platform Files By Ids (pins filter, OnResults, OnError), building the input by setting fileIds on an FMatchPlatformFilesFilter struct.
For a client and server game, prefer letting the SDK run the whole join flow. ICFCoreClientServerLibrary::AssureClientModsUpdated, reached from ICFCoreLibrary::ClientServerLibrary(), takes the server's file id list, calls MatchPlatformFiles internally, resolves the mods behind those files, checks premium ownership for regular and dynamic content mods, and installs what is missing with an install origin of ServerModule. Blueprint: Assure Client Mods Updated (pins Target, ServerFileIds, OnProgress, OnUpdated, OnError). See multiplayer and servers.
On the dedicated server side, AssureServerModsUpdated takes an FAssureServerModsUpdatedParams with modIds (TArray<int64>, the mods you want installed on the game server) and devModIds (TArray<int64>, the latest mod file in "Ready" state for testing unpublished revisions). devModIds take precedence over modIds. Blueprint: Assure Server Mods Updated (pins Target, params, OnProgress, OnUpdated, OnError). Set isServer to true in settings before you call it.
Load order after install
Resolving dependencies gets the right files on disk. It does not decide the order your game loads them in. The SDK persists an order but does not compute one.
- Derive the order from your dependency graph, dependencies first.
- Write it with
ICFCoreLibrary::UpdateInstalledModsProperties, oneFInstalledModPropertiesper mod withmodId,enabledandloadOrderset.loadOrderdefaults toMAX_int32, so any mod you have not ordered sorts last. - At load time read
FInstalledMod.loadOrderfromGetInstalledModsand load in that order, skipping disabled mods and unusable statuses.
FInstalledModProperties also carries id, an FGuid. id takes precedence over modId when both are set, and it is the only way to identify unmanaged mods. Leave id unset when you want modId used.
Blueprint: Update Installed Mods Properties (pins InInstalledModsProperties, OnSuccess, OnError) with an array of FInstalledModProperties structs, setting modId, enabled and loadOrder on each.
Related
- The in-game mod browser for the shipped UI that lists and installs mods.
- Mod installation methods for the taxonomy that decides what a module means for your game.
- Moderation for what drives
fileStatusand file clusters.