Provisioning game servers

How did we do?

Once you've configured your game server cluster and defined a game server template, you can provision game server instances on HiveMP Game Servers.

Manually via the Admin Console

You can launch game server instances manually in the Admin Console. You can use the manual provisioning UI for either development purposes, or if you expect to have a fixed number of game servers in operation and only need to scale manually.

To launch a game server instance manually:

  1. Log into the Admin Console.
  2. Open Game Server Instances by clicking on Game Servers in the navigation menu, then Instances.
  3. Click Provision Instance.
  4. For Game Server Template, search for and select the game server template you want to launch.
  5. For Game Server Cluster, search for and select the game server cluster you want to launch the instance in.
  6. For Version, enter the name of the Docker image tag to use when launching the instance.
  7. Click Create.

Once the provisioning request has completed, you'll be returned to the list of game server instances where you can observe the status of the requested instance. The Admin Console will automatically refresh any instances in the provisioning status, so you can see when they move into the Active status.

Manually via the API

To launch game servers on-demand, you currently need to write and deploy a HTTPS endpoint that your game client can call in order to make the provisioning request. This endpoint should authenticate with a known administrative account and call the PUT /provision endpoint on behalf of your application.

In this endpoint, you'll most likely want to accept the current session's API key and possibly a lobby ID, and then verify that a game server should be launched for the lobby in question. Your endpoint can then attach the game server ID to the lobby once provisioning has started, and game clients can check the status of the game server instance to join it once it's active.

Using Google Cloud Functions for custom endpoints

You can host this endpoint in any way you like, but to make things easier you can use a service such as Google Cloud Functions to host this logic.

The rest of this How-To Guide will focus on deploying a function into Google Cloud Functions that can handle this provisioning logic. You can then customise the logic to your game-specific needs.

Set up Google Cloud Functions in a Google Cloud project

To get started with Google Cloud Functions, open up the Google Cloud Console and enable the Google Cloud Functions API.

Creating a Google Cloud Function to handle provisioning requests

  1. Open the Google Cloud Console and under the Cloud Functions product, click Create function.
  2. For Name, set it to something appropriate such as "request-game-server".
  3. For Memory allocated, select 128MB. It's unlikely you'll need more than that for this endpoint, but if you're later modifying this provided script and you find you need more, you can increase it later.
  4. For Trigger, leave it as HTTP.
  5. For Source code, leave it as Inline editor. If you want to version your endpoint in source control or use gcloud to automatically deploy it through a build server, you can set that up later.
  6. For Runtime, select Node.js 8 (Beta). This enables the script to use await/async language features.

Now select the index.js tab, and paste the following script. Replace the configuration values with values for your project.

const hivemp = require('hivemp');
const sha256 = require('js-sha256').sha256;

const config = {
  projectId: "/* YOUR PROJECT ID */",
  publicApiKey: "/* A PUBLIC API KEY FOR YOUR PROJECT HERE */",
  adminEmailAddress: "/* THE ADMIN ACCOUNT EMAIL ADDRESS */",
  adminPassword: "/* THE ADMIN ACCOUNT PASSWORD */",
  gameServerTemplateId: "/* THE GAME SERVER TEMPLATE ID */",
  gameServerClusterId: "/* THE GAME SERVER CLUSTER ID */",
  gameServerVersion: "/* THE GAME SERVER VERSION */",
};

function makeError(errorText) {
  console.error(errorText);
  return hivemp.HiveMPErrorFactory.createApiError({
    code: 0,
    message: errorText,
    fields: null,
    data: null,
  });
}

exports.provisionGameServer = async (request, response) => {
  try {
    // Make sure this is the correct kind of request.
    if (request.method !== 'PUT') {
      throw makeError('Incorrect method used to invoke endpoint');
    }

    // Make sure the request type was correct.
    if (!request.get('content-type').startsWith('application/json')) {
      throw makeError('Expected JSON body for PUT request');
    }

    // Get the parameters out of the request body.
    const apiKey = request.body.apiKey;
    const lobbyId = request.body.lobbyId;
    if (apiKey === undefined || lobbyId === undefined ||
        apiKey === null || lobbyId === null) {
      throw makeError('Expected "apiKey" and "lobbyId" to be set in request');
    }

    // Verify that the provided API key points to a valid session and that
    // it's for the current project.
    const sessionClient = new hivemp.UserSession.UserSessionClient(apiKey);
    const session = await sessionClient.sessionGET({
      id: undefined /* Retrieve the current session information */,
    });
    if (session.projectId !== config.projectId) {
      throw makeError('Provided API key was not for the expected project');
    }

    // Verify the current session is the host of the given lobby, and that the
    // lobby exists.
    const lobbyClient = new hivemp.Lobby.LobbyClient(apiKey);
    const lobby = await lobbyClient.lobbyGET({
      id: lobbyId
    });
    if (lobby.ownerSessionId !== session.id) {
      throw makeError('You must be the owner of the game lobby to provision a game server for it');
    }

    // Authenticate with the HiveMP Sessions API with an account that has
    // administrative access to the project you want to provision game servers for.
    const adminSessionClient = new hivemp.UserSession.UserSessionClient(config.publicApiKey);
    const adminSessionResponse = await adminSessionClient.authenticatePUT({
      authentication: {
        tokens: null,
        emailAddress: config.adminEmailAddress,
        passwordHash: sha256("HiveMPv1" + config.adminPassword),
        marketingPreferenceOptIn: false,
        twoFactor: null,
        requestedRole: "builtin:admin",
        metered: false,
        promptForProject: false,
        projectId: config.projectId,
      }
    });

    // Verify we could authenticate with HiveMP.
    if (adminSessionResponse.authenticatedSession === null) {
      throw makeError('Unable to authenticate with HiveMP.');
    }
    const adminSession = adminSessionResponse.authenticatedSession;

    // TODO: You could store and retrieve the administrative session in Google Cloud Datastore
    // to reduce the number of authentication requests you send to HiveMP.

    // Verify the game lobby doesn't already have a game server being provisioned for it.
    const attributeClient = new hivemp.Attribute.AttributeClient(adminSession.apiKey);
    let hasAttribute = false;
    let gameServerInfoAttribute = null;
    try {
      gameServerInfoAttribute = await attributeClient.attributeGET({
        id: lobbyId,
        key: 'game-server',
        ownerId: config.projectId,
      });
      hasAttribute = true;
    } catch (err) {
    }
    if (hasAttribute) {
      // Return the existing game server ID to the client.
      response.status(200);
      response.send({
        gameServerId: gameServerInfoAttribute.id,
      });
      return;
    }

    // Request provisionment of a game server instance, and assign the game server ID as
    // an attribute to the game lobby.
    const gameServerClient = new hivemp.GameServer.GameServerClient(adminSession.apiKey);
    const gameServer = await gameServerClient.provisionPUT({
      provisioningRequest: {
        templateId: config.gameServerTemplateId,
        gameServerClusterId: config.gameServerClusterId,
        version: config.gameServerVersion,
      }
    });

    // Set the game server attribute with the game server ID.
    await attributeClient.attributePUT({
      id: lobbyId,
      key: 'game-server',
      value: gameServer.id,
      useProjectAsOwner: true,
    });

    // Return the game server ID to the client.
    response.status(200);
    response.send({
      gameServerId: gameServer.id,
    });
    return;
  } catch (err) {
    // If we encounter an error, forward the API error to the client or send
    // a generic error response if it's some other kind of issue.
    if (hivemp.HiveMPErrorFactory.isApiError(err)) {
      response.status(500);
      response.send(hivemp.HiveMPErrorFactory.getApiErrorData(err));
    } else {
      console.error(err);
      response.status(500);
      response.send({
        code: 0,
        message: 'Unknown error occurred while processing request',
        fields: null,
        data: null,
      });
    }
  }
};

Now select the package.json tab, and paste the following script:

{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "hivemp": "^2.0.43",
    "js-sha256": "^0.9.0"
  }
}

Then, complete the rest of the setup:

  1. For Function to execute, enter "provisionGameServer".
  2. Click Create.

Download the example application to test your endpoint

In order to test your endpoint, we've prepared a sample C# client application that logs into HiveMP, creates a game lobby, makes a request to a Google Cloud Functions endpoint and then waits for a game server to be provisioned.

You can clone the repository for the example application from GitHub.

You can make sure your endpoint is working by following these steps:

  1. Create a public API key for your project to use in your client application. To create a public API key, refer to Using a Public API Key.
  2. Clone the example application's repository using Git, and open the solution file in Visual Studio.
  3. Edit the properties of the console application in the solution, and add the API_KEY and GCF_ENDPOINT environment variables.
  4. The API_KEY environment variable should have a value of the public API key you just created.
  5. The GCF_ENDPOINT environment variable should be the HTTPS URL to the Google Cloud Function you created previously. You can find the URL under the Trigger tab when viewing the function in the Google Cloud Console.
  6. Start the application in Visual Studio. When prompted, enter the email address and password of a HiveMP account (you can create a test account for this if you wish). The application will output it's progress as it creates a game lobby, hits the endpoint and waits for a game server to be provisioned.

If the application successfully obtains a game server ID that at least reaches the provisioning status, you'll know your Google Cloud Function is working correctly. If you encounter errors, refer to the logs for the Google Cloud Function in the Google Cloud Console to diagnose the issue further.

Suggested improvements to the Google Cloud Function

You can modify the implementation of the Google Cloud Function endpoint to increase functionality for your game. Suggested improvements include:

  • Selecting a different game server cluster based on the region you want to launch an instance in. For example, you could give game lobbies a "region" attribute, and then use that region attribute to select an appropriate cluster.
  • Selecting a different Docker image tag based on the game client version. When your game client creates a game lobby, you could attach a "clientVersion" attribute, and then use that to choose the matching dedicated server version.

Further reading