Workshop and user content
Publish, discover, and download content through Steam Workshop.
Configure Workshop for your Steam app. Decide which content formats your game accepts and how it loads them; Workshop stores and distributes files but does not mount Unreal content for you.
Query content
Browse the first page of items ranked by votes. Create WorkshopQuery (SIK UGC Query Handle) and WorkshopQueryPending (Boolean, false).
Loading the interactive viewer.
Game module dependency: SteamIntegrationKit.
WorkshopBrowserGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "Nodes/UGC/SIK_SendQueryUGCRequest_AsyncFunction.h"
#include "WorkshopBrowserGameInstance.generated.h"
UCLASS()
class UWorkshopBrowserGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
void BrowseWorkshop(FSIK_AppId AppId);
virtual void Shutdown() override;
private:
UPROPERTY()
TObjectPtr<USIK_SendQueryUGCRequest_AsyncFunction> Request;
FSIK_UGCQueryHandle Query;
bool bQueryPending = false;
UFUNCTION()
void QueryFinished(FSIK_UGCQueryHandle Handle, TEnumAsByte<ESIK_Result> Result, int32 Returned,
int32 Total, bool bCached);
};WorkshopBrowserGameInstance.cpp
#include "WorkshopBrowserGameInstance.h"
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "SteamIntegrationKit.h"
void UWorkshopBrowserGameInstance::BrowseWorkshop(FSIK_AppId AppId)
{
if (bQueryPending)
{
UE_LOG(LogSIK, Warning, TEXT("A Workshop query is already running"));
return;
}
Query = USIK_UGCLibrary::CreateQueryAllUGCRequest(UGCQuery_RankedByVote,
UGCMatchingUGCType_Items, AppId, AppId, 1);
bQueryPending = true;
Request = USIK_SendQueryUGCRequest_AsyncFunction::SendQueryUGCRequest(Query);
Request->RegisterWithGameInstance(this);
Request->OnSuccess.AddDynamic(this, &UWorkshopBrowserGameInstance::QueryFinished);
Request->OnFailure.AddDynamic(this, &UWorkshopBrowserGameInstance::QueryFinished);
Request->Activate();
}
void UWorkshopBrowserGameInstance::QueryFinished(FSIK_UGCQueryHandle Handle,
TEnumAsByte<ESIK_Result> Result, int32 Returned,
int32 Total, bool bCached)
{
if (!bQueryPending)
return;
if (Result == ESIK_Result::ResultOK)
{
for (int32 Index = 0; Index < Returned; ++Index)
{
FSIK_SteamUGCDetails Details;
if (USIK_UGCLibrary::GetQueryUGCResult(Query, Index, Details) &&
Details.Result == ESIK_Result::ResultOK)
UE_LOG(LogSIK, Log, TEXT("%s"), *Details.Title);
else
UE_LOG(LogSIK, Warning, TEXT("Workshop item details are unavailable"));
}
}
else
UE_LOG(LogSIK, Warning, TEXT("Workshop query failed: %d"), int32(Result.GetValue()));
USIK_UGCLibrary::ReleaseQueryUGCRequest(Query);
bQueryPending = false;
Request = nullptr;
}
void UWorkshopBrowserGameInstance::Shutdown()
{
if (Request)
Request->Cancel();
Super::Shutdown();
}Pages start at 1; result indexes start at 0. Apply filters and Set SIK Return Metadata before sending the query. Read all required details before releasing its handle. Use Create SIK Query UGC Details Request when you already know the published file IDs.
Subscribe and download
Download a known item and read its folder once it is installed and up to date. The watcher reports progress once per second and stops after five minutes. It does not change subscriptions or cancel Steam’s download.
For the installation graph, create InstallItem (SIK Published File Id), InstallActive (Boolean, false), InstallTimer (Timer Handle), and PollsRemaining (Integer) in an Actor.
Loading the interactive viewer.
Loading the interactive viewer.
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/UGC/SIK_SubscribeItem_AsyncFunction.h"
#include "Nodes/UGC/SIK_UnsubscribeItem_AsyncFunction.h"
bool DownloadWorkshopItem(FSIK_PublishedFileId Item)
{
return USIK_UGCLibrary::DownloadItem(Item, false);
}
bool SubscribeWorkshopItem(FSIK_PublishedFileId Item, const FScriptDelegate &Ready,
const FScriptDelegate &Failed)
{
auto *Request = USIK_SubscribeItem_AsyncFunction::SubscribeItem(Item);
if (!Request)
return false;
Request->OnSuccess.Add(Ready);
Request->OnFailure.Add(Failed);
Request->Activate();
return true;
}
bool UnsubscribeWorkshopItem(FSIK_PublishedFileId Item, const FScriptDelegate &Ready,
const FScriptDelegate &Failed)
{
auto *Request = USIK_UnsubscribeItem_AsyncFunction::UnsubscribeItem(Item);
if (!Request)
return false;
Request->OnSuccess.Add(Ready);
Request->OnFailure.Add(Failed);
Request->Activate();
return true;
}Subscription handlers take (TEnumAsByte<ESIK_Result>, FSIK_PublishedFileId).
WorkshopDownloadActor.h
#pragma once
#include "GameFramework/Actor.h"
#include "Nodes/UGC/SIK_UGCSubsystem.h"
#include "WorkshopDownloadActor.generated.h"
UCLASS()
class AWorkshopDownloadActor : public AActor
{
GENERATED_BODY()
public:
void DownloadWorkshopItem(FSIK_PublishedFileId Item);
void StopWatchingWorkshopDownload();
virtual void EndPlay(const EEndPlayReason::Type Reason) override;
private:
UPROPERTY()
TObjectPtr<USIK_UGCSubsystem> UGC;
FSIK_PublishedFileId InstallItem;
FTimerHandle InstallTimer;
int32 PollsRemaining = 0;
bool bInstallActive = false;
void PollWorkshopDownload();
UFUNCTION()
void DownloadFinished(TEnumAsByte<ESIK_Result> Result, FSIK_PublishedFileId Item,
FSIK_AppId App);
};WorkshopDownloadActor.cpp
#include "WorkshopDownloadActor.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/Utils/SIK_UtilsLibrary.h"
#include "SteamIntegrationKit.h"
#include "TimerManager.h"
void AWorkshopDownloadActor::DownloadWorkshopItem(FSIK_PublishedFileId Item)
{
if (bInstallActive)
{
UE_LOG(LogSIK, Warning, TEXT("A Workshop download is already being watched"));
return;
}
UGameInstance *Instance = GetGameInstance();
UGC = Instance ? Instance->GetSubsystem<USIK_UGCSubsystem>() : nullptr;
if (!UGC)
{
UE_LOG(LogSIK, Warning, TEXT("Workshop subsystem is unavailable"));
return;
}
InstallItem = Item;
UGC->OnDownloadItemResult.AddUniqueDynamic(this, &AWorkshopDownloadActor::DownloadFinished);
bInstallActive = true;
PollsRemaining = 300;
if (!USIK_UGCLibrary::DownloadItem(Item, false))
{
StopWatchingWorkshopDownload();
UE_LOG(LogSIK, Warning, TEXT("Steam could not queue this download"));
return;
}
GetWorldTimerManager().SetTimer(InstallTimer, this,
&AWorkshopDownloadActor::PollWorkshopDownload, 1.0f, true);
PollWorkshopDownload();
}
void AWorkshopDownloadActor::PollWorkshopDownload()
{
if (!bInstallActive)
return;
const int32 State = USIK_UGCLibrary::GetItemState(InstallItem);
// Installed (4), without NeedsUpdate (8), Downloading (16), or DownloadPending (32).
if ((State & 60) == 4)
{
int64 Size = 0;
FString Folder;
int32 Timestamp = 0;
const bool bReadable =
USIK_UGCLibrary::GetItemInstallInfo(InstallItem, Size, Folder, Timestamp);
StopWatchingWorkshopDownload();
if (bReadable)
UE_LOG(LogSIK, Log, TEXT("%s"), *Folder);
else
UE_LOG(LogSIK, Warning, TEXT("Steam could not read the installed folder"));
return;
}
if (--PollsRemaining <= 0)
{
StopWatchingWorkshopDownload();
UE_LOG(LogSIK, Warning,
TEXT("Stopped waiting after five minutes; Steam may continue downloading"));
return;
}
int64 Downloaded = 0;
int64 Total = 0;
if (USIK_UGCLibrary::GetItemDownloadInfo(InstallItem, Downloaded, Total))
UE_LOG(LogSIK, Log, TEXT("Download: %lld / %lld bytes"), Downloaded, Total);
}
void AWorkshopDownloadActor::DownloadFinished(TEnumAsByte<ESIK_Result> Result,
FSIK_PublishedFileId Item, FSIK_AppId App)
{
if (!bInstallActive || Item.PublishedFileId != InstallItem.PublishedFileId ||
App.AppID != USIK_UtilsLibrary::GetAppID().AppID)
return;
if (Result == ESIK_Result::ResultOK)
PollWorkshopDownload();
else
{
StopWatchingWorkshopDownload();
UE_LOG(LogSIK, Warning, TEXT("Workshop download failed: %d"), int32(Result.GetValue()));
}
}
void AWorkshopDownloadActor::StopWatchingWorkshopDownload()
{
bInstallActive = false;
GetWorldTimerManager().ClearTimer(InstallTimer);
if (UGC)
UGC->OnDownloadItemResult.RemoveDynamic(this, &AWorkshopDownloadActor::DownloadFinished);
}
void AWorkshopDownloadActor::EndPlay(const EEndPlayReason::Type Reason)
{
StopWatchingWorkshopDownload();
Super::EndPlay(Reason);
}A successful download request means queued, not installed. Use Get SIK Item Download Info for progress and check item state before loading files: an installed copy can still need an update. Subscription and installation are separate; Unsubscribe SIK Item is not a replacement for unloading content already in use.
Validate the installed content’s format and version before loading it. Do not execute arbitrary downloaded code or trust paths supplied inside a user’s content package.
Publish or update an item
Create the item once and persist its Published File ID. Updates use that ID; creating a new item for every edit produces separate Workshop entries. Create PublishedItem (SIK Published File Id), WorkshopUpdate (SIK UGC Update Handle), and WorkshopUploading (Boolean, false). Store the returned Published File ID in your own save data; the example variable lasts only for the current run.
Create a Workshop entry
Loading the interactive viewer.
Upload a revision and read progress
Loading the interactive viewer.
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/UGC/SIK_SubmitItemUpdate_AsyncFunction.h"
#include "Nodes/UGC/SIK_CreateItem_AsyncFunction.h"
bool CreateWorkshopEntry(FSIK_AppId AppId, const FScriptDelegate &Created,
const FScriptDelegate &Failed)
{
auto *Request = USIK_CreateItem_AsyncFunction::CreateItem(AppId, WorkshopFileTypeCommunity);
if (!Request)
return false;
Request->OnSuccess.Add(Created);
Request->OnFailure.Add(Failed);
Request->Activate();
return true;
}
bool UploadWorkshopRevision(FSIK_AppId AppId, FSIK_PublishedFileId Item,
const FString &ContentFolder, const FString &Title,
const FString &PreviewFile, const FString &ChangeNote,
FSIK_UGCUpdateHandle &OutUpdate, const FScriptDelegate &OnUploaded,
const FScriptDelegate &OnFailed)
{
const auto Update = USIK_UGCLibrary::StartItemUpdate(AppId, Item);
OutUpdate = Update;
if (!USIK_UGCLibrary::SetItemContent(Update, ContentFolder) ||
!USIK_UGCLibrary::SetItemTitle(Update, Title) ||
!USIK_UGCLibrary::SetItemPreview(Update, PreviewFile))
return false;
auto *Request = USIK_SubmitItemUpdate_AsyncFunction::SubmitItemUpdate(Update, ChangeNote);
if (!Request)
return false;
Request->OnSuccess.Add(OnUploaded);
Request->OnFailure.Add(OnFailed);
Request->Activate();
return true;
}Creation handlers take (TEnumAsByte<ESIK_Result>, FSIK_PublishedFileId, bool bUserNeedsToAcceptWorkshopLegalAgreement). Upload handlers take (TEnumAsByte<ESIK_Result>, bool bUserNeedsToAcceptWorkshopLegalAgreement). The helper returns dispatch status; handle completion through its success and failure delegates. Keep one upload active per saved update handle, and clear your uploading flag on either completion path.
Pass the absolute folder containing the files to upload. Keep it available until completion. Set preview, description, tags, and metadata on the update handle before submitting. Call the progress event from your upload UI or a timer. In C++, call GetItemUpdateProgress(OutUpdate, BytesProcessed, BytesTotal) only while uploading and read the byte counts only when the returned status is not ItemUpdateStatusInvalid. Use Get SIK Item Update Progress while the upload is active; create a new update handle for the next revision.
If creation or upload says the user must accept the Workshop legal agreement, direct them to Steam’s agreement flow. Do not report the item as publicly available until that requirement and its visibility settings are satisfied. Follow Valve’s Workshop implementation guide.
Ownership, visibility, and failures
Publishing requires the signed-in account to have permission to update the item. Creator App ID and consumer App ID can differ when a separate tool publishes content for a game; use the intended pair when querying.
Handle upload, download, and query failures separately. An empty successful query can reflect visibility or filters, not a failed Steam connection. Do not discard a saved Published File ID after a transient upload failure.
Use Delete SIK Item only for a deliberate removal of a published entry. Removing a subscription, removing a favorite, and deleting a published item are different actions.