From 69c167eeb8bd934746c75d6df9057d12a20a7b04 Mon Sep 17 00:00:00 2001 From: Stephen Whittle Date: Mon, 27 Jun 2022 11:43:06 +1000 Subject: [PATCH] 2.17.1896 * SubmitNewModFileForMod now supports platform information for games with CrossPlatform Modfiles enabled * FModioModInfo now exposes mod visibility * FModioFilterParams can now sort by DownloadsTotal * Deferred unsubscriptions are now treated as successes * New VerifyUserAuthenticationAsync method for verifying authentication state on the server * EnableModManagement now reports failure reasons to the log * ErrorCodeMatches method should now be correctly exposed to Blueprint --- Config/Localization/Modio.ini | 52 ++ Doc/documentation.html | 534 ++++++++++++++++-- Doc/getting-started.adoc | 129 ++++- Doc/img/nd_img_Add.png | Bin 0 -> 2096 bytes Doc/img/nd_img_BreakToComponents.png | Bin 0 -> 8766 bytes Doc/img/nd_img_Divide.png | Bin 0 -> 2095 bytes Doc/img/nd_img_EqualTo.png | Bin 0 -> 2136 bytes Doc/img/nd_img_GreaterThan.png | Bin 0 -> 2278 bytes Doc/img/nd_img_K2_ArchiveModAsync.png | Bin 0 -> 16139 bytes Doc/img/nd_img_K2_EnableModManagement.png | Bin 15429 -> 16273 bytes Doc/img/nd_img_LessThan.png | Bin 0 -> 2251 bytes Doc/img/nd_img_MakeFromComponents.png | Bin 0 -> 9897 bytes Doc/img/nd_img_NotEqualTo.png | Bin 0 -> 2154 bytes Doc/img/nd_img_Subtract.png | Bin 0 -> 1945 bytes README.adoc | 2 +- Source/Modio/Modio.Build.cs | 2 +- .../Internal/Convert/CreateModFileParams.h | 5 + .../Private/Internal/Convert/FilterParams.h | 2 + .../Internal/Convert/ModCollectionEntry.h | 9 + .../Modio/Private/Internal/Convert/ModInfo.h | 2 + .../Internal/Convert/ModProgressInfo.h | 11 +- .../Modio/Private/Internal/ModioConvert.cpp | 41 +- Source/Modio/Private/Internal/ModioConvert.h | 6 +- .../Libraries/ModioCommonTypesLibrary.cpp | 30 +- Source/Modio/Private/ModioSubsystem.cpp | 37 +- Source/Modio/Private/Tests/ToUnreal.cpp | 31 +- Source/Modio/Private/Types/ModioImage.cpp | 16 +- .../Types/ModioModCollectionEntryUImpl.cpp | 5 + .../Private/Types/ModioReportParamsUImpl.cpp | 5 + .../Libraries/ModioCommonTypesLibrary.h | 45 +- .../Libraries/ModioErrorConditionLibrary.h | 2 +- .../Modio/Public/Libraries/ModioSDKLibrary.h | 10 +- Source/Modio/Public/ModioSDK.h | 19 +- Source/Modio/Public/ModioSettings.h | 2 +- Source/Modio/Public/ModioSubsystem.h | 40 +- Source/Modio/Public/Types/ModioCommonTypes.h | 17 +- .../Public/Types/ModioCreateModFileParams.h | 2 + Source/Modio/Public/Types/ModioFilterParams.h | 3 +- .../Public/Types/ModioModCollectionEntry.h | 17 +- Source/Modio/Public/Types/ModioModInfo.h | 17 +- .../Modio/Public/Types/ModioModProgressInfo.h | 10 +- Source/Modio/Public/Types/ModioReportParams.h | 12 +- Source/Modio/Public/Types/ModioUnsigned64.h | 207 +++++++ .../Private/Tests/ModioUnsigned64Tests.cpp | 151 +++++ Source/ThirdParty/NativeSDK | 2 +- Source/uring/Public/liburing/compat.h | 27 - 46 files changed, 1306 insertions(+), 196 deletions(-) create mode 100644 Config/Localization/Modio.ini create mode 100644 Doc/img/nd_img_Add.png create mode 100644 Doc/img/nd_img_BreakToComponents.png create mode 100644 Doc/img/nd_img_Divide.png create mode 100644 Doc/img/nd_img_EqualTo.png create mode 100644 Doc/img/nd_img_GreaterThan.png create mode 100644 Doc/img/nd_img_K2_ArchiveModAsync.png create mode 100644 Doc/img/nd_img_LessThan.png create mode 100644 Doc/img/nd_img_MakeFromComponents.png create mode 100644 Doc/img/nd_img_NotEqualTo.png create mode 100644 Doc/img/nd_img_Subtract.png create mode 100644 Source/Modio/Public/Types/ModioUnsigned64.h create mode 100644 Source/ModioTests/Private/Tests/ModioUnsigned64Tests.cpp delete mode 100644 Source/uring/Public/liburing/compat.h diff --git a/Config/Localization/Modio.ini b/Config/Localization/Modio.ini new file mode 100644 index 00000000..66591cae --- /dev/null +++ b/Config/Localization/Modio.ini @@ -0,0 +1,52 @@ +[CommonSettings] +SourcePath=Plugins/Modio/Content/UI/Localization +DestinationPath=Plugins/Modio/Content/UI/Localization +ManifestName=Modio.manifest +ArchiveName=Modio.archive +PortableObjectName=Modio.po +ResourceName=Modio.locres +NativeCulture=en +CulturesToGenerate=en + +;Gather text from source code +[GatherTextStep0] +CommandletClass=GatherTextFromSource +SearchDirectoryPaths=Plugins/Modio/Source/ModioUI +FileNameFilters=*.cpp +FileNameFilters=*.h +FileNameFilters=*.c +FileNameFilters=*.inl +FileNameFilters=*.mm +FileNameFilters=*.ini +FileNameFilters=*.csv +ShouldGatherFromEditorOnlyData=false + +[GatherTextStep1] +CommandletClass=GatherTextFromAssets +IncludePathFilters=Plugins/Modio/Content/* +ExcludePathFilters=Plugins/Modio/Content/UI/Localization/* +PackageFileNameFilters=*.uasset +PackageFileNameFilters=*.umap + + +;Write Manifest +[GatherTextStep2] +CommandletClass=GenerateGatherManifest + +;Write Archives +[GatherTextStep3] +CommandletClass=GenerateGatherArchive + +;Import localized PO files +[GatherTextStep4] +CommandletClass=InternationalizationExport +bImportLoc=true + +;Write Localized Text Resource +[GatherTextStep5] +CommandletClass=GenerateTextLocalizationResource + +;Export PO files +[GatherTextStep6] +CommandletClass=InternationalizationExport +bExportLoc=true \ No newline at end of file diff --git a/Doc/documentation.html b/Doc/documentation.html index f7d716d9..4872faeb 100644 --- a/Doc/documentation.html +++ b/Doc/documentation.html @@ -802,6 +802,8 @@

mod.io UE4 Plugin Documentation

  • Checking for errors
  • Inspecting ErrorCodes more deeply
  • Parameter Validation Errors
  • +
  • Submitting a new mod
  • +
  • Submitting a file for a mod
  • @@ -847,6 +849,7 @@

    mod.io UE4 Plugin Documentation

  • ClearUserDataAsync
  • AuthenticateUserExternalAsync
  • AuthenticateUserEmailAsync
  • +
  • ArchiveModAsync
  • Is Mod Management Busy
  • Get Last Validation Error
  • ForceUninstallModAsync
  • @@ -897,6 +900,7 @@

    mod.io UE4 Plugin Documentation

  • ModioModDependency
  • ModioOptionalModProgressInfo
  • ModioModProgressInfo
  • +
  • ModioUnsigned64
  • ModioReportParams
  • ModioTerms
  • ModioLink
  • @@ -937,6 +941,16 @@

    mod.io UE4 Plugin Documentation

  • Get Project Environment
  • Get Project Api Key
  • ToString (Filesize)
  • +
  • ModioUnsigned64 - ModioUnsigned64
  • +
  • ModioUnsigned64 != ModioUnsigned64
  • +
  • Make from Components
  • +
  • ModioUnsigned64 < ModioUnsigned64
  • +
  • ModioUnsigned64 > ModioUnsigned64
  • +
  • ModioUnsigned64 == ModioUnsigned64
  • +
  • ModioUnsigned64 / float
  • +
  • ModioUnsigned64 / ModioUnsigned64
  • +
  • Break to Components
  • +
  • ModioUnsigned64 + ModioUnsigned64
  • SubmitNewModFileForModFromMemory
  • LoadModFileToMemory
  • @@ -955,6 +969,7 @@

    mod.io UE4 Plugin Documentation

  • EModioLogoSize
  • EModioPortal
  • EModioEnvironment
  • +
  • EModioErrorCondition
  • EModioSortDirection
  • EModioSortFieldType
  • EModioImageState
  • @@ -1129,7 +1144,7 @@

    Thread-safety guarantees

    Non-blocking, asynchronous interface

    -

    The plugin communicates with the mod.io servers, the filesystem on the device you’re using, and platform-provided services for authentication. All of these may not return results immediately, so many functions provided by the ModioSubsystem are non-blocking and asynchronous.

    +

    The plugin communicates with the mod.io servers, the filesystem on the device you’re using, and platform-provided services for authentication. All of these may not return results immediately, so many functions provided by the ModioSubsystem are non-blocking and asynchronous. For example, the initialization function returns immediately. However, your game should consider the ModioSybsystem as initialized only when the init callback executes.

    @@ -1213,18 +1228,16 @@

    Mod Data Directory

    - + - - - + + + - - - - + +
    WindowsLinuxOSX

    Windows

    Linux

    OSX

    ${FolderID_Public}/mod.io

    TBD

    TBD

    ${USER_HOME}/mod.io

    ${USER_HOME}/Library/Application Support/mod.io

    @@ -1304,6 +1317,9 @@

    Plugin quick-start: Ini
  • The error-handling in this sample has been omitted. See Plugin quick-start: Error Handling for more information on error handling.

  • +
  • +

    To fully initialize the SDK, you must receive confirmation from the callback. Consider that most functions return after invocation, nonetheless, their effects are only visible in their callback function

    +
  • @@ -1547,6 +1563,52 @@

    SSO/External authentication

    authenticate user external +
    +

    Note that the SDK will automatically URL encode parameters (such as the auth token) when making the request.

    +
    +
    +
    Steam Authentication Example
    +
    +

    In order to use the Steam authentication functionality, you must add your games Encrypted App Ticket Key from Steamworks. On your games profile on mod.io, go to Edit > Options and add the key. You can then call AuthenticateUserExternalAsync and provide the users Encrypted App Ticket as the Auth Token. Note that the Auth Token must be Base64 encoded when passed

    +
    +
    +

    Below is a sample Blueprint method that will get the users current Encrypted App Ticket that you can use in your Authentication request. Add this to a BlueprintLibrary in your games codebase.

    +
    +
    +C++ Example +
    +
    +
    +
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnGetTicket, int32, LocalUserNum, FString, TokenData);
    +UFUNCTION(BlueprintCallable)
    +static void GetSteamAuthTicket(int32 LocalUserNum, FOnGetTicket Callback)
    +{
    +    //Get the steam subsystem
    +    FOnlineSubsystemSteam* SteamSubsystem = static_cast<FOnlineSubsystemSteam*>(IOnlineSubsystem::Get());
    +    //Add a handler to the subsystem for when the ticket has been retrieved
    +    SteamSubsystem->GetEncryptedAppTicketInterface()->OnEncryptedAppTicketResultDelegate.AddLambda(
    +        [LocalUserNum, OnComplete = Callback](bool bEncryptedDataAvailable, int32 ResultCode) {
    +
    +            TArray<uint8> TokenData;
    +            if (bEncryptedDataAvailable)
    +            {
    +                //If the ticket was retrieved successfully, get its data
    +                SteamSubsystem->GetEncryptedAppTicketInterface()->GetEncryptedAppTicket(TokenData);
    +            }
    +            //Call the user callback with the base64-encoded ticket, ready for submission via AuthenticateUserExternalAsync
    +            OnComplete.ExecuteIfBound(LocalUserNum, FBase64::Encode(TokenData));
    +        });
    +    //Begin the actual async request for the ticket, which will invoke the above lambda when it completes
    +    SteamSubsystem->GetEncryptedAppTicketInterface()->RequestEncryptedAppTicket(nullptr, 0);
    +}
    +
    +
    +
    +

    Note that if you are on 4.27 or above, Epic provides a helper method in OnlineIdentityInterface::GetLinkedAccountAuthToken that will get the current account’s auth token without having to take a direct dependency on FOnlineSubsystemSteam. Ensure that the token is Base64 encoded when being passed to AuthenticateUserExternalAsync.

    +
    +
    +
    +

    Token Lifetime & Re-Authentication

    @@ -2000,6 +2062,91 @@

    Parameter Validation Errors


    +
    +

    Submitting a new mod

    +
    +

    In order to submit a mod, you have to first create a mod handle using GetModCreationHandle and use that handle when calling SubmitNewModAsync

    +
    +
    +Blueprint Example +
    +
    +
    +submit new mod +
    +
    +
    +
    +
    +C++ Example +
    +
    +
    +
    void UModioManager::SubmitNewMod()
    +{
    +	if (GEngine->GetEngineSubsystem<UModioSubsystem>())
    +	{
    +		FModioModCreationHandle Handle = GEngine->GetEngineSubsystem<UModioSubsystem>()->GetModCreationHandle();
    +
    +		FModioCreateModParams Params;
    +		Params.Name = TEXT("My Awesome Mod");
    +		Params.Description = TEXT("This is an amazing mod");
    +		Params.PathToLogoFile = TEXT("C:\\temp\\image.png");
    +
    +		GEngine->GetEngineSubsystem<UModioSubsystem>()->SubmitNewModAsync(Handle, Params, FOnSubmitNewModDelegateFast::CreateUObject(this, &UModioManager::OnSubmitNewModCallback));
    +	}
    +}
    +
    +void UModioManager::OnSubmitNewModCallback(FModioErrorCode ErrorCode, TOptional<FModioModID> ModId)
    +{
    +	if (ErrorCode == false)
    +	{
    +		// Mod was submitted successfully. Use ModId to submit some files to it.
    +	}
    +}
    +
    +
    +
    +
    +
    +
    +

    Submitting a file for a mod

    +
    +

    Once you have successfully submitted a mod, you can then submit a file for that mod using SubmitNewModFileForMod. When you submit a file, you pass a ModioCreateModFileParams containing the directory of the files that you want to submit. The plugin will then compress this folder into a zip file and upload it as the active version of the mod. Note that there is no callback for this method; you’ll get notified of the completed upload by the Mod Management callbacks.

    +
    +
    +Blueprint Example +
    +
    +

    As an example, after the callback for submitting a mod has completed, you can get the Mod Id to use for file submission.

    +
    +
    +
    +submit new mod file +
    +
    +
    +
    +
    +C++ Example +
    +
    +
    +
    void UModioManager::SubmitNewModFile(FModioModID ModId)
    +{
    +	if (GEngine->GetEngineSubsystem<UModioSubsystem>())
    +	{
    +		FModioCreateModFileParams Params;
    +		Params.PathToModRootDirectory = TEXT("C:\\temp\\mod_folder");
    +
    +		GEngine->GetEngineSubsystem<UModioSubsystem>()->SubmitNewModFileForMod(ModId, Params);
    +	}
    +}
    +
    +
    +
    +
    +
    @@ -3627,7 +3774,7 @@

    EnableModManagement

    -
    void K2_EnableModManagement(FOnModManagementDelegate Callback)
    +
    FModioErrorCode K2_EnableModManagement(FOnModManagementDelegate Callback)
    @@ -3648,6 +3795,10 @@
    Parameters

    Callback

    This callback handler will be invoked with a ModManagementEvent for each mod operation performed by the SDK

    + +

    Return Value

    + +
    @@ -3868,6 +4019,83 @@
    Error Values

    +

    ArchiveModAsync

    +
    +
    +nd img K2 ArchiveModAsync +
    +
    +
    +
    +
    void K2_ArchiveModAsync(FModioModID Mod, FOnErrorOnlyDelegate Callback)
    +
    +
    +
    +

    Archives a mod. This mod will no longer be able to be viewed or retrieved via the SDK, but it will still exist should you choose to restore it at a later date. Archiving is restricted to team managers and administrators only. Note that restoration and permanent deletion of a mod is possible only via web interface.

    +
    +
    Requirements
    +
    +
      +
    • +

      authenticated-user

      +
    • +
    • +

      initialized-sdk

      +
    • +
    • +

      no-rate-limiting

      +
    • +
    +
    +
    Parameters
    + ++++ + + + + + + + + + + + + + + +

    Target

    Modio Subsystem Object Reference

    Mod

    The mod to be archived.

    Callback

    +
    Error Values
    + ++++ + + + + + + + + + + + + + + + + + + +

    ApiError::InsufficientPermission

    The authenticated user does not have permission to archive this mod.This action is restricted to team managers and administrators only.

    GenericError::SDKNotInitialized

    SDK not initialized

    NetworkError

    Couldn’t connect to mod.io servers

    EntityNotFoundError

    Specified mod does not exist or was deleted

    +
    +
    +

    Is Mod Management Busy

    @@ -3882,7 +4110,7 @@

    Is Mod Management Busy

    Checks if the automatic management process is currently installing or removing mods

    -
    Parameters
    +
    Parameters
    @@ -3920,7 +4148,7 @@

    Get Last Validation Error

    If the last request to the mod.io servers returned a validation failure, this function returns extended information describing the fields that failed validation.

    -
    Requirements
    +
    Requirements
    • @@ -3928,7 +4156,7 @@
      Requirements
    -
    Parameters
    +
    Parameters
    @@ -3966,7 +4194,7 @@

    ForceUninstallModAsync

    Forcibly uninstalls a mod from the system. This is intended for use when a host application requires more room for a mod that the user wants to install, and as such will return an error if the current user is subscribed to the mod. To remove a mod the current user is subscribed to, use UnsubscribeFromModAsync.

    -
    Parameters
    +
    Parameters
    @@ -3987,7 +4215,7 @@
    Parameters
    -
    Error Values
    +
    Error Values
    @@ -4025,7 +4253,7 @@

    Disable Mod Management

    Disables automatic installation or uninstallation of mods based on the user’s subscriptions. Allows currently processing installation to complete; will cancel any pending operations when called.

    -
    Parameters
    +
    Parameters
    @@ -4825,22 +5053,22 @@

    Variables

    - + - + - + - + @@ -4854,6 +5082,13 @@

    Variables


    +

    ModioUnsigned64

    +
    +

    Trivial Blueprint-compatible wrapper around an unsigned 64-bit integer

    +
    +
    +
    +

    ModioReportParams


    @@ -5017,7 +5252,7 @@

    Set Portal

    Changes the portal for the provided set of initialization options

    -

    Parameters

    +

    Parameters

    int64

    FModioUnsigned64

    TotalDownloadSize

    Total size of the downloaded file

    int64

    FModioUnsigned64

    CurrentlyDownloadedBytes

    Current amount downloaded in bytes

    int64

    FModioUnsigned64

    TotalExtractedSizeOnDisk

    Total size on disk when fully extracted

    int64

    FModioUnsigned64

    CurrentlyExtractedBytes

    Amount of data currently extracted

    @@ -5059,7 +5294,7 @@

    Make Initialize Options

    Make initialization options, should only be used in conjunction with InitializeAsync

    -

    Parameters

    +

    Parameters

    @@ -5105,7 +5340,7 @@

    Make Game Id

    Create a game id from a integer, should only be used in conjunction with InitializeAsync

    -

    Parameters

    +

    Parameters

    @@ -5139,7 +5374,7 @@

    Make Auth Params

    Creates an AuthenticationParams object

    -

    Parameters

    +

    Parameters

    @@ -5185,7 +5420,7 @@

    Make Api Key

    Create a ApiKey id from a string, should only be used in conjunction with InitializeAsync

    -

    Parameters

    +

    Parameters

    @@ -5216,7 +5451,7 @@

    Get Value

    int32 GetValue(FModioErrorCode Error)
    -

    Parameters

    +

    Parameters

    @@ -5251,7 +5486,7 @@

    Get Message

    FString GetMessage(FModioErrorCode Error)
    -

    Parameters

    +

    Parameters

    @@ -5286,7 +5521,7 @@

    List User Subscription Async

    Runs a filter over the user’s subscription list

    -

    Parameters

    +

    Parameters

    @@ -5401,7 +5636,7 @@

    Get Logo Size

    FVector2D GetLogoSize(UTexture* Logo, EModioLogoSize LogoSize)
    -

    Parameters

    +

    Parameters

    @@ -5440,7 +5675,7 @@

    Get Gallery Size

    FVector2D GetGallerySize(UTexture* GalleryImage, EModioGallerySize GallerySize)
    -

    Parameters

    +

    Parameters

    @@ -5479,7 +5714,7 @@

    Get Avatar Size

    FVector2D GetAvatarSize(UTexture* Avatar, EModioAvatarSize AvatarSize)
    -

    Parameters

    +

    Parameters

    @@ -5518,7 +5753,7 @@

    Get Path

    FString GetPath(FModioModCollectionEntry Entry)
    -

    Parameters

    +

    Parameters

    @@ -5553,7 +5788,7 @@

    Get Mod State

    EModioModState GetModState(FModioModCollectionEntry Entry)
    -

    Parameters

    +

    Parameters

    @@ -5588,7 +5823,7 @@

    Get Mod Profile

    FModioModInfo GetModProfile(FModioModCollectionEntry Entry)
    -

    Parameters

    +

    Parameters

    @@ -5623,7 +5858,7 @@

    Get ID

    FModioModID GetID(FModioModCollectionEntry Entry)
    -

    Parameters

    +

    Parameters

    @@ -5661,7 +5896,7 @@

    Get Percent (integer64/integer64)

    Dividend/Divisor and return the floating point result with no checks *

    -

    Parameters

    +

    Parameters

    @@ -5696,7 +5931,7 @@

    Is Valid Security Code Format

    bool IsValidSecurityCodeFormat(FString String)
    -

    Parameters

    +

    Parameters

    @@ -5731,7 +5966,7 @@

    Is Valid Email Address Format

    bool IsValidEmailAddressFormat(FString String)
    -

    Parameters

    +

    Parameters

    @@ -5822,7 +6057,7 @@

    ToString (Filesize)

    FText Filesize_ToString(int64 FileSize, int32 MaxDecimals, TEnumAsByte<EFileSizeUnit> Unit)
    -

    Parameters

    +

    Parameters

    @@ -5854,6 +6089,146 @@

    Returns


    +

    ModioUnsigned64 - ModioUnsigned64

    +
    +
    +nd img Subtract +
    +
    +
    +
    +
    FModioUnsigned64 Subtract(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 != ModioUnsigned64

    +
    +
    +nd img NotEqualTo +
    +
    +
    +
    +
    bool NotEqualTo(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    Make from Components

    +
    +
    +nd img MakeFromComponents +
    +
    +
    +
    +
    FModioUnsigned64 MakeFromComponents(int32 High, int32 Low)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 < ModioUnsigned64

    +
    +
    +nd img LessThan +
    +
    +
    +
    +
    bool LessThan(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 > ModioUnsigned64

    +
    +
    +nd img GreaterThan +
    +
    +
    +
    +
    bool GreaterThan(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 == ModioUnsigned64

    +
    +
    +nd img EqualTo +
    +
    +
    +
    +
    bool EqualTo(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 / float

    +
    +
    +nd img DivideFloat +
    +
    +
    +
    +
    float DivideFloat(FModioUnsigned64 LHS, float RHS)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 / ModioUnsigned64

    +
    +
    +nd img Divide +
    +
    +
    +
    +
    FModioUnsigned64 Divide(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +
    +

    Break to Components

    +
    +
    +nd img BreakToComponents +
    +
    +
    +
    +
    void BreakToComponents(FModioUnsigned64 In, int32 High, int32 Low)
    +
    +
    +
    +
    +
    +

    ModioUnsigned64 + ModioUnsigned64

    +
    +
    +nd img Add +
    +
    +
    +
    +
    FModioUnsigned64 Add(FModioUnsigned64 LHS, FModioUnsigned64 RHS)
    +
    +
    +
    +
    +

    SubmitNewModFileForModFromMemory

    @@ -5868,7 +6243,7 @@

    SubmitNewModFileForModFromMemory

    Queues the upload of a new mod file release for the specified mod, using the submitted parameters. This upload method accepts a a block of memory TArray<uint8> rather than a file path. The upload’s progress can be tracked in the same way as downloads; when completed, a Mod Management Event will be triggered with the result code for the upload.

    -

    Requirements

    +

    Requirements

    • @@ -5879,7 +6254,7 @@

      Requirements

    -

    Parameters

    +

    Parameters

    @@ -5917,7 +6292,7 @@

    LoadModFileToMemory

    Loads an installed mod file into memory.

    -

    Requirements

    +

    Requirements

    • @@ -5928,7 +6303,7 @@

      Requirements

    -

    Parameters

    +

    Parameters

    @@ -6399,11 +6774,64 @@

    Values


    +

    EModioErrorCondition

    +

    Values

    +
    ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    NoError

    NetworkError

    When this condition is true, the error code represents a connection or HTTP error between the client and the mod.io server.

    ConfigurationError

    When this condition is true, the error code indicates the SDK’s configuration is not valid - the game ID or API key are incorrect or the game has been deleted.

    InvalidArgsError

    When this condition is true, the error code indicates the arguments passed to the function have failed validation or were otherwise invalid.

    FilesystemError

    When this condition is true, the error code indicates a permission or IO error when accessing local filesystem data.

    InternalError

    When this condition is true, the error code represents an internal SDK error - please inform mod.io of the error code value.

    EntityNotFoundError

    When this condition is true, the error code indicates that a specified game, mod, media file or mod file was not found.

    UserTermsOfUseError

    When this condition is true, the error code indicates that the user has not yet accepted the mod.io Terms of Use.

    SubmitReportError

    When this condition is true, the error code indicates that a report for the specified content could not be submitted.

    UserNotAuthenticatedError

    When this condition is true, the error code indicates that a user is not authenticated.

    +
    +
    +

    EModioSortDirection

    Enum indicating which direction sorting should be applied

    -

    Values

    +

    Values

    @@ -6427,7 +6855,7 @@

    EModioSortFieldTy

    Enum indicating which field should be used to sort the results

    -

    Values

    +

    Values

    @@ -6458,13 +6886,17 @@

    Values

    + + + +

    DateUpdated

    use date mod was marked live

    DownloadsTotal

    use date mod was last updated


    EModioImageState

    -

    Values

    +

    Values

    @@ -6496,7 +6928,7 @@

    EModioModState

    Enum representing the current state of a mod

    -

    Values

    +

    Values

    @@ -6533,7 +6965,7 @@

    Values

    EModioRating

    -

    Values

    +

    Values

    @@ -6558,7 +6990,7 @@

    Values

    EModioReportType

    -

    Values

    +

    Values

    @@ -6603,7 +7035,7 @@

    Values

    EFileSizeUnit

    -

    Values

    +

    Values

    @@ -6638,7 +7070,7 @@

    Values

    diff --git a/Doc/getting-started.adoc b/Doc/getting-started.adoc index 4894f1f8..0bc254df 100644 --- a/Doc/getting-started.adoc +++ b/Doc/getting-started.adoc @@ -73,7 +73,7 @@ NOTE: The plugin event loop, any internal event handlers, and all callbacks you ==== Non-blocking, asynchronous interface -The plugin communicates with the mod.io servers, the filesystem on the device you're using, and platform-provided services for authentication. All of these may not return results immediately, so many functions provided by the ModioSubsystem are non-blocking and asynchronous. +The plugin communicates with the mod.io servers, the filesystem on the device you're using, and platform-provided services for authentication. All of these may not return results immediately, so many functions provided by the ModioSubsystem are non-blocking and asynchronous. For example, the initialization function returns immediately. However, your game should consider the ModioSybsystem as initialized only when the init callback executes. NOTE: All async methods in the public API end with the suffix `Async`. @@ -110,9 +110,8 @@ The plugin stores mods in a game-specific directory in the following directory b [stripes=odd,frame=none,cols="1,^1,^1"] |=== -|Windows | Linux | OSX - -|`${FolderID_Public}/mod.io` | TBD |TBD +| Windows | Linux | OSX +|`${FolderID_Public}/mod.io` | `${USER_HOME}/mod.io` | `${USER_HOME}/Library/Application Support/mod.io` |=== However, this value can be overridden in one of two ways: @@ -156,6 +155,7 @@ image::img/initasync_getoptions.png[] .Notes * The error-handling in this sample has been omitted. See <> for more information on error handling. +* To fully initialize the SDK, you must receive confirmation from the callback. Consider that most functions return after invocation, nonetheless, their effects are only visible in their callback function ==== @@ -348,6 +348,45 @@ Here's what steps 1 and 2 might look like in Blueprint: image::img/authenticate_user_external.png[] +Note that the SDK will automatically URL encode parameters (such as the auth token) when making the request. + +===== Steam Authentication Example + +In order to use the Steam authentication functionality, you must add your games https://partner.steamgames.com/apps/sdkauth[Encrypted App Ticket Key] from Steamworks. On your games profile on mod.io, go to Edit > Options and add the key. You can then call <> and provide the users Encrypted App Ticket as the Auth Token. Note that the Auth Token must be Base64 encoded when passed + +Below is a sample Blueprint method that will get the users current Encrypted App Ticket that you can use in your Authentication request. Add this to a BlueprintLibrary in your games codebase. + +.C++ Example +[%collapsible] +==== +[source] +---- +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnGetTicket, int32, LocalUserNum, FString, TokenData); +UFUNCTION(BlueprintCallable) +static void GetSteamAuthTicket(int32 LocalUserNum, FOnGetTicket Callback) +{ + //Get the steam subsystem + FOnlineSubsystemSteam* SteamSubsystem = static_cast(IOnlineSubsystem::Get()); + //Add a handler to the subsystem for when the ticket has been retrieved + SteamSubsystem->GetEncryptedAppTicketInterface()->OnEncryptedAppTicketResultDelegate.AddLambda( + [LocalUserNum, OnComplete = Callback](bool bEncryptedDataAvailable, int32 ResultCode) { + + TArray TokenData; + if (bEncryptedDataAvailable) + { + //If the ticket was retrieved successfully, get its data + SteamSubsystem->GetEncryptedAppTicketInterface()->GetEncryptedAppTicket(TokenData); + } + //Call the user callback with the base64-encoded ticket, ready for submission via AuthenticateUserExternalAsync + OnComplete.ExecuteIfBound(LocalUserNum, FBase64::Encode(TokenData)); + }); + //Begin the actual async request for the ticket, which will invoke the above lambda when it completes + SteamSubsystem->GetEncryptedAppTicketInterface()->RequestEncryptedAppTicket(nullptr, 0); +} +---- +Note that if you are on 4.27 or above, Epic provides a helper method in OnlineIdentityInterface::GetLinkedAccountAuthToken that will get the current account's auth token without having to take a direct dependency on FOnlineSubsystemSteam. Ensure that the token is Base64 encoded when being passed to <>. +==== + ==== Token Lifetime & Re-Authentication By default, tokens issued via email token exchange have a lifetime of 1 year. You can verify that a user has been successfully authenticated by using <>. A success and non-null result indicates that a user has been authenticated. @@ -713,3 +752,85 @@ image::img/get_last_validation_error.png[] ''' +==== Submitting a new mod + +In order to submit a mod, you have to first create a mod handle using <> and use that handle when calling <> + +.Blueprint Example +[%collapsible] +==== + +image::img/submit_new_mod.png[] + +==== + +.C++ Example +[%collapsible] +==== + +[source] +---- + +void UModioManager::SubmitNewMod() +{ + if (GEngine->GetEngineSubsystem()) + { + FModioModCreationHandle Handle = GEngine->GetEngineSubsystem()->GetModCreationHandle(); + + FModioCreateModParams Params; + Params.Name = TEXT("My Awesome Mod"); + Params.Description = TEXT("This is an amazing mod"); + Params.PathToLogoFile = TEXT("C:\\temp\\image.png"); + + GEngine->GetEngineSubsystem()->SubmitNewModAsync(Handle, Params, FOnSubmitNewModDelegateFast::CreateUObject(this, &UModioManager::OnSubmitNewModCallback)); + } +} + +void UModioManager::OnSubmitNewModCallback(FModioErrorCode ErrorCode, TOptional ModId) +{ + if (ErrorCode == false) + { + // Mod was submitted successfully. Use ModId to submit some files to it. + } +} + +---- + +==== + + +==== Submitting a file for a mod + +Once you have successfully submitted a mod, you can then submit a file for that mod using <>. When you submit a file, you pass a <> containing the directory of the files that you want to submit. The plugin will then compress this folder into a zip file and upload it as the active version of the mod. Note that there is no callback for this method; you'll get notified of the completed upload by the Mod Management callbacks. + +.Blueprint Example +[%collapsible] +==== + +As an example, after the callback for submitting a mod has completed, you can get the Mod Id to use for file submission. + +image::img/submit_new_mod_file.png[] + +==== + +.C++ Example +[%collapsible] +==== + +[source] +---- + +void UModioManager::SubmitNewModFile(FModioModID ModId) +{ + if (GEngine->GetEngineSubsystem()) + { + FModioCreateModFileParams Params; + Params.PathToModRootDirectory = TEXT("C:\\temp\\mod_folder"); + + GEngine->GetEngineSubsystem()->SubmitNewModFileForMod(ModId, Params); + } +} + +---- + +==== diff --git a/Doc/img/nd_img_Add.png b/Doc/img/nd_img_Add.png new file mode 100644 index 0000000000000000000000000000000000000000..08114dd0de8b49b3e445bfc9c08dbceaf0148f30 GIT binary patch literal 2096 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 Z{Xf)^SP4qbC6oXF002ovPDHLkV1oB>_xk_< literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_EqualTo.png b/Doc/img/nd_img_EqualTo.png new file mode 100644 index 0000000000000000000000000000000000000000..473f6f158228ede886ad1d8c68b5cdecc34d290d GIT binary patch literal 2136 zcmV-e2&eanP)NlY*Y1%e>~we)}&G{ph26w8|z<=};XfeW0taMR*1;7U;; zUi6??A|aBhplBe35DNjx5xbUIz8yEdB_U%&RYJEr$*sx)PG&eWPjvYIssi{e9+m@lBA?fe$m$9)i znVXv<5>3#5mO|o1zP&K|<sJP$N)g{M|A2*)v+_`hUZlp$vKq&-TTU%v(eB8u<_wV1Q zStIlT3vTi`_4V~~;=~CV930eXmguOg2m+jjIsMWD&d$zqOvAfqZuEW8XjIzT+Jx*p zd-m*#+*Aiq1c<=(>(`~DqeHkdrec?5AF6-Idp@VGuFiP9W5!x<3OER(T;#9RsCbC%~nFnH}$0cHqNGvn#YYa6C z7XsX_{=Ut%S1iBsJw66+MHY7lE-NMiQ) z;X+n_tuF-V{ZgrvQ4{l#iQV1ZCMYa7LMpl|OFa9AShMHFau`5n#7f-|Cvi!f_=l!F znSKawS}l>(pJHX6lp9vtLxceNMv3r)7ko70M=~`#jmOIp?pCD2QgB-4%5)^=PM^<;+PY+9r1V3)mHI zRn$i-LWV$YnQLfhCvD%J&xvdj$G$0|3=dN|> z=0n(55%~y}Q`-Xqc7b%S!s8(!hA|$eT6jDwF-Q{?MZgn*Ws}cyRIOA5d?1h~-?JS* zADz@BJP5pu$6(BTe+gO#WimWB&hA0)=AKZXxGbr7+FjONUts5+o*vo1A2DkgVcyxP zQ>O}6tKF|~;}=lq0P&h}U*?`Xgib@Tl$1@-bx1pwdkg$H5W zvfBOJ_g>91JsycS1;tWQ_$Yx7Q0etZI&89Ki3z(12#82u{N+KxaEXL&+GSQnCX;_2 zy#x){7^wT}Q77mV?|h{s<~ zf^iCd9ct(3MlbjZ%Gm7eX2rxU7iwk-^B$1jykqcO@2#WmBm%u>k)>DkTxfy1NR=XHu$=)sj0pDamn>>NZ56oggBdGs&kZ zRVA?xeZx}nal6IWULQRk$(Ot#GSiP`4h|AISnDu-p4rVzCavzskq-%`_8+ieXsV(J zk@9{U7O7!TY7hGYkGvo`DSdc$Di5CJbK01k^cy!X# zbGv021Vo;L5--gB^8D?2zHyDq5I>l#{{V5^ZcMFM)#AM@hGf-?k50Kddbx9(xpEG literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_GreaterThan.png b/Doc/img/nd_img_GreaterThan.png new file mode 100644 index 0000000000000000000000000000000000000000..ffffcbb7512b1e685fc090827599c6b98fd992bf GIT binary patch literal 2278 zcmVgtlKSFg&zz<|un z&9S8_@bma46M_2?ggQ>0Iwfb%o|TG<3b}OYlC-zC$CuU5InqFNb+v4(+9q9hyJUEH zSVl)j*^@T#?bt5GgZru1>;3cO$&+&C%o*wJ?UjocFY4hpR%3Z`?{c}+G|6FldU{$$ zMn=T4tYyP=EVy)Fv>S37LStrC{n!__Kcy@M{y;iuoBmF<(cDtpvwpNJ#<;$1X$=}l5 zqz3x?`=!3VUg%kqv5Gl@) zWyrPmYqDT2IQm6 z*W!>|S6AnF-D-M|idA?}B=kzed?ZHTiCCT*5pSbNL4_C#UE(!=&uq_iZH_cR>kS5j ziq)J;qz@iEs3zr=Y6!ab+rXNS#hAS&hQ$dyEJpCLSpGX=`HrjnK4H=eg-u_JV`G8@PS)fPAFX1q%773_BD!xBgHHL{aOJ|i&k<{AOYVGXu#v7YAuri z4R92Wk^+|x^thZ5=U6fO0HoU6e4~g;M?U2YYfi%MS}}d!LvFFDH_GUQ3~M0bCM%fD zfa%zBEmx&j=AeXwf#f|xzWBb9fH|dP!k#8saAA&~zm>d<&dG=d78Vw8Y{kR7bj&G) z%?~%6C!-*7XAD><^C@Q(+A<6GMEuLPJrkGJsTtKk>SJYa5MJ>Icr7+1?ue_@&Zivj z0jx3gD}3@`VtJjqW*SHxTtC*ZSnkNCFL5njvS3;=pH({YDHj;lz^)l|D_5-6x!j4w zHpa@*5edupxkyn&ZDbEz%u-FQc5SaUd@LI$V_JJ9^MvHru0f09&xVv`kQs^@0K9|P#Chy zIBagR6J#EdWs5#UL(jl3!HEFlm{J7Z4ju=n4Y-zp)0u}C!A1EEM+`g|92}I6jt+S~ z@mvMcmv6>K3&`zp%g&uUrS)*DI_jL7w)er;S_ym6KtGs6aWdliv>rT!8*JnX_X6i| zKaFcDq`c+G$$cv2)|D*hSO(IQ`SnCwTbmp@gmpTI7F}L$1D=gP#nW%L#yK~UMQ$8{k7Q^ts&VbEHenwr!+8(T5IyJ0|gwutLkI#pyYfuc?oEtbGCoz4{VXuYFp z0*j(c+mC$MXv9$6qeqXbklpzI#_w#dU%##by5sZK`ndt)F@1Q>yRX2UOF2~2VhJoi z4(TV_t0A!+m zhY~vHamt{@62QP?k(sM-;}9rk!Ixur4Wl!7Snt6qO@WJLw24i&9EG+P6cprS7b;F; z*?){!?$T0-Fgqch;nHFWYzUiW1eU}rpH({YDaQ{QDt^M%W6NauHy7KBS^kS}ZEgu% zqV0s;b?Pev2_+6~Rq6N&K`0l7o8ZB<{72M{rkvETWmd1J2;@uPzV#i5$z;?8J|9WG ztcC?MpK=73RJ0$*9Ut5G`ZsGDSOnMcMh3%a=saK6<(**kej)U9_hO@SNzaLADYqM@z$;A?qk zE2?5>t4haSOC+>a1M5hu85N4+NgtUmOlq|03(TV}w%3gBf*7AHp6X?xtuoV|TAf-t z;~F5i@OI>%lxr@=dbbeE9eTsj&{ht`wAeh`p-TC`ZHxj32remg>$S`2-~83UqRabj z{`OJ#EDa4PJfoJ&^4faS&|;nzQMkq~g%0OwM|HPS)4(E*vEbf89p9u@kCx6e4G=%# zn3TRo4u@#X>q`g3Z5hSRxv?3oc=$UuLEq=K1~?SI)nXI%{uC0IEZ7cwUwV$s)Awty zmpAoKe3DW!%Z)Fv2MIm#iTXKb8enXD?@F!z4@ui%Mzii45C8xG07*qoM6N<$f}lBK AZ~y=R literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_K2_ArchiveModAsync.png b/Doc/img/nd_img_K2_ArchiveModAsync.png new file mode 100644 index 0000000000000000000000000000000000000000..c588e88acc61aeaca1865fccd937c70ac7ed71b4 GIT binary patch literal 16139 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 literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_K2_EnableModManagement.png b/Doc/img/nd_img_K2_EnableModManagement.png index 04e92d52df4daad4b90baf5e2615a2976910a90c..bc03eedf96a6e3ceac893076ba3d2dd0319e1a78 100644 GIT binary patch delta 16148 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 N$w(-Q*NPg4{4Xntqdout diff --git a/Doc/img/nd_img_LessThan.png b/Doc/img/nd_img_LessThan.png new file mode 100644 index 0000000000000000000000000000000000000000..d29da95d36d43d18fdaf5bf7181d7efb7fffb228 GIT binary patch literal 2251 zcmV;+2sHPJP)KA6h5=C%N<2Pj42?1lx0+mL?H@V$xANfgP|%h#amtyAM)Z)h%fpZlJXb$YFR)Y zOiB}D2uPp`Duz3V#E75*vdms4-`Bef3+~DcEX#s@s(N>(`}AebH|O-}?lU6xs0eJ$ zNS01pE(7AQ7imp0*5EjW^1H>w#d6}r38}8Gmc0Bt$FH@%SXhwB$w_(e;DL;djmg~H9Gh-L`Ym1`@xbMq@wlq0O3s}-Cl@YUkbV31DKL9_ zdZf3vR|WWnyAN zEX!J9rlY~7)AXdKrbez@xuP`n=FOWa@+f*3@gz`IRwkpPqiPO*^5hBGijk&x5e=@D zmzO73uU?hT&Q5KHDV=RXG}AQC)ZqE~d5&7)njRkihw16*QeIvzME}N(8{6cu^e`!b z!NEbPtgIAz)=XH%e2DB{BARPuXJ;#|y?ptyo?|JoZAOM+aL!fUG?z^vgPL2$0>foU zYg?-5wl+m-%`RSb(0cu)-gkXYUhv2s3D<-WjESOd4X{Qrm%e6d!u zx;%jrAf6-_e|W>KEjtvE&YPd=0*(u3o95GZHL;u$!!4HhrdT z&9z*GVwocn^!wtE2<4LVx%kXyiYMqkDvK`6(F=Fu*U=>jk-*~OB95(ic()#P3Sjdi z9nO=H7JD!TG?eA!GcrOvi`^pL72BS%>+0NuO2E0T^pC(RK7iLEH}*hWd7*Oh@fyGy z&whkY?vKr{b2m)_&cXF!4U6TD+X?;~LmagKq7H)jF3uk=Vvq zn>);5<$NrXSsPyG4v6^2VTwyb8cX24_u)1FWV=1hm{VVM%0^S_Z$z*tk36~q9Hxk^;%uK1TuUDy~qa*3yRsz`lfQ@H2BS_DY z;EGBiQWa7b%7+pF@EZkgR#ujrI(16w>gt4AyA?!@C9u;-a7Be%LEONl)-nurI_)7v_zxaDsG{~c|86BwE3t@k-^+_>N*!8w?wni@;cxg;7S+8ZJCv8=fpZEPb0*~GdG(&lF|{CJJ) zTz;__i8125JUct9KB(HnA-vM`3}np%Py$IUt_)vuiA0SZOAr=pCN!@*ZE)$}pd#wv zU^sNtm%;}Jd-m*+GiS~SKbztAHh1pakz2QJK@2;HLWqXVGM~!{xa!Ccr$XV-R`HFB zxXa-IR0C)j7lNDOM}FK>ZftB6&bzm7-`+vU;!#$EtCmBN+}AxSHJ0G|hA`u|z>Pzo zoCRNw;WdoT;9A zVmJ+*C(g04i23|(C_1#&+YKkHJr=jl+e8V(LFhF-`i4V+a12|GMJluvUNiZ4O< z08_)2ou*_;K!F=s)Z6Mf655InzE*~|qA7;9s(dbodH^t%Yhilf72~@g1)_UpXsgV%IcrmMCtLyq7v7Hi<+$Z6tamf8+@Uud32mh*rpBi5 z4wd8Q+`bjwBe*2z+8e5;e^XZiOD^wMsXInJvUNy6;Th3f*4Ns*4k@N+6@_c`Qs`ug z4pa}znFN+_j0U#@ZG7ph9nGC$5+HuWF)n?95}Ih8wwDZu+X{-FbE6AR;O5Wh41Jx_ z5}+x5t;WV0eG@X5H8>O(y?h^Crmr_&F0aa;_{61nn!CEfyU6HWovH7)Oah0|bKhFM Z{}0J{NupzYKm`B*002ovPDHLkV1l~NN7?`Y literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_MakeFromComponents.png b/Doc/img/nd_img_MakeFromComponents.png new file mode 100644 index 0000000000000000000000000000000000000000..f5fffb678a90d150a944c82ef9082d9a4d2f637d GIT binary patch literal 9897 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 literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_NotEqualTo.png b/Doc/img/nd_img_NotEqualTo.png new file mode 100644 index 0000000000000000000000000000000000000000..8581f2b7e2960dd3de946f60e9ef321277d42f50 GIT binary patch literal 2154 zcmV-w2$lDVP)y)>HFZ#6aw$#p5(lld+xb+=AQZW+;hJw z;*C~A?K7~Yx#VoX@45z@myA3bZzKP5V`HQ2*s()8J3FPWzD{bF)k-K7l6X9xr?}28 zGz)~oVVRzumW71{nVg)Ip`jre8yl0^*;&5oZt!op-Z>Xoh_rhh9UXGuzyUdY_^_;9 zyH8zg)a{QAS2a zWPX00Z{7s{FTDxbz_JTUzaKhuNRA#oDh&+{a_-zY>Fw=JFRRlfVuAJR*UOrwHPU~* zU&hDBO|!VGpx zGcz+XF)<;QWi9HK++bl4JJr?ICC85+SDxg;g$pHW8r`g17HDZ{kMgjiQN%B%-q}@t)OrcOxmL-7z|2lYpW1V=g*%n zQ#0vi(*ied-jw$CcHzj0j8Sh7#6LTp#j35XRbJ)Tv12+$p~cFa48vf|qDD&`W`hiR zmKJ6h0YfhJUXq9z@nj}5&?Skgjbc@_N+PgY0*U)##78Ay4oFqvhNn8TQej(w(emY;sy8c-Vguy~iX@W7ECA<;0uVip(vu2_hQ7iB zD!o*#bqde|>~$PcU}1t0uoq&CBIYoFq1F;IiYPhqsb^U8k_fhn8TtZxsYhj$(SicE zKynzQU}Hv2&z{8!tP;z-Es0n(cMFj(p${c$K2kb~%FPl9AjVm^oV$$9DM$-MA`vXJ zbF+cSQy=5?RpXe<3nG{`;fbtxW+?+6g!$Ans_p#9pCaK!%M6+2UXrAzr$_ee*^_$O z*|TTWs%uK1Z*p3|f8P+hjZx1A^eW4=7h6uvIh(^Og(@C_C=-U_Lz5;1GfYOZTa&7oFcm|B2h9I((4)8MM-+BRn(O`iw9jd6P5Ns=T zk~y#3_EyNY0u)a*VU_6g5 zSeL~umF%y}op$ZoCHwbd86ywzsOQO(C-Z2%ob0XmY>Yt$z~L;9tbuGJm`lF*@wL}M zzYo`3o+wKdwg}~vXVE~(X`+J9;5-VuB93|p4F2_#cx9S$=2P!6cu7D(Ig;a3CXa$>pcD@na=od84+9De zDgDv%_YsF|v*%fO6a?pkq;m055Si`uSEtr4cnc6Lh(6^H-*37Mb1l_43c}Hja}ja{9M?ERY(~d|Ezj)-7Fv1r$CU&%*NByDY(qWl}`p z#65K0StbqF&Gcu16fWFgxrRDE@mG)5E^!MG55z@I`WQLvk~EjM4Tyt9kja59Vvj|3cda`b7Q%ga7~l07*qoM6N<$f|kGqo&W#< literal 0 HcmV?d00001 diff --git a/Doc/img/nd_img_Subtract.png b/Doc/img/nd_img_Subtract.png new file mode 100644 index 0000000000000000000000000000000000000000..c210b80f11c83097b944b421bbdc16e16f22dd47 GIT binary patch literal 1945 zcmV;K2WI$*P)&`2pE=@t7TP^{KlGzK433=h~uF+QOW$b&EZ1zzAUsNvm&S3=tA zgT^R{NU1bw6)BXqT!KZQ(96u3%W-{s?$a}!b7oGbQ`jpxGw1BR_T{W^*4}G>dk^4D zc8J>Na#{M?a2pVZopO~FqlCs=l7F$awG{&c1L*DTMN@MV8XFoB3WX4l$4eB~*_B!# z91i2*!-rT|S;4(~_i*jnHO$V=VsUYiRlg(GZ^hau1}=XY_xJ4CgTsdpYr6u-S;p$KI|DfORM`vdzi2nKW=Qqh~>2A^j zH*VZOPfrgRS@V)r%t1;1IqkVtV`HQ8+Eb@a={c4b+hk%G2IpKAO>@~KV$gFtm|^$~ z437*WVn!Ujmg(z5BDfb;T_+O09q=U{z=+SlXHFoP7qQz;|Cu|Z8?!d{eI=KNnoE!kwmJPB|tw_fYYOu9_kPc9hU|IPO8>A9$0{*a9R|& zd|<@oL^#L7oDv`vx4Ei_N|$`<8P+lq{!W;oFGMfX)T%N%!D9=g!ej-r88sbyuH}0H zmU#<_ShRSLBp;!V5j7txokUQJQ;QN zoiW5inNK~VKB*JA3WW1$Py4bu)l&<&A1hSGr=g&LtBmG)8Idl!g`h z0NC8yea_q38-D-v9h7W zIhI4Qc&rf~rHYmfvOvY)Zcx8RN6jH`>~Uj#sIy4XluJXyGTG`=fz?G^FjE?C=-!+7+K zD9aDPg=D~0uI!@VW>MvP6??a}^49JBxNMBc99tA90g(BkXQel1>Sdd?K~Ao^|)F5oSUJ~NRtU&M%s*0gg!$B^jWfE{aLOBvfvVJCzoFW z0Qr{zNR!!#qR%9sdI>qO9+2;CUrF84l#}|kPUUt=;gSW{4k)`c(r5Dbku~Wv=2MU0 zQnnlb8s1Fa>(8oLAOo&rB7^i9_a4`x&-lNg+@r1Gs}kZ@nV_$l32V2Z47iTF7@bZX zZIxrIcA}29_KJ6l9`6ZkIrN!b-8#*c1r)fhHV)GCnf&1^|7fdJMfyyodqbbOdK0aj zu|OJ6#>fm|+{xAg`b^FzBanZz6^j+pXYP8WwQVdwa3$KopEh}GCpE}=w_Z*QFdAm? z#5tBjF+JN0`fTmib#7@3yi9N@?!7lzPJh;}1u`M;XSLf$-Lo<*pzut4F3W50EyId6 zT14TRyA*n@Mmwsz<<0^b9CN|FEOmV9t{$yjV-_HO#IdM+A~_tQ^|iTdK-}^qch1et z7?6iQauf7@O$( f?3I=$OSS$#to$s|uY0e;00000NkvXXu0mjf%vQys literal 0 HcmV?d00001 diff --git a/README.adoc b/README.adoc index 2f778064..5df4a362 100644 --- a/README.adoc +++ b/README.adoc @@ -65,7 +65,7 @@ The settings have the following parameters: == Further reading -To begin using the Plugin, either from Blueprint or from C++, please read our link:doc/getting-started.adoc[Getting Started Guide] for a detailed explanation of initialization and usage. +To begin using the Plugin, either from Blueprint or from C++, please read our link:Doc/getting-started.adoc[Getting Started Guide] for a detailed explanation of initialization and usage. * link:Doc/getting-started.adoc#plugin-quick-start-initialization-and-teardown[SDK initialization and event loop] * link:Doc/getting-started.adoc#plugin-quick-start-user-authentication[Authentication] diff --git a/Source/Modio/Modio.Build.cs b/Source/Modio/Modio.Build.cs index 04b05f9d..42e186a0 100644 --- a/Source/Modio/Modio.Build.cs +++ b/Source/Modio/Modio.Build.cs @@ -217,7 +217,7 @@ private void AddCommonHeaderPaths(string GeneratedHeaderPath) PublicIncludePaths.AddRange(new string[] { Path.Combine(GeneratedHeaderPath, "Public") }); - + ConditionalAddModuleDirectory(new DirectoryReference(Path.Combine(GeneratedHeaderPath, "Public"))); // Add common private includes from the Native SDK PrivateIncludePaths.AddRange(new string[] { diff --git a/Source/Modio/Private/Internal/Convert/CreateModFileParams.h b/Source/Modio/Private/Internal/Convert/CreateModFileParams.h index 951004b2..cd888000 100644 --- a/Source/Modio/Private/Internal/Convert/CreateModFileParams.h +++ b/Source/Modio/Private/Internal/Convert/CreateModFileParams.h @@ -25,5 +25,10 @@ FORCEINLINE Modio::CreateModFileParams ToModio(const FModioCreateModFileParams& Out.Changelog = ToModioOptional(In.Changelog); Out.bSetAsActive = ToModioOptional(In.bSetAsActiveRelease); Out.MetadataBlob = ToModioOptional(In.MetadataBlob); + if (In.ModfilePlatforms.IsSet()) + { + Out.Platforms = ToModio(In.ModfilePlatforms.GetValue()); + } + return Out; } \ No newline at end of file diff --git a/Source/Modio/Private/Internal/Convert/FilterParams.h b/Source/Modio/Private/Internal/Convert/FilterParams.h index 9d90157a..01b8d0f7 100644 --- a/Source/Modio/Private/Internal/Convert/FilterParams.h +++ b/Source/Modio/Private/Internal/Convert/FilterParams.h @@ -44,6 +44,8 @@ FORCEINLINE Modio::FilterParams::SortFieldType ToModio(EModioSortFieldType Envir return Modio::FilterParams::SortFieldType::DateMarkedLive; case EModioSortFieldType::DateUpdated: return Modio::FilterParams::SortFieldType::DateUpdated; + case EModioSortFieldType::DownloadsTotal: + return Modio::FilterParams::SortFieldType::DownloadsTotal; } return Modio::FilterParams::SortFieldType::ID; diff --git a/Source/Modio/Private/Internal/Convert/ModCollectionEntry.h b/Source/Modio/Private/Internal/Convert/ModCollectionEntry.h index 1347f6ef..505376c3 100644 --- a/Source/Modio/Private/Internal/Convert/ModCollectionEntry.h +++ b/Source/Modio/Private/Internal/Convert/ModCollectionEntry.h @@ -47,6 +47,15 @@ FModioModCollectionEntry ToUnreal(const Modio::ModCollectionEntry& In) Out.ModID = ToUnreal(In.GetID()); Out.ModPath = ToUnreal(In.GetPath()); Out.ModProfile = ToUnreal(In.GetModProfile()); + Out.SizeOnDisk = FModioUnsigned64(0); + if (In.GetSizeOnDisk()) + { + if (*In.GetSizeOnDisk()) + { + Out.SizeOnDisk = ToUnreal(*In.GetSizeOnDisk()); + } + } + return Out; } diff --git a/Source/Modio/Private/Internal/Convert/ModInfo.h b/Source/Modio/Private/Internal/Convert/ModInfo.h index 4c9556b2..53245ff8 100644 --- a/Source/Modio/Private/Internal/Convert/ModInfo.h +++ b/Source/Modio/Private/Internal/Convert/ModInfo.h @@ -47,6 +47,8 @@ FORCEINLINE FModioModInfo ToUnreal(const Modio::ModInfo& In) Out.ProfileDateUpdated = ToUnrealDateTime(In.ProfileDateUpdated); Out.ProfileDateLive = ToUnrealDateTime(In.ProfileDateLive); Out.ProfileMaturityOption = ToUnreal(In.ProfileMaturityOption); + Out.bVisible = In.bVisible; + Out.MetadataBlob = FString(In.MetadataBlob.c_str()); // Converting verbatim rather than via TCHAR as ToUnreal does if (In.FileInfo.has_value()) diff --git a/Source/Modio/Private/Internal/Convert/ModProgressInfo.h b/Source/Modio/Private/Internal/Convert/ModProgressInfo.h index 3edcbfaa..17534119 100644 --- a/Source/Modio/Private/Internal/Convert/ModProgressInfo.h +++ b/Source/Modio/Private/Internal/Convert/ModProgressInfo.h @@ -1,11 +1,11 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #pragma once @@ -18,6 +18,7 @@ FORCEINLINE FModioModProgressInfo ToUnreal(const Modio::ModProgressInfo& In) FModioModProgressInfo Out; Out.TotalDownloadSize = ToUnreal(In.TotalDownloadSize); Out.CurrentlyDownloadedBytes = ToUnreal(In.CurrentlyDownloadedBytes); + Out.CurrentlyExtractedBytes = ToUnreal(In.CurrentlyExtractedBytes); Out.TotalExtractedSizeOnDisk = ToUnreal(In.TotalExtractedSizeOnDisk); Out.ID = ToUnreal(In.ID); return Out; diff --git a/Source/Modio/Private/Internal/ModioConvert.cpp b/Source/Modio/Private/Internal/ModioConvert.cpp index d223cab8..4ea11b70 100644 --- a/Source/Modio/Private/Internal/ModioConvert.cpp +++ b/Source/Modio/Private/Internal/ModioConvert.cpp @@ -1,4 +1,5 @@ #include "Internal/ModioConvert.h" +#include "Types/ModioUnsigned64.h" std::string ToModio(const FString& String) { @@ -47,19 +48,21 @@ FString ToUnreal(const std::string& String) return UTF8_TO_TCHAR(String.c_str()); } - - FString ToUnreal(const Modio::filesystem::path& Path) { return UTF8_TO_TCHAR(Path.generic_u8string().c_str()); } +FModioUnsigned64 ToUnreal(const Modio::FileSize& In) +{ + return FModioUnsigned64(In); +} + FDateTime ToUnrealDateTime(std::int64_t UnixTimestamp) { return FDateTime::FromUnixTimestamp(UnixTimestamp); } - Modio::ApiKey ToModio(const FModioApiKey& In) { return Modio::ApiKey(TCHAR_TO_UTF8(*In.ToString())); @@ -93,7 +96,6 @@ Modio::UserID ToModio(const FModioUserID& In) return Modio::UserID(In.UserID); } - Modio::LogLevel ToModio(EModioLogLevel UnrealLogLevel) { switch (UnrealLogLevel) @@ -174,6 +176,37 @@ Modio::Environment ToModio(EModioEnvironment Environment) return Modio::Environment::Test; } +Modio::ModfilePlatform ToModio(EModioModfilePlatform Platform) +{ + switch (Platform) + { + case EModioModfilePlatform::Android: + return Modio::ModfilePlatform::Android; + case EModioModfilePlatform::iOS: + return Modio::ModfilePlatform::iOS; + case EModioModfilePlatform::Linux: + return Modio::ModfilePlatform::Linux; + case EModioModfilePlatform::Mac: + return Modio::ModfilePlatform::Mac; + case EModioModfilePlatform::Oculus: + return Modio::ModfilePlatform::Oculus; + case EModioModfilePlatform::PS4: + return Modio::ModfilePlatform::PS4; + case EModioModfilePlatform::PS5: + return Modio::ModfilePlatform::PS5; + case EModioModfilePlatform::Switch: + return Modio::ModfilePlatform::Switch; + case EModioModfilePlatform::Windows: + return Modio::ModfilePlatform::Windows; + case EModioModfilePlatform::XboxOne: + return Modio::ModfilePlatform::XboxOne; + case EModioModfilePlatform::XboxSeriesX: + return Modio::ModfilePlatform::XboxSeriesX; + } + checkf(false, TEXT("Unhandled value in ToModio(EModioModfilePlatform Platform)")); + return Modio::ModfilePlatform::Windows; +} + Modio::Portal ToModio(EModioPortal Portal) { switch (Portal) diff --git a/Source/Modio/Private/Internal/ModioConvert.h b/Source/Modio/Private/Internal/ModioConvert.h index 559c8185..28084188 100644 --- a/Source/Modio/Private/Internal/ModioConvert.h +++ b/Source/Modio/Private/Internal/ModioConvert.h @@ -21,8 +21,6 @@ #include #include - - int64 ToUnreal(std::int64_t Value); double ToUnreal(double Value); bool ToUnreal(bool Value); @@ -38,6 +36,7 @@ struct FModioMetadata ToUnreal(const Modio::Metadata& In); struct FModioModTag ToUnreal(const Modio::ModTag& In); struct FModioModInfo ToUnreal(const Modio::ModInfo& In); struct FModioModDependency ToUnreal(const Modio::ModDependency& In); +struct FModioUnsigned64 ToUnreal(const Modio::FileSize& In); std::string ToModio(const FString& String); std::vector ToModio(const TArray& StringArray); @@ -54,6 +53,7 @@ Modio::AvatarSize ToModio(EModioAvatarSize AvatarSize); Modio::GallerySize ToModio(EModioGallerySize GallerySize); Modio::LogoSize ToModio(EModioLogoSize LogoSize); Modio::Environment ToModio(EModioEnvironment Environment); +Modio::ModfilePlatform ToModio(EModioModfilePlatform Platform); Modio::Portal ToModio(EModioPortal Portal); Modio::Language ToModio(EModioLanguage Language); @@ -70,7 +70,6 @@ typename TEnableIf>::Va return static_cast(Flags.RawValue()); } - template TArray ToUnreal(std::vector&& OriginalArray) { @@ -111,5 +110,4 @@ std::vector ToModio(const TArray& OriginalArray) return Out; } - #pragma endregion diff --git a/Source/Modio/Private/Libraries/ModioCommonTypesLibrary.cpp b/Source/Modio/Private/Libraries/ModioCommonTypesLibrary.cpp index 214f338f..3a424e6e 100644 --- a/Source/Modio/Private/Libraries/ModioCommonTypesLibrary.cpp +++ b/Source/Modio/Private/Libraries/ModioCommonTypesLibrary.cpp @@ -1,11 +1,11 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #include "Libraries/ModioCommonTypesLibrary.h" @@ -21,11 +21,10 @@ FString UModioCommonTypesLibrary::Conv_GameIDToString(FModioGameID GameId) return GameId.ToString(); } -FModioAuthenticationParams UModioCommonTypesLibrary::MakeAuthParams(const FString AuthToken, - const FString EmailAddress, - const bool bHasAcceptedTOS) +FModioAuthenticationParams UModioCommonTypesLibrary::MakeAuthParams(const FString AuthToken, const FString EmailAddress, + const bool bHasAcceptedTOS) { - return FModioAuthenticationParams { AuthToken, EmailAddress, bHasAcceptedTOS }; + return FModioAuthenticationParams {AuthToken, EmailAddress, bHasAcceptedTOS}; } FModioApiKey UModioCommonTypesLibrary::MakeApiKey(const FString ApiKey) @@ -73,7 +72,9 @@ FString UModioCommonTypesLibrary::Conv_UserIDToString(FModioUserID UserID) return UserID.ToString(); } -FModioInitializeOptions UModioCommonTypesLibrary::MakeInitializeOptions(int64 GameId, const FString& APIKey, EModioEnvironment GameEnvironment, EModioPortal PortalInUse) +FModioInitializeOptions UModioCommonTypesLibrary::MakeInitializeOptions(int64 GameId, const FString& APIKey, + EModioEnvironment GameEnvironment, + EModioPortal PortalInUse) { FModioInitializeOptions Options; Options.GameId = FModioGameID(GameId); @@ -99,3 +100,12 @@ FModioInitializeOptions UModioCommonTypesLibrary::SetSessionIdentifier(const FMo return DuplicateOptions; } +bool UModioCommonTypesLibrary::EqualTo(const FModioModID& A, const FModioModID& B) +{ + return A == B; +} + +bool UModioCommonTypesLibrary::NotEqualTo(const FModioModID& A, const FModioModID& B) +{ + return A != B; +} \ No newline at end of file diff --git a/Source/Modio/Private/ModioSubsystem.cpp b/Source/Modio/Private/ModioSubsystem.cpp index 2874ad5e..ba1f35b2 100644 --- a/Source/Modio/Private/ModioSubsystem.cpp +++ b/Source/Modio/Private/ModioSubsystem.cpp @@ -47,9 +47,10 @@ void UModioSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); - if (const UModioSettings* Settings = GetDefault()) + if (const UModioSettings* Settings = GetMutableDefault()) { SetLogLevel(Settings->LogLevel); + UE_LOG(LogModio, Display, TEXT("Setting log level to %d"), (int32) Settings->LogLevel); } ImageCache = MakeUnique(); } @@ -118,9 +119,9 @@ void UModioSubsystem::FetchExternalUpdatesAsync(FOnErrorOnlyDelegateFast OnFetch }); } -void UModioSubsystem::EnableModManagement(FOnModManagementDelegateFast Callback) +FModioErrorCode UModioSubsystem::EnableModManagement(FOnModManagementDelegateFast Callback) { - Modio::EnableModManagement([this, Callback](Modio::ModManagementEvent Event) { + return Modio::EnableModManagement([this, Callback](Modio::ModManagementEvent Event) { // @todo: For some smarter caching, look at the event and see if we should invalidate the cache InvalidateUserInstallationCache(); Callback.ExecuteIfBound(ToUnreal(Event)); @@ -157,9 +158,9 @@ void UModioSubsystem::K2_FetchExternalUpdatesAsync(FOnErrorOnlyDelegate OnFetchD FOnErrorOnlyDelegateFast::CreateLambda([OnFetchDone](FModioErrorCode ec) { OnFetchDone.ExecuteIfBound(ec); })); } -void UModioSubsystem::K2_EnableModManagement(FOnModManagementDelegate Callback) +FModioErrorCode UModioSubsystem::K2_EnableModManagement(FOnModManagementDelegate Callback) { - EnableModManagement(FOnModManagementDelegateFast::CreateLambda( + return EnableModManagement(FOnModManagementDelegateFast::CreateLambda( [Callback](FModioModManagementEvent Event) { Callback.ExecuteIfBound(Event); })); } @@ -549,18 +550,17 @@ void UModioSubsystem::K2_ReportContentAsync(FModioReportParams Report, FOnErrorO [Callback](FModioErrorCode ec) { Callback.ExecuteIfBound(ec); })); } - TArray ToUnreal(const std::vector& OriginalArray) { - TArray Result; + TArray Result; - Result.Reserve(OriginalArray.size()); - for (const auto& It : OriginalArray) - { - Result.Emplace(ToUnreal(It)); - } + Result.Reserve(OriginalArray.size()); + for (const auto& It : OriginalArray) + { + Result.Emplace(ToUnreal(It)); + } - return Result; + return Result; } void UModioSubsystem::GetModDependenciesAsync(FModioModID ModID, FOnGetModDependenciesDelegateFast Callback) @@ -631,6 +631,17 @@ void UModioSubsystem::K2_ArchiveModAsync(FModioModID Mod, FOnErrorOnlyDelegate C Mod, FOnErrorOnlyDelegateFast::CreateLambda([Callback](FModioErrorCode ec) { Callback.ExecuteIfBound(ec); })); } +void UModioSubsystem::VerifyUserAuthenticationAsync(FOnErrorOnlyDelegateFast Callback) +{ + Modio::VerifyUserAuthenticationAsync([Callback](Modio::ErrorCode ec) { Callback.ExecuteIfBound(ToUnreal(ec)); }); +} + +void UModioSubsystem::K2_VerifyUserAuthenticationAsync(FOnErrorOnlyDelegate Callback) +{ + VerifyUserAuthenticationAsync(FOnErrorOnlyDelegateFast::CreateLambda([Callback](FModioErrorCode ec){ + Callback.ExecuteIfBound(ec);})); +} + /// File scope implementations #pragma region Implementation diff --git a/Source/Modio/Private/Tests/ToUnreal.cpp b/Source/Modio/Private/Tests/ToUnreal.cpp index a304496d..a3633973 100644 --- a/Source/Modio/Private/Tests/ToUnreal.cpp +++ b/Source/Modio/Private/Tests/ToUnreal.cpp @@ -1,16 +1,18 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ +#include "Internal/Convert/ModStats.h" +#include "Internal/ModioConvert.h" #include "Libraries/ModioSDKLibrary.h" #include "Misc/AutomationTest.h" -#include "Internal/Convert/ModStats.h" +#include "Types/ModioUnsigned64.h" #if WITH_DEV_AUTOMATION_TESTS @@ -32,7 +34,22 @@ bool FModioConvertDataToUnrealFormatTest::RunTest(const FString& Parameters) TestEqual("RatingPercentagePositive", (int32)OutObj.RatingPercentagePositive, (int32)InObj.RatingPercentagePositive); TestEqual("RatingWeightedAggregate", FMath::IsNearlyEqual((float) OutObj.RatingWeightedAggregate, (float) InObj.RatingWeightedAggregate), true); - TestEqual(TEXT("RatingDisplayText"), OutObj.RatingDisplayText.Equals(FString(InObj.RatingDisplayText.c_str())), true); + TestEqual(TEXT("RatingDisplayText"), OutObj.RatingDisplayText.Equals(FString(InObj.RatingDisplayText.c_str())), + true); + + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64ConversionTest, "Modio.DataConversion.NativeToUnreal.ModioUnsigned64", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64ConversionTest::RunTest(const FString& Parameters) +{ + uint64 TestValue = 0xF00DF00DF00DF00D; + Modio::FileSize InFileSize = Modio::FileSize(TestValue); + FModioUnsigned64 OutValue = ToUnreal(InFileSize); + TestEqual(TEXT("Underlying"), TestValue, OutValue.Underlying); return true; } diff --git a/Source/Modio/Private/Types/ModioImage.cpp b/Source/Modio/Private/Types/ModioImage.cpp index 0ef59acb..92a56cd6 100644 --- a/Source/Modio/Private/Types/ModioImage.cpp +++ b/Source/Modio/Private/Types/ModioImage.cpp @@ -1,11 +1,11 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #include "Types/ModioImageWrapper.h" @@ -15,15 +15,14 @@ #include "Engine/Texture2DDynamic.h" #include "IImageWrapper.h" #include "IImageWrapperModule.h" +#include "Internal/ModioConvert.h" #include "Misc/EngineVersionComparison.h" #include "Misc/FileHelper.h" -#include "Internal/ModioConvert.h" #include "ModioImageCache.h" #include "ModioSubsystem.h" #include "Modules/ModuleManager.h" #include "RenderingThread.h" - #if !UE_SERVER static TOptional> GetImageData(TSharedPtr ImageWrapper, ERGBFormat InFormat); static ERGBFormat GetDesiredErgbFormat(); @@ -43,7 +42,8 @@ class UTexture2DDynamic* FModioImageWrapper::GetTexture() const return Texture; } -TOptional FModioImageWrapper::LoadTextureDataFromDisk(const FString& ImagePath) +TOptional FModioImageWrapper::LoadTextureDataFromDisk( + const FString& ImagePath) { #if !UE_SERVER IImageWrapperModule& ImageWrapperModule = diff --git a/Source/Modio/Private/Types/ModioModCollectionEntryUImpl.cpp b/Source/Modio/Private/Types/ModioModCollectionEntryUImpl.cpp index dd345f20..cc16b500 100644 --- a/Source/Modio/Private/Types/ModioModCollectionEntryUImpl.cpp +++ b/Source/Modio/Private/Types/ModioModCollectionEntryUImpl.cpp @@ -37,3 +37,8 @@ const FString FModioModCollectionEntry::GetPath() const return {}; } } + +FModioUnsigned64 FModioModCollectionEntry::GetSizeOnDisk() const +{ + return FModioUnsigned64(SizeOnDisk); +} \ No newline at end of file diff --git a/Source/Modio/Private/Types/ModioReportParamsUImpl.cpp b/Source/Modio/Private/Types/ModioReportParamsUImpl.cpp index f10a0b3a..344ca6b5 100644 --- a/Source/Modio/Private/Types/ModioReportParamsUImpl.cpp +++ b/Source/Modio/Private/Types/ModioReportParamsUImpl.cpp @@ -32,6 +32,11 @@ FModioReportParams::FModioReportParams(FModioModID Mod, EModioReportType Type, F ReporterContact) {} +FModioReportParams::FModioReportParams(FModioModID Mod, EModioReportType Type) : +FModioReportParams(ToModio(Mod), ResourceType::Mod, Type, FString(), FString(), FString()) +{} + + FModioReportParams::FModioReportParams(int64 ResourceID, ResourceType ReportedResourceType, EModioReportType Type, FString ReportDescription, TOptional ReporterName, TOptional ReporterContact) diff --git a/Source/Modio/Public/Libraries/ModioCommonTypesLibrary.h b/Source/Modio/Public/Libraries/ModioCommonTypesLibrary.h index 21367a92..d674301c 100644 --- a/Source/Modio/Public/Libraries/ModioCommonTypesLibrary.h +++ b/Source/Modio/Public/Libraries/ModioCommonTypesLibrary.h @@ -1,23 +1,22 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #pragma once #include "Kismet/BlueprintFunctionLibrary.h" +#include "Types/ModioAuthenticationParams.h" #include "Types/ModioCommonTypes.h" #include "Types/ModioInitializeOptions.h" -#include "Types/ModioAuthenticationParams.h" #include "ModioCommonTypesLibrary.generated.h" - UCLASS() class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary { @@ -41,14 +40,15 @@ class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary static FString Conv_GameIDToString(FModioGameID GameId); /** - * @brief Creates an AuthenticationParams object + * @brief Creates an AuthenticationParams object * @param AuthToken - Authentication provider-supplied OAuth token * @param EmailAddress - User email address, can be left blank * @param bHasAcceptedTOS - Has the user been shown the TOS and accepted the TOS? * @return The constructed FModioAuthenticationParams object for use with <> */ UFUNCTION(BlueprintPure, category = "mod.io|Utilities", meta = (NativeMakeFunc)) - static FModioAuthenticationParams MakeAuthParams(const FString AuthToken, const FString EmailAddress, const bool bHasAcceptedTOS); + static FModioAuthenticationParams MakeAuthParams(const FString AuthToken, const FString EmailAddress, + const bool bHasAcceptedTOS); /** * @brief Create a ApiKey id from a string, should only be used in conjunction with InitializeAsync @@ -57,7 +57,6 @@ class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary UFUNCTION(BlueprintPure, category = "mod.io|Utilities", meta = (NativeMakeFunc)) static FModioApiKey MakeApiKey(const FString ApiKey); - /** * @brief Converts a ApiKey string so you can debug output it * @param ApiKey Input Api Key @@ -123,7 +122,7 @@ class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary * @return FModioEmailAddress object */ UFUNCTION(BlueprintPure, - meta = (DisplayName = "ToEmailAdress (String)", CompactNodeTitle = "->", BlueprintAutocast), + meta = (DisplayName = "ToEmailAddress (String)", CompactNodeTitle = "->", BlueprintAutocast), Category = "mod.io|Utilities") static FModioEmailAddress Conv_StringToEmailAddress(const FString& Email); @@ -140,11 +139,14 @@ class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary /** * @brief Make initialization options, should only be used in conjunction with InitializeAsync * @param GameId - a positive integer that maps to your game - * @param APIKey - APIKey available at https://.test.mod.io/edit/api or https://.mod.io/edit/api + * @param APIKey - APIKey available at https://.test.mod.io/edit/api or + * https://.mod.io/edit/api * @param GameEnvironment - If your environment is setup on test or production */ UFUNCTION(BlueprintPure, category = "mod.io|Utilities", meta = (NativeMakeFunc)) - static FModioInitializeOptions MakeInitializeOptions(int64 GameId, const FString& APIKey, EModioEnvironment GameEnvironment, EModioPortal PortalInUse = EModioPortal::None); + static FModioInitializeOptions MakeInitializeOptions(int64 GameId, const FString& APIKey, + EModioEnvironment GameEnvironment, + EModioPortal PortalInUse = EModioPortal::None); /** * @brief Changes the portal for the provided set of initialization options @@ -156,5 +158,20 @@ class MODIO_API UModioCommonTypesLibrary : public UBlueprintFunctionLibrary static FModioInitializeOptions SetPortal(const FModioInitializeOptions& Options, EModioPortal PortalToUse); UFUNCTION(BlueprintPure, Category = "mod.io|Utilities") - static FModioInitializeOptions SetSessionIdentifier(const FModioInitializeOptions& Options, const FString& SessionIdentifier); + static FModioInitializeOptions SetSessionIdentifier(const FModioInitializeOptions& Options, + const FString& SessionIdentifier); + + /** + * @brief Compares two ModioModIDs, returning true if equal + */ + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Utilities", + meta = (CompactNodeTitle = "==", Keywords = "== equal", DisplayName = "ModioModID == ModioModID")) + static bool EqualTo(const FModioModID& A, const FModioModID& B); + + /** + * @brief Compares two ModioModIDs, returning true if not equal + */ + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Utilities", + meta = (CompactNodeTitle = "!=", Keywords = "!= not equal", DisplayName = "ModioModID != ModioModID")) + static bool NotEqualTo(const FModioModID& A, const FModioModID& B); }; diff --git a/Source/Modio/Public/Libraries/ModioErrorConditionLibrary.h b/Source/Modio/Public/Libraries/ModioErrorConditionLibrary.h index 81a7ecbe..cc081ddd 100644 --- a/Source/Modio/Public/Libraries/ModioErrorConditionLibrary.h +++ b/Source/Modio/Public/Libraries/ModioErrorConditionLibrary.h @@ -33,6 +33,6 @@ class MODIO_API UModioErrorConditionLibrary : public UBlueprintFunctionLibrary // reported here: // https://forums.unrealengine.com/t/ustruct-type-must-be-a-uclass-ustruct-or-uenum/358937 // Because of that, the line below was commented. - // UFUNCTION(BlueprintCallable, Category = "mod.io|Error Handling") + UFUNCTION(BlueprintCallable, Category = "mod.io|Error Handling") static bool ErrorCodeMatches(FModioErrorCode ErrorCode, EModioErrorCondition Condition); }; \ No newline at end of file diff --git a/Source/Modio/Public/Libraries/ModioSDKLibrary.h b/Source/Modio/Public/Libraries/ModioSDKLibrary.h index 28e3c711..db71e026 100644 --- a/Source/Modio/Public/Libraries/ModioSDKLibrary.h +++ b/Source/Modio/Public/Libraries/ModioSDKLibrary.h @@ -20,7 +20,7 @@ UENUM(BlueprintType) enum EFileSizeUnit { - Largest = 0, // Will take the largest one that becomes a number larger than 1 (i.e, 1300mb becomes 1.23MB) + Largest = 0, // Will take the largest one that becomes a number larger than 1 (i.e, 1300mb becomes 1.3gb) B = 1, KB = 1024, MB = 1024 * 1024, @@ -86,12 +86,12 @@ class UModioSDKLibrary : public UBlueprintFunctionLibrary */ UFUNCTION(BlueprintPure, Category = "mod.io|String", meta = (DisplayName = "ToString (Filesize)", CompactNodeTitle = "Filesize")) - static FText Filesize_ToString(int64 FileSize, int32 MaxDecimals = 2, EFileSizeUnit Unit = EFileSizeUnit::Largest); + static MODIO_API FText Filesize_ToString(int64 FileSize, int32 MaxDecimals = 2, EFileSizeUnit Unit = EFileSizeUnit::Largest); /** Converts an integer64 value to a string */ UFUNCTION(BlueprintPure, meta = (DisplayName = "ToString (integer64)", CompactNodeTitle = "->", BlueprintAutocast), Category = "mod.io|Utilities|String") - static FString Conv_Int64ToString(int64 InInt); + static MODIO_API FString Conv_Int64ToString(int64 InInt); /** * @brief Converts a passed in integer to text based on formatting options @@ -104,7 +104,7 @@ class UModioSDKLibrary : public UBlueprintFunctionLibrary */ UFUNCTION(BlueprintPure, meta = (DisplayName = "ToText (integer64)", AdvancedDisplay = "1", BlueprintAutocast), Category = "mod.io|Utilities|Text") - static FText Conv_Int64ToText(int64 Value, bool bAlwaysSign = false, bool bUseGrouping = true, + static MODIO_API FText Conv_Int64ToText(int64 Value, bool bAlwaysSign = false, bool bUseGrouping = true, int32 MinimumIntegralDigits = 1, int32 MaximumIntegralDigits = 324); /** @brief Dividend/Divisor and return the floating point result with no checks **/ @@ -112,5 +112,5 @@ class UModioSDKLibrary : public UBlueprintFunctionLibrary meta = (DisplayName = "Get Percent (integer64/integer64)", CompactNodeTitle = "Percent", Keywords = "/ % percent pct"), Category = "mod.io|Math|integer64") - static float Pct_Int64Int64(int64 Dividend, int64 Divisor); + static MODIO_API float Pct_Int64Int64(int64 Dividend, int64 Divisor); }; diff --git a/Source/Modio/Public/ModioSDK.h b/Source/Modio/Public/ModioSDK.h index 558e107b..2ee5613f 100644 --- a/Source/Modio/Public/ModioSDK.h +++ b/Source/Modio/Public/ModioSDK.h @@ -13,19 +13,18 @@ #define MODIO_PLATFORM_UNREAL 1 -#if defined(_MSC_VER) - #pragma warning(push) - // These warnings are in third party libraries and we don't care about those - #pragma warning(disable : 4265 4996 4668 4191 4583 4582) - // These warnings is in our code, and should be fixed - #pragma warning(disable : 4063) -#endif +/* +#pragma warning(push) +// These warnings are in third party libraries and we don't care about those +#pragma warning(disable : 4265 4996 4668 4191 4583 4582) +// These warnings is in our code, and should be fixed +#pragma warning(disable : 4063) + +*/ #include "modio/ModioSDK.h" #undef MODIO_PLATFORM_UNREAL #undef ASIO_NO_EXCEPTIONS -#if defined(_MSC_VER) - #pragma warning(pop) -#endif +//#pragma warning(pop) diff --git a/Source/Modio/Public/ModioSettings.h b/Source/Modio/Public/ModioSettings.h index a8ad678c..3726a825 100644 --- a/Source/Modio/Public/ModioSettings.h +++ b/Source/Modio/Public/ModioSettings.h @@ -45,7 +45,7 @@ class MODIO_API UModioSettings : public UObject EModioPortal DefaultPortal = EModioPortal::None; #if WITH_EDITORONLY_DATA - UPROPERTY(EditDefaultsOnly, config, Category = "Project|Automation Testing", meta = (ShowOnlyInnerProperties)) + UPROPERTY(EditDefaultsOnly, config, Category = "Project|Automation Testing") FModioInitializeOptions AutomationTestOptions; UPROPERTY(EditDefaultsOnly, config, Category = "Project|Automation Testing") diff --git a/Source/Modio/Public/ModioSubsystem.h b/Source/Modio/Public/ModioSubsystem.h index 5358dc9d..3eb9d485 100644 --- a/Source/Modio/Public/ModioSubsystem.h +++ b/Source/Modio/Public/ModioSubsystem.h @@ -183,7 +183,7 @@ class UModioSubsystem : public UEngineSubsystem * @error ModManagementError::ModManagementAlreadyEnabled|Mod management was already enabled. The mod management * callback has not been changed */ - MODIO_API void EnableModManagement(FOnModManagementDelegateFast Callback); + MODIO_API FModioErrorCode EnableModManagement(FOnModManagementDelegateFast Callback); /** * @brief Disables automatic installation or uninstallation of mods based on the user's subscriptions. Allows @@ -390,7 +390,8 @@ class UModioSubsystem : public UEngineSubsystem * @requires no-authenticated-user * @errorcategory NetworkError|Couldn't connect to mod.io servers * @error GenericError::SDKNotInitialized|SDK not initialized - * @error UserAuthError::AlreadyAuthenticated|Authenticated user already signed-in. Call ClearUserDataAsync to + * @error +|Authenticated user already signed-in. Call ClearUserDataAsync to * de-authenticate the old user, then Shutdown() and reinitialize the SDK first. **/ MODIO_API void RequestEmailAuthCodeAsync(const FModioEmailAddress& EmailAddress, FOnErrorOnlyDelegateFast Callback); @@ -413,7 +414,7 @@ class UModioSubsystem : public UEngineSubsystem /** * @brief Uses platform-specific authentication to associate a Mod.io user account with the current platform user - * @param User Authentication payload data to submit to the provider. + * @param User Authentication payload data to submit to the provider. * @param Provider The provider to use to perform the authentication * @param Callback Callback invoked once the authentication request has been made * @requires initialized-sdk @@ -427,6 +428,20 @@ class UModioSubsystem : public UEngineSubsystem MODIO_API void AuthenticateUserExternalAsync(const FModioAuthenticationParams& User, EModioAuthenticationProvider Provider, FOnErrorOnlyDelegateFast Callback); + /** + * @docpublic + * @brief Queries the server to verify the state of the currently authenticated user if there is one present. An + * empty ErrorCode is passed to the callback to indicate successful verification, i.e. the mod.io server was + * contactable and the user's authentication remains valid. + * @param Callback Callback invoked with the results of the verification process + * @requires initialized-sdk + * @requires no-rate-limiting + * @requires authenticated-user + * @errorcategory NetworkError|Couldn't connect to mod.io servers + * @error GenericError::SDKNotInitialized|SDK not initialized + * @error UserDataError::InvalidUser|No authenticated user + */ + MODIO_API void VerifyUserAuthenticationAsync(FOnErrorOnlyDelegateFast Callback); /** * @docpublic @@ -490,6 +505,7 @@ class UModioSubsystem : public UEngineSubsystem /** Get our image cache */ struct FModioImageCache& GetImageCache() const; + void ShowUserAuthenticationDialog(); /* @brief Archives a mod. This mod will no longer be able to be viewed or retrieved via the SDK, but it will still @@ -580,7 +596,7 @@ class UModioSubsystem : public UEngineSubsystem * by the SDK */ UFUNCTION(BlueprintCallable, DisplayName = "EnableModManagement", Category = "mod.io|Mod Management") - MODIO_API void K2_EnableModManagement(FOnModManagementDelegate Callback); + MODIO_API FModioErrorCode K2_EnableModManagement(FOnModManagementDelegate Callback); /** * @brief Provides progress information for a mod installation or update operation if one is currently in progress. @@ -770,6 +786,22 @@ class UModioSubsystem : public UEngineSubsystem EModioAuthenticationProvider Provider, FOnErrorOnlyDelegate Callback); + /** + * @docpublic + * @brief Queries the server to verify the state of the currently authenticated user if there is one present. An + * empty ErrorCode is passed to the callback to indicate successful verification, i.e. the mod.io server was + * contactable and the user's authentication remains valid. + * @param Callback Callback invoked with the results of the verification process + * @requires initialized-sdk + * @requires no-rate-limiting + * @requires authenticated-user + * @errorcategory NetworkError|Couldn't connect to mod.io servers + * @error GenericError::SDKNotInitialized|SDK not initialized + * @error UserDataError::InvalidUser|No authenticated user + */ + UFUNCTION(BlueprintCallable, DisplayName = "VerifyUserAuthenticationAsync", Category = "mod.io|Authentication") + MODIO_API void K2_VerifyUserAuthenticationAsync(FOnErrorOnlyDelegate Callback); + /** * @brief This function retrieves the information required for a game to display the mod.io terms of use to a player * who wishes to create a mod.io account diff --git a/Source/Modio/Public/Types/ModioCommonTypes.h b/Source/Modio/Public/Types/ModioCommonTypes.h index 74fa2eb8..744921dd 100644 --- a/Source/Modio/Public/Types/ModioCommonTypes.h +++ b/Source/Modio/Public/Types/ModioCommonTypes.h @@ -48,6 +48,22 @@ enum class EModioPortal : uint8 XboxLive }; +/** @brief Enum representing the platform(s) that a modfile is enabled for */ +enum class EModioModfilePlatform : uint8 +{ + Windows, + Mac, + Linux, + Android, + iOS, + XboxOne, + XboxSeriesX, + PS4, + PS5, + Switch, + Oculus +}; + /** @brief Enum representing mod logo sizes */ UENUM(BlueprintType) enum class EModioLogoSize : uint8 @@ -249,7 +265,6 @@ struct MODIO_API FModioGameID friend struct Modio::GameID ToModio(const FModioGameID& In); UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = true), Category = "mod.io|CommonTypes") int64 GameId; - }; template<> diff --git a/Source/Modio/Public/Types/ModioCreateModFileParams.h b/Source/Modio/Public/Types/ModioCreateModFileParams.h index 219581b5..c47a1c98 100644 --- a/Source/Modio/Public/Types/ModioCreateModFileParams.h +++ b/Source/Modio/Public/Types/ModioCreateModFileParams.h @@ -27,4 +27,6 @@ struct MODIO_API FModioCreateModFileParams TOptional bSetAsActiveRelease; TOptional MetadataBlob; + + TOptional> ModfilePlatforms; }; \ No newline at end of file diff --git a/Source/Modio/Public/Types/ModioFilterParams.h b/Source/Modio/Public/Types/ModioFilterParams.h index 21138cf3..31fc1972 100644 --- a/Source/Modio/Public/Types/ModioFilterParams.h +++ b/Source/Modio/Public/Types/ModioFilterParams.h @@ -31,7 +31,8 @@ enum class EModioSortFieldType : uint8 SubscriberCount, /** use number of subscribers */ Rating, /** use mod rating */ DateMarkedLive, /** use date mod was marked live */ - DateUpdated /** use date mod was last updated */ + DateUpdated, /** use date mod was last updated */ + DownloadsTotal /** use downloads total */ }; /// @brief Enum indicating which direction sorting should be applied diff --git a/Source/Modio/Public/Types/ModioModCollectionEntry.h b/Source/Modio/Public/Types/ModioModCollectionEntry.h index ffed236c..e95bb68c 100644 --- a/Source/Modio/Public/Types/ModioModCollectionEntry.h +++ b/Source/Modio/Public/Types/ModioModCollectionEntry.h @@ -1,22 +1,21 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #pragma once #include "Types/ModioCommonTypes.h" #include "Types/ModioModInfo.h" +#include "Types/ModioUnsigned64.h" #include "ModioModCollectionEntry.generated.h" - - namespace Modio { class ModCollectionEntry; @@ -48,9 +47,11 @@ struct MODIO_API FModioModCollectionEntry const FString GetPath() const; + FModioUnsigned64 GetSizeOnDisk() const; + private: friend struct FModioModCollectionEntry ToUnreal(const class Modio::ModCollectionEntry& In); - + FModioUnsigned64 SizeOnDisk; EModioModState ModState; FModioModID ModID; TOptional ModPath; diff --git a/Source/Modio/Public/Types/ModioModInfo.h b/Source/Modio/Public/Types/ModioModInfo.h index 412cd9e3..06e44a14 100644 --- a/Source/Modio/Public/Types/ModioModInfo.h +++ b/Source/Modio/Public/Types/ModioModInfo.h @@ -1,11 +1,11 @@ -/* +/* * Copyright (C) 2021 mod.io Pty Ltd. - * + * * This file is part of the mod.io UE4 Plugin. - * - * Distributed under the MIT License. (See accompanying file LICENSE or + * + * Distributed under the MIT License. (See accompanying file LICENSE or * view online at ) - * + * */ #pragma once @@ -19,7 +19,6 @@ #include "ModioModInfo.generated.h" - namespace Modio { struct ModInfo; @@ -34,7 +33,6 @@ enum class EModioMaturityFlags : uint8 Explicit = 8 }; - /** @brief Full mod profile including current release information, media links, and stats */ USTRUCT(BlueprintType) struct MODIO_API FModioModInfo @@ -89,6 +87,10 @@ struct MODIO_API FModioModInfo UPROPERTY(BlueprintReadOnly, Category = "Profile") EModioMaturityFlags ProfileMaturityOption; + /** @brief Is the mod marked as visible? */ + UPROPERTY(BlueprintReadOnly, Category = "Profile") + bool bVisible; + UPROPERTY(BlueprintReadOnly, Category = "Metadata") FString MetadataBlob; @@ -119,6 +121,7 @@ struct MODIO_API FModioModInfo /** @brief Stats and rating information for the mod */ UPROPERTY(BlueprintReadOnly, Category = "Stats") FModioModStats Stats; + friend struct FModioModInfo ToUnreal(const struct Modio::ModInfo& In); }; diff --git a/Source/Modio/Public/Types/ModioModProgressInfo.h b/Source/Modio/Public/Types/ModioModProgressInfo.h index b5e3b5ce..80376535 100644 --- a/Source/Modio/Public/Types/ModioModProgressInfo.h +++ b/Source/Modio/Public/Types/ModioModProgressInfo.h @@ -11,6 +11,8 @@ #pragma once #include "Types/ModioCommonTypes.h" +#include "Types/ModioUnsigned64.h" + #include "ModioModProgressInfo.generated.h" @@ -27,19 +29,19 @@ struct MODIO_API FModioModProgressInfo /** @brief Total size of the downloaded file */ UPROPERTY(BlueprintReadOnly,Category="ModProgressInfo") - int64 TotalDownloadSize; + FModioUnsigned64 TotalDownloadSize; /** @brief Current amount downloaded in bytes */ UPROPERTY(BlueprintReadOnly,Category="ModProgressInfo|Current") - int64 CurrentlyDownloadedBytes; + FModioUnsigned64 CurrentlyDownloadedBytes; /** @brief Total size on disk when fully extracted */ UPROPERTY(BlueprintReadOnly,Category="ModProgressInfo") - int64 TotalExtractedSizeOnDisk; + FModioUnsigned64 TotalExtractedSizeOnDisk; /** @brief Amount of data currently extracted */ UPROPERTY(BlueprintReadOnly,Category="ModProgressInfo|Current") - int64 CurrentlyExtractedBytes; + FModioUnsigned64 CurrentlyExtractedBytes; /** @brief The mod ID of the mod being processed */ UPROPERTY(BlueprintReadOnly,Category="ModProgressInfo") diff --git a/Source/Modio/Public/Types/ModioReportParams.h b/Source/Modio/Public/Types/ModioReportParams.h index 900eb66e..a8248e8f 100644 --- a/Source/Modio/Public/Types/ModioReportParams.h +++ b/Source/Modio/Public/Types/ModioReportParams.h @@ -35,10 +35,16 @@ USTRUCT(BlueprintType) struct MODIO_API FModioReportParams { GENERATED_BODY(); - - + FModioReportParams(); + /// @docpublic + /// @brief Creates a content report for a game. + /// @param Mod The ID of the content being reported + /// @param Type The nature of the content report + FModioReportParams(struct FModioModID Mod, EModioReportType Type); + + /// @docpublic /// @brief Creates a content report for a game. /// @param Game The ID of the game being reported /// @param Type The nature of the content report @@ -70,8 +76,6 @@ struct MODIO_API FModioReportParams /// empty FModioReportParams(struct FModioModID Mod, EModioReportType Type, FString ReportDescription, TOptional ReporterName, TOptional ReporterContact); - -private: enum class ResourceType : uint8 diff --git a/Source/Modio/Public/Types/ModioUnsigned64.h b/Source/Modio/Public/Types/ModioUnsigned64.h new file mode 100644 index 00000000..4d0eb5fc --- /dev/null +++ b/Source/Modio/Public/Types/ModioUnsigned64.h @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2021 mod.io Pty Ltd. + * + * This file is part of the mod.io UE4 Plugin. + * + * Distributed under the MIT License. (See accompanying file LICENSE or + * view online at ) + * + */ + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "ModioUnsigned64.generated.h" + +/// @brief Trivial Blueprint-compatible wrapper around an unsigned 64-bit integer +USTRUCT(BlueprintType) +struct MODIO_API FModioUnsigned64 +{ + GENERATED_BODY(); + + uint64 Underlying; + + FModioUnsigned64() : Underlying(0) {}; + FModioUnsigned64(const FModioUnsigned64& Other) = default; + FModioUnsigned64(FModioUnsigned64&& Other) = default; + + explicit FModioUnsigned64(uint64 Value) : Underlying(Value) {}; + + constexpr explicit operator uint64() const + { + return Underlying; + } + + constexpr explicit operator bool() const + { + return (bool) Underlying; + } + + constexpr explicit operator double() const + { + return double(Underlying); + } + constexpr explicit operator float() const + { + return float(Underlying); + } + FORCEINLINE FModioUnsigned64& operator=(const FModioUnsigned64& Other) + { + if (this != &Other) + { + Underlying = Other.Underlying; + } + return *this; + } + + FORCEINLINE friend bool operator<(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS.Underlying < RHS.Underlying; + } + FORCEINLINE friend bool operator>(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS.Underlying > RHS.Underlying; + } + FORCEINLINE friend bool operator==(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS.Underlying == RHS.Underlying; + } + FORCEINLINE friend bool operator!=(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return !(LHS == RHS); + } + FORCEINLINE friend bool operator==(const FModioUnsigned64& LHS, const uint64& RHS) + { + return LHS.Underlying == RHS; + } + + FORCEINLINE friend FModioUnsigned64 operator/(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return FModioUnsigned64(LHS.Underlying / RHS.Underlying); + } + + FORCEINLINE friend FModioUnsigned64 operator+(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return FModioUnsigned64(LHS) += RHS; + } + + FORCEINLINE friend FModioUnsigned64 operator-(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return FModioUnsigned64(LHS) -= RHS; + } + FORCEINLINE friend FModioUnsigned64 operator+(const FModioUnsigned64& LHS, const uint64 RHS) + { + return FModioUnsigned64(LHS.Underlying + RHS); + } + FORCEINLINE friend FModioUnsigned64 operator-(const FModioUnsigned64& LHS, const uint64 RHS) + { + return FModioUnsigned64(LHS.Underlying - RHS); + } + + FORCEINLINE friend double operator/(const FModioUnsigned64& LHS, double Divisor) + { + return LHS.Underlying / Divisor; + } + + FORCEINLINE FModioUnsigned64& operator+=(const FModioUnsigned64& Other) + { + Underlying += Other.Underlying; + return *this; + } + FORCEINLINE FModioUnsigned64& operator-=(const FModioUnsigned64& Other) + { + Underlying -= Other.Underlying; + return *this; + } +}; + +UCLASS() +class MODIO_API UModioUnsigned64Library : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() +public: + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "===", Keywords = "== equal", + DisplayName = "ModioUnsigned64 == ModioUnsigned64")) + static bool EqualTo(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS == RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "!=", Keywords = "!= not equal", + DisplayName = "ModioUnsigned64 != ModioUnsigned64")) + static bool NotEqualTo(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS != RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = ">", Keywords = "> greater than", + DisplayName = "ModioUnsigned64 > ModioUnsigned64")) + static bool GreaterThan(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS > RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "<", Keywords = "< less than", + DisplayName = "ModioUnsigned64 < ModioUnsigned64")) + static bool LessThan(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS < RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "-", Keywords = "- subtract minus", + DisplayName = "ModioUnsigned64 - ModioUnsigned64")) + static FModioUnsigned64 Subtract(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS - RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "+", Keywords = "+ add plus", + DisplayName = "ModioUnsigned64 + ModioUnsigned64")) + static FModioUnsigned64 Add(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS + RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "/", Keywords = "/ divide", DisplayName = "ModioUnsigned64 / ModioUnsigned64")) + static FModioUnsigned64 Divide(const FModioUnsigned64& LHS, const FModioUnsigned64& RHS) + { + return LHS / RHS; + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (CompactNodeTitle = "/", Keywords = "/ divide", DisplayName = "ModioUnsigned64 / float")) + static float DivideFloat(const FModioUnsigned64& LHS, const float RHS) + { + return (float) (LHS / (double) RHS); + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64") + static void BreakToComponents(const FModioUnsigned64& In, int32& High, int32& Low) + { + High = static_cast(In.Underlying >> 32); + Low = static_cast(static_cast(In.Underlying)); + } + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64") + static FModioUnsigned64 MakeFromComponents(const int32& High, const int32& Low) + { + uint64 RawValue = static_cast(High); + RawValue = RawValue << 32; + RawValue |= static_cast(Low); + return FModioUnsigned64(RawValue); + } + + UFUNCTION(BlueprintPure, BlueprintCallable, Category = "mod.io|Unsigned64", + meta = (DisplayName = "TruncateModioUnsigned64ToFloat", BlueprintAutocast)) + static float Conv_FModioUnsigned64ToFloat(const FModioUnsigned64& In) + { + return float(In.Underlying); + } +}; \ No newline at end of file diff --git a/Source/ModioTests/Private/Tests/ModioUnsigned64Tests.cpp b/Source/ModioTests/Private/Tests/ModioUnsigned64Tests.cpp new file mode 100644 index 00000000..e455ef66 --- /dev/null +++ b/Source/ModioTests/Private/Tests/ModioUnsigned64Tests.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2021 mod.io Pty Ltd. + * + * This file is part of the mod.io UE4 Plugin. + * + * Distributed under the MIT License. (See accompanying file LICENSE or + * view online at ) + * + */ + +#include "Engine.h" +#include "Libraries/ModioSDKLibrary.h" +#include "Misc/AutomationTest.h" +#include "Types/ModioUnsigned64.h" + +#if WITH_DEV_AUTOMATION_TESTS + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64ConversionBoolZeroTest, + "Modio.ModioUnsigned64.Conversion.Bool.ZeroFalse", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) +bool FModioUnsigned64ConversionBoolZeroTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 Empty = FModioUnsigned64(0); + TestEqual("ZeroIsFalse", (bool)Empty, false); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64ConversionBoolNonZeroTest, + "Modio.ModioUnsigned64.Conversion.Bool.NonZeroTrue", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64ConversionBoolNonZeroTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 NonZero = FModioUnsigned64(123); + TestEqual("NonZeroIsTrue", (bool)NonZero, true); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorLessTest, "Modio.ModioUnsigned64.Arithmetic.LessThan", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorLessTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(1); + FModioUnsigned64 Second = FModioUnsigned64(2); + TestEqual("LessThanTrue", First < Second, true); + TestEqual("LessThanFalse", Second < First, false); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorGreaterTest, "Modio.ModioUnsigned64.Arithmetic.GreaterThan", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorGreaterTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(1); + FModioUnsigned64 Second = FModioUnsigned64(2); + TestEqual("GreaterThanTrue", Second > First, true); + TestEqual("GreaterThanFalse", First > Second, false); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorEqualTest, "Modio.ModioUnsigned64.Arithmetic.Equal", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorEqualTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(123456789); + FModioUnsigned64 Second = FModioUnsigned64(123456789); + FModioUnsigned64 Third = FModioUnsigned64(987654321); + TestEqual("EqualsTrue", First == Second, true); + TestEqual("EqualsFalse", First == Third, false); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorNotEqualTest, "Modio.ModioUnsigned64.Arithmetic.NotEqual", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorNotEqualTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(123456789); + FModioUnsigned64 Second = FModioUnsigned64(123456789); + FModioUnsigned64 Third = FModioUnsigned64(987654321); + TestEqual("NotEqualsTrue", First != Third, true); + TestEqual("NotEqualsFalse", First != Second, false); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorSubtractTest, "Modio.ModioUnsigned64.Arithmetic.Subtract", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorSubtractTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(10); + FModioUnsigned64 Second = FModioUnsigned64(4); + FModioUnsigned64 Result = FModioUnsigned64(6); + TestEqual("SubtractionCorrectResult", First - Second, Result); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64OperatorAddTest, "Modio.ModioUnsigned64.Arithmetic.Add", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64OperatorAddTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 First = FModioUnsigned64(6); + FModioUnsigned64 Second = FModioUnsigned64(4); + FModioUnsigned64 Result = FModioUnsigned64(10); + TestEqual("AdditionCorrectResult", First + Second, Result); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64MakeBreakComponentTest, "Modio.ModioUnsigned64.MakeBreak", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64MakeBreakComponentTest::RunTest(const FString& Parameters) +{ + uint64 Value = 0xabcdF00d1234F00d; + FModioUnsigned64 First = FModioUnsigned64(Value); + int32 High; + int32 Low; + UModioUnsigned64Library::BreakToComponents(First, High, Low); + TestEqual("HighComponentCorrect", static_cast(High), 0xabcdf00d); + TestEqual("LowComponentCorrect", static_cast(Low), 0x1234f00d); + FModioUnsigned64 Second = UModioUnsigned64Library::MakeFromComponents(High, Low); + TestEqual("RoundTripCorrectResult", Second, First); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FModioUnsigned64ValueConversionTest, "Modio.ModioUnsigned64.Conversion.UInt64", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | + EAutomationTestFlags::ProductFilter) + +bool FModioUnsigned64ValueConversionTest::RunTest(const FString& Parameters) +{ + FModioUnsigned64 Value = FModioUnsigned64(0xabcdF00d1234F00d); + uint64 ConvertedValue = (uint64) Value; + TestEqual("ValueConversionCorrect", Value == ConvertedValue, true); + return true; +} + +#endif \ No newline at end of file diff --git a/Source/ThirdParty/NativeSDK b/Source/ThirdParty/NativeSDK index 09d9fc1b..302d4689 160000 --- a/Source/ThirdParty/NativeSDK +++ b/Source/ThirdParty/NativeSDK @@ -1 +1 @@ -Subproject commit 09d9fc1b7919f3c4619f0be7993fd218be8cdaf4 +Subproject commit 302d46897e0f7fbff3e685c3290dfc7e348b7193 diff --git a/Source/uring/Public/liburing/compat.h b/Source/uring/Public/liburing/compat.h deleted file mode 100644 index 9a9ad9bc..00000000 --- a/Source/uring/Public/liburing/compat.h +++ /dev/null @@ -1,27 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -#ifndef LIBURING_COMPAT_H -#define LIBURING_COMPAT_H - -#include -#include - -struct __kernel_timespec - { - int64_t tv_sec; - long long tv_nsec; - }; - -# include - - struct open_how - { - uint64_t flags; - uint64_t mode; - uint64_t resolve; - }; - -#ifndef __kernel_rwf_t - typedef int __kernel_rwf_t; -#endif - -#endif