Skip to main content

Getting Started

SDK structure and concepts

The mod.io SDK has a simple structure that primarily consists of a flat interface, with all public methods declared within ModioSDK.h.

Value objects

All data returned by the SDK uses a small set of classes, containing information such as:

  • Details for mods available for installation
  • Status information about in-progress mod management operations
  • Details and load paths for installed mods.

These objects return as pass-by-value. In other words, if you want to hold onto them once you've shut down the SDK you can do so. In contrast to interfaces that return values via interface pointers, no mod.io SDK objects require you to call dispose, release, or some other memory manager when their scope finishes.

This flexibility allows you to initialize the SDK, query the installed mods, and keep that list. Then shut down the SDK and stop running the SDK's event loop.

UTF-8 guarantees

The SDK uses UTF8 for all strings, stored in std::string, as does the mod.io REST API.

Non-blocking, asynchronous interface

The SDK communicates with the mod.io servers, the filesystem on the device it is running on, and platform-specific authentication services. All of these may not return results immediately; therefore, a large number of the SDK's public methods are non-blocking and asynchronous.

note

All asynchronous methods in the public API have the suffix Async.

Callback conventions

These asynchronous methods take a std::function-derived callback, which will be invoked exactly once with the results of the requested operation.

Every async callback takes an ErrorCode as its first parameter, with any results wrapped in Optional to check if a result is valid or empty.

Return values provided to your callback are passed by-value. The SDK does not expect you to have to call release or free up resources given to you.

note

Even if the SDK shuts down while asynchronous operations are pending, the remaining callbacks will still execute exactly once. In this case, the callback receives an ErrorCode to indicate a canceled state. Your project should handle gracefully this behavior as part of your error handling.

Maintaining the SDK event loop

In order to provide a non-blocking implementation, the SDK operates an internal event loop. This event loop only runs on the thread which calls RunPendingHandlers.

The event loop, all internal event handlers and callbacks provided to the mod.io SDK execute on the thread invoking RunPendingHandlers. RunPendingHandlers must only be called on one thread, otherwise, its behavior is undefined.

note

If you stop calling RunPendingHandlers, any pending asynchronous API methods will not complete and their associated callbacks will not be invoked. It also includes the internal data allocated for those operations, as well as the release of any allocated objects.

Thread Safety

Given that RunPendingHandlers performs all the work of the SDK and executes the callbacks that you provide as handlers for the completion of async functions, your application needs to be calling it at regular intervals. However, you may not wish to do so on the main thread of your application, given that the function has to execute for long enough to actually 'get some work done'.

The mod.io SDK supports the execution of RunPendingHandlers on a background thread while your application invokes other SDK functions on the main thread, for example in response to user input in your application's user interface.

Whilst it is safe to call all other SDK functions from a different thread to the one executing RunPendingHandlers, it is important to note that our existing guarantees are maintained, namely, you'll still receive exactly one callback invocation per asynchronous function you run, and callbacks you provide to those methods will be executed on the thread running RunPendingHandlers.

note

RunPendingHandlers should still only be called on a single thread - it is not safe to call RunPendingHandlers from multiple threads, either simultaneously or sequentially.

By using a background thread for RunPendingHandlers, you can decouple the frequency with which you perform SDK 'work' from the frequency of your application's main loop for greater performance.

See * Multithreading for more information.

Users and Local Profiles

The mod.io SDK uses a "Local Profile" throughout its lifetime. The Local Profile may optionally contain an authenticated user, once you have successfully authenticated using the appropriate SDK function. These local profiles essentially create a 'scope' for the current user to live in, so that a single system can support multiple authenticated users side-by-side without requiring deauthentication of the previous user. On console platforms, we suggest that this be a string representation of the platform-provided UserID, as this gives the best experience when it comes to things like user switching.

Internally, the SessionID is used to create a folder containing the authentication information and cached profile of the authenticated user (if any). For example, a game using the GDK on Xbox, using a sanitized string representation of the Xbox live ID as the SessionID, would have a folder structure in the persistent storage like the following:

<Persistent Storage>/mod.io/<Game ID>/<Xbox Live ID #1>/<Cached Auth>/<Profile data for Xbox Live User #1>
<Persistent Storage>/mod.io/<Game ID>/<Xbox Live ID #2>/<Cached Auth>/<Profile data for Xbox Live User #2>

When your game starts, you can detect the user associated with the current controller and pass in the stable string representation of their Xbox Live ID as the SessionID. If the user has previously authenticated with mod.io for this game on this device, their authentication status would be maintained.

In the case of a PC title with user-provided profile names, the folder structure would be more like the following:

%USERDATA%/mod.io/<Game ID>/MyProfile1/<Cached Auth>/<Profile data for mod.io account #1>
%USERDATA%/mod.io/<Game ID>/SomeOtherProfile/<Cached Auth>/<Profile data for mod.io account #2>
%USERDATA%/mod.io/<Game ID>/ThirdUserSpecifiedProfileName/<Cached Auth>/<Profile data for mod.io account #3>

This allows multiple players, such as siblings, to each have their own session that lives in the same Windows account.

An authenticated user is required to install mods and perform other operations. Check the requires section on any SDK function to see what operations need an authenticated user. However, anyone can freely browse and search your game's available mods and only prompt the user to authenticate/create an account when they wish to perform any restricted operations (such as rating or subscribing to a mod).

To change a Local Profile's authenticated user, call ClearUserDataAsync to remove the authenticated user, and then re-authenticate as normal.

note

A call to ClearUserDataAsync removes the authenticated user from the local device, and disables mod management. Any installed content is marked for uninstallation from local storage if no other Local Profiles contain authenticated users with active subscriptions to it.

To add a newly authenticated user or switch to one already-authenticated without removing the current one, swap to another Local Profile by calling ShutdownAsync, then re-initialize via InitializeAsync specifying a different Local Profile name in the initialization parameters you supply.

Error handling

Callback functions in the SDK either return a value or provide an ErrorCode value. It is a numeric error code with a category and an associated string message.

The SDK doesn't attempt to predict what your error-handling logic or requirements are. For example, if you call a function and receive an error code ec == Modio::HttpError::CannotOpenConnection, your application could potentially handle this by shutting down the SDK. Another application, however, might wish to retry after an interval determined by its own internal logic. As a result, the SDK defers to your application to decide how to handle errors for the functions you call.

For more details on the error codes and how to inspect their values, please see Error Handling and ErrorCode.

Mod Data Directories

The plugin stores mods in a game-specific directory in the following path by default:

WindowsLinuxOSX
${FolderID_Public}/mod.io${USER_HOME}/mod.io${USER_HOME}/Library/Application Support/mod.io
info

In Linux and macOS, mods and data binds to a single user. Every other client would have their own instance in their home directory.

However, this value can be overridden in one of two ways:

  • Globally for a system account

    On the first run of a game using the plugin, ${FolderID_LocalAppData}/mod.io/globalsettings.json will be created.

    This JSON object contains a RootLocalStoragePath element. A change to this string to a valid path on disk will globally redirect the mod installation directory for ALL games using the mod.io SDK for the current system account (it also includes the Unreal Engine 4 plugin). To ignore this override and enforce use of the default directory, set the extended parameter key IgnoreModInstallationDirectoryOverride to any string value when initializing the SDK.

    warning

    Changing this value while the SDK is initialized is not supported and behavior is undefined.

    info

    Consider that the mod.io SDK configuration folder is different from that where mod metadata and files stored.

  • Per-Local Profile override

    Per-game, Local Profile-specific settings are stored in ${FolderID_LocalAppData}/mod.io/${Game_ID}/${Local_Profile_Name}/user.json.

    Adding a RootLocalStoragePath element to this file will redirect the mod installation directory for this specific game only, for the current Local Profile. Removing this value will cause the game to revert back to the global value in globalsettings.json.


SDK quick-start

The following guides are provided to help you through the basic functions of the SDK as described in SDK structure and concepts.


Initialization and Teardown

note

This functionality is demonstrated in example 01_Initialization

SDK Configuration and Initialization

When you are ready to initialize the SDK for the current session, you'll need to call InitializeAsync, passing in your product's mod.io ID, your API key, the Local Profile Name, and a callback/handler so you know when the SDK is initialized correctly. Note that InitializeAsync's callback will be invoked after calling RunPendingHandlers from your project's main loop.

You should also ensure that you are targeting an appropriate Portal for mod.io to understand what storefront or app the request is originating from. This enabled additional storefront-based functionality, such as returning display name mappings for that portal.

[source,cpp]

// represents some external state so we know that the SDK is good to go
Modio::Optional<bool> SDKInitialized;

Modio::InitializeOptions Options;
Options.APIKey = Modio::ApiKey(YOUR_API_KEY);
Options.GameID = Modio::GameID(YOUR_GAME_ID);
Options.User = "LocalProfileName";
Options.GameEnvironment = Modio::Environment::Live;
Options.PortalInUse = Modio::Portal::Steam;

Modio::InitializeAsync(Options, [&SDKInitialized](Modio::ErrorCode ec) {
if (ec)
{
// SDK initialization failure
}
else
{
// SDK init OK - can now make other calls to the SDK, show mod browser UI, etc
}
});

It is worth considering that the "LocalProfileName" is used by the mod.io SDK to associate a local session to a user, as mentioned in Users and Local Profiles. It is possible to forward a user nickname as the LocalProfileName, then initialize the mod.io SDK. Any data related to a user session will be stored in its corresponding Mod Data Directory.

Notes

  • By convention you'll see these code examples pass in lambda functions as callback handlers, but you can use anything convertible to std::function with the appropriate signature.
  • The error-handling in this sample is deliberately kept brief. See Error Handling for more information on error handling.
  • You can perform calls to other functions, such as something that shows your mod browser UI implementation, directly in the callback. The primary caveat to doing much processing here is that you'll be running in the context of, and therefore blocking, the thread running the callback, which is the thread running RunPendingHandlers.

Extended Initialization Parameters

The ExtendedParameters field on InitializeOptions is a set of key-value pairs intended for platform-specific or special-case parameters that need to be passed to the SDK. Simply set the value before passing your initialization parameters in to InitializeAsync.

Options.ExtendedParameters["SomeParameterName"] = "SomeParameterValue";

General

The SDK supports the following general use parameters that can be set through ExtendedParameters:

ParameterDescription
PendingOnlyResultsOnly include mods in search results that are pending moderation. For moderation and testing purposes. Set to "true" to enable. Warning: Should only be enabled when the user is a moderator or admin of the game, setting in shipping builds is not recommended.
PlatformOverrideSet the platform to be used when making requests to show mods for that platform instead. For moderation and testing purposes. This parameter will soon be deprecated.

Storage Quota

The SDK supports the following storage quota parameters that can be set through ExtendedParameters:

ParameterDescription
ModStorageQuotaMBDefines a maximum quota of locally installed mods for this game, in megabytes (minimum 25 MB). This includes temp mods and those installed by other users on this system.
CacheStorageQuotaMBDefines a maximum quota for cached image data such as mod logos and gallery images, in megabytes (minimum 25 MB).

Example usage:

// Set mod storage quota to 500 MB
Options.ExtendedParameters["ModStorageQuotaMB"] = "500";

// Set cache storage quota to 250 MB
Options.ExtendedParameters["CacheStorageQuotaMB"] = "250";

By setting these values, the SDK will actively enforce the specified storage limits.

Metrics

The SDK supports the following metric parameters that can be set through ExtendedParameters:

ParameterDescription
MetricsSecretKeySet the secret key used by the metrics feature.

Shutting Down

To finalize and shut down the mod.io SDK is equally simple:

// State variable, stored in some valid scope
bool SDKShutdownComplete = false;

// Capture reference to the state variable in the lambda - could use shared_ptr for more safety instead
Modio::ShutdownAsync([&SDKShutdownComplete](Modio::ErrorCode ec)
{
SDKShutdownComplete = true;
});

while(!SDKShutdownComplete)
{
Modio::RunPendingHandlers();
}

note

ShutdownAsync uses a lock to ensure that global SDK state is not mutated out from underneath an invocation of RunPendingHandlers. It is not safe to call ShutdownAsync in any callback you provide to the SDK. Callbacks are executed during RunPendingHandlers execution, your application will deadlock while waiting for the enclosing RunPendingHandlers to complete. The lock is deliberately not implemented to support recursive locking, again because ShutdownAsync mutates data structures that RunPendingHandlers expects to remain unchanged for the duration of its scope.

note

You will need to continue to call RunPendingHandlers while the async shutdown is in progress to allow for intermediate handlers to finish running.

Event Loop (RunPendingHandlers)

As mentioned in Maintaining the SDK event loop, the SDK's internal event loop requires care and attention in the form of RunPendingHandlers.

ALL SDK work is performed during executions of RunPendingHandlers, therefore calling it as often as possible will improve performance of the SDK's I/O operations.

You can either call RunPendingHandlers on the main thread, or on a dedicated background thread or existing thread of your choice.

For optimal execution, RunPendingHandlers should be called at least once per frame.

For example, if you wish to call RunPendingHandlers on the main thread, it could be located into your project's main loop or into a tick-style function on an appropriate controller/manager object.

while(bGameIsRunning == true)
{
// other stuff
Modio::RunPendingHandlers();
// other stuff
}
note

RunPendingHandlers is not reentrant-safe. Do not call RunPendingHandlers inside a callback you give to the SDK, or your application will deadlock. Callbacks are run inside RunPendingHandlers, and your inner RunPendingHandlers call will block infinitely waiting for the enclosing scope to exit.


Browsing available mods

note

This functionality is demonstrated in example 02_ModQueries

Now that you've followed the instructions in Initialization and Teardown you can begin to query the available mods for information you can display to your end users. The primary way this is done is through ListAllModsAsync. Note that authentication is not required to browse mods.


Modio::ListAllModsAsync(Modio::FilterParams(), [](Modio::ErrorCode ec, Modio::Optional<Modio::ModInfoList> Results)
{
if (ec)
{
// Error handling
}
else
{
for (Modio::ModInfo& CurrentModProfile : *Results)
{
std::cout << CurrentModProfile.ProfileName;
}
}
});

You'll notice that ListAllModsAsync takes a FilterParams object as its first parameter. The default state of this object is set to ask for the first 100 results (the maximum number returnable in a query), sorting by mod ID.

To search for a specific query string, sort in a different order, or combine different filters, you can pass in a FilterParams object like this:

// Search queries
Modio::ListAllModsAsync(Modio::FilterParams().NameContains("SomeString"), ...)
// Sorting
Modio::ListAllModsAsync(Modio::FilterParams().SortBy(Modio::FilterParams::SortFieldType::DownloadsToday, Modio::SortDirection::Ascending), ...)

// Ranged results - starting at index 20, return 10 results
Modio::ListAllModsAsync(Modio::FilterParams.NameContains("Your Query").IndexedResults(20, 10), ...)

// Ranged results - return the 20th page of 10 results
Modio::ListAllModsAsync(Modio::FilterParams.NameContains("Your Query").PagedResults(20, 10), ...)

User Authentication

note

This functionality is demonstrated in example 03_Authentication

When you want players to subscribe to and use content, you must authenticate them. mod.io provides two ways for users to create an account to use the service: email authentication and single sign on (SSO) through an external authentication partner. The flow for these is slightly different.

Email authentication

mod.io allows users to create an account on the mod.io website using an email address. Once the user has accepted the mod.io Terms of Use and created an account, they can use that email address to log in and access mod.io services in your game.

Email authentication involves:

  1. Submitting the user's email address
  2. The user retrieving the one-time code mod.io sends to that address (externally to your application)
  3. Submitting the code provided by the user
Modio::RequestEmailAuthCodeAsync(Modio::EmailAddress(UserProvidedEmailAddress), [](Modio::ErrorCode ec)
{
// Handle errors if ec is truthy
});

// some time later, after the user inputs their authentication code

Modio::AuthenticateUserEmailAsync(Modio::EmailAuthCode(UserProvidedAuthCode), [](Modio::ErrorCode ec) {
if (ec)
{
// Authentication failure, inspect ec to determine what information to provide to the end user
}
else
{
// User is now authenticated and able to manage their subscriptions via SDK calls
}
});

note

Email authentication is not recommended for production releases. We strongly recommend the use of platform SSO to provide a seamless authentication experience for your players. Additional platform functionality such as platform display names, avatars and console certification requirements are supported via SSO flows.

SSO/External authentication

mod.io features single sign on authentication from a number of external providers. This currently includes:

  • Apple
  • Discord
  • Epic Games Store
  • GoG
  • Google
  • itch.io
  • Nintendo Switch
  • PlayStation™Network
  • Steam
  • Xbox Live
  • Oculus
  • OpenID

Please note that the ability to authenticate players using OpenID is a premium feature. If you are interested in mod.io premium features, please contact developers@mod.io.

To use SSO with mod.io, a user must have accepted the mod.io Terms of Use in order to create an account.

This means the external authentication flow is the following:

  1. Call AuthenticateUserExternalAsync , passing in any provider-specific parameters, setting AuthenticationParams::bUserHasAcceptedTerms to false, and indicating which authentication provider you wish to use
  2. Check the error code in the callback - if it indicates the user has not yet created an account or accepted the terms, call GetTermsOfUseAsync and display the provided information to your user
  3. If the user clicks the OK/affirmative button on your screen displaying the terms of use, repeat the call in step 1 but setting AuthenticationParams::bUserHasAcceptedTerms to true for this single authentication request only. For all subsequent authentication attempts, this parameter should be set back to false - the system will automatically authenticate the user if they've already accepted the latest terms. Otherwise, the system will return an error (403 Forbidden (error_ref 11074)) allowing you to show the updated terms
  4. Check the error code in the callback - a false-y error code indicates that authentication was successful, and users can now install and manage mods and subscriptions.
Modio::AuthenticationParams UserParams;
UserParams.AuthToken = "AuthenticationToken";
UserParams.UserEmail = "UserEmail";
UserParams.bUserHasAcceptedTerms = false; // Always start with false

Modio::AuthenticateUserExternalAsync(UserParams,Provider,[Provider](Modio::ErrorCode ec)
{
if (ec)
{
if (ec == Modio::ApiError::UserNoAcceptTermsOfUse)
{
// We need to display the terms of use to the user
Modio::GetTermsOfUseAsync([](Modio::ErrorCode ec, Modio::Optional<Modio::Terms> Terms)
{
if (ec)
{
// something went wrong fetching the terms, inspect ec to decide what to do
}
else
{
// Display the terms of use to the user, remember not to block in the callback here!
NonBlockingFunctionThatDisplaysTheTermsOfUse(Terms);
}
});
}
}
});

// Later sometime, when your user clicks accept on the terms of use
UserParams.bUserHasAcceptedTerms = true;
Modio::AuthenticateUserExternalAsync(UserParams,Provider,[](Modio::ErrorCode ec){/* ... */});

note

Changing users via AuthenticateUserExternalAsync (ie performing an authentication for a different user) will disable mod management.

important

The bUserHasAcceptedTerms parameter should only be set to true for the specific authentication request made immediately after the user accepts the terms dialog. For all subsequent authentication attempts, this parameter should be set to false. If the user has already accepted the latest terms, they will be authenticated automatically. If the terms have been updated since their last acceptance, setting this parameter to false will trigger the appropriate error (403 Forbidden, error_ref 11074) allowing you to show the updated terms.

important

The Modio::AuthenticationProvider is different to the PortalInUse initialization parameter (Modio::Portal). For example, the user may authenticate through Google, while using the Steam portal. DisplayNamePortal will be null if a portal is not defined, and it will reflect changes in the username of the corresponding portal that were set using the initialization step. For example, if the users Steam username is changed - DisplayNamePortal will return the new username once the SDK is reauthenticated.

Epic Games Authentication Example

In order to use Epic Games Authentication, you must ensure that you have configured Epic Account Services for your title. Once you have done this, you can request an ID Token for a user that has been logged into Epic. You must ensure that the scopes you are authentication with match the scopes for the application you are configuring in EAS.

note

If your title is not yet live, you can use Epic's DevAuthTool and the EOS_LCT_Developer credential type for testing.

static void EOS_CALL LoginCompleteCallbackFn(const EOS_Auth_LoginCallbackInfo* Data)
{
EOS_Auth_CopyIdTokenOptions IdTokenOptions;
IdTokenOptions.AccountId = Data->LocalUserId;
IdTokenOptions.ApiVersion = EOS_AUTH_COPYIDTOKEN_API_LATEST;

EOS_Auth_IdToken* IdToken;
EOS_EResult Result = EOS_Auth_CopyIdToken(ModioTest::EpicAuthHelper::instance().AuthHandle, &IdTokenOptions, &IdToken);

if (Result == EOS_EResult::EOS_Success)
{
std::string Token = IdToken->JsonWebToken;

Modio::AuthenticationParams User;
User.AuthToken = Token;
User.bURLEncodeAuthToken = true;

// Note: bUserHasAcceptedTerms should only be set to true if the user has just accepted the terms dialog
// Otherwise it should be false - the system will authenticate automatically if the user has accepted the latest terms
User.bUserHasAcceptedTerms = true;

Modio::AuthenticateUserExternalAsync(User, Modio::AuthenticationProvider::Epic, [](Modio::ErrorCode ec) {
if (ec)
{
std::cout << "Failed to authenticate to mod.io: " << ec.message() << std::endl;
}
else
{
std::cout << "Authentication complete" << std::endl;
}
});
}
else
{
std::cout << "Failed to get Epic ID Token";
}
}
Steam Authentication Example
note

This functionality is demonstrated in advanced example 01_SteamAuthAndEntitlements

In order to use the Steam authentication functionality, you must configure your game's Encrypted App Ticket Key from Steamworks. Once you have done that, you can request an Encrypted App Ticket for an authenticated user as follows, replacing your class references to your own Steam Subsystem as appropriate.

uint32 k_unSecretData = 0x5444;
SteamAPICall_t hSteamAPICall = SteamUser()->RequestEncryptedAppTicket(&k_unSecretData, sizeof(k_unSecretData));
SteamAuthHelperInstance->m_SteamCallResultEncryptedAppTicket.Set(hSteamAPICall, SteamAuthHelperInstance, &SteamAuthHelper::OnEncryptedAppTicketResponse);

Ensure that you have defined a Steam CCallResult to listen for a callback, such as CCallResult<SteamAuthHelper, EncryptedAppTicketResponse_t> m_SteamCallResultEncryptedAppTicket;.

Once you successfully receive that callback, you can get the Encrypted App Ticket as follows, ensuring you Base64 encode it.

std::string EncodedSteamAuthTicket;

unsigned char rgubTicket[1024];
uint32 cubTicket;
if (SteamUser()->GetEncryptedAppTicket(rgubTicket, sizeof(rgubTicket), &cubTicket))
{
EncodedSteamAuthTicket = base64_encode(rgubTicket, cubTicket);
std::cout << "Steam App Ticket received" << std::endl;
}

Finally, once you have your Base64 encoded app ticket, you can SSO to mod.io as follows:

Modio::AuthenticationParams AuthParams;
AuthParams.AuthToken = EncodedSteamAuthTicket;
AuthParams.bURLEncodeAuthToken = true;

// Note: bUserHasAcceptedTerms should only be set to true if the user has just accepted the terms dialog
// Otherwise it should be false - the system will authenticate automatically if the user has accepted the latest terms
AuthParams.bUserHasAcceptedTerms = true;

Modio::AuthenticateUserExternalAsync(
AuthParams, Modio::AuthenticationProvider::Steam, [&](Modio::ErrorCode ec) {
if (ec)
{
std::cout << "Failed to authenticate to mod.io: " << ec.message() << std::endl;
}
else
{
std::cout << "Authentication complete" << std::endl;
}
});

GOG Authentication Example

In order to use the GOG authentication functionality, you must configure your game's Encrypted App Ticket Key from GOG. Ensure that you are signing into GOG using galaxy::api::User()->SignInGalaxy. Once you have done that, you can request an Encrypted App Ticket for an authenticated user as follows, replacing your class references to your own GOG as appropriate.

galaxy::api::User()->RequestEncryptedAppTicket(nullptr, 0, &GOGAuthHelperInstance);

In your authentication helper, ensure that you are inheriting from galaxy::api::GlobalEncryptedAppTicketListener to get the result of the the app ticket request, via the OnEncryptedAppTicketRetrieveSuccess() method.

Once you receive that callback, you can get the value of the encrypted app ticket as follows:

unsigned char ticket[1024];
uint32_t ticketLength;

galaxy::api::User()->GetEncryptedAppTicket(ticket, sizeof(ticket), ticketLength);

std::string AuthTicket;
AuthTicket.assign(ticket, std::find(ticket, ticket + ticketLength, '\0'));

Finally, you can SSO to mod.io as follows:

Modio::AuthenticationParams AuthParams;
AuthParams.AuthToken = AuthTicket;
AuthParams.bURLEncodeAuthToken = true;

// Note: bUserHasAcceptedTerms should only be set to true if the user has just accepted the terms dialog
// Otherwise it should be false - the system will authenticate automatically if the user has accepted the latest terms
AuthParams.bUserHasAcceptedTerms = true;

Modio::AuthenticateUserExternalAsync(
AuthParams, Modio::AuthenticationProvider::GoG, [&](Modio::ErrorCode ec) {
if (ec)
{
std::cout << "Failed to authenticate to mod.io: " << ec.message() << std::endl;
}
else
{
std::cout << "Authentication complete" << std::endl;
}
});
note

Note that you do not need to base64 encode the app ticket that you receive from GOG; you only need to ensure it is URL encoded, either by setting bURLEncodeAuthToken=true or doing it yourself before passing it to AuthenticateUserExternalAsync.


Mod management and subscriptions

note

This functionality is demonstrated in example 05_SubscriptionManagement

Now you've shown the user some mods based on a query they've submitted through your UI. The user has picked one that they'd like to install. This section explains how to trigger an installation process and the files downloaded to the filesystem.

Installation management and mod filepaths

A subscription marks a mod as requiring installation, whereas an unsubscription indicates uninstallation. But, how do you actually control when the SDK does those things? After all, you don't want a mod to be uninstalled after your main program has loaded those files into memory, locking them from deletion. Likewise, you probably don't want to be using networking or processor resources during gameplay for downloading mods. In order to give you control over when these processes occur, without forcing you to shut down the SDK entirely, you can call EnableModManagement and DisableModManagement.

In order to display a notification to your users when a mod is finished installing or updating, EnableModManagement asks you to provide a callback. Because EnableModManagement is not an async function (ie it doesn't end with *Async), the function handler operates differently compared to other asynchronous results callbacks you use elsewhere in the SDK. A handler given to this function will be held by the SDK until a corresponding call to DisableModManagement or ShutdownAsync takes place. The handler will be invoked every time a mod is automatically installed, updated, or uninstalled by the SDK's internal event loop.

Modio::EnableModManagement([](Modio::ModManagementEvent ModEvent)
{
if (ModEvent.Status && ModEvent.Event == Modio::ModManagementEvent::EventType::Installed)
{
std::cout << "Mod with ID: " << ModEvent.ID << " is installed" << std::endl;
}
else
{
std::cout << "Mod with ID: " << ModEvent.ID << " failed to install: " << ModEvent.Status.message() << std::endl;
}
});

// Some time later: check if there's a mod being installed, or more mods that require installation pending
if (!Modio::IsModManagementBusy())
{
// This will reset any in-progress installations to pending, so we're only calling it if nothing's being processed
Modio::DisableModManagement();
}

Mod subscriptions

A user indicates they want to have a mod installed by 'subscribing' to it. The mod.io servers stores subscriptions and associates them with a particular user's mod.io account. When a user 'unsubscribes' to a mod, they indicate that mod should be uninstalled from any device they're logged in on.

The API for managing subscriptions is simple and consists of a call to either SubscribeToModAsync or UnsubscribeFromModAsync with the ID of the mod in question and a callback to receive the status of the request:

note

To subscribe to a mod, EnableModManagement must be called beforehand.

When subscribing to a mod, you can also pass in a bool to indicate if you want to subscribe to all dependencies for the given mod. If dependencies are also subscribed, the mod.io servers will also associate them with the current user.

note

Currently when dependencies are included during subscription, they will not automatically be downloaded. To ensure the latest content is downloaded, FetchExternalUpdatesAsync must be called after subscribing.

// Subscription
Modio::SubscribeToModAsync(ModID, IncludeDependencies, [](Modio::ErrorCode ec)
{
if (ec)
{
// Didn't subscribe, show a message to the user
}
else
{
// Successfully subscribed on the server
}
});

// Unsubscription
Modio::UnsubscribeFromModAsync(ModID, [](Modio::ErrorCode ec)
{
if (ec)
{
// Couldn't unsubscribe, show error
}
else
{
// Server records unsubscription to remove the user's association to this mod
}
});

External subscription changes

Remember that the mod.io service is available as a website besides the integration within your application. Users can manage their subscriptions (and therefore installations) outside of your game. Consequently, you must query the server for any external subscription changes. To do this, use FetchExternalUpdatesAsync to synchronise the server state with the SDK's local subscriptions:

Modio::FetchExternalUpdatesAsync([](Modio::ErrorCode ec)
{
if (ec)
{
// Couldn't fetch external subscription data, handle error
}
else
{
// The SDK's internal state synchronised. This is an acknowledgment of success
}
});
note

You should call FetchExternalUpdatesAsync at particular times in your application when you want to ensure that the state is up-to-date. The mod.io SDK will apply rate-limiting internally if you try to call it too often.

In case you need to prepare for changes happening beforehand, call PreviewExternalUpdatesAsync. This function retrieves a list of updates between the users local mod state, and the server-side state. It allows you to identify which mods will be modified when you call FetchExternalUpdatesAsync next in order to perform any content management (such as unloading files) that might be required. Its use is very similar:

Modio::PreviewExternalUpdatesAsync([](Modio::ErrorCode ec, std::map<Modio::ModID, Modio::UserSubscriptionList::ChangeType> ListOfChanges)
{
if (ec)
{
// Couldn't preview external subscription data, handle error
}
else
{
// Take notice of the changes brought inside variable "ListOfChanges". It serves as acknowledgment of success
}
});
Checking the user subscription list

In order to see which mods the user has subscribed to, call QueryUserSubscriptions to retrieve a collection of ModCollectionEntry objects, one for each subscribed mod. Each of these objects contains the mod's state, profile information, ID, and other data suitable for showing users a list of their subscriptions.

note

This collection includes mods that are still in the process of being installed. Make sure to check the result of ModCollectionEntry::GetModState before trying to load files from the mods in this collection. Alternatively, use QueryUserInstallations as described in Retrieving mod directory paths for loading.

A distinction exists between functions QueryUserInstallations and QuerySystemInstallations. The first fetches the subset of the user's subscribed mods that are installed and therefore ready for loading. QueryUserInstallations is more relevant for most cases to personalize the content shown to the user. On the other hand, a call to QuerySystemInstallations returns all mods installed on the system (including those the current user is subscribed to). This provides insight into mods installed by other users.

If local space is a concern, here are some options to manage storage:

The first option focuses on the removal of mods the user has not interacted with, whereas the second option would actively uninstall mods the user has previously considered and subscribed to. Consider other alternatives when designing your game to support mods.

Retrieving mod directory filepaths for loading

Once the user can pick mods and subscribe to them (i.e. mark them for installation), mod.io SDK management can alter the filesystem and retrieve mods. We need to know where they are on the filesystem to load them into your gameplay.

The easiest way to do this is by using QueryUserInstallations. This function allows you to specify if you want to include outdated mods or not. It returns a collection of ModCollectionEntry objects that you can query for folder paths you can use to load files into your title.

std::vector<Modio::filesystem::path> ModPaths;

// It iterates over all the installed mods that are up-to-date
bool bIncludeOutdatedMods = false;
for (std::pair<Modio::ModID, Modio::ModCollectionEntry>& Entry : Modio::QueryUserInstallations(bIncludeOutdatedMods))
{
ModPaths.push_back(Entry.second().GetPath());
}

// You can now append whatever filenames you expect in a mod to the paths and load those in
for (Modio::filesystem::path& Path : ModPaths)
{
YourGame::FileHandle ModManifest = YourGame::OpenFile(Path / "mod_manifest.txt");
}

In-game mod submission

note

This functionality is demonstrated in example 08_SubmitMod

Submitting a mod from inside your game and making it visible to other players involves two steps:

  • Submission of the mod
  • Submission of the mod's data (aka 'the mod file')

These steps are outlined below. Mods can also be edited after submission, as detailed in Edit an existing mod

Submitting a new mod

To submit a mod, first create a mod handle using GetModCreationHandle and use that handle when calling SubmitNewModAsync. Note that the newly created mod will remain hidden until a mod file is added in the next step.


Modio::ModCreationHandle Handle = GetModCreationHandle();

Modio::CreateModParams Params;

Params.PathToLogoFile = "C:/temp/image.png";
Params.Name = "My Awesome Mod";
Params.Summary = "This is an amazing mod";
// add any additional optional parameters

Modio::SubmitNewModAsync(Handle, Params, [](Modio::ErrorCode ec, Modio::Optional<Modio::ModID> NewModID)
{
if (ec)
{
// error handling
}
else
{
// capture NewModID as needed for subsequent use
}
});

Submitting a file for a mod

Once you have successfully submitted a mod, you can submit a file for that mod using SubmitNewModFileForMod. When you submit a file, pass a Modio::CreateModFileParams containing the directory of the files that you want to submit. The SDK will 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 be notified of the completed upload by the Mod Management callback.


Modio::CreateModFileParams Params;

Params.RootDirectory = "C:/temp/mod_folder";
// add any additional optional parameters

// Use NewModID returned in SubmitNewModAsync() callback
Modio::SubmitNewModFileForMod(NewModID, Params);


Edit an existing mod

Mod details can be edited in-game using SubmitModChangesAsync. This function allows you to edit multiple parameters with a single call. It takes a Modio::ModID of the mod to edit, a Modio::EditModParams containing one or more parameters to be altered, and a callback that will contain an optional updated Modio::ModInfo object on success.

Note that updating the mod file itself is done via SubmitNewModFileForMod, as detailed in Submitting a file for a mod.


Modio::EditModParams EditParams;

// Add one or more parameters to edit
EditParams.Name = "My Edited Mod Name";
EditParams.Summary = "My edited summary";

Modio::SubmitModChangesAsync(ModID, EditParams, [](Modio::ErrorCode ec, Modio::Optional<Modio::ModInfo> UpdatedModInfo)
{
if (ec)
{
// error handling
}
else
{
// capture or display UpdatedModInfo as needed
}
});


Content reporting

note

This functionality is demonstrated in example 07_ReportMod

The SDK has full support for reporting content that breaches guidelines - this can be entire games, users, or mods themselves. When reporting a mod, users can provide a report type, a reason and their contact details. These reports will appear on your game's moderation dashboard to be actioned. Shipping your title on consoles generally requires moderation and reporting capability being provided in your game.

To report a piece of content, call ReportContentAsync, providing a Modio::ReportParams containing the paramters for the report.


Modio::ReportParams Params = Modio::ReportParams(Modio::ModID(ModID), Modio::ReportType::DMCA, "This mod contains copyrighted content",
"ReporterName", "Reporter@Email.com");

Modio::ReportContentAsync(Params, [](Modio::ErrorCode ec)
{
if (ec)
{
// error handling
}
else
{
// content successfully reported
}
});


User mute and unmute functions

Users have the ability to disable updates from other user's mods. This will prevent mod.io from returning mods authored by the muted user. There are three functions to perform these actions:

  • Mute a user
  • Unmute a user
  • List muted users
note

To perform any of these actions, the muting user must be authenticated.

Mute a user

note

This functionality is demonstrated in example 14_MuteUnmuteUser

To mute a user, call MuteUserAsync with the corresponding Modio::UserID and a callback, given the asynchronous nature of the function


Modio::MuteUserAsync(UserID, [](Modio::ErrorCode ec)
{
if (ec)
{
// error handling
}
else
{
// user successfully muted
}
});

Unmute a user

To perform the inverse operation, unmute a user, call UnmuteUserAsync with the corresponding Modio::UserID and a callback, given the asynchronous nature of the function


Modio::UnmuteUserAsync(UserID, [](Modio::ErrorCode ec)
{
if (ec)
{
// error handling
}
else
{
// user successfully unmuted
}
});

List muted users

GetMutedUsersAsync returns a Modio::UserList on success, containing information on users previously muted by the authenticated user.


Modio::GetMutedUsersAsync([](Modio::ErrorCode ec, Modio::Optional<Modio::UserList> UserList)
{
if (ec)
{
// error handling
}
else
{
// capture or display UserList as needed
}
});


Temporary Mod Sets

note

This functionality is demonstrated in example 11_TempModSets

In some situations, you may want mods to only exist on a temporary basis - for instance, in Multiplayer environments where you don't want to subscribe a user to a piece of content. Temporary Mod Sets allow management of these more transient pieces of content separately from subscriptions. Temp Mod Sets do not require authentication to be used, however Mod Management must still be enabled to use Temp Mods.

Temp Mods are downloaded in a folder separately from subscriptions, and are not updated or handled when you call FetchExternalUpdatesAsync. That means you can prioritize download and installation of Temp Mods outside of the regular subscription flow.

To use Temp Mods, you can start a TempModSet by calling InitTempModSet and passing a list of Mod IDs to be downloaded and extracted. At anytime while a TempModSet is open, you can call AddToTempModSet to add mods to the set (which will be instantly downloaded and extracted). If you no longer need a mod, you can call RemoveFromTempModSet which will remove the file. Once you have finished with a TempModSet, you can call CloseTempModSet which will delete all temporary mods. Temporary mods are also deleted the next time you re-initialize the SDK.

Like regular mods, Temp Mods can be queried using QueryTempModSet to get a ModCollectionEntry with an installation path.

Installing Temporary Mods

Modio::EnableModManagement([](Modio::ModManagementEvent ModEvent)
{
if (ModEvent.Status && ModEvent.Event == Modio::ModManagementEvent::EventType::Installed)
{
std::cout << "Mod with ID: " << ModEvent.ID << " is installed" << std::endl;
}
else
{
std::cout << "Mod with ID: " << ModEvent.ID << " failed to install: " << ModEvent.Status.message() << std::endl;
}
});

std::vector<Modio::ModID> ModIds = {8, 4, 5};

Modio::InitTempModSet(ModIds);

while(Modio::IsModManagementBusy())
{
Modio::RunPendingHandlers();
}

This call will start a TempModSet and install Mods with IDs 8, 4 and 5.

note

If you add an already subscribed mod to TempModSet, it will not download be downloaded as the player will already have that content. If you try to unsubscribe from it while it's in TempModSet, the SDK it will wait for it to be removed from TempModSet before processing the unsubscribe.

Multithreading

note

This functionality is demonstrated in example 04_Threading

As mentioned in the quick-start section on initialization, the SDK supports RunPendingHandlers being run on a secondary thread that already exists or a dedicated background thread you create specifically to peform SDK work.

note

Callbacks you provide to the SDK will run on the thread running RunPendingHandlers - if you host RunPendingHandlers on a different thread, it is your responsibility to synchronize with the main thread if you wish to pass results back to it, through callbacks you provide to the SDK.

This allows the SDK to avoid blocking the main thread of your application while performing IO.

Using an existing secondary thread

If you have an existing secondary thread which is not heavily utilized, you can call RunPendingHandlers on that thread:

while(bRunBackgroundThread == true)
{
// other stuff
Modio::RunPendingHandlers();
// other stuff
}

Just remember, that performance of the SDK is proportional to the amount of CPU cycles you give it, so if you use an existing secondary thread you'll need to ensure that that thread's loop runs at a high enough frequency.

Using a dedicated background thread

Using a dedicated background thread for RunPendingHandlers will ensure the best performance for the SDK's I/O operations by allowing SDK functionality to execute at a rate not limited by your application's main loop frequency or existing background loop frequencies.

//Assumption: bHaltBackgroundThread is a threadsafe flag or atomic boolean whose lifetime is guaranteed to be longer than that of the handler thread
HandlerThread = std::thread([&bHaltBackgroundThread]() {
while (!bHaltBackgroundThread)
{
Modio::RunPendingHandlers();
//Use one of the following if you intend to call ShutdownAsync in your program:
std::this_thread::yield();
//Change the sleep duration here as appropriate
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
note

ShutdownAsync takes a lock on the SDK's internal state in order to finalize the pending task queue and shut down internal services.

If you call RunPendingHandlers in a background thread, especially a dedicated background thread, and invoke ShutdownAsync to close the SDK while your application continues to run, on certain platforms you may find the mutex/lock to be unfair to the point that the main thread cannot take the lock, because the background thread does not get suspended by the OS' scheduler often enough.

If this occurs, we recommend you use either a yield or high-resolution sleep after each invocation of RunPendingHandlers to allow the main thread to take the shutdown lock. Alternatively, you can use some kind of signaling mechanism to pause the background thread calling RunPendingHandlers, call ShutdownAsync, and then allow that background thread to resume looping.

If the SDK runs for the lifetime of your application and you do not call ShutdownAsync this is not necessary.

Monetization

note

This functionality is demonstrated in example 09_MonetizationWalletManagement (for initialization and wallet management) and 10_MonetizationPurchase (for purchase functionality).

The mod.io SDK supports all of the mod.io monetization features, allowing you sell a per-game virtual currency to your players that they can use to purchase mods, with a share of the revenue split between creators and your studio. Visit mod.io monetization system for an overview.

Every platform requires specific setup for monetization features to work, with regards to the virtual currency configuration and API calls, however the following documentation is generically applicable, with only small differences per-platform that are documented within the platform-specific monetization documentation.

note

You can use our sandbox test environment at test.mod.io for testing monetization functionality, which allows for simulating real-world payments using dummy credit cards and credentials. When you initialize the SDK, use Modio::Environment::Test as the environment parameter, along with your test.mod.io title's GameID and APIKey.

Initialization

The mod.io monetization features are enabled as part of the onboarding process on your game profile. Once that is setup, there is nothing further you need to do for initialization in the SDK.

Ensure that you have set the appropriate Portal when initializing the SDK for the portal you are using for purchasing - for instance, on Steam, you must initialize with Modio::Portal::Steam in order to redeem entitlements for Steam.

Getting the user's wallet

On startup, you can make a call to <<GetUserWalletBalanceAsync>> to get the balance of the current user's wallet. If no wallet exists for the user, one will be created for them automatically. This call returns the users wallet balance for the current game. On startup is the only time you need to make this call.

We recommend that you cache the value of this result in your game code rather than making consistent calls to <<GetUserWalletBalanceAsync>> and update your local state from the return values of other calls that affect wallet balance.

		Modio::GetUserWalletBalanceAsync([](Modio::ErrorCode ec, Modio::Optional<uint64_t> WalletBalance) {
if (!ec && WalletBalance.has_value())
{
GlobalState.WalletAmount = WalletBalance.value();
}
else
{
// Error handling
}
});

Syncing Entitlements

note

This functionality is demonstrated in example 12_RefreshEntitlements. For Steam, this functionality is demonstrated in advanced example 01_SteamAuthAndEntitlements. For consoles, check the examples folder in the platform module.

If you are supporting the purchase of virtual currency packs on platform storefronts, entitlement refreshing is the method by which those virtual currency packs are consumed and converted into mod.io virtual currency credits for users to purchase UGC with.

Each platform has a specific way of setting up entitlements for consumption, but generally speaking the way you consume those entitlements is the same. Read each platform's Marketplace documentation for how to configure entitlements and any platform-specific information for entitlement consumption.

You should always start by calling GetUserWalletBalanceAsync to ensure that a user has a wallet created, or entitlements cannot be consumed. To consume entitlements, call RefreshUserEntitlementsAsync as follows:

const Modio::EntitlementParams EntitlementParams;
Modio::RefreshUserEntitlementsAsync(
EntitlementParams,
[&](Modio::ErrorCode ec, Modio::Optional<Modio::EntitlementConsumptionStatusList> Entitlements) {
if (ec)
{
std::cout << "Failed to refresh user entitlements: " << ec.message() << std::endl;
}
else
{
if (Entitlements.has_value() && Entitlements->Size() > 0)
{
if (Entitlements->WalletBalance.has_value())
{
UserWalletBalance = Entitlements->WalletBalance->Balance;

std::cout << "Entitlements consumed: " << Entitlements->Size() << std::endl;
std::cout << "Updated UserWalletBalance is " << UserWalletBalance;
}
}
else
{
std::cout << "No entitlements synced; nothing further to do." << std::endl;
}
}
});
note

The WalletBalance returned in Modio::EntitlementConsumptionStatusList will be 0 if no entitlements have been consumed. You should conditionally update your local state based on whether any entitlements have actually been consumed.

Generally speaking, you should do this on startup in case the user has made a purchase outside of the game, and after a user has made a purchase on a platform - most platforms will have some callback or indicator that this has occurred. Check that the Modio::Optional<Modio::EntitlementConsumptionStatusList> is valid and its size is greater than 0.

Querying & Purchasing Mods

As part <<ListAllModsAsync>>, you can include an additional filter for whether you list paid mods. By default, only free mods are shown, but you can set RevenueType on the <<FilterParams>> object passed to <<ListAllModsAsync>> to include free and paid content, or just paid content. All mods returned will have a Price property, indicating the virtual currency price that must be paid in order to purchase.

Modio::ListAllModsAsync(Modio::FilterParams().RevenueType(Modio::FilterParams::RevenueFilterType::FreeAndPaid), [](Modio::ErrorCode ec, Modio::Optional<Modio::ModInfoList> Results)
{
if (ec)
{
// Error handling
}
else
{
for (Modio::ModInfo& CurrentModProfile : *Results)
{
std::cout << CurrentModProfile.Price;
}
}
});

Purchasing Mods

You can call <<PurchaseModAsync>> to purchase a given mod. PurchaseModAsync takes two parameters = the ModID of the mod to purchase, and the ExpectedPrice, which is the price displayed to the user from <<ListAllModsAsync>>. You must include this parameter for safety, so the user is not charged more or less than the price displayed to them in case the price of the mod has changed between the call to ListAllModsAsync and purchase time. Once a mod is purchased, it is automatically subscribed to for the user.

You should validate that the user has enough virtual currency to make the purchase by comparing it to the balance you received from GetUserWalletBalanceAsync. Note this is purely for user experience (ie for graying out the purchase button in the UI, or upselling the user a virtual currenct pack), and PurchaseModAsync will return an error if the user does not have enough in their wallet.

The updated wallet balance after the purchase amount is subtracted is returned in the callback of <<PurchaseModAsync>>.

Modio::PurchaseModAsync(ModId, ModPrice, [](Modio::ErrorCode ec, Modio::Optional<Modio::TransactionRecord> Transaction) {
if (ec)
{
// Error handling
}
else
{
if (Transaction.has_value())
{
GlobalState.WalletAmount =
Transaction.value().UpdatedUserWalletBalance;
}
}
});

Showing user purchases

Even though all purchased mods are automatically subscribed, the user can still unsubscribe from them and uninstall them; however, they still remain owned and purchased by the user. They must re-subscribe to the mod in order to have it installed. This is facilitated by <<FetchUserPurchasesAsync>>, which will fetch a list of a users purchased mods. After a successful call, you can then display them with <<QueryUserPurchases>>, allowing re-subscription if necessary.

Getting a User Delegation Token

User Delegation Tokens can be used by a backend server for S2S (Server to Server) transactions/functionality. You can get one for the current user by calling <<GetUserDelegationToken>>, the callback for which contains the Token as a std::string.

Metrics Play Sessions

note

This functionality is demonstrated in example 13_MetricsSession.

The mod.io SDK supports all of the mod.io metrics features, allowing you to start a metrics play sesion, keeping that session alive via a heartbeat (automatically called, or manually handled) and then ending that session. Metric sessions allow you to track which mods your players interact with most frequently. Visit mod.io metrics system for an overview.

note

Running metrics play sessions is a premium feature. If you are interested in mod.io premium features, please contact developers@mod.io.

Metrics based on the platform and portal, are transparently taken care of with no additional consideration needed when using the SDK.

Initialization

The mod.io metrics features are enabled as part of generating a Metrics Secret Key your API settings in your game dashboard, e.g. https://mod.io/g/game-name/admin/api-key. Once this key has been generated, you need to pass it in as an ExtendedParameters field in your InitializeOptions when Initializing the mod.io SDK, e.g.:

Modio::InitializeOptions initializeOptions;
initializeOptions.ExtendedParameters["MetricsSecretKey"] = "00000000-1111-2222-3333-444444444444";

Failing to set up the Metrics Secret Key will result in a `Modio::MetricsError::SessionNotInitialized`` error being returned when using the metrics functionality.

Starting a Metrics Session

You can call MetricsSessionStartAsync to start a new session tracking the usage of mods in the context of your game. You'll notice that MetricsSessionStartAsync takes a MetricsSessionParams object as its parameter. This contains an optional Session Id, as well as a required vector of mods to track.

note

If a Session Id is not provided, a random one will be created for you.

Modio::MetricsSessionParams Params = Modio::MetricsSessionParams {{}, ModIds};

Modio::MetricsSessionStartAsync(Params, [](Modio::ErrorCode ec) {
{
if (ec)
{
// Error has occurred while attempting to start a session
}
else
{
// Session started successfully
}
});

The Metrics Session Params accepts an optional Session Id in the form of a Modio::Guid which you may want to use to associate the new session with any supplementary telemetry you are gathering in your game.

Metrics heartbeat

To ensure that the session is kept alive, a heartbeat is required to be submitted at most every 5 minutes. We recommend doing this a bit earlier than the threshold to ensure you do not miss the window.

There are two methods provided to control the behaviour of the heart beat, MetricsSessionSendHeartbeatOnceAsync which you can call at your desired precision, as well as a single call and forget MetricsSessionSendHeartbeatAtIntervalAsync with a desired interval.

Calling MetricsSessionSendHeartbeatOnceAsync will submit a single heartbeat, and return an error code if something went wrong. If no error has occured, the heartbeat has been successfully sent.

Modio::MetricsSessionSendHeartbeatOnceAsync([](Modio::ErrorCode ec) 
{
if (ec)
{
// Error has occurred while submitting a heartbeat
}
else
{
// Heartbeat successfully sent
}
});

Calling MetricsSessionSendHeartbeatAtIntervalAsync requires a parameter with the desired interval frequency in seconds. An error code will be returned if something went wrong, otherwise you will receive a false-y error if the interval loop has been shut down successfully (such as ending a session).

uint32_t IntervalSeconds = 150;
Modio::MetricsSessionSendHeartbeatAtIntervalAsync(IntervalSeconds, [](Modio::ErrorCode ec)
{
if (ec)
{
// Error has occurred while submitting a heartbeat
}
else
{
// Heartbeat interval loop has been shut down successfully
}
});

Ending a Metrics Session

To complete a session, for example when finishing a match, or quitting out of your game, you can call MetricsSessionEndAsync. As with the other calls, you will receive an error if anything has gone wrong, otherwise the operation successfully completed.

Modio::MetricsSessionEndAsync([](Modio::ErrorCode ec) 
{
if (ec)
{
// Error has occurred while attempting to end a session
}
else
{
// Metrics session successfully completed
}
});

Error Handling

The majority of mod.io SDK functions return a Modio::ErrorCode. In particular, asynchronous callbacks execute with a Modio::ErrorCode as the first parameter.

Checking for errors

You can check if a Modio::ErrorCode represents a success or failure by checking its 'truthyness'. If an ErrorCode evaluates to true, the function failed.

Modio::ErrorCode ec;
if (ec)
{
// Error code was truthy, therefore an error occurred.
}
else
{
// Error code was false-y, therefore the operation succeeded
}
Handling Failures

Sometimes an entitlement may fail to be consumed. To verify this, you should check if Modio::Optional<Modio::EntitlementConsumptionStatusList>::EntitlementsThatRequireRetry() has a value. If so, you can retry the call to RefreshUserEntitlementsAsync. If the failure persists, you should show an error, indicating that the customer contact mod.io support.

Inspecting ErrorCodes more deeply

Sometimes, this information will be all that is required, just a simple 'success/fail' that you can handle.

In many cases, however, you will want to perform some degree of inspection on an ErrorCode to determine specific information about that error. If nothing else you can display a reason for the failure to the end user.

Direct Queries

It's possible to query the raw value of an ErrorCode by comparing it against a particular enum value. For example, to check if a particular ErrorCode represents a filesystem error of 'Not enough space', you could do the following:

if (ec == Modio::FilesystemError::InsufficientSpace)
{
// Handle insufficient space by possibly deleting some files.
}
else
{
// Other error handling here
}

Of course, this means you can chain such checks together:

if (ec == Modio::FilesystemError::InsufficientSpace)
{
// Handle insufficient space by possibly deleting some files.
}
else if (ec == Modio::FilesystemError::NoPermission)
{
// Handle permissions error by asking the user to re-run as admin, or prompt for priviledge elevation.
}
else
{
// Other error handling here
}

This isn't ideal though, for several reasons:

  • It's considerably verbose
  • It doesn't check for semantic equivalency, only literal equivalency. In other words, some other error that derives from similar issues would return false because the codes don't match
  • It requires you to handle each case regardless of whether you need to or not
  • It scales poorly if there are several error codes with equivalent semantics in this context.

We can address these by using 'semantic queries' against the error code rather than directly comparing numerical values.

Semantic Queries

The SDK provides a function with several overloads that you can use to query for the semantic meaning of an ErrorCode.

Firstly, you can query if an ErrorCode is equivalent to a specific raw enum value:

Modio::ErrorCode ec;
if (Modio::ErrorCodeMatches(ec, Modio::HttpError::CannotOpenConnection))
{
// We couldn't connect to the mod.io server
}

This can be chained together like the literal value comparison mentioned earlier:

Modio::ErrorCode ec;
if (Modio::ErrorCodeMatches(ec, Modio::HttpError::CannotOpenConnection))
{
// We couldn't connect to the mod.io server
}
else if (Modio::ErrorCodeMatches(ec, Modio::HttpError::ServerClosedConnection))
{
// Server unexpectedly closed the connection
}

However, this still requires knowledge of the different types of HTTP errors. In your application you probably don't need to handle them differently. The semantics of networking errors are largely 'try the function again later'.

This is where the second overload of ErrorCodeMatches comes in. It allows you to query if the error satisfies a particular condition, such as 'does this code represent some kind of networking error':

Modio::ErrorCode ec;
if (Modio::ErrorCodeMatches(ec, Modio::ErrorConditionTypes::NetworkError))
{
// Error code represents some kind of network error
}
else
{
// Error code is not a network error
}

By querying if the error meets a specific condition, you can focus on handling a family of errors (in this case, network transmission errors) without needing to deal with individual errors within that group. No more manually checking against individual HttpError values, just a single query.

As a second example, when you ask the SDK to retrieve information about a specific mod, that ModID might be invalid or deleted. Both of these result in an error, which you could handle like the following:

// Inside a Modio::GetModInfoAsync callback
if (Modio::ErrorCodeMatches(ec, Modio::ApiError::RequestedModNotFound))
{
// The ModID wasn't valid, we couldn't find it
}
else if (Modio::ErrorCodeMatches(ec, Modio::ApiError::RequestedModDeleted))
{
// The ModID used to be valid, but the mod was deleted
}
else
{
// Some other error...
}

However, you may not care about the reasons the mod couldn't be retrieved, just that the mod information did not return a valid object.

In this case, you can query if the error code matches the EntityNotFoundError condition:

// In Modio::GetModInfoAsync callback
if (Modio::ErrorCodeMatches(ec, Modio::ErrorConditionTypes::EntityNotFoundError))
{
// The mod couldn't be found. Handle appropriately.
}

By grouping these codes into semantic checks, it helps you to potentially consolidate your error handling into a more limited set of generic error handlers rather than needing to deal with each potential outcome individually.

Putting it all together

By combining queries of categories with queries of specific values, you can handle general families of errors at a single location with special-case clauses for a particular error as necessary:

Modio::GetModInfoAsync(ModID, [](Modio::ErrorCode ec, Modio::Optional<Modio::ModInfo> Info)
{
if (ec)
{
if (Modio::ErrorCodeMatches(ec, Modio::ErrorConditionTypes::NetworkError)) // NetworkError group
{
// Error code represents some network error kind. Possibly ask the user to try again later.
}
else if (Modio::ErrorCodeMatches(ec, Modio::ErrorConditionTypes::EntityNotFoundError)) // Entity Not Found group
{
// An mod entity is not located with this configuration. Therefore, the list you're fetching the ModID from is probably stale. A remedy could be to fetch an updated version of the list from the server.
}
else if (Modio::ErrorCodeMatches(ec, Modio::GenericError::SDKNotInitialized)) // SDK not initialized group
{
// Your application is trying to call SDK functions without initializing the SDK first
}
}
});

Parameter Validation Errors

Some of the SDK functions may return errors that indicate a parameter or data validation failure. For these cases, the SDK parses the error response from the mod.io API and stores the information about which parameters failed validation until the next network request executes. If an SDK function returns an error which matches Modio::ErrorConditionTypes::InvalidArgsError, you can call GetLastValidationError in your callback to retrieve those errors and display appropriate feedback to the end-user.