IMVU API for Unity

The IMVU API for Unity is a library that allows you to load IMVU's assets (avatars and outfits) from within the Unity game engine via a set of IMVU APIs. There are two main parts to this library: The REST APIs and the AssetLoader.

1. Setup

To work with the API, you need to install the unitypackage, setup the shaders, register your app with IMVU, and setup the oauth settings. Even if you're just running the demos that come with the API, you still need to do all of these things. If building for Android, an extra step is required to make authorization work, which is detailed below under the Android section.

1.1 Install Package

To install the API, open the IMVUnity.unitypackage or IMVUnityWithPhoton.unitypackage files in Unity. It will add all of the files to your project. Remember, imports are additive in Unity. When upgrading to a new version of a unity package you should first delete the old one.

1.2 Setup Shaders

In order for our avatars to render properly you need to add the IMVU shaders to your Always Loaded Shaders. To do this, open Edit->Project Settings->Graphics, and add the shaders in the IMVUnity/Shaders folder that you intend to use to the Always Included Shaders list. If you're going to use Unlit materials, include the Unlit shaders. If you're going to use Lit materials, include the Lit shaders. If you're going to use PhysicallyLit materials, include the PhysicallyLit shaders. This Unity documentation should prove useful: http://docs.unity3d.com/Manual/class-GraphicsSettings.html

1.3 Register the App

In order for a user to be able to connect to your app, you'll need to register the app with IMVU. To do this, go to http://imvu.com/developer . Click New to create a new app. Enter the name of the new app, and optionally provide a short description and an icon. Don't worry about making it perfect, as you can come back and change all of this information at any time. You also need to list all of the IMVU avatar accounts that will be developing or testing the app under the Allowed Users section. This includes your own IMVU account. Until the app is live these are the only accounts that will be able to connect to test your app.

Once you submit the app you'll need to add at least one platform that you intend the app to run on. Click the New button next to the app and select a platform, enter a URL, enter a redirect URL and specify the capabilities your application needs in order to function. The available platforms, right now, are Web, iOS and Android. The URL of a web app is the URL where the app is hosted. For an Android or iOS app, it's the app's URL on the appropriate app store. You can put a placeholder URL here, until you have a real one.

The redirect URL is used by the connection process to send the authentication token back to your app. For the web platform, you can enter any valid URL for the redirect URL. For iOS and Android you need to specify a URL with a custom protocol. The protocol is the part of the URL that goes before the colon. For normal web URLs, this is http or https. You'll want to invent something unique, because if another app on the same device is trying to use the same protocol for something, this could cause problems. It should ideally reflects the name of your game and your company. For instance, if you're Awesome Co., and your app is Super Great Ball, your custom protocol might be awesomeco-super-great-ball. The redirect URL will need to be a complete URL, so in the above example you might make it awesomeco-super-great-ball://blah/. The part after the double-slash doesn't really matter. Alongside whatever URL you want to use here, also include the URL http://localhost:8888/. This is necessary to permit you to connect to this app from the Unity editor. (Note: you can use a port other than 8888, as long as you match the port number in the steps below as well.)

The capabilities specify what your app will be allowed to do with the user's account once they log in. The first time a user logs in to your app, they'll be asked to grant the app permission to perform these capabilities. A very long list of capabilities may be scary to the user, so it's a good idea to only ask for the capabilities you really need. The capabilities are:

  • user:login - You always need this capability. It grants your app permission to connect as the user and read their profile data, including their profile look. See section 3.3 UserModel for the distinction between profile look and current look.
  • avatar:read - This grants your app permission to access the user's current look. Note that if you do not have this capability then when you attempt to load the logged in user's avatar, you will get their profile look instead of their current look. See section 3.3 UserModel for the distinction between profile look and current look.
  • outfit:read - This grants your app permission to access the user's saved outfits.
  • user friends:read - This grants your app permission to access the list of the user's friends.
  • feed:read - This grants your app permission to read the user's feed.
  • feed:write - This grants your app permission to post to the user's feed. Note that you will still need the user's explicit permission for each post.

1.4 OAuth Settings

Once the app is registered on the website, you'll need to go back to Unity and add data from the registration process into your app. From the Window menu select IMVU Settings.

For each of the platforms you registered on the website, fill in the redirect URL you invented for them. The platforms you're not using can be left blank.

The Editor, Windows and OS X platforms are not supported for publishing at this time, which is why they're not present on the website. They exist here purely for development purposes. For now, fill in the http://localhost:8888/ URL for them. The Windows and OS X options are primarily useful for running multiple local copies of your app, when testing networking.

For the App ID, use the large alphanumeric hash associated with the platform. For Editor, Windows and OS X, you can pick the hash from any of the platforms you've registered with your app.

For more info on these settings, see section 5. Settings below.

1.5 Android

In order to authenticate on Android, the AndroidManifest.xml must contain the proper protocol. The package installs an AndroidManifest.xml file in the correct location, though if you have an existing Android build you may need to merge it with your existing AndroidManifest.xml. The important part of the provided file is the Activity tag, and it's contents, with the android:name="com.imvu.oauth.MainActivity". If you do not have an existing Android build with it's own AndroidManifest.xml, you can just use the one we provide.

The one change you need to make is editing the protocol. Note again that it's important that this protocol be unique. If there is another app installed on the user's Android device which uses the same protocol, it will prompt the user which app should handle the connection, even though only your app will be capable of handling it correctly. Look for these lines in the manifest file:

<!-- Set your app's custom protocol on the next line -->
<data android:scheme="put-protocol-here" />

And change the put-protocol-here string to your custom protocol. So if your redirect URL is awesomeco-super-greeat-ball://blah/, as in the example above, the protocol is awesomeco-super-great-ball, and you'll want to edit it to:

<!-- Set your app's custom protocol on the next line -->
<data android:scheme="awesomeco-super-great-ball" />

2. Example Code

This is a minimal example of using the API to load an avatar, once all the setup above is done.

using IMVU;

class TestLoader : LocalAssetLoader {
    void Start() {
        Imvu.Connect().Then(
            userModel => Load(userModel)
        ).MatchErr(
            error => Debug.LogError(error)
        );
    }
}

The connection process is handled for you when you call Imvu.Connect(). This returns a Promise<UserModel, Error>. Promises here function very much like JavaScript promises, and they're documented in more detail at the end of this document. Then it calls Load(), a function inherited from LocalAssetLoader, which loads an IMVU avatar from a UserModel onto this GameObject. This returns a Promise<AssetInfo, Error>. The avatar will already be setup to work with Mecanim, so if you've placed an Animator on it it'll already be animating.

If there are any actual errors at either step, it will fall through to the Catch() and log the error.

3. REST

The REST APIs, at their root, are a set of HTTP-based APIs for accessing IMVU. Inside the APIs this is hidden behind a clean, strongly-typed API. Aside from the fact that many operations are asynchronous, you don't have to worry about the fact that there are HTTP requests behind the scenes. The REST APIs are accessed through RestModel and RestCollection classes.

3.1 RestModel Class

A RestModel is a base class for representing a single data node. It is generic over the type it stores. It contains an info member variable of that type. For instance, UserModel is a subclass of RestModel<UserInfo>, and its info member variable will have type UserInfo.

Each subclass of RestModel will also contain getters for each of the other data nodes related to it. These getters will always return another RestModel.

The set of RestModels that exist right now are: * UserModel - Data about an IMVU user. * OutfitModel - Data about a saved outfit. * FeedElementModel - Data about a single feed post. * ProductModel - Data about an IMVU product.

3.2 RestCollection Class

A RestCollection is a base class for representing a list of data nodes. This looks like:

class RestCollection<T> : RestModel<CollectionInfo<T>> {
    public CollectionInfo<T> info;

    public Promise<Unit, Error> NextPage();
    public delegate void AddEventHandler(T model);
    public event AddEventHandler add;
}

The CollectionInfo class is as follows:

class CollectionInfo<T> {
    public List<Promise<T, Error>> items;
    public int totalCount;
}

It's generic over the type of RestModel it stores. Like RestModel, it contains an info member variable, with the type RestCollection, generic over the same type. For example, the FriendCollection inherits from RestColletion<UserModel>, and it's info member variable will have type CollectionInfo<UserModel>.

Note that collections are paginated. The totalCount field represents the total size of the collection, which may be larger than the number currently loaded, which is why it needs to be a separate field instead of just using items.Count. Calling NextPage() will load the next page. The promise it returns will be accepted when the request for the next page is complete and populated into the collection, or rejected if the request fails. There is also the add event, which will be called each time a new element is added to the collection. Currently this only happens during loading. So you can respond to a new page loading either by using the promise returned by NextPage, or one element at a time by using the add event.

The set of RestCollections that exist right now are:

  • FriendCollection - A collection of UserModels, obtained by calling UserModel.GetFriends().
  • OutfitCollection - A collection of OutfitModels, obtained by calling UserModel.GetOutfits().
  • FeedElementCollection - A collection of FeedElementModels, obtained by calling UserModel.GetPersonalFeed(), UserModel.GetSubscribedFeed() or UserModel.GetRecommendedFeed().
  • ProductCollection - A collection of ProductModels, obtained by calling OutfitModel.GetProducts() or AvatarModel.GetProducts().

3.3 UserModel Class

This model represents a user:

class UserModel : RestModel<UserInfo> {
    public UserInfo info;

    // Get the user's friends
    public Promise<FriendCollection, Error> GetFriends(int limit = 0);
    // Get the user's current look
    public Promise<ProductCollection, Error> GetCurrentLook();
    // Get the user's profile look
    public Promise<ProductCollection, Error> GetProfileLook();
    // Get the user's saved outfits
    public Promise<OutfitCollection, Error> GetOutfits();
    // Get the user's personal feed posts
    public Promise<FeedElementCollection, Error> GetPersonalFeed();
    // Get the user's subscribed feed, which is the aggregate of their posts and their friends' posts
    public Promise<FeedElementCollection, Error> GetSubscribedFeed();
    // Get the user's recommended feed, which is a collection of whitelisted posts
    public Promise<FeedElementCollection, Error> GetRecomendedFeed();
}

The limit argument to GetFriends determines the page size of the resulting FriendCollection. The default value of 0 means to use the server's default page size.

The difference between the current look and the profile look is that current look should be used whenever the user is actually present, and the profile look when they're not. The current look reflects what they last put on while using dress up in the IMVU client, website or mobile app, while the profile look is the look they've saved to their profile. Note that the current look can only be accessed on the UserModel for the logged in user. If called on any other user, it will return an error. The only way to access the current look of another user is through the PhotonAssetLoader or UNetAssetLoader, which will synchronize that user's current look when they connect to a synchronous multiplayer experience.

The UserInfo data is as follows:

struct UserInfo {
    public string gender;
    public string country;
    public string state;
    public Uri thumbnailUrl;
    public bool online;
    public string username;
    public int id;
}

You can use the thumbnailUrl URL to load the user's profile image. It's recommended to use the TextureLoader for this purpose.

3.4 OutfitModel class

This model represents an outfit, which is a look plus a name and an image:

class OutfitModel : RestModel<OutfitInfo> {
    // Get the list of products making up the outfit's look
    public Promise<ProductCollection, Error> GetProducts();

    public OutfitInfo info;
}

The OutfitInfo data is as follows:

public struct OutfitInfo {
    public string name;
    public Uri image;
    public List<int> pids;
}

You can use the image URL to load an image of the outfit. It's recommended to use the TextureLoader for this purpose.

3.5 ProductModel class

This model represents an IMVU product:

class ProductModel : RestModel<ProductInfo> {
    public ProductInfo info;
}

The ProductInfo data is a follows:

public struct ProductInfo {
    public int pid;
    public string name;
    public string creatorName;
    public string rating;
    public Uri productPage;
    public Uri creatorPage;
    public string gender;
    public List<string> categories;
    public List<string> types;
    public Uri previewImage;
}

The rating field is one of GA or AP, which determines whether the product is appropriate for all audiences (GA or General Audience), or require an access pass (AP for Access Pass).

The gender and categories fields are set by the creator. They will usually reflect the proper nature of the product, but they aren't guaranteed to be correct.

The types field is a list of additional categories which are set programatically, and these are reliably accurate.

3.6 FeedElementModel Class

This model represents a feed post:

class FeedElementModel : RestModel<FeedElementInfo> {
    public FeedElementInfo info;

    // Get the user who made this post
    public Promise<UserModel, Error> GetUser();
}

The FeedElementInfo is as follows:

struct FeedElementInfo {
    public string time;
    public string type;
    public FeedElementPayloadInfo payload;
}

public struct FeedElementPayloadInfo {
    public int height;
    public int width;
    public Uri url;
    public Uri thumbnailUrl;
    public string message;
}

The type field is one of message or photo. If it's a message post, then the only field of the payload that will be populated is message. If it's a photo post, then all the fields will be populated. You can use the url and thumbnailUrl URLs to load the attached image for a photo post. It's recommended to use the TextureLoader for this purpose.

3.7 FriendCollection Class

This collection represents a user's friend list:

class FriendCollection : RestCollection<UserModel> {
    public CollectionInfo<UserModel> info;
}

3.8 OutfitCollection class

This collection represents a set of saved outfits:

class OutfitCollection : RestCollection<OutfitModel> {
    public CollectionInfo<OutfitModel> info;
}

3.9 FeedElementCollection Class

This collection represents a set of feed posts, generally just called a feed:

class FriendElementCollection : RestCollection<FeedElementModel> {
    public CollectionInfo<FeedElementModel> info;

    // Post an image to this feed
    public Promise<FeedElementModel, Error> PostImage(Texture2D image);
}

Note that PostImage() is only valid on the personal feed of the currently logged-in user. If you call it on any other feed, it will fail. This call will pop up a confirmation dialog showing the user the image you intend to post, so you can not post to the user's feed without their consent. If they choose not to post it, the promise will resolve to an error.

3.10 ProductCollection class

This collection represents the set of products in a particular look:

class ProductCollection {
    public CollectionInfo<ProductModel> info;
}

3.11 Imvu Class

The entry point to the whole API is the Imvu class. For the moment, this has one method:

class Imvu {
    public static Promise<UserModel, Error> Connect(bool storeToken = false);
    public static void Disconnect();
}

The Connect() method will handle the whole connection process for you. By default, the oauth token will be stored, so the next time the app is run it will automatically be logged in to the same account. If you don't want this behavior, then pass in false for storeToken.

If you call Connect() multiple times, it will always return the same promise, unless the user cancelled or the connection failed. This means that if you need the logged-in user's UserModel in multiple places, you can call Connect() from each of those places independently. When the user cancels or the connection fails, the stored promise is cleared, which allows you to attempt to connect again.

The Disconnect() method will clear the oauth token. All stored RestModels will stop functioning, and should no longer be used. The next time Connect() is called, it will go through the connection process again, and return a new UserModel.

4. Asset Loading

In order to load an IMVU asset, you need to create a GameObject and put a LocalAssetLoader on it. If you want this loading to synchronize properly in a multiplayer experience, you'll also need a UNetAssetLoader or a PhotonAssetLoader, depending on whether you're using UNet or Photon for your network layer.

4.1 LocalAssetLoader

The LocalAssetLoader is a MonoBehaviour. If it's placed on a GameObject, you can then call its Load() method to load that asset onto that GameObject. Its signature is:

class LocalAssetLoader : MonoBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

While the LocalAssetLoader can be used directly, it's also common to subclass it, like so:

class MyAvatar : IMVU.LocalAssetLoader {
    void Start() {
        Imvu.Connect().Then(
            userModel => Load(userModel, Setup)
        ).Then(
            assetInfo => {
                // do stuff with the avatar
            }
        );
    }
}

4.2 UNetAssetLoader

The UNetAssetLoader has the same API as the LocalAssetLoader:

class UNetAssetLoader : NetworkBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

The main feature of the UnetAssetLoader is that if Load() is called while connected to a UNet server, it will cause the same asset to be loaded on the matching GameObject on all clients.

4.3 PhotonAssetLoader

The PhotonAssetLoader has the same API as the LocalAssetLoader:

class PhotonAssetLoader : Photon.MonoBehaviour {
    Promise<AssetInfo, Error> Load(UserModel user, LoadOptions options = null);
    Promise<AssetInfo, Error> Load(OutfitModel outfit, LoadOptions options = null);
}

The optional LoadOptions argument can override the current load settings. See section 4.5 LoadOptions for more info.

The main feature of the PhotonAssetLoader is that if Load() is called while connected to a Photon server, it will cause the same asset to be loaded on the matching GameObject on all clients.

NOTE: When using the Photon demo you need to apply your Photon AppID to the demo and you need to turn on "Auto-Join Lobby". Go to window->Photon Unity Networking->PUN Wizard->Locate Photon Server Settings. Ensure your Photon AppID is here and "Auto-Join Lobby" is turned on. "Auto-Join Lobby" can also be turned on programmatically. This setting is important to be aware of.

4.4 AssetInfo

The AssetInfo class contains basic information about the asset:

class AssetInfo {
    public List<SkeletonData> skeletons;
}

The skeletons field is a list of the skeletons on the asset. The first is always the root skeleton. After that come the skeletons of each of the attachments. The SkeletonData provides the following:

public class SkeletonData {
    public Transform root;
    public Transform[] bones;

    // Check if a bone exists, by name
    public bool HasBone(string bname);
    // Get a bone, by name
    public Transform GetBone(string bname);
    // Get the index of a bone, by name
    public int GetBoneIndex(string bname);
}

This allows you to attach geometries or additional behaviours to bones. For instance, it's used heavily by the Equip demo, to attach equipment to bones, or the Ragdoll demo, to attach physics to bones.

4.5 LoadOptions

The LoadOptions class can be provided as an optional argument to all of the various Load functions on LocalAssetLoader, UNetAssetLoader and PhotonAssetLoader, in order to override the normal default load settings. It's constructor looks like:

public LoadOptions(MaterialType? materialType = null, List<LodSetting> lodSettings = null);

Since these are both optional arguments, you can specify only the subset you wish to change, as so:

LoadOptions(materialType: MaterialType.Unlit);
LoadOptions(lodSettings: new List<LodSetting> { new LodSetting(LodLevel.Low, 100) });
LoadOptions(
    materialType: MaterialType.Unlit,
    lodSettings: new List<LodSetting> { new LodSetting(LodLevel.Low, 100) });

4.6 Level of Detail

The API supports loading asset meshes at several different levels of detail and can set up loaded GameObjects to automatically switch between them depending on the percentage of screen space that they occupy. This lets you display assets with dense meshes when close up, when detail is needed, but then switch to meshes with fewer triangles when at a distance, to improve drawing performance.

By default, they are configured to load each GameObject with several levels of detail and automatically switch between them, which has obvious benefits. However, there are cases when you may want to specify custom level-of-detail settings:

  • If you specify fewer levels of detail than the default, the load will make fewer network requests for mesh data, saving some loading time.
  • If you specify just one level of detail, the load can omit the LodComponent that does the automated switching between levels, saving some per-frame CPU overhead.
  • Custom settings also let you tune the default transition points for display quality or performance.

You can customize the default level of detail settings in the IMVU Settings window, or override the defaults at run time, as detailed in section 5.3 Load Settings. You can also use the LoadOptions described in section 4.5 LoadOptions to override the defaults on a specific asset.

Custom level-of-detail settings are specified by a list of LodSetting objects. Each LodSetting object specifies a LodLevel and a maximum screen coverage percentage. There are four LodLevels available:

  • LodLevel.Maximum - The most detailed mesh, as originally authored.
  • LodLevel.Medium - An automatically generated mesh with lower detail (about 50%).
  • LodLevel.Low - An automatically generated mesh with very low detail (about 10%).
  • LodLevel.Hidden - Displays no meshes at all.

Note that the actual amount of detail reduction depends on the geometry of the mesh. For some models, reduction may not be possible, in which case the original mesh is used for all levels.

Screen coverage percentage is expressed as a float from 0-100. At runtime, the screen coverage is estimated from the bounding boxes of the meshes, and the level with the smallest coverage value greater than the object's current coverage (capped to 100%) is used. If you do not specify levels up to 100% coverage, the maximum level will be automatically included.

For example, if we were loading an asset that we know will never be displayed in detail, we could save some network bandwidth by omitting the maximum level when loading, using these LoD settings:

new List<LodSetting>() {
    new LodSetting(LodLevel.Hidden, 0.01f),
    new LodSetting(LodLevel.Low, 1.0f),
    new LodSetting(LodLevel.Medium, 100.0f),
}

Or, if we were loading an asset to only be displayed in detail, we could save some network requests and runtime CPU by loading just the maximum level, with these LoD settings:

new List<LodSetting>() {
    new LodSetting(LodLevel.Maximum, 100.0f),
}

4.7 Material Types

There are three different types of shaders available for the avatars:

  • Unlit - These are the simplest shaders. They have no lighting and no shadows. They will tend to be the fastest.
  • Lit - These shaders use the Unity legacy lighting system. They are lit and recieve and cast shadows.
  • PhysicallyLit - These use Unity 5's new lighting system. They are more advanced, but IMVU avatars can't take advantage of most of the additional features they provide, and there are some bugs in how some avatars render on certain platforms with these shaders. This option is not currently recommended.

You can customize the default material type in the IMVU Settings window, or override the defaults at run time, as detailed in section 5.3 Load Settings. You can also use the LoadOptions described in section 4.5 LoadOptions to override the defaults on a specific asset.

4.8 TextureLoader

The TextureLoader facilitates the loading images over HTTP into a Texture2D. This is easy to do if you use the WWW class directly, but if you use TextureLoader you'll get the benefits of our network stack, which is much more reliable. The TextureLoader API is as follows:

class TextureLoader {
    public Promise<Texture2D, Error> Load(Uri url);
}

5. Settings

The IMVU Settings window can be accessed by going to Window -> IMVU Settings. This window is divided vertically into three sections: the clear buttons, the oauth settings and the load settings.

5.1 Clear Buttons

  • Disconnect - If you run your app from within the editor, and connect, then your oauth token will be stored in the local store, via Unity's PlayerPrefs. This is normally a convenience, as it means you don't have to connect again each time you play. However, if you need to clear that connection data, you can come here and press the Disconnect button.
  • Clear Load Settings - There are calls you can make at runtime, which are detailed below, which allow you to change the default load settings. When you do so, you can choose to have those detaults stored in the local store, via Unity's PlayerPrefs, to override the defaults configured in this window. That still works when running the app in the editor. This button will clear those defaults.

5.2 OAuth Settings

This lists a set of platforms, and for each platform you can provide an App ID and a Redirect URL.

  • App ID - The ID of the app you wish to identify as. This can be garnered from the developer page, as detailed in Setup above. Note that there is a different App ID for each platform. For iOS, Android or WebGL, be sure to use App IDs from those platforms. The Editor, Windows and OS X builds exist only for development and debugging purposes, and should get the App ID associated with whichever platform you're focusing on developing for at the time.
  • Redirect URL - The URL that the OAuth token will be forwarded to after the user logs in. This needs to be one of the URLs configured on for the app on the Developer page. For the Editor, Windows and OS X, the redirect URL needs to be http://localhost:XXXX, where the XXXX is a port number. We use 8888, but any number which is not in use on your development machine can work, provided it's consistent with the URL entered on the developer page.

5.3 Load Settings

This specifies the default settings used to load an IMVU asset. Any of these can be overriden when loading specific assets, as described in section 4.5 LoadOptions. The defaults can also be changed at run time, and these changes can optionally be stored into the local store via Unity's PlayerPrefs. This allows you to make these settings configurable by the user, if desired.

See sections 4.6 Level of Detail and 4.7 Material Types for detailed descriptions of what the Default LOD Settings and Default Material Type settings mean.

To change the settings at runtime, these functions are provided in the Settings class:

public static void SetDefaultLodSettings(List<LodSetting> lodSettings, bool save = false);
public static void SetDefaultMaterialType(MaterialType matType, bool save = false);

In both of these, the optional save argument determines whether the change with be saved to the local store. If it's not specified, the change will not be saved.

6. Common Classes

The Result and Promise classes may be confusing to many Unity developers. For a more thorough treatment of how they work and why they're useful, see this post: http://engineering.imvu.com/2015/12/11/promises-in-unity/

6.1 Error Class

The Error class just encodes a human-readable error message with a machine-readable error code. The error code is typically in the form <SYSTEM>-<NUM>, where <SYSTEM> is the system the error occurred in, and <NUM> is a number that will uniquely identify the error within that system. For instance, errors coming from the LocalAssetLoader are in the form LAL-01, LAL-02, etc. If the same error occurs in multiple places, they should still have unique numbers, in order to aid debugging.

Class Layout

class Error {
    public string message;
    public string code;

    public Error(string message, string code);
}

6.2 Result Class

A Result represents a value that may or may not exist. If it doesn't exist, it houses an error explaining why. It's generic over the type of the value. In order to maintain type safety, the contents of the Result are private. They're accessed through lambdas with the Match(), MatchVal(), MatchErr(), Then() or Catch() methods.

In proper computer science terms: Result is a sum type, with simulated pattern matching through the Match(), MatchVal(), MatchErr(), Then() and Catch() methods.

Class Layout

public class Result<ValueT> {
    public static Result<ValueT> Val(ValueT val);
    public static Result<ValueT> Err(Error err);
    public bool HasValue();
    public bool HasError();
    public RetT Match<RetT>(Func<ValueT, RetT> valFunc, Func<Error, RetT> errFunc);
    public void Match(Action<ValueT> valFunc, Action<Error> errFunc);
    public void MatchVal(Action<ValueT> valFunc);
    public RetT MatchVal<RetT>(Func<ValueT, RetT> valFunc, RetT defaultValue);
    public void MatchErr(Action<Error> errFunc);
    public RetT MatchErr<RetT>(Func<Error, RetT> errFunc, RetT defaultError);
    public Result<NewValT> Then<NewValT>(Func<ValueT, Result<NewValT>> valFunc);
    public Result<NewValT> Then<NewValT>(Func<ValueT, Result<NewValT>> valFunc, Func<Error, Result<NewValT>> errFunc);
    public Promise<NewValT> Then<NewValT>(Func<ValueT, Promise<NewValT>> valFunc);
    public Promise<NewValT> Then<NewValT>(Func<ValueT, Promise<NewValT>> valFunc, Func<Error, Promise<NewValT>> errFunc);
    public Result<ValueT> Catch(Func<Error, Result<ValueT>> errFunc);
    public Promise<ValueT> Catch(Func<Error, Promise<ValueT>> errFunc);
}

Result.Val

public static Result<ValueT> Val(ValueT val)

This static function will construct a new result with a value.

Example:

var result = Result<int>.Val(42);

Result.Err

public static Result<ValueT> Err(Error err)

This static function will construct a new result with an error.

Example:

var result = Result<int>.Err(new Error("The operation failed", "EX-1"));

Result.HasValue

public bool HasValue();

Returns true if the Result has a value, or false otherwise.

Result.HasError

public bool HasError();

Returns true if the Result has an error, or false otherwise.

Result.Match

void Match(Action<ValueT> valFunc, Action<Error> errFunc);
RetT Match<RetT>(Func<ValueT, RetT> valFunc, Func<ValueT, RetT> errFunc);

Match() will take two functions arguments. If it has a value, it will execute the first function, passing it the value. If it has an error, it will execute the second function, passing it the error. This guarantees you will never accidentally access the wrong field on the Result.

The first overload takes functions with no return type. The second overload takes functions with matching return types. In this case, whichever function is run will have it's return type returned by Match(). Note that it's necessary that both functions have the same return type, because Match() can only have one return type.

Example:

IMVU.Result<int> result = FunctionThatMightFail();
result.Match(
    val => Debug.Log("Success! Value: " + val),
    err => Debug.LogError(err)
);

Result.MatchVal

public void MatchVal(Action<ValueT> valFunc);
public RetT MatchVal<RetT>(Func<ValueT, RetT> valFunc, RetT defaultValue);

MatchVal() takes a funciont which will only be executed if the Result currently has a value.

The first overload takes a function with no return type. The second overload takes a function with a return type, and a default value. If the function is called, it's return value will be returned by MatchVal(). Otherwise, it will return the default value.

Example:

IMVU.Result<int> result = FunctionThatMightFail();
result.MatchVal(
    val => Debug.Log("Success! Value: " + val)
);

Result.MatchErr

public void MatchErr(Action<Error> errFunc);
public RetT MatchErr<RetT>(Func<Error, RetT> errFunc, RetT defaultError);

MatchErr() takes a funciont which will only be executed if the Result currently has an error.

The first overload takes a function with no return type. The second overload takes a function with a return type, and a default error. If the function is called, it's return value will be returned by MatchErr(). Otherwise, it will return the default error.

Example:

IMVU.Result<int> result = FunctionThatMightFail();
result.MatchErr(
    err => Debug.LogError(err)
);

Result.Then

public Result<NewValT> Then<NewValT>(Func<ValueT, Result<NewValT>> valFunc);
public Result<NewValT> Then<NewValT>(Func<ValueT, Result<NewValT>> valFunc, Func<Error, Result<NewValT>> errFunc);
public Promise<NewValT> Then<NewValT>(Func<ValueT, Promise<NewValT>> valFunc);
public Promise<NewValT> Then<NewValT>(Func<ValueT, Promise<NewValT>> valFunc, Func<Error, Promise<NewValT>> errFunc);

Then() takes one or two function arguments. The versions that take two function arguments will execute the first function if the Result has a value, and the second one if it has an error. The versions that take only one function argument will execute that function only if the Result has a value. These functions must return a Result or a Promise, which will in turn by returned by Then(). In the single argument versions, if there is an error result, then a new Result or Promise is generated with that error and returned.

Then() is designed for chaining. Since it always returns a new Result or Promise, you can always call Then() or Catch() again on the output. Unhandled errors also fall through to the next stage in the chain, allowing any errors that happened in the chain to fall through to a common error handler at the end.

Result.Catch

public Result<ValueT> Catch(Func<Error, Result<ValueT>> errFunc);
public Promise<ValueT> Catch(Func<Error, Promise<ValueT>> errFunc);

Catch() takes a function argument, which will be executed only if the Result has an error. This function must return a new Result or Error, which will in turn be returned by Catch(). If the Result contains a value, then that value will be packaged into a new Result or Error and returned.

Catch() is designed for chaining, to be used in conjunction with Then(). Since it always returns a new Result or Promise, you can always call Then() or Catch() again on the output. Unhandled values also fall through to the next stage in the chain.

6.3 Promise Class

A Promise represents a value that may not exist yet. This is designed to function very much like a JavaScript promise, even mirroring its API as closely as possible. Think of it as an asynchronous Result. The difference is that when you give it functions, they won't be called until the Promise is resolved. If the Promise is already resolved, they'll be called immediately.

Class Layout

    public class Promise<ValueT> {
        public Promise();
        public Promise(Result<ValueT> _value);
        public Promise(Promise<ValueT> promise);
        public static Promise<ValueT> Val(ValueT val);
        public static Promise<ValueT> Err(Error err);
        public void Match(Action<ValueT> valFunc, Action<Error> errFunc);
        public void MatchVal(Action<ValueT> valFunc);
        public void MatchErr(Action<Error> errFunc);
        public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Result<NewValueT>> valFunc);
        public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Result<NewValueT>> valFunc, Func<Error, Result<NewValueT>> errFunc);
        public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Promise<NewValueT>> valFunc);
        public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Promise<NewValueT>> valFunc, Func<Error, Promise<NewValueT>> errFunc);
        public Promise<ValueT> Catch(Func<Error, Result<ValueT>> errFunc);
        public Promise<ValueT> Catch(Func<Error, Promise<ValueT>> errFunc);
        public void Resolve(Result<ValueT> result);
        public void Accept(ValueT acceptValue);
        public void Reject(Error rejectValue);
        public static Promise<List<ValueT>> All(List<Promise<ValueT>> promises);

Constructors

public Promise();
public Promise(Result<ValueT> _value);
public Promise(Promise<ValueT> promise);

The default constructor (with no arguments) constructs an unresolved promise. The Result constructor will create an already-resolved promise using the value of the Result. It's the equivalent of calling the default constructor then calling Resolve(). The Promise constructor makes this Promise's state mirror the one that's passed in, so when that Promise is resolved, this new one will be as well, with the same resolution.

Promise.Val

public static Promise<ValueT> Val(ValueT val)

This static function will construct a new Promise that's already accepted.

Example:

var result = Promise<int>.Val(42);

Promise.Err

public static Promise<ValueT> Err(Error err)

This static function will construct a new Promise that's already rejected.

Example:

var result = Promise<int>.Err(new Error("The operation failed", "EX-1"));

Promise.Match

void Match(Action<ValueT> valFunc, Action<Error> errFunc);

Match() will take two functions arguments, one of which will be called when the Promise resolves. If it's accepted, the first will be called. If it's rejected, the second will be called.

Example:

IMVU.Promise<int> promise = AsynchronousFunction();
promise.Match(
    val => Debug.Log("Success! Value: " + val),
    err => Debug.LogError(err)
);

Promise.MatchVal

public void MatchVal(Action<ValueT> valFunc);

MatchVal() takes a funcion which will executed when and if the Promise is accepted.

Example:

IMVU.Promise<int> promise = AsynchronousFunction();
promise.MatchVal(
    val => Debug.Log("Success! Value: " + val)
);

Promise.MatchErr

public void MatchErr(Action<Error> errFunc);

MatchErr() takes a funcion which will be executed when and if the Promise is rejected.

Example:

IMVU.Promise<int> promise = AsynchronousFunction();
promise.MatchErr(
    err => Debug.LogError(err)
);

Promise.Then

public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Result<NewValueT>> valFunc);
public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Result<NewValueT>> valFunc, Func<Error, Result<NewValueT>> errFunc);
public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Promise<NewValueT>> valFunc);
public Promise<NewValueT> Then<NewValueT>(Func<ValueT, Promise<NewValueT>> valFunc, Func<Error, Promise<NewValueT>> errFunc);

Then() takes one or two function arguments, which will be called when the Promise is resolved, or called immediately if the Promise is already resolved. The versions that take two function arguments will execute the first function if the Promise is accepted, and the second one if it's rejected. The versions that take only one function argument will execute that function only if it's accepted. These functions must return a Result or a Promise, which will turn into a Promise returned by Then(). In the single argument versions, if the Promise is rejected, then a new Promise is generated with the same error and returned.

Then() is designed for chaining. Since it always returns a new Promise, you can always call Then(), Catch(), Match(), MatchVal() or MatchErr() again on the output. Unhandled errors also fall through to the next stage in the chain, allowing any errors that happened in the chain to fall through to a common error handler at the end.

Examples:

AsynchronousFunction().Then(
    val => val.GetAnotherAsyncThing()
).Match(
    val => Debug.Log("Async thing: " + val),
    error => Debug.LogError(error)
);
AsynchronousFunction().Then(
    val => user.GetAnotherAsyncThing(),
    error => {
        Debug.LogError("AsynchronousFunction failed: " + error);
        return Promise<OutfitCollection>.Err(error);
    }
).Match(
    val => Debug.Log("Async thing: " + val),
    error => Debug.LogError("GetAnotherAsyncThing failed: " + error)
);

Promise.Catch

public Promise<ValueT> Catch(Func<Error, Result<ValueT>> errFunc);
public Promise<ValueT> Catch(Func<Error, Promise<ValueT>> errFunc);

Catch() takes one function argument, which will be called when the Promise is resolved, or called immediately if the Promise is already resolved, but only if the Promise is rejected. If the Promise is accepted, the new Promise returned by Catch() will also be accepted with the same value.

Catch() is designed for chaining. Since it always returns a new Promise, you can always call Then(), Catch(), Match(), MatchVal() or MatchErr() again on the output. Since Catch() only handles errors, successes will fall through allowing them to be handled further down the chain.

Example:

AsynchronousFunction().Catch(
    error => {
        Debug.Log("First attempt at AsynchronousFunction failed. Trying again.");
        return AsynchronousFunction();
    }
).Match(
    val => Debug.Log("Async thing: " + val),
    error => Debug.LogError("GetAnotherAsyncThing failed: " + error)
);

Promise.Resolve

public void Resolve(Result<ValueT> result);

Resolve() resolves the promise, with a Result. If the Result has a value, the Promise will be accepted. If it doesn't have a value, the Promise will be rejected.

Example:

var promise = new Promise<int>();
promise.Resolve(Result<Int>.Val(42));

Promise.Accept

public void Accept(ValueT acceptValue);

Accept() accepts the promise, with the value passed in.

Example:

var promise = new Promise<int>();
promise.Accept(42);

Promise.Reject

public void Reject(Error rejectValue);

Reject() rejects the promise, with the value passed in.

Example:

var promise = new Promise<int>();
promise.Reject(new Error("Error! Failure! Panic!", "EX-1"));

Promise.All

public static Promise<List<ValueT>> All(List<Promise<ValueT>> promises);

All() takes a list of promises and results a Promise to a list of values. If all of the input Promises are accepted, the resulting Promise will be accepted, and will compile all of the values from all of the promises into a list, as the value for the new Promise. It will maintain the order of the list when doing so. If any one of the Promises passed in is rejected, the resulting Promise will be rejected. The error used will be the error from the first Promise in the list to be rejected.

Example:

var promises = new List<Promise<int>> {
    GetPromise(),
    GetPromise(),
    GetPromise()
};
Promise<int>.All(promises).Match(
    values => {
        foreach (var value in values) {
            Debug.Log("Got value " + value);
        }
    },
    error => Debug.LogError("One of the promises failed: " + error)
);