Skip to main content

C++ SDK Structure

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

This guide covers:

Value objects

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

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

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 UGC, 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 UGC 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 UGC and only prompt the user to authenticate/create an account when they wish to perform any restricted operations (such as rating or subscribing to UGC).

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 UGC 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.

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 UGC, 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 UGC couldn't be retrieved, just that the UGC 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.

Mod data directories

The plugin stores UGC 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, UGC 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 UGC 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 UGC 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 UGC 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.

Next steps

Now you know more about how the plugin works, it's time to set up User Authentication or skip straight to Searching for UGC.

If you've already done this, we recommend working your way through the C++ SDK Getting Started Guides as they will teach you how to implement the mod.io fundamentals before moving onto exploring our Features.