diff --git a/commands/eval.js b/commands/eval.js deleted file mode 100644 index c7f57c9..0000000 --- a/commands/eval.js +++ /dev/null @@ -1,16 +0,0 @@ -const name = 'eval' -const description = 'secure!!1' -const usages = [''] -const aliases = ['eval'] -const enabled = true - -const permLevel = 0 - -const util = require('util') - -function execute (bot, cmd, player, args, handler) { - const result = bot.eval(args.join(' ').replace(/\xa7.?/g, '')) - bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ text: util.inspect(result), color: bot.colors.primary })) -} - -module.exports = { name, description, usages, aliases, enabled, execute, permLevel } diff --git a/commands/help.js b/commands/help.js index 538c441..1296d80 100644 --- a/commands/help.js +++ b/commands/help.js @@ -1,81 +1,98 @@ -const name = 'help' -const description = 'Lists commands or shows info about a command.' -const usages = ['[command]'] -const aliases = ['help'] -const enabled = true +const { CommandDispatcher, builder: { LiteralArgumentBuilder: { literal }, RequiredArgumentBuilder: { argument } }, arguments: { StringArgumentType: { greedyString } } } = require('brigadier-commands') -const permLevel = 0 +module.exports = { + register (dispatcher) { + const node = dispatcher.register( + literal('help') + .executes(this.listCommands) + .then( + argument('command', greedyString()) + .executes(this.showCommandInfo) + ) + ) -function execute (bot, cmd, entity, args) { - if (args.length > 0) { - if (!bot.commands.isCommand(args[0])) { return bot.core.run(`/tellraw @a ${JSON.stringify({ text: `Unknown command: ${bot.prefix}${args[0]}`, color: bot.colors.error })}`) } + node.description = 'Lists commands or shows info about a command' + node.permissionLevel = 0 + }, - const command = bot.commands.info(args.shift()) + listCommands (context) { + const source = context.source + const bot = source.bot + + const nodes = bot.commands.dispatcher.root.getChildren() + + const publicList = [] + const trustedList = [] + const adminList = [] + const unknownList = [] + nodes.forEach(node => { + if (node.redirect) return // ignore aliases + + const msg = { + color: 'dark_aqua', + text: bot.prefix + node.name + ' ', + clickEvent: { action: 'suggest_command', value: bot.prefix + 'help ' + node.name }, + hoverEvent: { action: 'show_text', value: 'Click to see info about the command' } + } + if (node.permissionLevel === 0) { + msg.color = 'green' + publicList.push(msg) + } else if (node.permissionLevel === 1) { + msg.color = 'red' + trustedList.push(msg) + } else if (node.permissionLevel === 2) { + msg.color = 'dark_red' + adminList.push(msg) + } else { + unknownList.push(msg) + } + }) + + const msg = [{ text: 'Commands - ', color: 'gray' }, ...publicList, ...trustedList, ...adminList, ...unknownList] + source.sendFeedback(msg, false) + }, + + showCommandInfo (context) { + const source = context.source + const bot = source.bot + + const nodes = bot.commands.dispatcher.root.getChildren() + + const commandName = context.getArgument('command') + let node = nodes.find(node => node.name === commandName) + if (node.redirect) node = node.redirect + + const aliases = [node, ...nodes.filter(_node => _node.redirect === node)].map(node => node.name) + const usages = [...bot.commands.dispatcher.getSmartUsage(node, source, false).values()] let msg - if (command.usages.length !== 1) { + if (usages.length !== 1) { msg = [ - { text: bot.prefix + command.name, color: bot.colors.primary }, - { text: ' (' + command.aliases.join(', ') + ')', color: 'white' }, - { text: ` - ${command.description}\n`, color: 'gray' } + { text: bot.prefix + node.name, color: bot.colors.primary }, + { text: ' (' + aliases.join(', ') + ')', color: 'white' }, + { text: ` - ${node.description}\n`, color: 'gray' } ] - command.usages.forEach((usage, i) => { - msg.push(bot.prefix + command.name) + usages.forEach((usage, i) => { + msg.push(bot.prefix + node.name) msg.push({ text: ` ${usage}\n`, color: bot.colors.secondary, - clickEvent: { action: 'suggest_command', value: command.name + ' ' + usage } - // hoverEvent: { action: 'show_text', value: 'Click to teleport' } + clickEvent: { action: 'suggest_command', value: node.name + ' ' + usage } }) }) msg[msg.length - 1].text = msg[msg.length - 1].text.slice(0, -1) } else { msg = [ - { text: bot.prefix + command.name, color: bot.colors.primary }, - { text: ' (' + command.aliases.join(', ') + ')', color: 'white' }, + { text: bot.prefix + node.name, color: bot.colors.primary }, + { text: ' (' + aliases.join(', ') + ')', color: 'white' }, { - text: ` ${command.usages[0]}`, + text: ` ${usages[0]}`, color: bot.colors.secondary, - clickEvent: { action: 'suggest_command', value: command.name + ' ' + command.usages[0] } + clickEvent: { action: 'suggest_command', value: node.name + ' ' + usages[0] } }, - { text: ` - ${command.description}`, color: 'gray' } + { text: ` - ${node.description}`, color: 'gray' } ] } - return bot.core.run(`minecraft:tellraw @a ${JSON.stringify(msg)}`) + source.sendFeedback(msg, false) } - let commands = [] - Object.keys(bot.commands.commands).forEach((command) => { - if (bot.commands.isCommand(command) && !commands.includes(bot.commands.info(command))) { commands.push(bot.commands.info(command)) } - }) - commands = commands.filter((command) => command.enabled) - - const publicList = [] - const trustedList = [] - const adminList = [] - const unknownList = [] - commands.forEach((command) => { - const msg = { - color: 'dark_aqua', - text: bot.prefix + command.name + ' ', - clickEvent: { action: 'run_command', value: bot.prefix + aliases[0] + ' ' + command.name }, - hoverEvent: { action: 'show_text', value: 'Click to see info about the command' } - } - if (command.permLevel === 0) { - msg.color = 'green' - publicList.push(msg) - } else if (command.permLevel === 1) { - msg.color = 'red' - trustedList.push(msg) - } else if (command.permLevel === 2) { - msg.color = 'dark_red' - adminList.push(msg) - } else { - unknownList.push(msg) - } - }) - - const msg = [{ text: 'Commands - ', color: 'gray' }, ...publicList, ...trustedList, ...adminList, ...unknownList] - bot.core.run(`/tellraw @a ${JSON.stringify(msg)}`) } - -module.exports = { name, description, usages, aliases, enabled, execute, permLevel } diff --git a/index.js b/index.js index da72f23..6d9ea62 100644 --- a/index.js +++ b/index.js @@ -27,15 +27,16 @@ filepath += '.log' fs.writeFileSync(filepath, '') const servers = [ + 'play.kaboom.pw:25565:kaboom', 'chipmunk.land:25565:kaboom' ] const bots = createBots(servers, { - username: 'MusicBot', + username: ' ', prefix: "'", colors: { primary: 'green', secondary: 'dark_green', error: 'red' }, version: '1.20.4', - randomizeUsername: false, + randomizeUsername: true, autoReconnect: true // 'online-mode': { enabled: false, username: 'removed lol', password: null } }) diff --git a/package-lock.json b/package-lock.json index 1473ea1..12104bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,11 +4,11 @@ "requires": true, "packages": { "": { - "name": "chipmunktest", "dependencies": { "@mozilla/readability": "^0.4.1", "@skeldjs/client": "^2.15.17", "@tonejs/midi": "^2.0.27", + "brigadier-commands": "git+https://code.chipmunk.land/ChipmunkMC/node-brigadier-commands.git", "colorsys": "^1.0.22", "fluent-ffmpeg": "^2.1.2", "kahoot.js-api": "^2.4.0", @@ -517,6 +517,10 @@ "concat-map": "0.0.1" } }, + "node_modules/brigadier-commands": { + "version": "1.0.0", + "resolved": "git+https://code.chipmunk.land/ChipmunkMC/node-brigadier-commands.git#c89271d021a1537d3045a93850e0c7ccb6efd9ae" + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3941,6 +3945,10 @@ "concat-map": "0.0.1" } }, + "brigadier-commands": { + "version": "git+https://code.chipmunk.land/ChipmunkMC/node-brigadier-commands.git#c89271d021a1537d3045a93850e0c7ccb6efd9ae", + "from": "brigadier-commands@git+https://code.chipmunk.land/ChipmunkMC/node-brigadier-commands.git" + }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", diff --git a/package.json b/package.json index eb60d47..c711433 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "prismarine-nbt": "^2.2.0", "rfb2": "^0.2.2", "standard": "^16.0.4", - "urban-dictionary": "git+https://code.chipmunk.land/ChipmunkMC/urban-dictionary.git" + "urban-dictionary": "git+https://code.chipmunk.land/ChipmunkMC/urban-dictionary.git", + "brigadier-commands": "git+https://code.chipmunk.land/ChipmunkMC/node-brigadier-commands.git" } } diff --git a/plugins/commands.js b/plugins/commands.js index f8b78b0..b25f8dd 100644 --- a/plugins/commands.js +++ b/plugins/commands.js @@ -1,10 +1,13 @@ const fs = require('fs') const path = require('path') const util = require('util') +const { CommandDispatcher, builder: { LiteralArgumentBuilder: { literal }, RequiredArgumentBuilder: { argument } }, arguments: { StringArgumentType: { greedyString } }, exceptions: { CommandSyntaxException } } = require('brigadier-commands') +const CommandSource = require('../util/command_source') +const TextMessage = require('../util/text_message') function inject (bot) { bot.commands = { - commands: {}, + dispatcher: new CommandDispatcher(), add, execute, info, @@ -14,57 +17,109 @@ function inject (bot) { } bot.on('message', (player, message) => { - if (!message.startsWith(bot.prefix)) { return } + if (!message.startsWith(bot.prefix)) return - const args = message.slice(bot.prefix.length).split(' ') - const command = args.shift().toLowerCase() - - if (!isCommand(command)) { return bot.core.run(`/tellraw @a ${JSON.stringify({ text: `Unknown command: ${bot.prefix}${command}`, color: bot.colors.error })}`) } - - bot.commands.execute(bot, command, player, args) + const sendFeedback = message => bot.core.run('minecraft:tellraw @a ' + JSON.stringify(message)) + bot.commands.execute(message.substring(bot.prefix.length), new CommandSource({ bot, sendFeedback })) }) function add (command) { - if (!isValid(command)) throw new Error('Invalid command', 'invalid_command') - command.aliases.forEach(alias => (bot.commands.commands[alias.toLowerCase()] = command)) + if (command.register) { + command.register(bot.commands.dispatcher) + return + } + + if (isValid(command)) { + bot.console.warn(`Command '${command.aliases[0]}' is using the legacy command system!`) + + const _execute = args => command.execute(bot, command.aliases[0], {}, args) + + const node = bot.commands.dispatcher.register( + literal(command.aliases[0]) + .executes(context => { _execute([]); return 0 }) + .then( + argument('args', greedyString()) + .executes(context => { _execute(context.getArgument('args').split(' ')); return 0 }) + ) + ) + for (let i = 1; i < command.aliases.length; i++) { + bot.commands.dispatcher.register( + literal(command.aliases[i]) + .executes(context => { _execute([]); return 0 }) + .redirect(node) + ) + } + + // add metadata for help command + node.description = command.description + node.permissionLevel = command.permLevel + + return + } + + throw new Error('Invalid command', 'invalid_command') } + function loadFromDir (dirpath) { fs.readdirSync(dirpath).forEach(filename => { const filepath = path.resolve(dirpath, filename) if (!filepath.endsWith('js') || !fs.statSync(filepath).isFile()) return try { bot.commands.add(require(filepath)) - } catch (err) { - bot.console.error('Error loading command ' + filepath + ': ' + util.inspect(err)) + } catch (error) { + bot.console.error('Error loading command ' + filepath + ': ' + util.inspect(error)) } }) } + function info (command) { const info = bot.commands.commands[command] ?? command if (isValid(info)) { return info } } - function isCommand (command) { return bot.commands.info(command) != null } - async function execute (bot, command, player, args, ...custom) { - const info = bot.commands.info(command) - if (info == null) { - bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ text: 'Unknown command: ' + bot.prefix + command, color: bot.colors.error })) - return - } - if (!info.enabled) { - bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ text: bot.prefix + command + 'is disabled', color: bot.colors.error })) - return - } - if (info.permLevel > 0) { - bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ text: 'Trusted commands are currently disabled', color: bot.colors.error })) - return - } + + function isCommand (command) { return true } + + function execute (command, source) { try { - return await info.execute(bot, command, player, args, ...custom) - } catch (err) { - bot.console.error('Error executing command ' + command + ': ' + util.inspect(err)) - bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ text: err.message, color: bot.colors.error })) + bot.commands.dispatcher.execute(command, source) + } catch (error) { + if (error instanceof CommandSyntaxException) { + const text = (error._message instanceof TextMessage) ? error._message.text : error._message.getString() + source.sendError(text) + const context = getContext(error) + if (context) source.sendError(context) + + return + } + + source.sendError({ translate: 'command.failed', hoverEvent: { action: 'show_text', contents: error.stack } }) } } + + function getContext (error) { + const _cursor = error.cursor + const input = error.input + + if (input == null || _cursor < 0) { + return null + } + + const text = [{ text: '', color: 'gray', clickEvent: { action: 'suggest_command', value: bot.prefix + input } }] + + const cursor = Math.min(input.length, _cursor) + + if (cursor > CommandSyntaxException.CONTEXT_AMOUNT) { + text.push('...') + } + + text.push( + input.substring(Math.max(0, cursor - CommandSyntaxException.CONTEXT_AMOUNT), cursor), + { text: input.substring(cursor), color: 'red', underline: true }, + { translate: 'command.context.here', color: 'red', italic: true } + ) + + return text + } } function isValid (command) { diff --git a/plugins/console.js b/plugins/console.js index 00fabfb..9cdbb3f 100644 --- a/plugins/console.js +++ b/plugins/console.js @@ -1,6 +1,8 @@ const fs = require('fs') const util = require('util') const moment = require('moment') +const CommandSource = require('../util/command_source') +const parseText = require('../util/text_parser') const ansimap = { 0: '\x1b[0m\x1b[30m', 1: '\x1b[0m\x1b[34m', @@ -72,24 +74,18 @@ function inject (bot) { function handleLine (line) { if (bot.host !== bot.console.host && bot.console.host !== 'all') return if (line.startsWith('.')) { - const args = line.slice(1).trim().split(' ') - const command = args.shift() - - if (!bot.commands.isCommand(command)) { - bot.console.error('Unknown command: ' + command) - return - } - const info = bot.commands.info(command) - try { - info.execute(bot, command, bot.player, args) - } catch (err) { - bot.console.error(`Error executing ${command} in console: ${util.inspect(err)}`) - } + const source = new CommandSource({ bot, sendFeedback }) + bot.commands.execute(line.substring(1), source) } else { bot.fancyMsg('test', '_ChipMC_', line) rl?.prompt(true) } } + + function sendFeedback (message) {console.log(message) + const { raw } = parseText(message);console.log(raw) + bot.console.log(raw) + } } } diff --git a/util/command_source.js b/util/command_source.js new file mode 100644 index 0000000..1fe562b --- /dev/null +++ b/util/command_source.js @@ -0,0 +1,13 @@ +class CommandSource { + constructor ({ bot, permissionLevel = 0, sendFeedback = () => {} } = {}) { + this.bot = bot + this.permissionLevel = permissionLevel + this.sendFeedback = sendFeedback + } + + sendError (error) { + this.sendFeedback([{ text: '', color: 'red' }, error], false) + } +} + +module.exports = CommandSource diff --git a/util/text_message.js b/util/text_message.js new file mode 100644 index 0000000..b9e41ba --- /dev/null +++ b/util/text_message.js @@ -0,0 +1,18 @@ +const parseText = require('./text_parser.js') + +class TextMessage { + constructor (text) { + this.text = text + } + + getString () { + const { clean } = parseText(this.text) + return clean + } + + toString () { + return this.getString() + } +} + +module.exports = TextMessage diff --git a/util/text_parser.js b/util/text_parser.js index 9e4df84..6a0941c 100644 --- a/util/text_parser.js +++ b/util/text_parser.js @@ -1,4 +1,4 @@ -const { language } = require('minecraft-data')('1.17.1') +const { language } = require('minecraft-data')('1.20.4') const colormap = { black: 'ยง0', @@ -69,6 +69,10 @@ function parseText (json) { function parseJson (json, parent) { if (typeof json === 'string') { json = { text: json } + } else if (Array.isArray(json)) { + const root = json.shift() + root.extra = json + json = root } json.color ??= parent.color @@ -94,24 +98,24 @@ function parseJson (json, parent) { raw += json[''] } if (json.translate) { // I checked with the native minecraft code. This is how Minecraft does the matching and group indexing. -hhhzzzsss - if (language[json.translate]) { - const _with = json.with ?? [] - let i = 0 - raw += language[json.translate].replace(/%(?:(\\d+)\\$)?(s|%)/g, (g0, g1) => { - if (g0 === '%%') { - return '%' + let format = language[json.translate] + if (typeof format !== 'string') format = json.fallback + if (typeof format !== 'string') format = json.translate + + const _with = json.with ?? [] + let i = 0 + raw += format.replace(/%(?:(\\d+)\\$)?(s|%)/g, (g0, g1) => { + if (g0 === '%%') { + return '%' + } else { + const idx = g1 ? parseInt(g1) : i++ + if (_with[idx]) { + return parseJson(_with[idx], json) } else { - const idx = g1 ? parseInt(g1) : i++ - if (_with[idx]) { - return parseJson(_with[idx], json) - } else { - return '' - } + return '' } - }) - } else { - raw += json.translate - } + } + }) } if (json.extra) { json.extra.forEach((extra) => {