Creating and uploading UGC
The Creation section of the cfcore plugin in cfcore-sdk-ue: creating a mod record, uploading a mod file against it, chunking large files, splitting source from cooked uploads, and reading back what the server accepted.
What this page adds. The cloud cooking pages already document the pipeline and the upload flows with Unreal C++ samples: overview, how it works, upload flows, references. This page adds what those omit: every request-struct field, the avatar rule, the markup enums, the chunked transport, the progress and uploaded-file models, packaging, hashing, and the exact Blueprint nodes and pins.
The SDK is initialized and the player is authenticated. Game id and API key come from the Developer Portal at console.curseforge.com (see setting up your game). Every Creation call that reaches the network checks authentication first and completes with ECFCoreErrorCodes::UserNotAuthenticated (description user is not authenticated or token is invalid) before sending anything. Every Blueprint Creation node adds a second guard, IsInitialized, firing OnError with ECFCoreErrorCodes::FailedToInitialize (description Not initialized). The C++ interfaces do not, so from C++ call Creation only after initialization completes.
Which upload path to use
| Path | Who builds it | What it covers |
|---|---|---|
cf-ue-editor-plugin | CurseForge | An Unreal Editor plugin for your mod authors. See below |
cfcore Creation, Blueprint | You | Four nodes on UCFCoreSubsystem, listed below |
cfcore Creation, C++ | You | ICFCoreCreation, including the in-memory buffer overloads Blueprint does not expose |
cfcore API layer, C++ | You | ICFCoreApiCreation, including chunk-by-chunk control |
The three cfcore rows are layers of one feature: UCFCoreSubsystem forwards to ICFCoreCreation, which forwards to ICFCoreApiCreation. Pick the highest layer that exposes what you need.
The editor plugin. Shipped separately at https://github.com/curseforge-sdks/cf-ue-editor-plugin. Your mod authors use it to publish from inside your creation kit. It adds a UGC menu with Sign In, Sign Out and Share UGC, then packages and uploads the mod the author selects. By default it uploads the mod source and lets CurseForge cook it, and authors can cook locally instead.
The two C++ layers report errors differently. ICFCoreCreation completes with TOptional<FCFCoreError>, carrying an ECFCoreErrorCodes value plus a nested apiError. ICFCoreApiCreation completes with TOptional<FCFCoreApiResponseError> and no ECFCoreErrorCodes at all, so there you branch on the boolean flags and on errorCode. Do not reuse one handler across both layers.
The creation surface at a glance
| Operation | Blueprint node | ICFCoreCreation | ICFCoreApiCreation |
|---|---|---|---|
| Create a mod record | Create New Mod | CreateMod, path or buffer | CreateMod, buffer |
| Update a mod record | Update Existing Mod | UpdateMod, path or buffer | UpdateMod, buffer |
| Upload a mod file | Create Mod File | CreateModFile, path or buffer | CreateModFile, buffer |
| Start a chunked mod file | not exposed | reached automatically | CreateModFileChunked |
| Upload one chunk | not exposed | reached automatically | UploadModFileChunk |
| Upload a cooked file | Create Cooked Mod File | CreateCookedModFile, path or buffer | CreateCookedModFile, buffer |
| Start a chunked cooked file | not exposed | reached automatically | CreateCookedModFileChunked |
| Cancel an upload | not exposed | CancelCreation | CancelCreation |
ICFCoreCreation::CancelCreation does not currently cancel anything. Calling it neither aborts the in-flight upload nor invokes its own delegate. Uploads started through the file-path overloads of CreateModFile and CreateCookedModFile go through this layer, so they cannot be cancelled from game code today, only by SDK shutdown. Uploads driven manually through ICFCoreApiCreation can still be cancelled, using the id returned by UploadModFileChunk with ICFCoreApiCreation::CancelCreation, as shown in chunked upload for large files below.
Blueprint nodes and pins
All four nodes sit in the palette category cfcore|Creation, alongside the BlueprintPure make nodes on UCFCoreBPLibrary that build their request structs.
| Node | Pins |
|---|---|
| Create New Mod | create_mod_request, avatar_image_filename, on_success (payload mod), on_error (payload error) |
| Update Existing Mod | mod_id, update_mod_request, avatar_image_filename, on_success (payload mod), on_error |
| Create Mod File | ModId, CreateModFileRequest, LocalFilenameToUpload, OnCreateModFileRequestId (payload mod_file_request_id), OnProgress (payload progress), OnSuccess (payload uploaded_file), OnError |
| Create Cooked Mod File | ModId, SourceFileId, CreateCookedModFileRequest, LocalFilenameToUpload, OnCreateModFileRequestId, OnProgress, OnSuccess, OnError |
MakeCreateModRequest | class_id, name, summary, description_type, description, primary_category_id, game_category_ids, is_experimental |
MakeUpdateModRequest | Identical to MakeCreateModRequest |
MakeCreateModFileRequest | changelog_type, changelog, filename, displayName, gameVersionIds, releaseType, cookingOptions, isMarkedForManualRelease. No fileLength pin, which is harmless because the node uses the file-path overload |
MakeCreateCookedModFileRequest | Does not exist. Populate the struct's two members with a Make Struct node |
Blueprint gets the file-path form only. The in-memory TSharedPtr<TArray<uint8>> overloads of CreateMod, UpdateMod, CreateModFile and CreateCookedModFile are C++ only, and there are no chunked upload or cancel nodes at all.
Delegates, by layer. On ICFCoreCreation the error parameter is const TOptional<FCFCoreError>&; on ICFCoreApiCreation it is const TOptional<FCFCoreApiResponseError>&. Every result parameter below is passed by const reference except int64.
| Delegate | Result parameter | Layers | Blueprint pin |
|---|---|---|---|
FCreateModDelegate | TOptional<FCFCoreMod> | both | mod |
FUpdateModDelegate | TOptional<FCFCoreMod> | both | mod |
FCreateModFileDelegate | TOptional<FUploadedModFile> | both | uploaded_file |
FCreateModFileChunkedDelegate | TOptional<FModFileChunkedInfo> | API only | not exposed |
FUploadModFileChunkDelegate | bool | API only | not exposed |
FCreateModFileRequestIdDelegate | int64, no error | Creation only | mod_file_request_id |
FFileTransferProgressDelegate | FFileTransferProgress, no error | both | progress |
FCancelCreationDelegate | error only | Creation only | not exposed |
FCFCoreErrorDelegate | Blueprint only | not applicable | error |
The mod pin on Create New Mod and Update Existing Mod fires through FCFCoreCreateModDelegate and FCFCoreUpdateModDelegate respectively. The progress pin fires through FCreateModFileProgressDelegate.
Creating a mod record
A mod record is metadata only. It has no downloadable content until you attach a mod file.
How it works
- Resolve the
classId: callICFCoreApi::GetCategorieswithFCFCoreGetCategoriesFilter::classesOnlytrue. A returnedFCategorywithisClasstrue is a class, and itsidis yourclassId. - Resolve
primaryCategoryIdandgameCategoryIdsfrom the same call, filtering byFCFCoreGetCategoriesFilter::classId. - Fill an
FCreateModRequest. - Point the call at an avatar image on disk, or hand it the image bytes.
- Call
CreateModand keep theidon the returnedFCFCoreMod. Every later upload needs it.
FCreateModRequest, every field. Field names are the wire names.
| Field | Type | Default | Notes |
|---|---|---|---|
classId | int64 | 0 | 1130 (kNoUploadToClass) if the class disallows uploads |
name | FString | empty | 1121 (kModNameAlreadyExists) if the name is taken |
summary | FString | empty | Short text |
descriptionType | ECFCoreMakrupType | PlainText | See the markup tables below |
description | FString | empty | Interpreted per descriptionType |
primaryCategoryId | int64 | 0 | One category id |
gameCategoryIds | TArray<int64> | empty | Serialized as one form field per element |
isExperimental | bool | false | Marks the mod experimental |
#include <cfcore_context.h>
#include <api/models/creation/create_mod_request.h>
void AMyModUploader::CreateModRecord() {
FCreateModRequest request;
request.classId = MyResolvedClassId;
request.name = TEXT("Frostfall Weather Pack");
request.summary = TEXT("Adds four winter weather states.");
request.descriptionType = ECFCoreMakrupType::Markdown;
request.description = TEXT("## Frostfall\nFour new weather states.");
request.primaryCategoryId = MyResolvedPrimaryCategoryId;
request.gameCategoryIds = { MyResolvedPrimaryCategoryId };
cfcore::CFCoreContext::GetInstance()->Creation()->CreateMod(
request, TEXT("C:/creators/frostfall/avatar.png"),
cfcore::ICFCoreCreation::FCreateModDelegate::CreateLambda(
[this](const TOptional<FCFCoreMod>& opt_mod,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) {
// code for local failures, apiError.errorCode for server failures.
UE_LOG(LogTemp, Error, TEXT("Create mod failed: %s (%d, api %d)"),
*opt_err.GetValue().description,
(int32)opt_err.GetValue().code,
opt_err.GetValue().apiError.errorCode);
return;
}
CreatedModId = opt_mod.GetValue().id; // Every later upload needs this.
})
);
}
Blueprint: Create New Mod, with the request built by MakeCreateModRequest.
The avatar is required. The Blueprint node and the file-path C++ overload both read the avatar from disk first. A failed read gives ECFCoreErrorCodes::FileSystemError, description failed to read avatar from disk, and no request is sent. Only the buffer overloads treat the avatar as optional, attaching the binary part only when the buffer is valid and non-empty. From Blueprint, treat the avatar path as required.
Updating a mod record
FUpdateModRequest publicly derives from FCreateModRequest and adds nothing: same eight fields, same wire names.
The update path uses the same CreateModRequestSerializer, which writes every field unconditionally. An update is a full replacement, not a patch: read the current values, change what you need, and send the whole struct back, or you blank every field left at its default. Read current values with Get My Mods (ICFCoreApi::Authorized()->GetMyMods), which returns every mod the signed-in player created or is a member of for the running game. Those FCFCoreMod objects carry no file information: for the files, take the ids and call Get Mods Info By Ids (ICFCoreApi::GetMods).
Blueprint: Update Existing Mod, with the request built by MakeUpdateModRequest.
Markup types for descriptions and changelogs
Two different enums with different value sets. They are not interchangeable: Text exists only on the changelog enum, PlainText only on the description enum. Both structs default to a usable value, so the failure mode is a wrongly rendered description, not a rejected upload.
ECFCoreMakrupType, used by descriptionType on both mod requests:
| Value | Numeric | Notes |
|---|---|---|
PlaceHolderDoNotUse | 0 | Do not send it |
WysiwygHtml | 1 | |
PlainText | 2 | The struct default |
BBCode | 3 | |
Creole | 4 | |
Markdown | 5 | |
RawHtml | 6 | |
StandardBBCode | 8 | Note the gap: there is no value 7 |
RawCSS | 9 |
ECFCoreChangelogMarkupType, used by FCreateModFileRequest::changelogType:
| Value | Numeric | Notes |
|---|---|---|
PlaceHolderDoNotUse | 0 | Do not send it |
Text | 1 | The struct default |
HTML | 2 | |
Markdown | 3 |
Uploading a mod file
How it works
- Have a mod id, from
CreateModor Get My Mods. - Have the payload as one file on disk, or as a byte array in memory. Pack a directory tree first (see packaging below).
- Fill an
FCreateModFileRequest. - Call
CreateModFilewith the mod id, the request, and the payload. - Drive your progress UI from the progress delegate and read the
FUploadedModFilefrom the completion delegate.
FCreateModFileRequest, every field:
| Field | Type | Default | Notes |
|---|---|---|---|
changelogType | ECFCoreChangelogMarkupType | Text | Always sent |
changelog | FString | empty | Always sent, even when empty |
filename | FString | empty | Always sent. See the auto-fill rule below |
displayName | FString | empty | Omitted from the request entirely when empty |
gameVersionIds | TArray<int64> | empty | Repeated field. See the -1 rule below |
releaseType | ECFCoreFileReleaseType | Release | None 0, Release 1, Beta 2, Alpha 3 |
cookingOptions | FModFileCookingOptions | see cooking below | Sent as JSON, and only when cookingOptions.isSourceFile is true |
fileLength | int64 | 0 | Always sent. Mandatory for chunked uploads only. Overwritten from disk on the file-path overloads |
isMarkedForManualRelease | bool | false | Whether the mod is manually or automatically published after moderation approval |
filename behaves differently on the two overloads. On the file-path overload an empty filename is filled from FPaths::GetCleanFilename of the local path. On the buffer overload it is not: an empty filename fails immediately with ECFCoreErrorCodes::MissingParameter, description Missing filename parameter, before any request is sent. The matching server error is 1133 (kNoFileName).
An empty gameVersionIds is not sent as an empty list. A single value of -1 is substituted instead, meaning take the latest version. To target specific versions, resolve real ids with Get Versions Info (ICFCoreApi::GetVersions, or GetVersionsDetailed for the detailed form). An unknown id returns 1137 or 1138; a valid id not permitted for your class returns 1139.
#include <api/models/creation/create_mod_file_request.h>
#include <api/models/creation/uploaded_mod_file.h>
void AMyModUploader::UploadModFile(int64 ModId) {
FCreateModFileRequest request;
request.changelogType = ECFCoreChangelogMarkupType::Markdown;
request.changelog = TEXT("- Added blizzard state\n- Fixed fog density");
request.displayName = TEXT("Frostfall 1.2.0");
request.releaseType = ECFCoreFileReleaseType::Release;
// filename and fileLength stay at their defaults: the file-path overload
// fills both from disk.
cfcore::CFCoreContext::GetInstance()->Creation()->CreateModFile(
ModId, request, TEXT("C:/creators/frostfall/Frostfall-1.2.0.zip"),
cfcore::ICFCoreCreation::FCreateModFileRequestIdDelegate::CreateLambda(
[this](int64 upload_id) {
// Arrives before any request is sent, and is NOT a valid cancel id.
ActiveCreationId = upload_id;
}),
cfcore::ICFCoreCreation::FFileTransferProgressDelegate::CreateLambda(
[this](const FFileTransferProgress& progress) {
// 0..100 across the whole file, not per chunk. Delivered on the game
// thread, so this can touch UMG widgets directly.
OnUploadProgress(progress.progress, progress.bytesPerSecond);
}),
cfcore::ICFCoreCreation::FCreateModFileDelegate::CreateLambda(
[this](const TOptional<FUploadedModFile>& opt_file,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) { return; }
// Keep fileId to attach cooked files to this source later.
UploadedSourceFileId = opt_file.GetValue().fileId;
})
);
}
Blueprint: Create Mod File, with the request built by MakeCreateModFileRequest.
The progress and uploaded-file models
FFileTransferProgress:
| Field | Type | Meaning |
|---|---|---|
progress | int32 | Percentage, computed across the whole file |
transferredBytes | int64 | Total bytes transferred so far |
bytesPerSecond | int64 | Current transfer rate |
FUploadedModFile, delivered on success. All three fields are read-only.
| Field | Type | Meaning |
|---|---|---|
fileId | int64 | The new file's id. This is the sourceFileId for any cooked file you attach later |
directory | FString | Server-side directory |
filename | FString | Server-side filename |
FUploadedModFile is not FFile. It carries no status, no hashes and no download URL. For processing and moderation state, or hashes, fetch the file with Get Files Info By Ids or Get Mods Info By Ids. On the chunked path it is populated from FModFileChunkedInfo before any bytes move (StartUploadingChunks copies fileId, directory and fileName up front) and FinalizeCreation returns that same struct on both success and failure, so a failed chunked upload can deliver both a populated FUploadedModFile and an error. Check the error first.
Chunked upload for large files
Chunking is not something you opt into from the game layer. It is the default behaviour of the file-path overloads.
| Path you call | What happens | Ceiling |
|---|---|---|
ICFCoreCreation::CreateModFile with a local filename | Always goes through the chunked service, even for a small file, which becomes a single chunk | Bounded by chunk count, not memory |
ICFCoreCreation::CreateModFile with a buffer | One single POST, no chunking | Smaller files only, limited to 2^31 - 1 bytes, about 2 GB |
ICFCoreApiCreation::CreateModFileChunked plus UploadModFileChunk | You drive each chunk | For large files from disk, for example larger than 1 GB |
How it works
- The SDK stats the local file. An invalid stat fails the upload with
ECFCoreErrorCodes::MissingFileInformation. fileLengthon the request is overwritten with the real size on disk. Whatever you set is ignored on this path.- The create-chunked call returns an
FModFileChunkedInfo. An emptyuploadIdfails the upload withECFCoreErrorCodes::ApiError, descriptionMissing uploadId from server, andapiError.failedToParseServerResponseset. - Total chunks is the file size divided by the chunk size, rounded up. The chunk index starts at 1.
- Each chunk is read from disk at its offset and posted with
UploadModFileChunk. On success the index advances and the retry budget resets. - When the current index equals the total, the upload is complete.
The chunk size is 100 MB, from kMaxChunkSizeInBytes. Match it if you drive chunking yourself. A zero-byte file is not rejected up front: it passes the initial stat check as a valid, empty file, and the total-chunks calculation rounds up to zero. The first chunk read then asks for a full 100 MB chunk against an empty file, overruns it, and the upload finalizes with ECFCoreErrorCodes::FileSystemError instead. Check the file size yourself before starting an upload on a file that might be empty.
FModFileChunkedInfo, returned by CreateModFileChunked and CreateCookedModFileChunked. All read-only.
| Field | Type | Meaning |
|---|---|---|
fileId | int64 | The file id the chunks assemble into |
directory | FString | Server-side directory |
fileName | FString | Server-side filename |
uploadId | FString | The upload session handle. Feed into FModFileChunkMetadata::chunkedUploadId |
FModFileChunkMetadata, sent with every chunk. All read-only, and the field names are the wire names. The chunk bytes go in the multipart field named file.
| Field | Type | Default | Meaning |
|---|---|---|---|
chunkedUploadId | FString | empty | The uploadId from FModFileChunkedInfo |
totalChunks | int32 | 0 | How many chunks the file is split into |
chunkIndex | int32 | 1 | One-based, not zero-based |
chunkIndex is one-based, not zero-based.
Retry and failure behaviour on the SDK-driven chunked path:
| Behaviour | Value or rule |
|---|---|
| Retry budget per chunk | Starts at 3 (kMaxUploadChunkRetries) |
| When a retry happens | Only when the API error has serverUnreachable set. Any other error fails the whole upload immediately |
| How the budget is spent | HandleUploadChunkError decrements per serverUnreachable error and finalizes at 0, so a chunk gets its first attempt plus up to two re-attempts |
| Budget reset | Back to 3 after every successfully uploaded chunk |
| On retry exhaustion | The whole creation is finalized with the API error |
| On a disk read failure mid-upload | Finalized with ECFCoreErrorCodes::FileSystemError |
| On SDK shutdown | Every in-flight creation is cancelled. CreateModFileChunkedServiceImpl subscribes to the lifetime shutdown event and cancels its whole in-progress map. Uploads finalize with ECFCoreErrorCodes::UploadCancelled |
A transient HTTP error that is not serverUnreachable, for example a token expiring mid-upload, is not retried. Your uploader needs a resume-from-scratch path, not just a spinner.
Driving chunks yourself, through the API layer:
#include <api/models/creation/mod_file_chunked_info.h>
#include <api/models/creation/mod_file_chunk_metadata.h>
void AMyModUploader::StartManualChunkedUpload(int64 ModId, int64 FileLength) {
FCreateModFileRequest request;
request.filename = TEXT("Frostfall-2.0.0.zip");
request.fileLength = FileLength; // Mandatory when you drive chunking.
cfcore::CFCoreContext::GetInstance()->Api()->Creation()->CreateModFileChunked(
ModId, request,
cfcore::ICFCoreApiCreation::FCreateModFileChunkedDelegate::CreateLambda(
[this](const TOptional<FModFileChunkedInfo>& opt_info,
const TOptional<FCFCoreApiResponseError>& opt_err) {
// Note the error type on this layer: FCFCoreApiResponseError.
if (opt_err.IsSet() || !opt_info.IsSet()) return;
if (opt_info.GetValue().uploadId.IsEmpty()) return;
FModFileChunkMetadata metadata;
metadata.chunkedUploadId = opt_info.GetValue().uploadId;
metadata.totalChunks = MyComputedTotalChunks;
metadata.chunkIndex = 1; // One-based.
UploadOneChunk(metadata);
})
);
}
void AMyModUploader::UploadOneChunk(const FModFileChunkMetadata& Metadata) {
// Offset is (chunkIndex - 1) * kMaxChunkSizeInBytes.
TSharedPtr<TArray<uint8>> chunk = ReadChunkFromDisk(Metadata.chunkIndex);
// The return value is the HTTP-layer id, and the only id
// ICFCoreApiCreation::CancelCreation accepts.
ActiveCancelId =
cfcore::CFCoreContext::GetInstance()->Api()->Creation()->UploadModFileChunk(
Metadata, chunk,
cfcore::ICFCoreApiCreation::FFileTransferProgressDelegate(),
cfcore::ICFCoreApiCreation::FUploadModFileChunkDelegate::CreateLambda(
[this, Metadata](const bool& success,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet()) return; // Retry only on serverUnreachable.
if (Metadata.chunkIndex == Metadata.totalChunks) return; // Done.
FModFileChunkMetadata next = Metadata;
next.chunkIndex++;
UploadOneChunk(next);
})
);
}
Blueprint: no chunked nodes exist, and FModFileChunkMetadata is read-only to Blueprint, so hand-rolled chunking is C++ only. Create Mod File and Create Cooked Mod File get chunking transparently, so everything above applies to them even though their pins do not mention chunks.
Cooked and source uploads
Unreal content is cooked per target platform before a client can load it. The SDK models this as two file kinds attached to one mod, and a cooked file is always derived from an already-uploaded source file id.
| Model | What you upload | What cooks it | autoCookingType |
|---|---|---|---|
| Cloud cooking, all platforms | One source file, marked isSourceFile | The CurseForge backend, per its server-side configuration | All |
| Cloud cooking, PC only | One source file, marked isSourceFile | The backend, restricted to PC-supported platforms | PCOnly |
| Manual cooking | One source file, then one cooked file per platform | You, on your own machines or build farm | Manual |
FModFileCookingOptions, set on FCreateModFileRequest::cookingOptions:
| Field | Type | Default | Meaning |
|---|---|---|---|
isSourceFile | bool | false | Set true when uploading a source file for cooking. The rest of the options are ignored when it is false |
autoCookingType | ECFCoreAutoCookingType | All | Use this enum to support PCOnly or manual cooking |
cookerVersion | FString | empty | For cloud cooking |
ECFCoreAutoCookingType:
| Value | Numeric | Meaning |
|---|---|---|
All | 0 | Automatically cook based on server configurations |
PCOnly | 1 | Only cook for PC supported platforms (based on server configurations) |
Manual | 2 | Do not auto-cook for any platforms |
All does not mean every platform in ECFCorePlatform. It cooks based on server-side configuration resolved outside the SDK. Confirm your set in the Developer Portal at console.curseforge.com or with cfforstudios@overwolf.com before assuming console coverage.
Uploading a manually cooked file
How it works
- Upload the source with
CreateModFile,cookingOptions.isSourceFiletrue andcookingOptions.autoCookingTypeset toManual. - Keep the
fileIdfrom the resultingFUploadedModFile. That is yoursourceFileId. - Cook the content yourself, once per target platform.
- For each platform, call
CreateCookedModFilewith the mod id, thesourceFileId, and anFCreateCookedModFileRequestnaming that platform.
FCreateCookedModFileRequest:
| Field | Type | Default | Notes |
|---|---|---|---|
platform | ECFCorePlatform | None | Which platform this file was cooked for. Sent as the stripped display name, not the numeric |
fileLength | int64 | 0 | Mandatory for chunked uploads only. Overwritten from disk on the file-path overload |
ECFCorePlatform, the full set:
| Value | Numeric | Value | Numeric |
|---|---|---|---|
None | 0 | Mac | 7 |
Windows | 1 | IOS | 8 |
XboxOne | 2 | TVOS | 9 |
XboxXS | 3 | Android | 10 |
Linux | 4 | Switch | 11 |
PS4 | 5 | WindowsServer | 12 |
PS5 | 6 | LinuxServer | 13 |
WindowsServer and LinuxServer are distinct from Windows and Linux. If your game ships dedicated servers that load UGC, a client-cooked Windows file is not a server file. Cook and upload both.
The enum containing a platform does not mean your game may publish for it. 1160 (kPlatformNotAllowed) is a configuration issue, not a code bug: raise it with cfforstudios@overwolf.com. See also platforms support and Xbox and PlayStation compliance.
#include <api/models/creation/create_cooked_mod_file_request.h>
#include <api/models/enums/platform.h>
// Step 1: upload the source, flagged for manual cooking.
void AMyModUploader::UploadSourceForManualCooking(int64 ModId) {
FCreateModFileRequest request;
request.displayName = TEXT("Frostfall 2.1.0");
request.releaseType = ECFCoreFileReleaseType::Release;
request.cookingOptions.isSourceFile = true; // Required, or the rest is dropped.
request.cookingOptions.autoCookingType = ECFCoreAutoCookingType::Manual;
cfcore::CFCoreContext::GetInstance()->Creation()->CreateModFile(
ModId, request, TEXT("C:/creators/frostfall/src/Frostfall-2.1.0.zip"),
cfcore::ICFCoreCreation::FCreateModFileRequestIdDelegate(),
cfcore::ICFCoreCreation::FFileTransferProgressDelegate(),
cfcore::ICFCoreCreation::FCreateModFileDelegate::CreateLambda(
[this, ModId](const TOptional<FUploadedModFile>& opt_file,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) return;
UploadCookedForPlatform(ModId, opt_file.GetValue().fileId,
ECFCorePlatform::Windows);
})
);
}
// Step 2: one call per platform, against the source file id.
void AMyModUploader::UploadCookedForPlatform(int64 ModId, int64 SourceFileId,
ECFCorePlatform Platform) {
FCreateCookedModFileRequest request;
request.platform = Platform; // fileLength is filled from disk.
cfcore::CFCoreContext::GetInstance()->Creation()->CreateCookedModFile(
ModId, SourceFileId, request,
TEXT("C:/creators/frostfall/cooked/Win/Frostfall-2.1.0.zip"),
cfcore::ICFCoreCreation::FCreateModFileRequestIdDelegate(),
cfcore::ICFCoreCreation::FFileTransferProgressDelegate(),
cfcore::ICFCoreCreation::FCreateModFileDelegate::CreateLambda(
[](const TOptional<FUploadedModFile>& opt_file,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) {
UE_LOG(LogTemp, Error, TEXT("Cooked upload failed: %s (api %d)"),
*opt_err.GetValue().description,
opt_err.GetValue().apiError.errorCode);
}
})
);
}
Blueprint: Create Cooked Mod File, with the two-member request built by a Make Struct node.
Server errors specific to the source-and-cooked relationship:
| Code | Constant | What it means |
|---|---|---|
| 1103 | kNoSuchModFile | An uploaded mod file failed (internal server error), or a cooked file was uploaded for an unknown sourceFileId |
| 1105 | kModFileSourceMismatch | The cooked file does not match the named source |
| 1106 | kNotSourceModFile | The id you passed as sourceFileId is not a source file |
| 1107 | kModFileAuthorMismatch | Only the source file author can upload cooked files associated to that source |
| 1131 | kFileIsStillProcessing | The file is not yet in a state that accepts the next step |
| 1143 | kSourceFileNotInCookingStatus | The source file is not in a cooking state |
| 1144 | kNotAllSourceFilesReady | One or more source files are not ready |
| 1145 | kSourceFileAlreadyHasPlatform | A cooked file for that source and platform already exists |
1107 constrains your architecture, not just your call. If a build machine uploads cooked files, the same authenticated identity that uploaded the source must upload them. Do not split source upload and cooked upload across two accounts.
Reading back the cooker version. FFile::cookingInfo is an FFileCookingInfo whose only member is a read-only cookerVersion string, used for cloud cooking. In C++, call ICFCoreApi::GetFiles with an FCFCoreGetFilesFilter whose fileIds holds your file. Blueprint: Get Files Info By Ids (category cfcore|Api, pins FileIds, OnResults, OnError), then break the returned FFile.
Packaging and hashing
A mod file upload takes one file, not a directory. The SDK ships a zip helper so your uploader needs no third-party dependency. Zip first, then pass the resulting path to Create Mod File. The compression service is documented in full on Game info and platforms.
Reach the service in C++ through ICFCore::Utils, then ICFCoreUtils::Compression, which returns an ICompressionService*:
TFuture<ECompressionError> Zip(
const TSharedRef<TArray<FString>> files_to_zip,
const FString& output_zip_file,
FProgressDelegate on_progress);
Blueprint: Zip Paths, in the category cfcore|Utils|Compression.
| Pin | Type |
|---|---|
InPathsToZip | Paths to add to the archive |
InOutputZipFile | Destination archive path |
OnProgress | FUtilsCompressionProgressDelegate |
OnSuccess | FCFCoreSuccessDelegate |
OnError | FUtilsCompressionErrorDelegate |
FCompressionProgress carries progress (int32, percentage) and file (FString, the file being processed). ECompressionError: None 0, FailedToReadZip 1, FailedToExtractFile 2, FailedToWriteFile 3.
The zip helper does not report through FCFCoreError. In C++ the result arrives as an ECompressionError on the returned TFuture, and in Blueprint on OnError as an ECompressionError. Check for ECompressionError::None rather than looking for an ECFCoreErrorCodes value.
Hashing. Hashes are a verification surface, not an upload parameter. Hashes come back on the file model.
| Type | Field | Meaning |
|---|---|---|
FFile | hashes (TArray<FFileHash>) | Server-supplied hashes for the file |
FFileHash | value (FString) | The hash string |
FFileHash | algo (ECFCoreHashAlgo) | Which algorithm produced it. None 0, Sha1 1, Md5 2 |
FFile | fileFingerprint (int64) | A separate numeric fingerprint, distinct from the hashes array |
The SDK's own install pipeline reads hashes[0].algo and hashes[0].value, computes the matching digest over the local file, and fails installation with ECFCoreErrorCodes::DownloadedFileHasInvalidHash on a mismatch. It returns that same code when the algo is neither Sha1 nor Md5, and skips verification with a warning when hashes is empty. If you build your own post-upload verification, handle the empty array explicitly and read algo per entry rather than assuming one algorithm.
After the upload completes
A successful upload is not a published file. isMarkedForManualRelease on FCreateModFileRequest controls the publishing step: leave it false for automatic publishing after moderation approval, set it true when a content drop has to land alongside a game patch. For what happens next, see moderation and testing and launching your game.
The status enums are ECFCoreFileStatus on FFile::fileStatus and ECFCoreModStatus on FCFCoreMod::status, with full value lists in cloud cooking references. Poll with Get Files Info By Ids (ICFCoreApi::GetFiles) rather than treating a single non-Released status as a failure.
Error reference for creation
FCFCoreError carries isError, a code of type ECFCoreErrorCodes, a nested apiError of type FCFCoreApiResponseError, and a description. Check code for local failures and apiError.errorCode for server failures. When an FCFCoreError is built from a server response, code is always ECFCoreErrorCodes::ApiError and the description is copied from the server error.
Local ECFCoreErrorCodes values from the Creation section:
| Code | When it fires | Fix |
|---|---|---|
FailedToInitialize | Any Creation node on UCFCoreSubsystem when the SDK is not initialized. Description Not initialized | Initialize first |
UserNotAuthenticated | Every Creation call that reaches the network, before any request is sent | Authenticate first |
MissingParameter | CreateModFile buffer overload with an empty filename. Description Missing filename parameter | Set filename explicitly on the buffer overload |
FileSystemError | Avatar read failure (failed to read avatar from disk), or a chunk read failure mid-upload | Verify the path is readable and not locked |
MissingFileInformation | The local file could not be stat'ed. Description Please assure the local file exists on disk ({0}) on the mod-file path, none on the cooked path | Verify the path before calling |
ApiError | Chunked create returned an empty uploadId (description Missing uploadId from server, apiError.failedToParseServerResponse set), and every server-side failure | Retry; escalate to cfforstudios@overwolf.com if it persists |
UploadCancelled | The upload was cancelled, including by SDK shutdown | Expected on teardown |
FCFCoreApiResponseError also carries errorCode (int32), description (FString), and these booleans: cancelled, badRequest, entityNotFound, serverUnreachable, missingPrivileges, tokenExpired, resourceExpired, failedToParseServerResponse.
serverUnreachable is the only flag the chunked uploader treats as retryable. Mirror that in your own retry logic, and surface tokenExpired as a re-authentication prompt rather than a generic failure.
Numeric server codes in apiError.errorCode, from cfcore::api_error_codes. They are not ordered in any meaningful sequence. The source-and-cooked codes are in the table above; these are the rest that Creation returns.
| Code | Constant | Meaning |
|---|---|---|
| 0 | kNone | No error |
| 1100 | kNoSuchMod | Uploading a file for, or updating, a mod that does not exist |
| 1102 | kNoSuchGame | Creating a mod with an unknown game id |
| 1104 | kModGameMismatch | Uploading to a mod belonging to a different game than the SDK was initialized with |
| 1110 | kUserNotAllowedForMod | The authenticated account may not upload or update this mod |
| 1111 | kDeletedUserSuspected | Account state issue |
| 1120 | kModIsNotValid | The mod is not in a valid state |
| 1121 | kModNameAlreadyExists | The mod name is already taken |
| 1130 | kNoUploadToClass | The classId is configured to not allow uploads |
| 1132 | kFileSizeTooBig | Exceeded the maximum file size |
| 1133 | kNoFileName | Missing filename for the uploaded file |
| 1134 | kNoAdditionalFiles | No additional files |
| 1135 | kParentFileDoesNotExist | The parent file does not exist |
| 1136 | kGameVersionsNotAllowedForAddonFiles | Game versions are not allowed for this file kind |
| 1137 | kNoSuchGameVersion | Unknown game version. Re-resolve from the versions API |
| 1138 | kInvalidGameVersion | Unknown game version. Re-resolve from the versions API |
| 1139 | kGameVersionNotMappedToCategory | The game version is not allowed for the given classId |
| 1140 | kNoChildFileType | No child file type |
| 1141 | kUserCannotUploadMemberAccessOnlyContent | The authenticated account may not upload membership files (CurseForge Pro) |
| 1142 | kUserCannotUploadFiles | The authenticated account lacks privileges to upload a file for this mod |
| 1160 | kPlatformNotAllowed | The target platform is not permitted |
Three of these are permission errors that look like code errors: 1110, 1142, and 1107 above. None are fixed by changing your request. Check the account role on the mod in the Developer Portal at console.curseforge.com, then escalate to cfforstudios@overwolf.com.
Next steps
- To let players find and install what mod authors uploaded, see the in-game mod browser and mod installation methods.
- To confirm your class ids, category ids, and permitted cooking platforms, open the Developer Portal at console.curseforge.com.
- For file size limits, cooking platform enablement, permission errors on a mod, the status state machines, or the current state of
cf-ue-editor-plugin, contact cfforstudios@overwolf.com.