Skip to content
ccjmk edited this page Mar 18, 2022 · 9 revisions

This wiki is intended for anyone looking for information how to create new Commander commands, persisting them so they don't banish after closing Foundry, and maybe contributing to Commander itself!

You can find examples of commands for reference HERE.

API & Helpers

The module provides an API available at the Module instance, alongside some helper functions. You can access them by doing:

const {api, helpers} = game.modules.get('commander');

api.commands

Array of all registered commands.

api.execute (commandString)

Tries to execute the command matching the commandString; first it parses the command name and checked if the command exists and is allowed. If so, then gets the command schema and tries to match the input to it, extracting the arguments. Then calls the command's handler function with said arguments.

api.register (command, replace?)

Receives a Command to register; the command is only registered if it passes integrity checks and it does not already exist (commands are identified by name). You can replace existing commands by sending a boolean flag as second argument. Commands have to be of the following shape:

interface Command {
  name: string; // must be lowercase
  namespace: string; // must be lowercase; unused for now but mandatory
  description?: string;
  schema: string; // must start with name, followed by argument names prefixed with '$'
  args: Argument[];
  allow?: () => boolean;
  handler: (...params: any) => any;
}
interface Argument {
  name: string; // 'string'|'number'|'boolean'|'raw' names are reserved
  type: ARGUMENT_TYPES;
  suggestions?: (...params: any) => Suggestion[];
}
enum ARGUMENT_TYPES {
  'string', // accepts spaces ONLY IF you write the next between quotes.
  'number', // accepts numbers with decimals. It's just parseFloat(arg), so be tame with the decimals. Consider yourself warned!
  'boolean', // accepts 'true', 'on', 'false', 'off'
  'raw', // returns the whole remaining input string. If used with other arguments this MUST BE LAST.
}
interface Suggestion {
  content: string; // what is shown on the suggestion
  icon?: string; // icon is a font-awesome class name, takes precedence over img
  img?: string;
  bold?: boolean; // not implemented yet
  italics?: boolean; // not implemented yet
}

helpers.hasRole (role)

Receives a string and tries to match it to a role from CONST.USER_ROLES. If it's a valid role, returns the allow callback function that will check for this role whenever a command using that function is invoked. Example:

// while defining a new command..
api.register({
  name: "mycommand",
  allow: helpers.hasRole('TRUSTED'), // this command can be invoked by a user with role TRUSTED or more
  ...
})

helpers.hasPermissions (...permissions)

Receives a list of permission strings, and tries to match them to a permission from CONST.USER_PERMISSIONS. If all listed permissions are valid, returns the allow callback function that will check for ALL of these permissions whenever a command using that function is invoked. Example:

// while defining a new command..
api.register({
  name: "mycommand",
  allow: helpers.hasPermissions('ACTOR_CREATE', 'ITEM_CREATE'), // this command can be invoked by a user with both the ACTOR_CREATE and ITEM_CREATE permissions
  ...
})

Hooks

The tool provides the following hooks:

commanderReady

Called when Commander has finished initialization. At this point, the keybinding is functional and the API is available. Receives the Commander instance ready to be used. This hook will be called on ready.

commanderExecute

Called when Commander has been asked to run a command, either via the provided widget or directly via the API. Receives the execution string requested to be executed.

Persisting Commands

Currently the module doesn't provide yet a way to persist the registered Commands long-term (It's the reason the api doesn't provide any unregister too, as Commands will disappear on reload), therefore you will need to register your Commands on every session. Alternatives are:

  • Commands that come bundled with Commander are registered on setup, before calling the CommanderReady hook, so if you intend on overwriting any of the bundled Commands, you can do so safely on that hook by using the replace flag on register.

  • The simplest way would be adding all your Commands on a single macro you run once every game (though it's a rather lame solution, you can do better!)

  • Next best would be hooking on CommanderReady on a world script, that would execute itself on each reload.

// Example world script

console.info("Running world script");
Hooks.on("commanderReady", () => {
    let { api, helpers } = game.modules.get('commander');

    const newCommand = {...} // fill in the command data here - the command name doesn't match an existing command
    api.register(newCommand );

    const existingCommand = {...} // fill in the command data here - the command name matches an existing command
    api.register(existingCommand, true); // without this flag, this register will throw an exception
});
  • Alternatively, if you have a system/module that provides Commands, you can follow the world-script advice and hook to CommanderReady.