|
|
- import ChatCommand from "./chatCommand.mjs";
-
- /**
- * Registry for chat commands and utility methods.
- */
- class ChatCommands {
- /**
- * The map of currently registered commands and their aliases. Each alias has a separate entry that points to the
- * same @see ChatCommand instance.
- * @type {Map.<string, ChatCommand>}
- */
- commands = new Map();
-
- /**
- * A set of valid command start characters. These are used to quickly verify whether a command is present.
- * @type {Set.<string>}
- * @private
- */
- startChars = new Set();
-
- /**
- * Attaches the API to the game instance and registers a hook to handle chat messages.
- * @package
- */
- static initialize() {
- game.chatCommands = new ChatCommands();
- game.modules.get("_chatcommands").api = game.chatCommands;
- Hooks.on("chatMessage", (chat, message, data) => game.chatCommands.handleMessage(chat, message, data));
- Hooks.on('renderChatLog', (_, html) => {
- const loader = document.createElement("div");
- loader.id = "chatcommand-loading";
- loader.dataset.active = 0;
- html[0].querySelector("#chat-message").before(loader);
- });
- }
-
- /**
- * Returns the class implementing a single chat command.
- * @returns The @see ChatCommand class.
- */
- get commandClass() {
- return ChatCommand;
- }
-
- /**
- * Registers a single chat command using its data.
- * @see ChatCommand.constructor for valid fields.
- * @param {object|ChatCommand} command The command object to register.
- * @param {boolean=} override Force the new command to override existing entries. Defaults to false.
- */
- register(command, override = false) {
- if (command.commandKey) {
- command.name = command.commandKey;
- console.warn("Chat Commander | The commandKey property is deprecated. Please use the newer name property.");
- }
-
- if (command.iconClass) {
- command.icon = `<i class="fas ${command.iconClass}"></i>`;
- console.warn("Chat Commander | The iconClass property is deprecated. Please use the newer icon property.");
- }
-
- if (!command.module) {
- command.module = "Unknown";
- console.warn(`Chat Commander | Command ${command.name} does not have a module property (should be module ID).`);
- }
-
- if (command.gmOnly) {
- command.requiredRole = "GAMEMASTER";
- console.warn("Chat Commander | The gmOnly property is deprecated. Please use the newer requiredRole property.");
- }
-
- if (command.invokeOnCommand) {
- console.warn("Chat Commander | The invokeOnCommand property is deprecated. Please use the newer callback property.");
- command.callback = async (chat, parameters, messageData) => {
- let text = command.invokeOnCommand(chat, parameters.trimEnd(), messageData);
- if (text instanceof Promise) text = await text;
- if (!command.shouldDisplayToChat) return {};
- return {
- content: text,
- type: command.createdMessageType ?? messageData.createdMessageType
- }
- }
- }
-
- if (!(command instanceof ChatCommand)) command = new ChatCommand(command);
- command.names.forEach(c => {
- const existing = this.commands.get(c);
- if (existing) {
- if (override || existing.module === "core") {
- // Allow force override or replacing core commands.
- console.info(`Chat Commander | Overriding existing command ${c}.`);
- } else if (c === command.name) {
- if (c === existing.name) {
- // Both commands are original names, use a namespace to disambiguate.
- console.warn(`Chat Commander | Using namespace for command ${c} due to conflict.`);
- command.name = c = c[0] + command.module + "." + command.name.substring(1);
- } else {
- // Allow replacing aliases.
- console.warn(`Chat Commander | Overriding alias ${c} with new command.`);
- existing.removeAlias(c);
- }
- } else {
- // Prevent aliases from replacing commands.
- console.warn(`Chat Commander | Prevented alias override for command ${c}.`);
- command.removeAlias(c);
- return;
- }
- }
-
- this.commands.set(c, command);
- this.startChars.add(c[0]);
- });
-
- console.info(`Chat Commander | Module ${command.module} registered command ${command.name} with ${command.aliases.length} aliases.`);
- }
-
- /**
- * Unregisters the given chat command and its aliases.
- * @param {string|ChatCommand} name The name of the command or the command itself.
- */
- unregister(name) {
- const command = typeof (name) === "string" ? this.commands.get(name.toLowerCase()) : name;
- if (!command) return;
-
- command.names.forEach(c => this.commands.delete(c));
- console.info(`Chat Commander | Unregistered command ${command.name} with ${command.aliases.length}`);
- }
-
- /**
- * Creates a selectable list entry for a single command.
- * @param {string} command The command that the entry applies when it is selected.
- * @param {string} content An HTML string containing the displayed entry.
- * @returns {HTMLElement} An HTML element containing a selectable command entry.
- */
- createCommandElement(command, content) {
- const el = document.createElement("li");
- el.dataset.command = command;
- el.classList.add("context-item", "command");
- el.tabIndex = 0;
- el.innerHTML = content;
- return el;
- }
-
- /**
- * Creates a non-selectable list entry for displaying additional information.
- * @param {string} content An HTML string containing the displayed entry.
- * @returns {HTMLElement} An HTML element containing an informational entry.
- */
- createInfoElement(content) {
- const el = document.createElement("li");
- el.classList.add("context-item", "info");
- el.disabled = true;
- el.innerHTML = content;
- return el;
- }
-
- /**
- * Creates a list entry displaying a separator.
- * @returns {HTMLElement} An HTML element containing a separator entry.
- */
- createSeparatorElement() {
- const el = document.createElement("li");
- el.classList.add("context-item", "separator");
- el.disabled = true;
- el.appendChild(document.createElement("hr"));
- return el;
- }
-
- /**
- * Creates a list entry displaying a loading indicator.
- * @returns {HTMLElement} An HTML element containing a loading entry.
- */
- createLoadingElement() {
- const el = document.createElement("li");
- el.classList.add("context-item", "loading");
- el.disabled = true;
- return el;
- }
-
- /**
- * Creates an informational element indicating that more results are available.
- * @returns {HTMLElement} An HTML element containing the overflow hint.
- */
- createOverflowElement() {
- return this.createInfoElement(`<p class="notes">${game.i18n.localize("_chatcommands.extraEntries")}</p>`);
- }
-
- /**
- * Checks if the given text might contain a command. Note that this only checks if the first character is a command
- * character, not that a command with that input actually exists.
- * @param {string} text The text to check.
- * @returns {boolean} True if the input might be a command, false otherwise.
- */
- isCommand(text) {
- return text && this.startChars.has(text[0].toLowerCase());
- }
-
- /**
- * Parses the given text to find a command and its parameters.
- * @param {string} text The text to search for commands in.
- * @returns {object?} An object containing the command itself, the used alias and the parameters (or null if no
- * command was found).
- */
- parseCommand(text) {
- if (!this.isCommand(text)) return null;
-
- // Check for single character commands.
- const separator = game.chatCommands.commands.has(text[0].toLowerCase()) ? 1 : text.indexOf(' ');
-
- // Extract the name of the command.
- let alias = text.toLowerCase();
- if (separator !== -1) alias = alias.substring(0, separator);
- if (alias === "") return null;
-
- // Look for a command matching the name.
- const command = this.commands.get(alias);
- if (!command) return null;
-
- // Extract parameters.
- return {
- command,
- alias,
- parameters: separator > 0 ? text.substring(alias.length === 1 ? separator : separator + 1) : ""
- };
- }
-
- /**
- * Processes a chat message to check if it contains a command. If so, a permission check is performed, the command
- * is invoked and the invokeChatCommand hook is called. The result indicates whether a message will be sent.
- * @param {ChatLog} chat The chat that emitted the message.
- * @param {string} message The content of the message to send.
- * @param {object} messageData The data of the message to send.
- * @returns {boolean?} False if the command was handled, undefined otherwise.
- */
- handleMessage(chat, message, messageData) {
- const commandInfo = this.parseCommand(message);
- if (!commandInfo) return;
-
- if (!commandInfo.command.canInvoke()) {
- ui.notifications.error(game.i18n.format("_chatcommands.insufficientPermissions", { command: commandInfo.alias }));
- return false;
- }
-
- // Invoke the command with its parameters.
- let result = commandInfo.command.invoke(chat, commandInfo.parameters, messageData);
- if (result instanceof Promise) {
- const loader = chat.element[0].querySelector("#chatcommand-loading");
- loader.dataset.active++;
- result.then(r =>
- this._resumeMessageHandling(chat, commandInfo.command, commandInfo.parameters, r ?? {}, messageData))
- .finally(() => loader.dataset.active--);
- return false;
- } else {
- return this._resumeMessageHandling(chat, commandInfo.command, commandInfo.parameters, result, messageData);
- }
- }
-
- /**
- * Resumes asynchronous processing of a chat message that contains a command.
- * @param {ChatLog} chat The chat that emitted the message.
- * @param {ChatCommand} command The command that was invoked by the message.
- * @param {string} parameters The parameters of the command invocation.
- * @param {object?} result The result of the command invocation.
- * @param {object} messageData The data of the message to send.
- * @returns {boolean?} False if the hook listeners prevented core handling, undefined otherwise.
- * @private
- */
- _resumeMessageHandling(chat, command, parameters, result, messageData) {
- // Handle internally or forward to FoundryVTT core.
- const options = { handleCore: !result };
- result ??= {}; // Allow hook listeners to add content even if the command returned none.
- Hooks.callAll("invokeChatCommand", chat, command, parameters, result, options);
- if (result.content) {
- result.content = result.content.replace(/\n/g, "<br>");
- getDocumentClass("ChatMessage").create(foundry.utils.mergeObject(messageData, result));
- } else if (options.handleCore) return;
- return false;
- }
-
- /** @deprecated */
- registerCommand(command) {
- console.warn("Chat Commander | The registerCommand method is deprecated. Please use the newer register method.");
- this.register(command);
- }
-
- /** @deprecated */
- deregisterCommand(command) {
- console.warn("Chat Commander | The deregisterCommand method is deprecated."
- + " Please use the newer unregister method.");
- this.unregister(command.commandKey ?? command.name);
- }
-
- /** @deprecated */
- createCommand(commandKey, shouldDisplayToChat, invokeOnCommand, createdMessageType = 0) {
- console.warn("Chat Commander | The createCommand method is obsolete."
- + " Please create a command data object and pass it directly to the register method.");
- return {
- name: commandKey,
- shouldDisplayToChat,
- invokeOnCommand,
- createdMessageType,
- iconClass: "",
- description: ""
- };
- }
-
- /** @deprecated */
- createCommandFromData(data) {
- console.warn("Chat Commander | The createCommandFromData method is obsolete."
- + " Please pass the command data directly to the register method.");
- data.name ??= data.commandKey;
- data.shouldDisplayToChat ??= false;
- data.createdMessageType ??= 0;
- data.iconClass ??= "";
- data.description ??= "";
- data.requiredRole ??= "NONE";
- return data;
- }
- }
-
- export default ChatCommands;
|