Refactor the chat system
not sure if it's better or worse now. also, i have tried to optimize color codes, but this optimization seems unreliable (i might fix or remove it in the future)
This commit is contained in:
parent
a67478cfd9
commit
5f3560910b
18 changed files with 546 additions and 328 deletions
|
@ -1,16 +1,37 @@
|
|||
{
|
||||
bots: [
|
||||
{
|
||||
host: 'localhost',
|
||||
host: 'example.com',
|
||||
port: 25565,
|
||||
brand: 'kaboom', // TODO: Rename this
|
||||
|
||||
username: ' ',
|
||||
prefix: "'",
|
||||
colors: { primary: 'green', secondary: 'dark_green', error: 'red' },
|
||||
version: '1.20.4',
|
||||
randomizeUsername: true,
|
||||
autoReconnect: true
|
||||
matrix: {
|
||||
enabled: false,
|
||||
client: 'example',
|
||||
roomId: 'put the matrix room id here',
|
||||
commandPrefix: '!'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
all: {
|
||||
username: 'chipmunkbot',
|
||||
prefix: "default.",
|
||||
colors: { primary: 'green', secondary: 'dark_green', error: 'red' },
|
||||
version: '1.20.4',
|
||||
randomizeUsername: true,
|
||||
autoReconnect: true,
|
||||
|
||||
features: {
|
||||
amnesicCommandBlocks: true,
|
||||
commandNamespaces: true
|
||||
}
|
||||
},
|
||||
|
||||
matrixClients: {
|
||||
example: {
|
||||
baseUrl: 'put the homeserver url here',
|
||||
accessToken: 'put your access token here',
|
||||
userId: 'put the user id here'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,20 +9,19 @@ function inject (bot) {
|
|||
_lines: Array(100).fill(''),
|
||||
_filter
|
||||
}
|
||||
/*
|
||||
bot.on('chat', ({ raw }) => {
|
||||
const filtered = _filter(raw)
|
||||
bot.on('chat_color_code', message => {
|
||||
const filtered = _filter(message)
|
||||
bot.chatFilter._lines = [...bot.chatFilter._lines, ...filtered.split('\n')]
|
||||
while (bot.chatFilter._lines.length > 100) {
|
||||
bot.chatFilter._lines.shift()
|
||||
}
|
||||
|
||||
if (raw !== filtered) {
|
||||
if (message !== filtered) {
|
||||
bot._client.write('set_creative_slot', {
|
||||
slot: 36,
|
||||
item: {
|
||||
present: true,
|
||||
itemId: /* id *//*1,
|
||||
itemId: 1,
|
||||
itemCount: 1,
|
||||
nbtData: nbt.comp({
|
||||
i: nbt.string('\xa7r' + bot.chatFilter._lines.join('\xa7r\n'))
|
||||
|
@ -30,11 +29,10 @@ function inject (bot) {
|
|||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
bot.core.run('minecraft:tellraw @a ' + JSON.stringify({ nbt: 'SelectedItem.tag.i', entity: bot.uuid }))
|
||||
bot.core.run('minecraft:tellmessage @a ' + JSON.stringify({ nbt: 'SelectedItem.tag.i', entity: bot.uuid }))
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
function _filter (message) {
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
const { parseNbtText } = require('../util/chat/serialization.js')
|
||||
const parseText = require('../util/text_parser.js')
|
||||
const { parseNbtText } = require('../util/chat/utility')
|
||||
|
||||
const plainStringify = require('../util/chat/stringify/plain')
|
||||
const colorCodeStringify = require('../util/chat/stringify/color_code')
|
||||
const htmlStringify = require('../util/chat/stringify/html')
|
||||
const ansiStringify = require('../util/chat/stringify/ansi')
|
||||
|
||||
const kaboomParser = require('../util/chat/message_parser/kaboom')
|
||||
|
||||
function inject (bot) {
|
||||
bot.chat = {
|
||||
queue: [],
|
||||
patterns: [],
|
||||
parsers: [kaboomParser],
|
||||
|
||||
message (message) {
|
||||
bot._client.write('chat_message', {
|
||||
message,
|
||||
|
@ -31,7 +38,7 @@ function inject (bot) {
|
|||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (!bot.loggedIn) { return }
|
||||
if (!bot.loggedIn) return
|
||||
|
||||
const message = bot.chat.queue.shift()
|
||||
if (message != null) {
|
||||
|
@ -47,15 +54,20 @@ function inject (bot) {
|
|||
|
||||
bot.emit('profileless_chat', { message, senderName, type })
|
||||
bot.emit('chat', message)
|
||||
|
||||
tryParsingMessage(message, { senderName, players: bot.players, lang: bot.registry.language })
|
||||
})
|
||||
|
||||
bot.on('packet.player_chat', (packet) => {console
|
||||
bot.on('packet.player_chat', (packet) => {
|
||||
const plain = packet.plainMessage
|
||||
const unsigned = parseNbtText(packet.unsignedChatContent)
|
||||
const sender = bot.players.find(player => player.uuid === packet.senderUuid)
|
||||
const type = bot.registry?.chatFormattingById[packet.type]
|
||||
|
||||
bot.emit('player_chat', { unsigned, sender, type })
|
||||
bot.emit('player_chat', { plain, unsigned, sender, type: type.name })
|
||||
bot.emit('chat', unsigned)
|
||||
|
||||
tryParsingMessage(unsigned, { senderUuid: sender.uuid, players: bot.players, lang: bot.registry.language, plain })
|
||||
})
|
||||
|
||||
bot.on('packet.system_chat', (packet) => {
|
||||
|
@ -67,6 +79,28 @@ function inject (bot) {
|
|||
bot.emit('system_chat', message)
|
||||
bot.emit('chat', message)
|
||||
})
|
||||
|
||||
bot.on('chat', message => {
|
||||
const stringifyOptions = { lang: bot.registry.language }
|
||||
bot.emit('chat_plain', plainStringify(message, stringifyOptions))
|
||||
bot.emit('chat_color_code', colorCodeStringify(message, stringifyOptions))
|
||||
bot.emit('chat_ansi', ansiStringify(message, stringifyOptions))
|
||||
bot.emit('chat_html', htmlStringify(message, stringifyOptions))
|
||||
|
||||
bot.console.log(message)
|
||||
})
|
||||
|
||||
|
||||
function tryParsingMessage (message, data) {
|
||||
let parsed
|
||||
for (const parser of bot.chat.parsers) {
|
||||
parsed = parser(message, data)
|
||||
if (parsed) break
|
||||
}
|
||||
|
||||
if (!parsed) return
|
||||
bot.emit('message', parsed)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = inject
|
||||
|
|
|
@ -4,6 +4,7 @@ const util = require('util')
|
|||
const { CommandDispatcher, builder: { LiteralArgumentBuilder: { literal }, RequiredArgumentBuilder: { argument } }, arguments: { StringArgumentType: { greedyString } }, exceptions: { CommandSyntaxException } } = require('brigadier-commands')
|
||||
const CommandSource = require('../util/command/command_source')
|
||||
const TextMessage = require('../util/command/text_message')
|
||||
const colorCodeStringify = require('../util/chat/stringify/color_code')
|
||||
|
||||
function inject (bot) {
|
||||
bot.commands = {
|
||||
|
@ -16,16 +17,14 @@ function inject (bot) {
|
|||
isValid
|
||||
}
|
||||
|
||||
/*
|
||||
bot.on('message', (player, message) => {
|
||||
if (!message.startsWith(bot.prefix)) return
|
||||
bot.on('message', ({ sender, plain }) => {
|
||||
if (!plain.startsWith(bot.prefix)) return
|
||||
|
||||
function sendFeedback (message) {
|
||||
bot.core.run('minecraft:tellraw @a ' + JSON.stringify(message))
|
||||
bot.tellraw(message, '@a')
|
||||
}
|
||||
bot.commands.execute(message.substring(bot.prefix.length), new CommandSource({ bot, player, sendFeedback }))
|
||||
bot.commands.execute(plain.substring(bot.prefix.length), new CommandSource({ bot, player: sender, sendFeedback }))
|
||||
})
|
||||
*/
|
||||
|
||||
function add (command) {
|
||||
if (command.register) {
|
||||
|
|
|
@ -1,32 +1,8 @@
|
|||
const fs = require('fs')
|
||||
const util = require('util')
|
||||
const moment = require('moment')
|
||||
const colorCodeStringify = require('../util/chat/stringify/color_code')
|
||||
const ansiStringify = require('../util/chat/stringify/ansi')
|
||||
const CommandSource = require('../util/command/command_source')
|
||||
const parseText = require('../util/text_parser')
|
||||
const ansimap = {
|
||||
0: '\x1b[0m\x1b[30m',
|
||||
1: '\x1b[0m\x1b[34m',
|
||||
2: '\x1b[0m\x1b[32m',
|
||||
3: '\x1b[0m\x1b[36m',
|
||||
4: '\x1b[0m\x1b[31m',
|
||||
5: '\x1b[0m\x1b[35m',
|
||||
6: '\x1b[0m\x1b[33m',
|
||||
7: '\x1b[0m\x1b[37m',
|
||||
8: '\x1b[0m\x1b[90m',
|
||||
9: '\x1b[0m\x1b[94m',
|
||||
a: '\x1b[0m\x1b[92m',
|
||||
b: '\x1b[0m\x1b[96m',
|
||||
c: '\x1b[0m\x1b[91m',
|
||||
d: '\x1b[0m\x1b[95m',
|
||||
e: '\x1b[0m\x1b[93m',
|
||||
f: '\x1b[0m\x1b[97m',
|
||||
r: '\x1b[0m',
|
||||
l: '\x1b[1m',
|
||||
o: '\x1b[3m',
|
||||
n: '\x1b[4m',
|
||||
m: '\x1b[9m',
|
||||
k: '\x1b[6m'
|
||||
}
|
||||
|
||||
function inject (bot) {
|
||||
bot.console = {
|
||||
|
@ -50,19 +26,30 @@ function inject (bot) {
|
|||
}
|
||||
function _log (prefix, stdout, data) {
|
||||
// format it
|
||||
data = `[${moment().format('HH:mm:ss')} ${prefix}\u00a7r] ${data}\n`
|
||||
const _prefix = `[${formatDate()} ${prefix}\u00a7r] `
|
||||
const stringifyOptions = { lang: bot.registry.language }
|
||||
|
||||
const formattedData = _prefix + colorCodeStringify(data, stringifyOptions) + '\n'
|
||||
const ansi = ansiStringify(_prefix, stringifyOptions) + ansiStringify(data, stringifyOptions) + '\x1b[0m\n'
|
||||
|
||||
// log to file
|
||||
const filepath = bot.console.filepath
|
||||
if (filepath != null) {
|
||||
fs.appendFile(filepath, data, err => {
|
||||
fs.appendFile(filepath, formattedData, err => {
|
||||
if (err) console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
bot.tellraw(formattedData, '_ChipMC_')
|
||||
// log to stdout
|
||||
data = data.replace(/\u00a7.?/g, m => ansimap[m.slice(1)] ?? '') + '\x1b[0m'
|
||||
stdout.write(data)
|
||||
stdout.write(ansi + '')
|
||||
}
|
||||
|
||||
function formatDate (date = new Date()) {
|
||||
const hours = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
|
||||
return [hours, minutes, seconds].map(n => n.toString().padStart(2, '0')).join(':')
|
||||
}
|
||||
|
||||
function setRl (rl) {
|
||||
|
@ -83,8 +70,7 @@ function inject (bot) {
|
|||
}
|
||||
|
||||
function sendFeedback (text, sendFeedback) {
|
||||
const { raw } = parseText(message)
|
||||
bot.console.log(raw)
|
||||
bot.console.log(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const matrix = require('matrix-js-sdk')
|
||||
const htmlStringify = require('../util/chat/html_stringifier')
|
||||
const CommandSource = require('../util/command/command_source')
|
||||
const htmlStringify = require('../util/chat/stringify/html')
|
||||
|
||||
function inject (bot, options) {
|
||||
if (!options.matrix?.enabled) return
|
||||
|
@ -8,17 +8,33 @@ function inject (bot, options) {
|
|||
bot.matrix = {
|
||||
client: options.matrix.client ?? matrix.createClient(options.matrix),
|
||||
roomId: options.matrix.roomId,
|
||||
commandPrefix: options.matrix.commandPrefix
|
||||
commandPrefix: options.matrix.commandPrefix,
|
||||
inviteUrl: String(options.matrix.inviteUrl)
|
||||
}
|
||||
|
||||
bot.on('chat', async message => {
|
||||
sendMessage(message)
|
||||
const startTime = Date.now()
|
||||
|
||||
bot.on('chat_html', async html => {
|
||||
sendMessage(html)
|
||||
})
|
||||
|
||||
const matrixPrefix = {
|
||||
text: 'ChipmunkBot Matrix',
|
||||
hoverEvent: {
|
||||
action: 'show_text',
|
||||
contents: 'Click to copy the invite link for the Matrix space to your clipboard!'
|
||||
},
|
||||
clickEvent: {
|
||||
action: 'copy_to_clipboard', // * Minecraft, and Java's URL class in general, seem to hate `#`, so open_url does not work.
|
||||
value: bot.matrix.inviteUrl
|
||||
}
|
||||
}
|
||||
|
||||
bot.matrix.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
|
||||
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.sender.userId === bot.matrix.client.getUserId()) return
|
||||
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.getTs() < startTime || event.sender.userId === bot.matrix.client.getUserId()) return
|
||||
|
||||
const content = event.getContent()
|
||||
const permissionLevel = event.sender.powerLevelNorm
|
||||
let message = content.body
|
||||
|
||||
if (content.url) {
|
||||
|
@ -31,7 +47,7 @@ function inject (bot, options) {
|
|||
}
|
||||
}
|
||||
} else if (message.startsWith(bot.matrix.commandPrefix)) {
|
||||
const source = new CommandSource({ bot, permissionLevel: 1, sendFeedback })
|
||||
const source = new CommandSource({ bot, permissionLevel, sendFeedback })
|
||||
bot.commands.execute(message.substring(bot.matrix.commandPrefix.length), source)
|
||||
|
||||
return
|
||||
|
@ -41,21 +57,19 @@ function inject (bot, options) {
|
|||
text: String(event.sender.rawDisplayName || event.sender.name || event.sender.userId),
|
||||
hoverEvent: {
|
||||
action: 'show_text',
|
||||
contents: ['User ID: ', String(event.sender.userId), '\nClick to copy the user id']
|
||||
contents: [String(event.sender.userId), '\nPermission Level: ', String(permissionLevel), '\n\nClick to copy the User ID']
|
||||
},
|
||||
clickEvent: {
|
||||
action: 'copy_to_clipboard',
|
||||
value: String(event.sender.userId)
|
||||
}
|
||||
}
|
||||
bot.fancyMsg('ChipmunkBot Matrix', senderText, message)
|
||||
bot.fancyMsg(matrixPrefix, senderText, message)
|
||||
})
|
||||
|
||||
let dequeuingMessages = false
|
||||
let queue = []
|
||||
async function sendMessage (message) {
|
||||
const html = htmlStringify(message, { lang: bot.registry.language })
|
||||
|
||||
async function sendMessage (html) {
|
||||
queue.push(html)
|
||||
if (dequeuingMessages) return
|
||||
|
||||
|
@ -85,7 +99,8 @@ function inject (bot, options) {
|
|||
}
|
||||
|
||||
function sendFeedback (text, sendFeedback) {
|
||||
sendMessage(text)
|
||||
const html = htmlStringify(text, { lang: bot.registry.language })
|
||||
sendMessage(html)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { parseNbtText } = require('../util/chat/serialization.js')
|
||||
const { parseNbtText } = require('../util/chat/utility')
|
||||
|
||||
const gamemodes = ['survival', 'creative', 'adventure', 'spectator']
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
const { colors, formatting, reset } = require('./formatting.json')
|
||||
|
||||
function escapeSequenceStringify (text, { sequences, lang = {}, parent } = {}) {
|
||||
text = normalize(text)
|
||||
|
||||
let rootText = false
|
||||
if (parent == null) {
|
||||
parent = {}
|
||||
rootText = true
|
||||
}
|
||||
|
||||
let formattingString = ''
|
||||
|
||||
formattingString += sequences.colors[text.color ?? parent.color] || ''
|
||||
|
||||
if (text.bold ?? parent.bold) formattingString += sequences.formatting.bold
|
||||
if (text.italic ?? parent.italic) formattingString += sequences.formatting.italic
|
||||
if (text.underlined ?? parent.underlined) formattingString += sequences.formatting.underlined
|
||||
if (text.strikethrough ?? parent.strikethrough) formattingString += sequences.formatting.strikethrough
|
||||
if (text.obfuscated ?? parent.obfuscated) formattingString += sequences.formatting.obfuscated
|
||||
|
||||
if (!formattingString && !rootText) formattingString = sequences.reset
|
||||
|
||||
let string = formattingString
|
||||
|
||||
if (text.text != null) string += text.text
|
||||
if (text.translate != null) {
|
||||
let format
|
||||
if (Object.hasOwn(lang, text.translate)) format = lang[text.translate]
|
||||
else if (text.fallback != null) format = text.fallback
|
||||
else format = text.translate
|
||||
|
||||
const _with = text.with || []
|
||||
|
||||
string += format.replace(/%(?:(\d+)\$)?(s|%)/g, (g0, g1) => {
|
||||
if (g0 === '%%') return '%'
|
||||
|
||||
const idx = g1 ? parseInt(g1) : i++
|
||||
if (_with[idx]) {
|
||||
return escapeSequenceStringify(_with[idx], { sequences, lang, parent: text }) + formattingString
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
if (text.selector != null) string += text.selector
|
||||
if (text.keybind) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (text.extra) {
|
||||
for (const extra of text.extra) {
|
||||
string += escapeSequenceStringify(extra, { sequences, lang, parent: text })
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
const colorCodeMapper = colors => Object.fromEntries(colors.map(color => [color.name, color.code]))
|
||||
const colorCodeSequences = { colors: colorCodeMapper(colors), formatting: colorCodeMapper(formatting), reset: reset.code }
|
||||
const colorCodeStringify = (text, { lang }) => escapeSequenceStringify(text, { sequences: colorCodeSequences, lang })
|
||||
|
||||
const ansiMapper = colors => Object.fromEntries(colors.map(color => [color.name, color.ansi]))
|
||||
const ansiSequences = { colors: ansiMapper(colors), formatting: ansiMapper(formatting), reset: reset.ansi }
|
||||
const ansiSringify = (text, { lang }) => escapeSequenceStringify(text, { sequences: ansiSequences, lang })
|
||||
|
||||
function normalize (text) {
|
||||
if (typeof text === 'string') return { text }
|
||||
if (Array.isArray(text)) {
|
||||
const text2 = [...text]
|
||||
const text3 = { ...normalize(text2.shift()) }
|
||||
text3.extra = text2
|
||||
return text3
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
module.exports = { escapeSequenceStringify, colorCodeStringify, ansiSringify }
|
|
@ -18,11 +18,11 @@
|
|||
{"name": "white", "code": "§f", "ansi": "\u001b[0m\u001b[97m", "rgb": 16777215}
|
||||
],
|
||||
"formatting": [
|
||||
{"name": "bold", "code": "§l"},
|
||||
{"name": "italic", "code": "§o"},
|
||||
{"name": "underlined", "code": "§n"},
|
||||
{"name": "strikethrough", "code": "§m"},
|
||||
{"name": "obfuscated", "code": "§k"}
|
||||
{"name": "bold", "code": "§l", "ansi": "\u001b[1m"},
|
||||
{"name": "italic", "code": "§o", "ansi": "\u001b[3m"},
|
||||
{"name": "underlined", "code": "§n", "ansi": "\u001b[4m"},
|
||||
{"name": "strikethrough", "code": "§m", "ansi": "\u001b[9m"},
|
||||
{"name": "obfuscated", "code": "§k", "ansi": "\u001b[6m"}
|
||||
],
|
||||
"reset": {"name": "reset", "code": "§r", "ansi": "\u001b[0m"}
|
||||
}
|
92
util/chat/message_parser/kaboom.js
Normal file
92
util/chat/message_parser/kaboom.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
const util = require('util')
|
||||
const { getChangedFormatting } = require('../utility')
|
||||
|
||||
const { colors } = require('../formatting.json')
|
||||
const colormap = Object.fromEntries(colors.map(color => [color.name, '&' + color.code[1]]))
|
||||
|
||||
// This is the regex used for matching urls in extras, with ^ and $ added
|
||||
const urlRegex = /^((https?:\/\/(ww(w|\d)\.)?|ww(w|\d))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&/=]*))$/
|
||||
|
||||
function parseMessage (message, data) {
|
||||
if (message === null || typeof message !== 'object') return
|
||||
|
||||
if (message.text !== '' || !Array.isArray(message.extra) || message.extra.length < 3) return
|
||||
|
||||
const children = message.extra
|
||||
|
||||
const prefix = children[0]
|
||||
let displayName = data.senderName ?? { text: '' }
|
||||
let contents = { text: '' }
|
||||
|
||||
if (isSeparatorAt(children, 1)) { // Missing/blank display name
|
||||
if (children.length > 3) contents = children[3]
|
||||
} else if (isSeparatorAt(children, 2)) {
|
||||
displayName = children[1]
|
||||
if (children.length > 4) contents = children[4]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const playerListDisplayName = { extra: [prefix, displayName], text: '' }
|
||||
let sender
|
||||
if (data.senderUuid) {
|
||||
sender = data.players.find(player => player.uuid === data.senderUuid)
|
||||
} else {
|
||||
const playerListDisplayName = { extra: [prefix, displayName], text: '' }
|
||||
sender = data.players.find(player => util.isDeepStrictEqual(player.displayName, playerListDisplayName))
|
||||
}
|
||||
|
||||
if (!sender) return undefined
|
||||
|
||||
let plain = data.plain
|
||||
if (!plain) plain = getPlainContents(contents)
|
||||
|
||||
return { sender, contents, type: 'minecraft:chat', displayName, plain }
|
||||
}
|
||||
|
||||
function getPlainContents (contents) {
|
||||
let string = ''
|
||||
|
||||
let format = getPlainFormatting(contents, {})
|
||||
if (format.startsWith('&r')) format = format.substring(2)
|
||||
string += format
|
||||
|
||||
if (contents.text) string += contents.text
|
||||
|
||||
if (contents.extra) {
|
||||
let previousChild = contents
|
||||
for (const child of contents.extra) {
|
||||
string += getPlainFormatting(child, previousChild)
|
||||
string += child.text
|
||||
|
||||
previousChild = child
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
function getPlainFormatting (text, previousText) {
|
||||
if (isUrl(text)) return ''
|
||||
|
||||
const format = getChangedFormatting(text, previousText, {})
|
||||
let string = ''
|
||||
|
||||
if ('color' in format) string += colormap[format.color] || '&r'
|
||||
|
||||
if (format.bold) string += '&l'
|
||||
if (format.italic) string += '&o'
|
||||
if (format.underlined) string += '&n'
|
||||
if (format.strikethrough) string += '&m'
|
||||
if (format.obfuscated) string += '&k'
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
function isSeparatorAt (children, start) {
|
||||
return (children[start]?.text === ':' || children[start]?.text === '\xa7f:') && children[start + 1]?.text === ' '
|
||||
}
|
||||
|
||||
const isUrl = text => text.color === 'blue' && text.underlined && text.clickEvent?.action === 'open_url' && text.clickEvent?.value === (text.text?.includes?.('://') ? text.text : 'https://' + text.text) && urlRegex.test(text.text)
|
||||
|
||||
module.exports = parseMessage
|
|
@ -1,12 +0,0 @@
|
|||
function parseJsonText (json) {
|
||||
return JSON.parse(json)
|
||||
}
|
||||
|
||||
function parseNbtText (data) {
|
||||
if (typeof data.value !== 'object') return data.value
|
||||
if (Array.isArray(data.value)) return [...data.value]
|
||||
if (data.type === 'list') return data.value.value.map(value => parseNbtText({ value }))
|
||||
return Object.fromEntries(Object.entries(data.value).map(([key, value]) => ([key === '' ? 'text' : key, parseNbtText(value)])))
|
||||
}
|
||||
|
||||
module.exports = { parseJsonText, parseNbtText }
|
100
util/chat/stringify/ansi.js
Normal file
100
util/chat/stringify/ansi.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
const { colors, formatting, reset } = require('../formatting.json')
|
||||
const { normalize, intToRgb, getChangedFormatting } = require('../utility')
|
||||
|
||||
const colormap = Object.fromEntries(colors.map(color => [color.name, color.ansi]))
|
||||
const formatmap = Object.fromEntries(formatting.map(format => [format.name, format.ansi]))
|
||||
const colorcodemap = Object.fromEntries(colors.map(color => [color.code, color.ansi]))
|
||||
const formatcodemap = Object.fromEntries(formatting.map(format => [format.code, format.ansi]))
|
||||
const baseColors = colors.map(color => intToRgb(color.rgb))
|
||||
|
||||
function ansiStringify (text, { lang = {}, previousText, parent } = {}) {
|
||||
text = normalize(text)
|
||||
|
||||
let string = getFormatting(text, previousText, parent)
|
||||
|
||||
if (text.text != null) string += preprocessText(text.text)
|
||||
else if (text.translate != null) {
|
||||
let format
|
||||
if (Object.hasOwn(lang, text.translate)) format = lang[text.translate]
|
||||
else if (text.fallback != null) format = text.fallback
|
||||
else format = text.translate
|
||||
|
||||
const _with = text.with || []
|
||||
let i = 0
|
||||
|
||||
string += preprocessText(format).replace(/%(?:(\d+)\$)?(s|%)/g, (g0, g1) => {
|
||||
if (g0 === '%%') return '%'
|
||||
|
||||
const idx = g1 ? parseInt(g1) : i++
|
||||
if (_with[idx]) {
|
||||
return ansiStringify(_with[idx], { lang, previousText: text, parent: text }) + getFormatting(text, _with[idx], parent)
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
else if (text.selector != null) string += preprocessText(text.selector)
|
||||
else if (text.keybind) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (text.extra) {
|
||||
let previousChild = text
|
||||
for (const child of text.extra) {
|
||||
string += ansiStringify(child, { lang, previousText: previousChild, parent: text })
|
||||
previousChild = child
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
function getFormatting (text, previousText, parent) {
|
||||
const format = getChangedFormatting(text, previousText, parent)
|
||||
let string = ''
|
||||
|
||||
if ('color' in format) {
|
||||
if (format.color && format.color[0] === '#') {
|
||||
const rgb = parseInt(format.color.substring(1), 16) & 0xffffff
|
||||
const [r, g, b] = intToRgb(rgb)
|
||||
|
||||
string += `\x1b[38;2;${r};${g};${b}m`
|
||||
}
|
||||
else if (colormap[format.color]) string += colormap[format.color]
|
||||
else if (parent) string += reset.ansi
|
||||
}
|
||||
|
||||
if (format.bold) string += formatmap.bold
|
||||
if (format.italic) string += formatmap.italic
|
||||
if (format.underlined) string += formatmap.underlined
|
||||
if (format.strikethrough) string += formatmap.strikethrough
|
||||
if (format.obfuscated) string += formatmap.obfuscated
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
function preprocessText (input) {
|
||||
let string = ''
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const c = input[i]
|
||||
|
||||
if (c === '§') {
|
||||
if ((i + 1) >= input.length) break
|
||||
|
||||
const code = input.substring(i, i + 2)
|
||||
i++
|
||||
|
||||
if (colorcodemap[code]) string += colorcodemap[code]
|
||||
else if (formatcodemap[code]) string += formatcodemap[code]
|
||||
else if (code === reset.code) string += reset.ansi
|
||||
|
||||
continue
|
||||
}
|
||||
else string += c
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports = ansiStringify
|
71
util/chat/stringify/color_code.js
Normal file
71
util/chat/stringify/color_code.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
const { colors, formatting, reset } = require('../formatting.json')
|
||||
const { normalize, intToRgb, getNearestColor, getChangedFormatting } = require('../utility')
|
||||
|
||||
const colormap = Object.fromEntries(colors.map(color => [color.name, color.code]))
|
||||
const formatmap = Object.fromEntries(formatting.map(format => [format.name, format.code]))
|
||||
|
||||
function colorCodeStringify (text, { lang = {}, previousText, parent } = {}) {
|
||||
text = normalize(text)
|
||||
|
||||
let string = getFormatting(text, previousText, parent)
|
||||
|
||||
if (text.text != null) string += text.text
|
||||
else if (text.translate != null) {
|
||||
let format
|
||||
if (Object.hasOwn(lang, text.translate)) format = lang[text.translate]
|
||||
else if (text.fallback != null) format = text.fallback
|
||||
else format = text.translate
|
||||
|
||||
const _with = text.with || []
|
||||
let i = 0
|
||||
|
||||
string += format.replace(/%(?:(\d+)\$)?(s|%)/g, (g0, g1) => {
|
||||
if (g0 === '%%') return '%'
|
||||
|
||||
const idx = g1 ? parseInt(g1) : i++
|
||||
if (_with[idx]) {
|
||||
return colorCodeStringify(_with[idx], { lang, previousText: text, parent: text }) + getFormatting(text, _with[idx], parent)
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
else if (text.selector != null) string += text.selector
|
||||
else if (text.keybind) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (text.extra) {
|
||||
let previousChild = text
|
||||
for (const child of text.extra) {
|
||||
string += colorCodeStringify(child, { lang, previousText: previousChild, parent: text })
|
||||
previousChild = child
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
function getFormatting (text, previousText, parent) {
|
||||
const format = getChangedFormatting(text, previousText, parent)
|
||||
let string = ''
|
||||
|
||||
if ('color' in format) {
|
||||
if (format.color && format.color[0] === '#') {
|
||||
const rgb = parseInt(format.color.substring(1), 16) & 0xffffff
|
||||
string += colors[getNearestColor(rgb)].code
|
||||
}
|
||||
else if (colormap[format.color]) string += colormap[format.color]
|
||||
else if (parent) string += reset.code
|
||||
}
|
||||
|
||||
if (format.bold) string += formatmap.bold
|
||||
if (format.italic) string += formatmap.italic
|
||||
if (format.underlined) string += formatmap.underlined
|
||||
if (format.strikethrough) string += formatmap.strikethrough
|
||||
if (format.obfuscated) string += formatmap.obfuscated
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports = colorCodeStringify
|
|
@ -1,12 +1,8 @@
|
|||
const { colors } = require('./formatting.json')
|
||||
const { colors } = require('../formatting.json')
|
||||
const { normalize } = require('../utility')
|
||||
|
||||
const colorsByName = {}
|
||||
const colorsByCode = {}
|
||||
for (const color of colors) {
|
||||
const hex = '#' + color.rgb.toString(16).padStart(6, '0')
|
||||
colorsByName[color.name] = hex
|
||||
colorsByCode[color.code] = hex
|
||||
}
|
||||
const colormap = Object.fromEntries(colors.map(color => [color.name, color.rgb]))
|
||||
const colorcodemap = Object.fromEntries(colors.map(color => [color.code, color.rgb]))
|
||||
|
||||
const reservedCharacters = {
|
||||
'"': '"',
|
||||
|
@ -22,7 +18,7 @@ function htmlStringify (text, { lang = {} } = {}) {
|
|||
let string = ''
|
||||
|
||||
if (text.text != null) string += preprocessText(text.text)
|
||||
if (text.translate != null) {
|
||||
else if (text.translate != null) {
|
||||
let format
|
||||
if (Object.hasOwn(lang, text.translate)) format = lang[text.translate]
|
||||
else if (text.fallback != null) format = text.fallback
|
||||
|
@ -42,8 +38,8 @@ function htmlStringify (text, { lang = {} } = {}) {
|
|||
return ''
|
||||
})
|
||||
}
|
||||
if (text.selector != null) string += preprocessText(text.selector)
|
||||
if (text.keybind) {
|
||||
else if (text.selector != null) string += preprocessText(text.selector)
|
||||
else if (text.keybind) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
@ -53,7 +49,10 @@ function htmlStringify (text, { lang = {} } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
if (text.color) string = `<font color="${text.color[0] === '#' ? ('#' + text.color.substring(1).padStart(6, '0')) : colorsByName[text.color]}">${string}</font>`
|
||||
if (text.color) {
|
||||
const rgb = text.color[0] === '#' ? parseInt(text.color.substring(1), 16) : colormap[text.color]
|
||||
if (rgb) string = `<font color="${rgb.toString(16).padStart(6, '0')}">${string}</font>`
|
||||
}
|
||||
|
||||
// formatting
|
||||
if (text.bold) string = `<b>${string}</b>`
|
||||
|
@ -78,7 +77,7 @@ function preprocessText (input) {
|
|||
const code = input.substring(i, i + 2)
|
||||
i++
|
||||
|
||||
const hex = colorsByCode[code]
|
||||
const hex = colorcodemap[code]
|
||||
if (hex) {
|
||||
string += closing
|
||||
string += `<font color="${hex}">`
|
||||
|
@ -93,10 +92,10 @@ function preprocessText (input) {
|
|||
}
|
||||
|
||||
if (code === '§l') { string += '<b>'; closing += '</b>' }
|
||||
if (code === '§o') { string += '<i>'; closing += '</i>' }
|
||||
if (code === '§n') { string += '<u>'; closing += '</u>' }
|
||||
if (code === '§m') { string += '<strike>'; closing += '</strike>' }
|
||||
if (code === '§k') { string += '<blink>'; closing += '</blink>' }
|
||||
else if (code === '§o') { string += '<i>'; closing += '</i>' }
|
||||
else if (code === '§n') { string += '<u>'; closing += '</u>' }
|
||||
else if (code === '§m') { string += '<strike>'; closing += '</strike>' }
|
||||
else if (code === '§k') { string += '<blink>'; closing += '</blink>' }
|
||||
|
||||
continue // Do not append the escape sequence itself to the string
|
||||
}
|
||||
|
@ -109,15 +108,4 @@ function preprocessText (input) {
|
|||
return string
|
||||
}
|
||||
|
||||
function normalize (text) {
|
||||
if (typeof text === 'string') return { text }
|
||||
if (Array.isArray(text)) {
|
||||
const text2 = [...text]
|
||||
const text3 = { ...normalize(text2.shift()) }
|
||||
text3.extra = text2
|
||||
return text3
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
module.exports = htmlStringify
|
43
util/chat/stringify/plain.js
Normal file
43
util/chat/stringify/plain.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
const { normalize } = require('../utility')
|
||||
|
||||
function plainStringify (text, { lang = {} } = {}) {
|
||||
text = normalize(text)
|
||||
|
||||
let string = ''
|
||||
|
||||
if (text.text != null) string += text.text
|
||||
else if (text.translate != null) {
|
||||
let format
|
||||
if (Object.hasOwn(lang, text.translate)) format = lang[text.translate]
|
||||
else if (text.fallback != null) format = text.fallback
|
||||
else format = text.translate
|
||||
|
||||
const _with = text.with || []
|
||||
let i = 0
|
||||
|
||||
string += format.replace(/%(?:(\d+)\$)?(s|%)/g, (g0, g1) => {
|
||||
if (g0 === '%%') return '%'
|
||||
|
||||
const idx = g1 ? parseInt(g1) : i++
|
||||
if (_with[idx]) {
|
||||
return plainStringify(_with[idx], { lang })
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
else if (text.selector != null) string += text.selector
|
||||
else if (text.keybind) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (text.extra) {
|
||||
for (const extra of text.extra) {
|
||||
string += plainStringify(extra, { lang })
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
module.exports = plainStringify
|
89
util/chat/utility.js
Normal file
89
util/chat/utility.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
const { colors, formatting } = require('./formatting.json')
|
||||
|
||||
const formatNames = Object.fromEntries(formatting.map(format => [format.name, true]))
|
||||
const baseColors = colors.map(color => intToRgb(color.rgb))
|
||||
|
||||
function parseJsonText (json) {
|
||||
return JSON.parse(json)
|
||||
}
|
||||
|
||||
function parseNbtText (data) {
|
||||
if (typeof data.value !== 'object') return data.value
|
||||
if (Array.isArray(data.value)) return [...data.value]
|
||||
if (data.type === 'list') return data.value.value.map(value => parseNbtText({ value }))
|
||||
return Object.fromEntries(Object.entries(data.value).map(([key, value]) => ([key === '' ? 'text' : key, parseNbtText(value)])))
|
||||
}
|
||||
|
||||
function normalize (text) {
|
||||
if (typeof text === 'string') return { text }
|
||||
if (Array.isArray(text)) {
|
||||
const text2 = [...text]
|
||||
const text3 = { ...normalize(text2.shift()) }
|
||||
text3.extra = text2
|
||||
return text3
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function rgbToInt (r, g, b) {
|
||||
return (r & 0xff) << 16 | (g & 0xff) << 8 | (b & 0xff)
|
||||
}
|
||||
|
||||
function intToRgb (int) {
|
||||
const r = (int >> 16) & 0xff
|
||||
const g = (int >> 8) & 0xff
|
||||
const b = int & 0xff
|
||||
|
||||
return [r, g, b]
|
||||
}
|
||||
|
||||
|
||||
// skidded from https://github.com/edqx/amongus-experiments/blob/master/index5.js#L34C1-L56C2
|
||||
function getNearestColor (rgb) {
|
||||
if (typeof rgb === 'number') rgb = intToRgb(rgb)
|
||||
const [red, green, blue] = rgb
|
||||
|
||||
let nearestDist = Infinity
|
||||
let nearestColorIdx
|
||||
for (let i = 0; i < baseColors.length; i++) {
|
||||
const baseColor = baseColors[i]
|
||||
const [baseR, baseG, baseB] = baseColor
|
||||
|
||||
const diffR = (red - baseR) * (red - baseR)
|
||||
const diffG = (green - baseG) * (green - baseG)
|
||||
const diffB = (blue - baseB) * (blue - baseB)
|
||||
|
||||
const dist = Math.sqrt(diffR + diffG + diffB)
|
||||
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist
|
||||
nearestColorIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
return nearestColorIdx
|
||||
}
|
||||
|
||||
function getChangedFormatting (text, previousText, parent = {}) {
|
||||
// Merge both texts with their parents
|
||||
text = { ...parent, ...text }
|
||||
if (previousText) previousText = { ...parent, ...previousText }
|
||||
|
||||
const getAllFormatting = () => Object.fromEntries(Object.entries(text).filter(([key, value]) => formatNames[key] || key === 'color'))
|
||||
|
||||
if (!previousText || text.color !== previousText.color) return getAllFormatting()
|
||||
|
||||
const format = {}
|
||||
for (const key in formatNames) {
|
||||
if (text[key] && previousText[key]) continue
|
||||
if (previousText[key] && !text[key]) return getAllFormatting() // Color codes do not have any way to unset specific formattings
|
||||
if (text[key] == null) continue
|
||||
|
||||
format[key] = format[key]
|
||||
}
|
||||
format.color = text.color
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
module.exports = { parseJsonText, parseNbtText, normalize, rgbToInt, intToRgb, getNearestColor, getChangedFormatting }
|
|
@ -1,4 +1,4 @@
|
|||
const parseText = require('../text_parser.js')
|
||||
const plainStringify = require('../chat/stringify/plain')
|
||||
|
||||
class TextMessage {
|
||||
constructor (text) {
|
||||
|
@ -6,8 +6,7 @@ class TextMessage {
|
|||
}
|
||||
|
||||
getString () {
|
||||
const { clean } = parseText(this.text)
|
||||
return clean
|
||||
return plainStringify(this.text, { lang: {} })
|
||||
}
|
||||
|
||||
toString () {
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
const { language } = require('minecraft-data')('1.20.4')
|
||||
|
||||
const colormap = {
|
||||
black: '§0',
|
||||
dark_blue: '§1',
|
||||
dark_green: '§2',
|
||||
dark_aqua: '§3',
|
||||
dark_red: '§4',
|
||||
dark_purple: '§5',
|
||||
gold: '§6',
|
||||
gray: '§7',
|
||||
dark_gray: '§8',
|
||||
blue: '§9',
|
||||
green: '§a',
|
||||
aqua: '§b',
|
||||
red: '§c',
|
||||
light_purple: '§d',
|
||||
yellow: '§e',
|
||||
white: '§f',
|
||||
reset: '§r'
|
||||
}
|
||||
|
||||
const ansimap = {
|
||||
'§0': '\x1b[0m\x1b[30m',
|
||||
'§1': '\x1b[0m\x1b[34m',
|
||||
'§2': '\x1b[0m\x1b[32m',
|
||||
'§3': '\x1b[0m\x1b[36m',
|
||||
'§4': '\x1b[0m\x1b[31m',
|
||||
'§5': '\x1b[0m\x1b[35m',
|
||||
'§6': '\x1b[0m\x1b[33m',
|
||||
'§7': '\x1b[0m\x1b[37m',
|
||||
'§8': '\x1b[0m\x1b[90m',
|
||||
'§9': '\x1b[0m\x1b[94m',
|
||||
'§a': '\x1b[0m\x1b[92m',
|
||||
'§b': '\x1b[0m\x1b[96m',
|
||||
'§c': '\x1b[0m\x1b[91m',
|
||||
'§d': '\x1b[0m\x1b[95m',
|
||||
'§e': '\x1b[0m\x1b[93m',
|
||||
'§f': '\x1b[0m\x1b[97m',
|
||||
'§r': '\x1b[0m',
|
||||
'§l': '\x1b[1m',
|
||||
'§o': '\x1b[3m',
|
||||
'§n': '\x1b[4m',
|
||||
'§m': '\x1b[9m',
|
||||
'§k': '\x1b[6m'
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a native minecraft text component in string form.
|
||||
* @param {string} json_string - A text component string, such as the chat packet's 'message' property.
|
||||
* @returns {object} Parsed message in { raw, clean, ansi } form.
|
||||
*/
|
||||
function parseText (json) {
|
||||
let raw = parseJson(json, { color: 'reset' })
|
||||
if (raw.startsWith('§r')) {
|
||||
raw = raw.substring(2)
|
||||
}
|
||||
const clean = raw.replace(/§./g, '').replace(/§/g, '')
|
||||
const ansi = raw.replace(/§[a-f0-9rlonmk]/g, (m) => ansimap[m])
|
||||
return { raw, clean, ansi }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a native minecraft text component in JSON form.
|
||||
* @param {object} json - The json message.
|
||||
* @param {object} parent - The parent json.
|
||||
* @returns {string} The parsed raw string.
|
||||
*/
|
||||
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
|
||||
json.bold ??= parent.bold
|
||||
json.italic ??= parent.italic
|
||||
json.underlined ??= parent.underlined
|
||||
json.strikethrough ??= parent.strikethrough
|
||||
json.obfuscated ??= parent.obfuscated
|
||||
|
||||
let raw = ''
|
||||
// if (json.color.startsWith('#'))
|
||||
// raw += '§' + color
|
||||
// else
|
||||
raw += colormap[json.color] || ''
|
||||
if (json.bold) raw += '§l'
|
||||
if (json.italic) raw += '§o'
|
||||
if (json.underlined) raw += '§n'
|
||||
if (json.strikethrough) raw += '§m'
|
||||
if (json.obfuscated) raw += '§k'
|
||||
if (json.text) {
|
||||
raw += json.text
|
||||
}
|
||||
if (json.translate) { // I checked with the native minecraft code. This is how Minecraft does the matching and group indexing. -hhhzzzsss
|
||||
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 {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (json.extra) {
|
||||
json.extra.forEach((extra) => {
|
||||
raw += parseJson(extra, json)
|
||||
})
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
module.exports = parseText
|
Loading…
Reference in a new issue