mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-04-22 06:02:07 +02:00
246 lines
8.6 KiB
JavaScript
246 lines
8.6 KiB
JavaScript
import { getBlockId, getItemId } from "../../utils/mcdata.js";
|
|
import { actionsList } from './actions.js';
|
|
import { queryList } from './queries.js';
|
|
|
|
let suppressNoDomainWarning = false;
|
|
|
|
const commandList = queryList.concat(actionsList);
|
|
const commandMap = {};
|
|
for (let command of commandList) {
|
|
commandMap[command.name] = command;
|
|
}
|
|
|
|
export function getCommand(name) {
|
|
return commandMap[name];
|
|
}
|
|
|
|
const commandRegex = /!(\w+)(?:\(([\s\S]*)\))?/
|
|
const argRegex = /(?:"[^"]*"|'[^']*'|[^,])+/g;
|
|
|
|
export function containsCommand(message) {
|
|
const commandMatch = message.match(commandRegex);
|
|
if (commandMatch)
|
|
return "!" + commandMatch[1];
|
|
return null;
|
|
}
|
|
|
|
export function commandExists(commandName) {
|
|
if (!commandName.startsWith("!"))
|
|
commandName = "!" + commandName;
|
|
return commandMap[commandName] !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Converts a string into a boolean.
|
|
* @param {string} input
|
|
* @returns {boolean | null} the boolean or `null` if it could not be parsed.
|
|
* */
|
|
function parseBoolean(input) {
|
|
switch(input.toLowerCase()) {
|
|
case 'false': //These are interpreted as flase;
|
|
case 'f':
|
|
case '0':
|
|
case 'off':
|
|
return false;
|
|
case 'true': //These are interpreted as true;
|
|
case 't':
|
|
case '1':
|
|
case 'on':
|
|
return true;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} value - the value to check
|
|
* @param {number} lowerBound
|
|
* @param {number} upperBound
|
|
* @param {string} endpointType - The type of the endpoints represented as a two character string. `'[)'` `'()'`
|
|
*/
|
|
function checkInInterval(number, lowerBound, upperBound, endpointType) {
|
|
switch (endpointType) {
|
|
case '[)':
|
|
return lowerBound <= number && number < upperBound;
|
|
case '()':
|
|
return lowerBound < number && number < upperBound;
|
|
case '(]':
|
|
return lowerBound < number && number <= upperBound;
|
|
case '[]':
|
|
return lowerBound <= number && number <= upperBound;
|
|
default:
|
|
throw new Error('Unknown endpoint type:', endpointType)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// todo: handle arrays?
|
|
/**
|
|
* Returns an object containing the command, the command name, and the comand parameters.
|
|
* If parsing unsuccessful, returns an error message as a string.
|
|
* @param {string} message - A message from a player or language model containing a command.
|
|
* @returns {string | Object}
|
|
*/
|
|
function parseCommandMessage(message) {
|
|
const commandMatch = message.match(commandRegex);
|
|
if (!commandMatch) return `Command is incorrectly formatted`;
|
|
|
|
const commandName = "!"+commandMatch[1];
|
|
|
|
let args;
|
|
if (commandMatch[2]) args = commandMatch[2].match(argRegex);
|
|
else args = [];
|
|
|
|
const command = getCommand(commandName);
|
|
if(!command) return `${commandName} is not a command.`
|
|
|
|
const params = commandParams(command);
|
|
const paramNames = commandParamNames(command);
|
|
|
|
if (args.length !== params.length)
|
|
return `Command ${command.name} was given ${args.length} args, but requires ${params.length} args.`;
|
|
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const param = params[i];
|
|
//Remove any extra characters
|
|
let arg = args[i].trim();
|
|
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
|
|
arg = arg.substring(1, arg.length-1);
|
|
}
|
|
|
|
if (arg.includes('=')) {
|
|
// this sanitizes syntaxes like "x=2" and ignores the param name
|
|
let split = arg.split('=');
|
|
args[i] = split[1];
|
|
}
|
|
|
|
//Convert to the correct type
|
|
switch(param.type) {
|
|
case 'int':
|
|
arg = Number.parseInt(arg); break;
|
|
case 'float':
|
|
arg = Number.parseFloat(arg); break;
|
|
case 'boolean':
|
|
arg = parseBoolean(arg); break;
|
|
case 'BlockName':
|
|
case 'ItemName':
|
|
if (arg.endsWith('plank'))
|
|
arg += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks"
|
|
case 'string':
|
|
break;
|
|
default:
|
|
throw new Error(`Command '${commandName}' parameter '${paramNames[i]}' has an unknown type: ${param.type}`);
|
|
}
|
|
if(arg === null || Number.isNaN(arg))
|
|
return `Error: Param '${paramNames[i]}' must be of type ${param.type}.`
|
|
|
|
if(typeof arg === 'number') { //Check the domain of numbers
|
|
const domain = param.domain;
|
|
if(domain) {
|
|
/**
|
|
* Javascript has a built in object for sets but not intervals.
|
|
* Currently the interval (lowerbound,upperbound] is represented as an Array: `[lowerbound, upperbound, '(]']`
|
|
*/
|
|
if (!domain[2]) domain[2] = '[)'; //By default, lower bound is included. Upper is not.
|
|
|
|
if(!checkInInterval(arg, ...domain)) {
|
|
return `Error: Param '${paramNames[i]}' must be an element of ${domain[2][0]}${domain[0]}, ${domain[1]}${domain[2][1]}.`;
|
|
//Alternatively arg could be set to the nearest value in the domain.
|
|
}
|
|
} else if (!suppressNoDomainWarning) {
|
|
console.warn(`Command '${commandName}' parameter '${paramNames[i]}' has no domain set. Expect any value [-Infinity, Infinity].`)
|
|
suppressNoDomainWarning = true; //Don't spam console. Only give the warning once.
|
|
}
|
|
} else if(param.type === 'BlockName') { //Check that there is a block with this name
|
|
if(getBlockId(arg) == null) return `Invalid block type: ${arg}.`
|
|
} else if(param.type === 'ItemName') { //Check that there is an item with this name
|
|
if(getItemId(arg) == null) return `Invalid item type: ${arg}.`
|
|
}
|
|
args[i] = arg;
|
|
}
|
|
|
|
return { commandName, args };
|
|
}
|
|
|
|
export function truncCommandMessage(message) {
|
|
const commandMatch = message.match(commandRegex);
|
|
if (commandMatch) {
|
|
return message.substring(0, commandMatch.index + commandMatch[0].length);
|
|
}
|
|
return message;
|
|
}
|
|
|
|
export function isAction(name) {
|
|
return actionsList.find(action => action.name === name) !== undefined;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} command
|
|
* @returns {Object[]} The command's parameters.
|
|
*/
|
|
function commandParams(command) {
|
|
if (!command.params)
|
|
return [];
|
|
return Object.values(command.params);
|
|
}
|
|
|
|
/**
|
|
* @param {Object} command
|
|
* @returns {string[]} The names of the command's parameters.
|
|
*/
|
|
function commandParamNames(command) {
|
|
if (!command.params)
|
|
return [];
|
|
return Object.keys(command.params);
|
|
}
|
|
|
|
function numParams(command) {
|
|
return commandParams(command).length;
|
|
}
|
|
|
|
export async function executeCommand(agent, message) {
|
|
let parsed = parseCommandMessage(message);
|
|
if (typeof parsed === 'string')
|
|
return parsed; //The command was incorrectly formatted or an invalid input was given.
|
|
else {
|
|
console.log('parsed command:', parsed);
|
|
const command = getCommand(parsed.commandName);
|
|
let numArgs = 0;
|
|
if (parsed.args) {
|
|
numArgs = parsed.args.length;
|
|
}
|
|
if (numArgs !== numParams(command))
|
|
return `Command ${command.name} was given ${numArgs} args, but requires ${numParams(command)} args.`;
|
|
else {
|
|
const result = await command.perform(agent, ...parsed.args);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getCommandDocs() {
|
|
const typeTranslations = {
|
|
//This was added to keep the prompt the same as before type checks were implemented.
|
|
//If the language model is giving invalid inputs changing this might help.
|
|
'float': 'number',
|
|
'int': 'number',
|
|
'BlockName': 'string',
|
|
'ItemName': 'string',
|
|
'boolean': 'bool'
|
|
}
|
|
let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world.
|
|
Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n
|
|
Do not use codeblocks. Only use one command in each response, trailing commands and comments will be ignored.\n`;
|
|
for (let command of commandList) {
|
|
docs += command.name + ': ' + command.description + '\n';
|
|
if (command.params) {
|
|
docs += 'Params:\n';
|
|
for (let param in command.params) {
|
|
docs += `${param}: (${typeTranslations[command.params[param].type]??command.params[param].type}) ${command.params[param].description}\n`;
|
|
}
|
|
}
|
|
}
|
|
return docs + '*\n';
|
|
}
|