Skip to main content

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

warning

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.

BehaviourWho owns it
Fetching a file's dependency listSDK. FFile.dependencies is populated by GetFiles, MatchPlatformFiles, and FCFCoreMod.latestFiles from GetMods and GetMod
Reading an installed mod's dependenciesSDK. FInstalledMod.installedFile and latestUpdatedFile are full FFile, so no network call is needed
Deciding which dependencies to installYou
Ordering the installsYou. All three Install overloads take one mod per call
Blocking an incompatible pairYou
Load order at runtimeYou compute it, the SDK persists it in FInstalledModProperties.loadOrder
Verifying the downloaded archive against a hashSDK, during install
Verifying installed modules against server fingerprintsSDK, through ICFCoreLibrary::PerformModsValidation
Choosing the right file for the running platformSDK, through MatchPlatformFiles and AssureClientModsUpdated

At minimum, your game needs to implement:

  1. A resolver that expands RequiredDependency entries into an install set.
  2. A conflict check in both directions: refuse a file whose dependencies name an Incompatible mod the player already has, and scan every installed mod's installedFile.dependencies for an Incompatible entry pointing back at what you are installing. ICFCoreLibrary::GetInstalledMods is the baseline, GetSystemInstalledMods if your rules are machine wide.
  3. A UI surface for OptionalDependency so the player opts in.
  4. A cycle guard and a visited set. A dependency can declare dependencies, and two mods can name each other.
  5. An uninstall rule. Uninstalling a mod that another installed mod requires breaks that other mod, and the SDK will not warn you.
  6. A roll back path. Stop in flight installs with ICFCoreLibrary::CancelInstallation, remove completed ones with Uninstall.
info

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.

FieldTypeDefaultMeaning
modIdint640The mod on the other end of the relation
fileIdint640The specific file of that mod, when the relation names one
relationTypeECFCoreFileRelationTypeNoneThe kind of relation
danger

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.

ValueNumericRecommended install policy
None0Default value. Treat as unclassified, log it, neither install nor block
EmbeddedLibrary1A shared library the mod expects to be present. Install before the mod that names it
OptionalDependency2Show as an optional add on. Never install without the player's consent
RequiredDependency3Install before the mod that names it. If it fails, cancel in flight installs, uninstall what completed, report the failure
Tool4An external or companion tool, not a runtime requirement. Surface as information
Incompatible5Refuse the install while the named mod is installed and enabled. Offer to disable or uninstall it first
Include6Content bundled with the mod. Do not double install. Surface as information
note

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

  1. Fetch the file the player asked for, with its dependencies array, and partition the array by relationType.
  2. Collect the fileId of every RequiredDependency into a pending set. Where fileId is 0, resolve the mod first and choose a file.
  3. Fetch all pending files in one GetFiles call, not one call per dependency.
  4. Repeat for each new file, tracking visited mod ids so a cycle cannot loop forever.
  5. Check collected Incompatible entries against the installed set, in both directions, and stop with a player facing message on a match.
  6. 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.

FieldTypeNotes
idint640 is treated as uninstalled or non-existent
gameIdint64The game this file belongs to
modIdint64The mod this file belongs to
isAvailableboolCheck before you try to install
displayNameFStringHuman readable name for UI
fileNameFStringThe file name as published
releaseTypeECFCoreFileReleaseTypeNone 0, Release 1, Beta 2, Alpha 3
fileStatusECFCoreFileStatusModeration and publishing status, see below
hashesTArray<FFileHash>Download integrity, see below
fileDateFDateTimePublish date
fileLengthint64The size of the archive as uploaded, in bytes
fileSizeOnDiskint64Expected total size of the extracted mod. Compared against the summed directory size during quick validation
downloadCountint64Popularity signal for UI
downloadUrlFStringMay be empty. Use ICFCoreApi::GetModFileDownloadUrl when it is
gameVersionsTArray<FString>Raw game version strings
sortableGameVersionsTArray<FSortableGameVersion>Structured versions, see below
dependenciesTArray<FFileDependency>The relation edges above
ExposeAsAlternativeboolWhether this file is exposed as an alternative. Capitalized unlike every other field on this struct
parentProjectFileIdint64The parent project's file, for child files
alternateFileIdint64An alternate file for this file
isServerPackboolThis file is itself a server pack
serverPackFileIdint64The server pack file belonging to this file
fileFingerprintint64A fingerprint for the file as a whole. The SDK's validation path uses the per module fingerprints, not this
modulesTArray<FFileModule>The installable units inside the file, see below
cookingInfoFFileCookingInfoCarries 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.

ValueNumericValueNumeric
None0Archived8
Processing1Testing9
ChangesRequired2Released10
UnderReview3ReadyForReview11
Approved4Deprecated12
Rejected5Baking13
MalwareDetected6AwaitingPublishing14
Deleted7FailedPublishing15

File indexes

FFileIndex is the lightweight summary in FCFCoreMod.latestFilesIndexes. Use it to pick a file without pulling every full FFile.

FieldTypeNotes
gameVersionFStringThe game version this entry targets
fileIdint64The file to fetch with GetFiles
fileNameFStringThe published file name
releaseTypeECFCoreFileReleaseTypeNone, Release, Beta, Alpha
gameVersionTypeIdint32The version type, see the get version types API
modLoaderECFCoreModLoaderTypeDefaults to Any
warning

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.

note

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.

warning

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:

  1. 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.
  2. An empty array logs a warning and continues. An unhashed file is not a failure.
  3. It uses hashes[0] only. That entry's value is the expected hash and its algo selects the algorithm. Later entries are ignored.
  4. Md5 and Sha1 are computed and compared case insensitively.
  5. Any other algo, including None, fails the install with DownloadedFileHasInvalidHash. A mismatch fails with the same code and logs both values.
warning

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.

CheckTriggerWhat it compares
Modules existQuick validation inside the SDKEvery 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 fingerprintsICFCoreLibrary::PerformModsValidationFor 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 diskQuick validation, gated on the modFileSizeValidation server side feature flagSums 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.

warning

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.

note

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.

CallFilterReturnsUse it when
ICFCoreApi::GetFilesFCFCoreGetFilesFilter, one field: TArray<int64> fileIdsThe files for exactly those idsYou already have the right ids for this platform, from latestFilesIndexes or a dependency's fileId
ICFCoreApi::MatchPlatformFilesFMatchPlatformFilesFilter, one field: TArray<int64> fileIdsThe files matching each input id for the current platformYou received ids from elsewhere, above all from a game server the player is joining
note

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.

KeyWhereEffect
isServerFCFCoreSettings and the matching Project Settings entrySet true on a game server so the platform is identified as a server platform, for example windows_server rather than windows
isServerPcOnlyFCFCoreSettings and Project SettingsRelevant only when isServer is true. Set it when your server only supports PC, non-console, clients
filterPcOnlyFCFCoreGetModsFilterApplicable 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
devModIdsFCFCoreGetModsFilterServer only. Requests the development versions of the listed mods
warning

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.

  1. Derive the order from your dependency graph, dependencies first.
  2. Write it with ICFCoreLibrary::UpdateInstalledModsProperties, one FInstalledModProperties per mod with modId, enabled and loadOrder set. loadOrder defaults to MAX_int32, so any mod you have not ordered sorts last.
  3. At load time read FInstalledMod.loadOrder from GetInstalledMods and load in that order, skipping disabled mods and unusable statuses.
note

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.