Creating a game extension for Vortex

From Nexus Mods Wiki
Revision as of 15:56, 13 August 2019 by Pickysaurus (talk | contribs)
Jump to: navigation, search
Info.png
Notice
Work in progress

This guide will explain how to create a very basic game extension for Vortex, touching on where to start with some of the more advanced features. It requires a basic understanding of Javascript/programming. I also recommend you use an application with syntax highlighting, such as Visual Studio Code or Notepad++ for the coding sections of the guide.

Getting Set Up

To get started, you first need a version of Vortex to work with. I recommend using the current release of Vortex installed at the default location. If you're familiar with Github and other development tools you can also follow the instructions on the Vortex Github to clone the repository and build a development environment. To keep things simple, we'll be using the first option.

You'll also need to gather some information about the game you want to support before starting. This information will help us build our extension.

  • Which game store(s) are you able to get this game from?
  • Do the game stores have any useful meta-data we can use? (SteamApp ID, Epic store codename, GOG app ID, registry key etc)
  • What is the structure of the game directory? Where is the main EXE file?
  • Where should mods be installed? Are there multiple different ways to install mods?
  • How are mod archives usually structured? How consistent are they?

If you can't answer most of these questions, you may have problems creating a completely functional extension. For our example, we'll be using Bloodstained: Ritual of the Night. Below are example answers to the setup questions.


  • Which game store(s) are you able to get this game from?

The game is available on GOG.com and Steam.

  • Do the game stores have any useful meta-data we can use? (SteamApp ID, Epic store codename, GOG app ID, registry key etc)

Steam App ID: 692850, GOG App ID: 1133514031

  • What is the structure of the game directory? Where is the main EXE file?

The main game EXE is located at "BloodstainedROTN/Binaries/Win64/BloodstainedRotN-Win64-Shipping.exe" and there is also a launcher located at "BloodstainedRotN.exe".

  • Where should mods be installed? Are there multiple different ways to install mods?

Most mods for this game are presented as .pak files which are placed in the folder "BloodstainedRotN/Content/Paks/~mods" or "BloodstainedRotN/Content/Paks/~mod". The "~mods" option is the community standard.

  • How are mod archives usually structured? How consistent are they?

The majority of mods have the .pak files on the root level of the mod archive, however, some mod archives contain variants or are structured in different ways.

Creating your extension

A basic game extension consists of 3 files an info file, a game image and a javascript file. First, we will need a folder for our extension. If you're using a regular installation of Vortex, navigate to AppData\Roaming\Vortex\plugins. If you're using a Github repository, you can create this folder at vortex\extensions\games. For consistency, give your folder the same name as your game with no spaces, prefixed with "game-" e.g. "game-bloodstainedritualofthenight". Now, open this folder and we're ready to get set up.


For the game image, we recommend taking the game tile from Nexus Mods, however, if you're not able to do this you can use something else. Just ensure it's the same ratio as the other images inside Vortex. It's also important that you name it "gameart", as this will help us find it later.


For the info file, create a new JSON file called "info.json" and fill it with the following information:

{
  "name": "Game: Bloodstained: Ritual of the Night",
  "author": "Pickysaurus",
  "version": "0.0.1",
  "description": "Support for Bloodstained: Ritual of the Night"
}

Note: Using a version less than 1.0.0 will flag this extension as Beta in the Vortex UI.


Finally, we'll create the main javascript file called "index.js". Inside the file, add the following basic code.

//Import some assets from Vortex we'll need.
const path = require('path');
const { fs, log, util } = require('vortex-api');

function main(context) {
	//This is the main function Vortex will run when detecting the game extension. 
	
	return true
}

module.exports = {
    default: main,
  };

With this code in place, your extension will now be recognised by Vortex (a restart will be required) but does not do anything. You can check the "Extensions" tab in advanced mode to see that it is loading without errors.


Registering your game

Now we have the basic information in place, we need to tell Vortex that we're adding a new game. First, we'll want to add some additional constants to the top of the index.js.

// Nexus Mods domain for the game. e.g. nexusmods.com/bloodstainedritualofthenight
const GAME_ID = 'bloodstainedritualofthenight';

//Steam Application ID, you can get this from https://steamdb.info/apps/
const STEAMAPP_ID = 692850;

//GOG Application ID, you can get this from https://www.gogdb.org/
const GOGAPP_ID = 1133514031;

Next, add the following code to your index.js above the "return true" line inside the "main" function and change it according to the instructions below:

context.registerGame({
    id: GAME_ID,
    name: 'Bloodstained: Ritual of the Night',
    mergeMods: true,
    queryPath: findGame,
    supportedTools: [],
    queryModPath: () => 'BloodstainedRotN/Content/Paks/~mods',
    logo: 'gameart.jpg',
    executable: () => 'BloodstainedROTN.exe',
    requiredFiles: [
      'BloodstainedRotN.exe',
      'BloodstainedROTN/Binaries/Win64/BloodstainedRotN-Win64-Shipping.exe'
    ],
    setup: prepareForModding,
    environment: {
      SteamAPPId: STEAMAPP_ID.toString(),
    },
    details: {
      steamAppId: STEAMAPP_ID,
      gogAppId: GOGAPP_ID,
    },
  });


Property Description
id This can be filled with the constant we defined in the previous step.
name The full title of your game, this will be how it appears inside Vortex.
mergeMods This defines if mods will be installed to the same folder (merged) or installed to their own folders.
queryPath We want to fill this with the root directory of the game. In this example, we'll be using the "findGame" function (discussed later) to allow us to check for the correct folder.
supportedTools See: Defining tools
queryModPath This is where we tell Vortex how to find the mods folder.
logo Make sure this matches the name of your gameart file.
executable This is where we tell Vortex how to find the main game executable, so we'll be able to launch the game.
requiredFiles Fill this with an array of ket files that should be found in the game folder. Vortex will know that it has found the correct folder for the game if all requiredFiles are present.
setup This property is optional but is used if we need to make the game folder ready to accept mods. In this example, we need to create the "~mods" folder using the prepareForModding function.
environment If the game must be run through Steam you should include the SteamAppId property as shown above. This allows the game to be launched directly from the EXE file, rather than through the Steam Client. We have to convert our STEAMAPP_ID to a string as otherwise, it isn't a valid variable.
details We can store the SteamAppId/GOGAppId here in case we need it, you can also add other details which may be used by other extensions.

Game detection

Now we need to create the "findGame" function mentioned earlier. This will be what Vortex uses to discover the game during a search. You can use different methods (or a combination) to detect the game. The most common instances are SteamApp ID and registry key.

Find our game with Steam:

function findGame() {
  return util.steam.findByAppId(STEAMAPP_ID.toString())
      .then(game => game.gamePath);
}

Find our game from the registry:

//Add this to the top of the file
const winapi = require('winapi-bindings');

function findGame() {
    const instPath = winapi.RegGetValue(
      'HKEY_LOCAL_MACHINE',
      'SOFTWARE\\WOW6432Node\\GOG.com\\Games\\' + GOGAPP_ID,
      'PATH');
    if (!instPath) {
      throw new Error('empty registry key');
    }
    return Promise.resolve(instPath.value);
}

Using both Steam and registry methods together:

//Add this to the top of the file
const winapi = require('winapi-bindings');

function findGame() {
  try {
    const instPath = winapi.RegGetValue(
      'HKEY_LOCAL_MACHINE',
      'SOFTWARE\\WOW6432Node\\GOG.com\\Games\\' + GOGAPP_ID,
      'PATH');
    if (!instPath) {
      throw new Error('empty registry key');
    }
    return Promise.resolve(instPath.value);
  } catch (err) {
    return util.steam.findByAppId(STEAMAPP_ID.toString())
      .then(game => game.gamePath);
  }
}

At this point, you have now created a working game extension. This extension will take the contents of the downloaded mod archive and deploy them into the BloodstainedRotN/Content/Paks/~mods folder. As you might be aware, not every mod is packed in a consistent way. We might want a way of checking the archive structure and altering it slightly to fit the standardised modding pattern. This is where mod installation patterns become important.

Mod installation patterns

In order to have Vortex understand different formats that mods can take, we want to register an installer that will be used to check for the relevant files. In our example, Bloodstained: Ritual of the Night, most mods take the form of a PAK file which must be placed in the ~mods folder. The problem arises when authors pack the mod inside a subfolder when packing the archive, without a mod installer this would be deployed to ~mods/MyFolder rather than ~mods and could not be loaded by the game.

To register an installer, we'll need to add the following line to the main function, after registering the game.

context.registerInstaller('bloodstainedrotn-mod', 25, testSupportedContent, installContent);

The variables when registering an installer are as follows:

  • 'bloodstainedrotn-mod' - This string must be unique and signifies the installer's "name".
  • 25 - This is the installer priority. Vortex uses this internally for the order installers are checked.
  • testSupportedContent - This is the function we'll use to check if the files qualify for this installer.
  • installContent - If testSupportedContent passes, this is the function that will be performed on the files.


We also want to define what we're looking for, in this case, it's a PAK file extension. Add this to the top of the script, with the other constants.

const MOD_FILE_EXT = ".pak";

Next, we want to define the test and install functions for this pattern.

function testSupportedContent(files, gameId) {
  // Make sure we're able to support this mod.
  let supported = (gameId === BLOODSTAINED_ID) &&
    (files.find(file => path.extname(file).toLowerCase() === MOD_FILE_EXT) !== undefined);

  return Promise.resolve({
    supported,
    requiredFiles: [],
  });
}

Here, what we are doing is first ensuring the mod we're checking is for the correct game, then we'll check if any of the files in the archive have the PAK file extension. If either of these checks fail the archive will not be installed using this pattern. When we resolve the promise, we also need to include an empty array for requiredFiles. This is not used in our example.

function installContent(files) {
  // The .pak file is expected to always be positioned in the mods directory we're going to disregard anything placed outside the root.
  const modFile = files.find(file => path.extname(file).toLowerCase() === MOD_FILE_EXT);
  const idx = modFile.indexOf(path.basename(modFile));
  const rootPath = path.dirname(modFile);
  
  // Remove directories and anything that isn't in the rootPath.
  const filtered = files.filter(file => 
    ((file.indexOf(rootPath) !== -1) 
    && (!file.endsWith(path.sep))));

  const instructions = filtered.map(file => {
    return {
      type: 'copy',
      source: file,
      destination: path.join(file.substr(idx)),
    };
  });

  return Promise.resolve({ instructions });
}

Now, having passed the test we'll install the files. In this example, we're finding the file with the PAK extension and getting the path. Then we remove anything that isn't on the same level as the PAK file (including extra folders above the PAK). Finally, copy the files into the mod staging folder. This will result in the PAK file being on the top level when opening the mod in a file manager.

It is possible to repeat this section for additional mod installers, especially where a game has different types of mods installed to different locations. A good example of a game with multiple install paths is Blade & Sorcery.

The next question you might be asking is "What if a mod has several variants in the same download?". There is a solution to this, but it does require some cooperation from the mod authors in the community. We discuss Mod Installers in the next section.

Mod installers

A useful feature for mod authors is the ability to pack different variants of the same mod into a single package. This can be done by creating a Mod installer. In this section, we will discuss how to allow Vortex to process these mod installers, rather than running the installation pattern shown above. Vortex is actually able to process mod installers natively, we just need to make sure our custom mod patterns ignore the archive if it contains a mod installer.

The change is relatively simple, we just need to add a section to the test function we created earlier.

function testSupportedContent(files, gameId) {
  // Make sure we're able to support this mod.
  let supported = (gameId === BLOODSTAINED_ID) &&
    (files.find(file => path.extname(file).toLowerCase() === MOD_FILE_EXT) !== undefined);

  // Test for a mod installer.
  if (supported && files.find(file =>
      (path.basename(file).toLowerCase() === 'moduleconfig.xml')
      && (path.basename(path.dirname(file)).toLowerCase() === 'fomod'))) {
    supported = false;
  }

  return Promise.resolve({
    supported,
    requiredFiles: [],
  });
}

Mod installers can be identified by the presence of a folder called "fomod" and an XML document called "moduleconfig.xml". So we are checking if these two things are inside our mod archive if they are we'll abort this install method. Vortex will then run the mod installer and output the result to the ~mods folder.

Publishing your extension

Advanced options

Defining tools

Requiring an external modding tool

Multiple install patterns