Skip to main content

Multiplayer and dedicated servers

A modded session only works if every participant runs the same mods at the same file versions. cfcore-sdk-ue handles that with one interface, ICFCoreClientServerLibrary, and two entry points: one the server calls before it opens, one each client calls while it joins. Both converge on the same install and validation machinery.

The interface carries no transport. It takes mod ids on the server side, file ids on the client side, and returns installed mods. Getting the server's file id list to a joining client is your game's job. There is no matchmaking, server advertising, or session discovery in the SDK.

Before you call anything: the isServer prerequisite

AssureServerModsUpdated does nothing useful unless isServer is true in the FCFCoreSettings you pass to Initialize. Also set isServerPcOnly if the server accepts PC clients only, before calling Initialize, since neither field can be changed afterward.

SettingTypeDefaultEffect
isServerboolfalseThree effects: the reported platform gains the server suffix (windows_server, not windows), every request gains the x-mod-server-request: true header, and premium downloads skip the protected URL path
isServerPcOnlyboolfalseRead only when isServer is true. Becomes FCFCoreGetModsFilter::filterPcOnly on the server's mod query. Set this if your server only supports PC clients, for example a non-console dedicated server
maxConcurrentInstallationsint323Caps parallel installs in either flow. Clamped with FMath::Max<int32>(1, ...), so below 1 behaves as 1
dynamicContentCategoryIdsTSet<int64>emptyCategory ids treated as dynamic content. Empty disables the library-wide ownership pass on every client join
premiumMods.publicKeyPemFStringemptyPEM key used to verify the premium check response signature. Empty skips verification
note

filterPcOnly true returns the latest server mod that has a corresponding Windows mod; false returns one that has all supported platforms. A server accepting console clients should leave it false, or it can pin revisions whose only guaranteed counterpart is Windows.

warning

Neither flag can change after initialization. FCFCoreUpdatableSettings, the only struct UpdateSettings (Update Settings) accepts, holds just updateDefaultLanguage and defaultLanguage. A process is a server or a client for its whole lifetime.

The same three fields exist on UCFCoreEditorSettings for Project Settings configuration.

info

The SDK must be initialized, or both Blueprint nodes fire OnError with ECFCoreErrorCodes::FailedToInitialize and description Not initialized. Premium validation on the client path also needs an authenticated player who is online.

The client-server library

In C++: CFCoreContext::GetInstance()->Library()->ClientServerLibrary(). In Blueprint the two flow methods sit on UCFCoreSubsystem under cfcore|Library|ClientServer.

MethodRoleInputBlueprint node
AssureServerModsUpdatedServerFAssureServerModsUpdatedParamsAssure Server Mods Updated
AssureClientModsUpdatedClientTArray<int64> of server file idsAssure Client Mods Updated
GetInstalledModsEitherTSharedRef<TArray<int64>> of mod idsNone (C++ only)
warning

ICFCoreClientServerLibrary::GetInstalledMods has no Blueprint equivalent, no error channel, and runs no validation, so its statuses can be stale. From Blueprint use the general Get Installed Mods node and filter yourself, with Perform Mods Validation first if status matters.

C++ delegateParametersNotes
FModsUpdateProgressDelegateconst FModsUpdateProgress&, const TOptional<FLibraryProgress>&, const TOptional<FCFCoreMod>&Both optionals unset on a phase change, set together on a per-mod install tick
FModsUpdateDelegateTOptional<TArray<FInstalledMod>>, TOptional<FCFCoreError>Exactly one of the two is meaningful per call
FGetInstalledModsDelegateTArray<FInstalledMod>No error parameter

The Blueprint bridge flattens the optionals: OnProgress always receives UpdateProgress, ModInstallProgress, and Mod as plain values, substituting defaults unless both are set, and errors route to a separate OnError pin. Branch on Mod.id != 0 to tell a phase-change tick from a real per-mod tick.

note

There is no cancel-the-join call and no free-disk-space call in the current release. Do not plan a flow around either.

Server flow: assuring the server's mod set

How it works

  1. Pass the mod ids you want the server to run, plus devModIds if you need unpublished revisions for testing, which take precedence over modIds.
  2. The SDK fetches mod details for the requested ids, using isServerPcOnly to pick the right platform variant per mod.
  3. An unavailable mod is uninstalled locally and the call fails with DetectedUnavailableMod. Premium mods are exempt, since they should not become unavailable to players who bought them.
  4. The shared update and validation stage runs.
// Headers: cfcore_context.h, library/cfcore_library.h,
// library/cfcore_client_server_library.h,
// library/models/assure_server_mods_updated_params.h
// Error handling abbreviated for length.
void AMyDedicatedServer::AssureModSet(const TArray<int64>& RequiredModIds) {
FAssureServerModsUpdatedParams Params;
Params.modIds = RequiredModIds;

CFCoreContext::GetInstance()->Library()->ClientServerLibrary()->AssureServerModsUpdated(
Params,
ICFCoreClientServerLibrary::FModsUpdateProgressDelegate::CreateLambda(
[](const FModsUpdateProgress& Phase,
const TOptional<FLibraryProgress>& ModInstallProgress,
const TOptional<FCFCoreMod>& Mod) {
if (!ModInstallProgress.IsSet() || !Mod.IsSet()) {
return; // Phase change only: Validating, then Installing.
}
UE_LOG(LogTemp, Log, TEXT("%s: %d%%"), *Mod->name,
ModInstallProgress->dataTransfer.progress);
}),
ICFCoreClientServerLibrary::FModsUpdateDelegate::CreateLambda(
[this](TOptional<TArray<FInstalledMod>> InstalledMods,
TOptional<FCFCoreError> OptError) {
if (OptError.IsSet() && OptError->isError) {
HandleModSetFailure(*OptError);
return;
}
// Publish the file ids clients must match against.
TArray<int64> ServerFileIds;
for (const FInstalledMod& Installed : InstalledMods.GetValue()) {
ServerFileIds.Add(Installed.installedFile.id);
}
OpenServerForConnections(ServerFileIds);
}));
}

Blueprint: Assure Server Mods Updated (cfcore|Library|ClientServer), pins Target, params, OnProgress, OnUpdated, OnError. Read installedFile.id on each installed_mods entry to build the file id list you replicate to clients.

FAssureServerModsUpdatedParams fieldTypeMeaning
modIdsTArray<int64>The mod ids you want installed on the game server
devModIdsTArray<int64>Requests the latest mod file in Ready state instead of the published one, so mod authors can test mod revisions before making them public. Takes precedence over modIds. Server-only: the filter field it maps to works only for servers

Two gotchas, both surfacing as MissingInstalledMods. A mod whose mainFileId is not in its latestFiles is logged and silently dropped, failing later at final validation as a count mismatch. A mod id the API never returns (deleted, wrong game, typo) behaves identically. Log requested ids next to returned ids, and check for a "doesn't contain it's main file" log line, before suspecting disk problems.

Client flow: joining a server

The client path shares steps 1, 2 and 7 with the server flow. It differs in the middle.

How it works

  1. The SDK calls the platform matching endpoint with the server's file ids, which returns an array of FFile objects that match each of the file ids for the current platform. This is how a console client joins a server that reported Windows file ids. Zero matches fails with NoPlatformFilesMatched; an API failure surfaces as ApiError.
  2. Mod ids are taken from the matched files (FFile::modId) and details fetched through the same query stage.
  3. Premium ownership is validated for the matched set, then dynamic content ownership across the whole local library. Either failure ends the join.
  4. Installs run with origin = EModInstallOrigin::ServerModule and a tracking origin of join_srv.
// Progress lambda omitted: identical shape to the server sample above.
CFCoreContext::GetInstance()->Library()->ClientServerLibrary()->AssureClientModsUpdated(
ServerFileIds, ProgressDelegate,
ICFCoreClientServerLibrary::FModsUpdateDelegate::CreateLambda(
[this](TOptional<TArray<FInstalledMod>> InstalledMods,
TOptional<FCFCoreError> OptError) {
if (OptError.IsSet() && OptError->isError) {
if (OptError->code == ECFCoreErrorCodes::ModsNotOwnedByUser) {
ShowPurchaseRequiredScreen();
} else {
ShowJoinFailed(*OptError);
}
return;
}
LoadModsAndConnect(InstalledMods.GetValue());
}));

Blueprint: Assure Client Mods Updated (cfcore|Library|ClientServer), pins Target, ServerFileIds (array of integer64), OnProgress, OnUpdated, OnError. Never read installed_mods without checking the error first: when the error optional is set, the installed-mods optional is unset.

note

The join builds its own install params, so you cannot pass install options in, and throttleDownloadKbps is fixed at 0. For throttling, drive those installs yourself with ICFCoreLibrary::Install (Install Mod Extended) before or after the join. To run the platform match alone, ApiMatchPlatformFiles (Match Platform Files By Ids) exposes the same call.

Server flow versus client flow

AspectAssureServerModsUpdatedAssureClientModsUpdated
InputParams struct (mod ids, dev mod ids)TArray<int64> of file ids
Platform resolutionNone. Uses filterPcOnly at query timePer-platform file matching
File chosen per modThe mod's mainFileId from latestFilesThe matched FFile for this client's platform
Unavailable modsUninstalled locally, then DetectedUnavailableMod. Premium exemptNot checked
Premium ownershipNot checkedChecked for every non-freemium mod in the set
Dynamic content ownershipNot checkedChecked across the whole local library when dynamicContentCategoryIds is populated
Install originDefault (ModPage)ServerModule
TrackingNone addedorigin = join_srv
Download throttlingNot configurable (default 0)Not configurable (default 0)
Unpublished revisionsSupported through devModIdsNot available
Settings requiredisServer true, optionally isServerPcOnlyNone specific
Zero-match failureMissingInstalledMods at final validationNoPlatformFilesMatched before installing

The shared update and validation stage

Both flows end in the same sequence, which is why they are named "assure". This is converge-to-target, not download.

  1. Installed mods are fetched for the requested ids, fresh server details copied onto them, and a validation pass runs. An invalid mod's status is set to Invalid and persisted to on-disk state.
  2. The installed list is re-read so decisions use post-validation statuses.
  3. Each pair is tested. A mod installs if it is absent (installedFile.id == 0), if the required file id differs from the installed one (this covers downgrades as well as upgrades), or if its status is Invalid, Pending, or Modified.
  4. Mods needing work install in parallel, capped by maxConcurrentInstallations.
  5. All installs are awaited together. The first error is reported and the flow stops before final validation. No in-flight install is aborted by another one's failure.
  6. Final validation re-reads the installed mods. If none came back, if the count does not match the requested pairs, or if any pair still needs installing, the flow fails with MissingInstalledMods.
warning

Step 3 compares file ids, not dates. Joining a server that pins an older revision downgrades a client running a newer one, and leaving does not restore it. Plan a re-sync with ICFCoreLibrary::SyncWithServer (Synchronize Installed Mods with Server) when the player returns to single player.

note

Neither flow sets enabled or loadOrder. If every participant must load the same mods in the same order, set that with ICFCoreLibrary::UpdateInstalledModsProperties (Update Installed Mods Properties), which takes FInstalledModProperties entries carrying id, modId, enabled, and loadOrder.

Nothing reverts a join: leaving a session leaves those mods installed. Drive teardown from the ids you kept, with Uninstall (Uninstall Mod), SyncWithServer, or CancelInstallation (Cancel Mod Installation), which is also the only way to interrupt a join in progress. To fail early rather than mid-download, read freeDiskSizeInBytes from GetModsDirInfo (Get Mods Directory Info) first.

Progress reporting during a join

Progress arrives on two levels through one delegate: a coarse phase in FModsUpdateProgress, whose only field is state, and per-mod detail in FLibraryProgress paired with its FCFCoreMod. The library-wide OnModInstallProgress and OnModInstalled events also fire for installs these flows trigger.

EModsUpdateProgressStateValueEmitted by these flows
Validating0Yes, once at the start of both flows, both optionals unset
Installing1Yes, on every per-mod tick, both optionals set
SuccessfullyCompleted2No
FailedToComplete3No (it is the struct's default value)
warning

Completion is never signalled through the progress delegate. Drive terminal UI state from the update delegate, or from OnUpdated and OnError. There is no aggregate percentage either, so a "3 of 7 mods, 42% overall" screen has to be aggregated by your code from the per-mod ticks.

FLibraryProgress fieldTypeMeaning
modIdint64Mod this tick belongs to. 0 on a substituted Blueprint default
fileIdint64File being installed
stateELibraryProgressStateFine-grained stage
dataTransferFLibraryProgressDataTransferPercentage and throughput
FLibraryProgressDataTransfer fieldTypeMeaning
progressint32Percent 0-100
transferredBytesint64Only relevant for downloading or uploading
transferRateBytesPerSecondint64Only relevant for downloading or uploading
filenameFStringOnly relevant for file operations (zip, copy, move, and so on)

ELibraryProgressState in declaration order: Pending (default), Downloading, Uploading, Validating, PendingUnzipping, Unzipping, PendingZipping, Zipping, Patching, Copying, CleaningUp, Cancelling, SuccessfullyCompleted, FailedToComplete. Which you see depends on the commands a given mod needs, with Patching when a binary delta patch replaces a full download.

warning

Installs run in parallel, so ticks for different mods interleave. Key per-mod UI off FLibraryProgress::modId or FCFCoreMod::id, never off arrival order.

Premium entitlement validation when joining a server

If a server hosts premium mods, the join is the enforcement point, and AssureClientModsUpdated runs the check for you.

  1. Every mod in the matched set joins the check list unless FPremiumDetails::isFreemium is true. Non-premium mods are included deliberately: the server-side check is an additional layer of protection beyond the client's own premium flag.
  2. The list goes to ICFCorePremiumMods::CheckMods (Premium Mods Check), which calls the authorized premium check and verifies the response signature.
  3. Owned ids are subtracted from requested ids. Anything left is logged and the join fails with ModsNotOwnedByUser.
warning

This is a pass or fail gate, not a list. The error is constructed from the error code alone. The set of unowned mod ids is written to the SDK log and then discarded, so it does not reach your error handler. AssureClientModsUpdated can tell you that the player is missing something, and nothing more.

That matters if your connecting screen has to show which mods the player needs and what they cost, for example switching a Join button to Purchase with a price. Build that surface from your own call rather than from the error:

  1. Resolve the server's file ids to mods, as you already do to display the required mod list.
  2. Call ICFCorePremiumMods::CheckMods (Premium Mods Check) with those mod ids. Send every mod whose isFreemium is false, matching what the join does, so your list agrees with the gate.
  3. Subtract the returned owned ids yourself. What remains is the purchase list, and each mod's premiumDetails gives you tierPrice, currencySymbol, discountData and trialDetails for the button.
  4. Treat a later ModsNotOwnedByUser from the join as confirmation, not as the source of the list.

Doing your own check also keeps the price and the gate consistent, because both then read the same server response.

Mod shapeSent for the checkBlocks the join
Non-premium (isPremium false, isFreemium false)YesOnly if the response does not list the id as owned. Decided server side; no local premium flag is read here
Premium (isPremium true, isFreemium false)YesYes, unless the response lists the id as owned
Freemium (isFreemium true)NoNo. A freemium mod can be installed without purchasing, then queries the game to unlock features per purchase, so it gates itself
info

An authenticated player with a working connection is required, because the premium check requires the player to be online. Signature verification is optional: it passes immediately when no public key is configured, so the join proceeds unsigned. Turn it on with premiumMods.publicKeyPem, or at runtime with ICFCorePremiumMods::OverridePublicKey (Override Public Key), which the SDK recommends so players cannot override it.

Trials. Three conditions exempt a mod from an ownership check: it is not premium, it is freemium, or the player holds an active trial for it, an FOwnedPremiumMods::trialMods entry for that modId with isExpired false. The dynamic content pass below applies all three exemptions, so an active trial satisfies that pass. The matched-set pass here does not consult trialMods at all, and does not need to: the premium check reports a mod under an active trial as owned, so a trialling player can join a server that requires it for as long as the trial stays active.

The server side differs in two ways. A premium mod marked unavailable is treated as available anyway, and when isServer is true the SDK skips the protected premium download URL, since server mods are never protected and are treated as non-premium mods for that purpose. AssureServerModsUpdated runs no ownership check at all.

Recommended pattern. Let the join check the server's set, and keep a session-start check for what the player may use: ICFCorePremiumMods::GetPremiumModsV2 (Get My Premium Mods V2), honouring ownedMods plus non-expired trialMods before loading premium content. On ModsNotOwnedByUser, route to purchase with ApiGeneratePremiumCheckoutUrl (Generate Premium Checkout URL) plus PollModPurchase (Poll Mod Purchase) on PC, or ApiInitiatePurchase and ApiFinalizePurchase on console. Do not hand-roll a poll loop: the SDK ships PollModPurchase and StopPurchasePolling (Stop Purchase Polling).

Dynamic content in a multiplayer session

Dynamic content is how the SDK handles seeing other players' paid cosmetics: content downloaded automatically by the game so every player can share the visuals, so a skin resides on all players' machines but is not considered installed, viewable but not usable. FInstalledMod::dynamicContent true means the player has the mod installed but does not own it and did not ask for it. Create such installs through the normal install path with FInstallModAdditionalParams::dynamicContent set to true, exposed on Install Mod Extended; the SDK then stamps a dc tracking value of true and sets origin to DynamicDownload. A later real install flips the flag back to false.

EModInstallOriginValueWhere it comes from
ModPage0The default, and therefore what the server flow records
ServerModule1Set by AssureClientModsUpdated
SubscribeModule2Subscription-driven installs
DynamicDownload3Set when dynamicContent is true
ResumeDownload4A resumed download
ModTile5Installed from a mod tile, distinct from opening the full mod page. Passed by the caller, nothing in cfcore sets it
warning

AssureClientModsUpdated does not install the server's set as dynamic content. It leaves dynamicContent false, so the server's required mods become real installed mods. Peer cosmetics the joining player does not own are a separate concern you drive with Install Mod Extended.

info

Fill in dynamicContentCategoryIds, otherwise the SDK cannot enforce a premium ownership check on dynamic content when a player manipulates the local library metadata files on their device. Find your game's category ids in the Developer Portal at console.curseforge.com, under the relevant game class.

Gotcha, this check is library-wide, not session-wide. When that set is populated, every client join runs a second ownership pass that ignores the server's mod list. It scans all locally installed mods, keeps those whose dynamicContent is false and whose categories intersect dynamicContentCategoryIds, fetches owned premium mods with GetPremiumModsV2, drops any mod that is not premium, is freemium, or has an active trial, and for anything still unowned resets dynamicContent to true on disk and fails the join with ModsNotOwnedByUser. Three consequences:

  • Left empty, the default, the pass returns immediately. It costs nothing and protects nothing.
  • A join can fail over tampered local cosmetics unrelated to the server being joined. Your error copy should not claim the server requires a purchase.
  • If fewer mod details come back than there are ids to check, the join fails with MissingModsDetails instead. That is a data or connectivity problem, so retry rather than route to a store.

The category design is deliberate: the game treats those categories specially, so a mod whose category a player edited will simply not load.

Errors on a server start or a client join

Error codeRaised byMeaning and recovery
FailedToInitializeBoth Blueprint nodesSDK not initialized. Description is Not initialized
NoPlatformFilesMatchedClientNo file matched this client's platform. Treat as an incompatible server, not a retry
DetectedUnavailableModServerOne or more mods are set to unavailable on the CurseForge website. Those mods are already uninstalled locally. Fix the list, or the mods' availability in the Developer Portal at console.curseforge.com
ModsNotOwnedByUserClientThe player does not own one or more mods and needs to purchase them. Run your own CheckMods diff to build the purchase list
FailedToVerifySignatureClient, premium checkThe premium check response signature could not be verified. A trust failure, not an entitlement failure. Do not route the player to a store
MissingModsDetailsClient, dynamic content passMod details could not be retrieved from the server. Connectivity or unknown local content. Retryable
MissingInstalledModsBoth, at final validationAn install produced nothing, a mod was dropped for a missing main file, or a requested mod id did not resolve. Check the log
ApiErrorBothAn API failure during the details query, the platform match, or the premium check. FCFCoreError::apiError carries the detail
note

ModsNotOwnedByUser comes from either the joined mod set or the library-wide dynamic content pass, and the error does not say which, nor which mods. The CheckMods diff is also how you tell those two sources apart.

Check FCFCoreError::isError as well as the optional being set.

note

An empty input is not an error. Both flows fire the update delegate with an empty array and no error, so OnUpdated fires and OnError does not: a vanilla server and a vanilla join both look like success. The two Blueprint pins are mutually exclusive, exactly one runs per call.

Blocked servers and blocked server mods

CurseForge models player-side moderation of modded servers, which belongs in your join UI. ApiGetBlockedModsDetails (Get Blocked Mods Details) covers a player reporting a server, or reporting mods on a server, so that they either block the server or all servers that contain blocked mods. Both lists are scoped to the current game only.

FBlockedDetails fieldTypeMeaning
serverIdsTArray<FString>Server ids the player has blocked
modIdsTArray<int64>Mod ids blocked in the context of modded servers
blockedUIModIdsTArray<int64>Mods blocked by the user from within the in-game browser or website
blockedUIAuthorIdsTArray<int64>Authors blocked by the user from within the in-game browser or website

Reverse a block with ApiUnblockMods (Unblock Mods), which takes FUnblockModsRequest.

FUnblockModsRequest fieldTypeMeaning
blockedAuthorsTArray<int64>Authors blocked from within the in-game browser or website
blockedModsTArray<int64>Mods blocked from within the in-game browser or website
blockedServerModsTArray<int64>Mods blocked by the user from within the context of a server, for example a join server screen
blockedServersTArray<FString>Servers blocked by the user

Recommended pattern: call Get Blocked Mods Details before you present a server list, then filter or flag servers whose id is in serverIds or whose advertised mod set intersects modIds, so a player is never asked to install content they blocked. In C++ both calls are on ICFCoreApi::Authorized(). Rating, reporting, and the wider blocking model are on Player actions; the layers behind them are on Moderation.

Session reporting and ready-made UI

After a session ends, attribute it as a server session with AnalyticsSendGamePlaySession (Send Game Play Session Analytic). FGamePlaySessionParams carries sessionLengthInSecs (int32), sessionType (ECFCoreSessionType, Local = 0 or Server = 1), modIds, and serverName (the server identifier, when sessionType is Server). Install attribution is already handled: the join stamps origin = join_srv, separating installs the player made in the browser UI from installs made while joining a server. See Analytics and Dashboards.

The optional cfcore-sdk-ue-ui module ships Blueprint widgets for the join case under cfcore_ui/Content/Widgets/ServerMods/: BP_CFCore_ServerModsWidget, BP_CFCore_ServerModsLoading, and BP_CFCore_ServerModsSubMenu. These are shipped assets, not documented API. On gamepad platforms the plugin's UMG surfaces are driven by a virtual cursor, which your code turns on and off per player controller with UCFCoreVirtualCursorFunctionLibrary::EnableVirtualCursor (Enable Virtual Cursor) and DisableVirtualCursor (Disable Virtual Cursor), each taking the controller on the PC pin. The general in-game UI is on Mod browser.