UNPKG

@boem312/minecraft-server

Version:

A pure JS library to create Minecraft Java 1.16.3 servers

661 lines (523 loc) 21.8 kB
const { textModifiers, textColors, keybinds } = require('../../functions/loader/data'); const { language } = require('../../settings.json'); const fs = require('fs'); const path = require('path'); const englishMessages = Object.assign({}, JSON.parse( fs.readFileSync( path.join(__dirname, `../../data/messages/game/${language}.json`) ).toString() ), JSON.parse( fs.readFileSync( path.join(__dirname, `../../data/messages/realms/${language}.json`) ).toString() ) ); const CustomError = require('../utils/CustomError.js'); const { formatJavaString } = require('../../functions/formatJavaString.js'); const textModifiersWithoutReset = textModifiers.filter(({ name }) => name !== 'reset'); const textColorsWithDefault = [...textColors, { char: 'r', name: 'default', minecraftName: 'reset' }]; const hiddenProperties = [ '_input', '_string', '_uncolored', '_array', '_chat', '_hash' ]; const _p = Symbol('_privates') const events = Object.freeze([ 'change' ]); let properties = {}; for (const file of fs.readdirSync(path.join(__dirname, './Text/properties/public/dynamic/')).filter(a => a.endsWith('.js'))) properties[file.split('.js')[0]] = require(`./Text/properties/public/dynamic/${file}`); const defaultInheritedChatProperties = Object.freeze({ color: 'reset', insertion: '', clickEvent: { action: 'change_page', value: 0 }, hoverEvent: { action: 'show_text', value: '' }, ...Object.fromEntries(textModifiersWithoutReset.map(({ name }) => [name, false])) }); class Text { constructor(text) { Object.defineProperty(this, _p, { configurable: false, enumerable: false, writable: false, value: {} }); for (const hiddenProperty of hiddenProperties) this.p[hiddenProperty] = null; this.p._input = text || ''; this.events = Object.freeze(Object.fromEntries(events.map(a => [a, []]))); for (const [name, { get, set }] of Object.entries(properties)) Object.defineProperty(this, name, { configurable: false, enumerable: true, get, set }); this.p.reset = () => { for (const hiddenProperty of hiddenProperties) this[hiddenProperty] = null; }; this.p.emitChange = () => { for (const { callback } of this.events.change) callback(this); }; } get p() { let callPath = new Error().stack.split('\n')[2]; if (callPath.includes('(')) callPath = callPath.split('(')[1].split(')')[0]; else callPath = callPath.split('at ')[1]; callPath = callPath.split(':').slice(0, 2).join(':'); let folderPath = path.resolve(__dirname, '../../'); if (!callPath.startsWith(folderPath)) console.warn('(minecraft-server) WARNING: Detected access to private properties from outside of the module. This is not recommended and may cause unexpected behavior.'); return this[_p]; } set p(value) { console.error('(minecraft-server) ERROR: Setting private properties is not supported. Action ignored.'); } removeAllListeners(event) { if (event) this.events[event] = []; else for (const event of Object.keys(this.events)) this.events[event] = []; } on(event, callback) { if (!this.events[event]) this.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `event in <${this.constructor.name}>.on(${require('util').inspect(event)}, ...) `, { got: event, expectationType: 'value', expectation: Object.keys(this.events) }, this.on)); this.events[event].push({ callback, once: false }); } once(event, callback) { if (!this.events[event]) this.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `event in <${this.constructor.name}>.once(${require('util').inspect(event)}, ...) `, { got: event, expectationType: 'value', expectation: Object.keys(this.events) }, this.on)); this.events[event].push({ callback, once: true }); } [Symbol.toPrimitive](hint) { if (hint === 'string') return this.uncolored; else if (hint === 'default') return this.array; else if (hint === 'number') return this.uncolored.length; else return null; } static stringToUncolored(string) { let out = ''; let isSpecial = false; for (const char of string) { if (isSpecial) { isSpecial = false; continue; } if (char === '§') { isSpecial = true; continue; } out += char; } return out; } static arrayToString(a) { let array = Text.parseArray(a); let text = '§r'; let currentModifiers = []; let currentColor = 'default'; for (const component of array) { const componentText = getTextComponentDefaultText(component); if (componentText === '') continue; let modCanExtend = true; for (const currentModifier of currentModifiers) if (!component.modifiers.includes(currentModifier)) modCanExtend = false; let newMod = []; if (modCanExtend) for (const modifier of component.modifiers) if (!currentModifiers.includes(modifier)) newMod.push(modifier); if (component.color === currentColor) if (modCanExtend) { currentModifiers = component.modifiers; for (const v of newMod) text += ${textModifiers.find(({ name }) => name === v).char}`; text += componentText; } else { currentModifiers = component.modifiers; text += '§r'; if (component.color !== 'default') text += ${textColors.find(({ name }) => name === component.color).char}`; for (const modifier of component.modifiers) text += ${textModifiers.find(({ name }) => name === modifier).char}`; text += componentText; } else if (component.color === 'default') { currentColor = 'default'; currentModifiers = component.modifiers; text += '§r'; for (const modifier of component.modifiers) text += ${textModifiers.find(({ name }) => name === modifier).char}`; text += componentText; } else { currentColor = component.color; currentModifiers = component.modifiers; if (modCanExtend) { text += ${textColors.find(({ name }) => name === component.color).char}`; for (const v of newMod) text += ${textModifiers.find(({ name }) => name === v).char}`; text += componentText; } else { text += `§r§${textColors.find(({ name }) => name === component.color).char}`; for (const v of component.modifiers) text += ${textModifiers.find(({ name }) => name === v).char}`; text += componentText; } } } return text; } static parseArray(arr) { if (!Array.isArray(arr)) arr = [arr]; arr = arr.map(parseArrayComponent); arr = arr.filter(val => val.text !== ''); return arr; } static stringToArray(text) { let arr = []; let isModifier = false; let current = ''; let currentColor = 'default'; let currentModifiers = []; for (const val of text.split('')) { if (isModifier) { if (!textColors.find(({ char }) => char === val) && !textModifiers.find(({ char }) => char === val)) throw new CustomError('expectationNotMet', 'libraryUser', `colorLetter in ${this.constructor.name}.stringToArray(<includes colorLetter ${val}>) `, { got: val, expectationType: 'value', expectation: [...textColors.map(({ char }) => char), ...textModifiers.map(({ char }) => char)] }, Text.stringToArray).toString() else { if (textColors.find(({ char }) => char === val)) { let copy = Object.assign([], currentModifiers); arr.push({ text: current, color: currentColor, modifiers: copy }); current = ''; currentColor = textColors.find(({ char }) => char === val).name; } else if (textModifiers.find(({ char }) => char === val).name === 'reset') { let copy = Object.assign([], currentModifiers); arr.push({ text: current, color: currentColor, modifiers: copy }) current = ''; currentColor = 'default'; currentModifiers = []; } else { if (!currentModifiers.includes(textModifiers.find(({ char }) => char === val).name)) { let copy = Object.assign([], currentModifiers); arr.push({ text: current, color: currentColor, modifiers: copy }); current = ''; currentModifiers.push(textModifiers.find(({ char }) => char === val).name); } } } isModifier = false; continue; } if (val === '§') isModifier = true; else current += val; } arr.push({ text: current, color: currentColor, modifiers: currentModifiers }); return Text.parseArray(arr); } static arrayToChat(a) { // todo: "implement translate" is the todo that was here. After testing, translate seems to be working fine. Needs further testing and looking. let array = Text.parseArray(a); let out; for (const v of array) { let val = convertArrayComponentToChatComponent(v); if (val.text === '') continue; if (out === undefined) { out = val continue; } let levels = [out]; let levelDifferences = []; let lastLevel = out; while (true) { if (!lastLevel.extra) break; lastLevel = lastLevel.extra[lastLevel.extra.length - 1]; levels.push(lastLevel); } for (const levelIndex in levels) { const level = levels[levelIndex]; levelDifferences[levelIndex] = chatComponentInheritablePropertiesDifferenceAmount(level, val); } let lowestDiffLevel = levels[levelDifferences.indexOf(Math.min(...levelDifferences))]; if (compareChatComponentInheritableProperties(lastLevel, val)) { lastLevel.text += val.text; continue; } if (!lowestDiffLevel.extra) lowestDiffLevel.extra = []; lowestDiffLevel.extra.push(val); } if (out === undefined) out = { text: '' }; return Text.minifyChat(out); } static minifyChat(chat) { chat = Text.parseChat(chat); chat = Object.assign({}, chat); chat = minifyChatComponent(chat, defaultInheritedChatProperties); return chat; } static parseChat(chat) { return deMinifyChatComponent(chat); } } function parseArrayComponent(component) { let out; if (['string', 'number', 'boolean'].includes(typeof component)) out = { text: component, color: 'default', modifiers: [] } else { out = { color: component.color || 'default', modifiers: [...new Set(component.modifiers || [])].sort() } const [type, value] = getTextComponentTypeValue(component); out[type] = value; if (type === 'translate' && component.with) out.with = component.with.map(parseArrayComponent); if (component.insertion) out.insertion = component.insertion; if ( component.clickEvent && component.clickEvent.action && ['open_url', 'run_command', 'suggest_command', 'change_page'].includes(component.clickEvent.action) && component.clickEvent.value && ['number', 'string'].includes(typeof component.clickEvent.value) ) out.clickEvent = { action: component.clickEvent.action, value: component.clickEvent.value } if ( component.hoverEvent && component.hoverEvent.action && ['show_text'].includes(component.hoverEvent.action) && component.hoverEvent.value && component.hoverEvent.value !== undefined ) out.hoverEvent = { action: component.hoverEvent.action, value: Text.parseArray(component.hoverEvent.value) } } return out; } function deMinifyChatComponent(chat) { let obj; if (['string', 'number', 'boolean'].includes(typeof chat) || chat === null) obj = { text: `${chat}` }; if (Array.isArray(chat)) obj = { ...chat[0], extra: [...(chat[0].extra || []), ...(chat.slice(1) || [])] }; if ( typeof chat === 'object' && !Array.isArray(chat) && chat !== null ) obj = chat; for (const extra in obj.extra || []) obj.extra[extra] = deMinifyChatComponent(obj.extra[extra]); return obj; }; function minifyChatComponent(chat, inherited) { if (typeof chat !== 'object') chat = { text: chat }; let properties = {}; for (const { name } of [...textModifiersWithoutReset, ...['color', 'insertion', 'clickEvent', 'hoverEvent'].map(a => ({ name: a }))]) properties[name] = chat[name] ?? inherited[name]; let overwrittenProperties = {} for (const name in properties) if (!compareChatComponentInheritableProperty(properties[name], inherited[name], name)) overwrittenProperties[name] = properties[name]; for (const name in properties) if (overwrittenProperties[name] === undefined) delete chat[name]; else chat[name] = overwrittenProperties[name]; if (chat.hoverEvent) chat.hoverEvent.value = minifyChatComponent(chat.hoverEvent.value, defaultInheritedChatProperties); if (chat.with) chat.with = chat.with.map(a => minifyChatComponent(a, properties)); if (chat.extra) { chat.extra = chat.extra.map(a => minifyChatComponent(a, properties)); chat = [chat, ...chat.extra]; delete chat[0].extra; if (Object.keys(overwrittenProperties).length === 0 && chat.text !== undefined) chat[0] = convertChatComponentTextToPrimitive(chat[0].text); } else if (Object.keys(overwrittenProperties).length === 0 && chat.text !== undefined) chat = convertChatComponentTextToPrimitive(chat.text); return chat; } function convertArrayComponentToChatComponent({ with: wit, color, modifiers, insertion, clickEvent, hoverEvent } = {}) { let out = { color: textColorsWithDefault.find(({ name }) => name === color).minecraftName, ...convertModifierArrayToObject(modifiers) }; //todo: split following code into separate function const [type, value] = getTextComponentTypeValue(arguments[0]); out[type] = value; if (type === 'translate' && wit) out.with = wit.map(convertArrayComponentToChatComponent); if (insertion) out.insertion = insertion; else out.insertion = ''; if (clickEvent) out.clickEvent = { action: clickEvent.action, value: clickEvent.value } else out.clickEvent = { action: 'change_page', value: 0 } if (hoverEvent) out.hoverEvent = { action: hoverEvent.action, value: Text.arrayToChat(hoverEvent.value) } else out.hoverEvent = { action: 'show_text', value: '' } return out; } function convertModifierArrayToObject(modifiers) { return Object.fromEntries( textModifiersWithoutReset .map(({ name }) => name) .map(a => [a, modifiers.includes(a)]) ); } function convertChatComponentTextToPrimitive(text) { if (!isNaN(parseInt(text))) return parseInt(text); else if (text === 'true' || text === 'false') return text === 'true'; else return text; } function chatComponentInheritablePropertiesDifferenceAmount(a, b) { let difference = 0; if (a.color !== b.color) difference += `,color:"${b.color}"`.length; //todo: value is not being escaped. Use JSON.stringify instead if (a.insertion !== b.insertion) if (b.insertion !== undefined) difference += `,insertion:"${b.insertion}"`.length; //todo: value is not being escaped. Use JSON.stringify instead else difference += ',insertion:""'.length; if ( (a.clickEvent?.action !== b.clickEvent?.action) || (a.clickEvent?.value !== b.clickEvent?.value) ) if (b.clickEvent !== undefined) difference += `,clickEvent:{action:"${b.clickEvent?.action}",value:"${b.clickEvent?.value}"}`.length; //todo: values are not being escaped. Use JSON.stringify instead else difference += ',clickEvent:{action:"change_page",value:0}'.length; if ( (a.hoverEvent?.action !== b.hoverEvent?.action) || (Boolean(a.hoverEvent) !== Boolean(b.hoverEvent)) || ((a.hoverEvent && b.hoverEvent) ? !compareChatComponentInheritableProperties(a.hoverEvent?.value, b.hoverEvent?.value) : false) ) if (b.hoverEvent !== undefined) difference += `,hoverEvent:{action:"${b.hoverEvent?.action}",value:${JSON.stringify(b.hoverEvent?.value)}}`.length; //todo: b.hoverEvent.action is not being escaped. Use JSON.stringify instead else difference += ',hoverEvent:{action:"show_text",value:""}'.length; for (const { name } of textModifiersWithoutReset) if (a[name] !== b[name]) difference += `,${name}:${b[name]}`.length; return difference; } function compareChatComponentInheritableProperties(a, b) { if (typeof a !== typeof b) return false; if (typeof a !== 'object' && typeof b !== 'object') return a === b; if (getTextComponentTypeValue(a)[0] !== getTextComponentTypeValue(b)[0]) return false; for (const propertyName of ['color', 'insertion', 'clickEvent', ...textModifiersWithoutReset.map(({ name }) => name), 'hoverEvent']) if (!compareChatComponentInheritableProperty(a[propertyName], b[propertyName], propertyName)) return false; return true; } function compareChatComponentInheritableProperty(a, b, name) { //todo: make more clear what this function returns. Maybe rename to chatComponentInheritablePropertyEquals if (typeof a !== typeof b) return false; if ( typeof a !== 'object' && typeof b !== 'object' ) return a === b; if (name === 'clickEvent') return a.action === b.action && a.value === b.value; if (name === 'hoverEvent') return a.action === b.action && compareChatComponentInheritableProperties(a.value, b.value); //todo: Use CustomError throw new Error(`Don't know how to compare ${name}`); } function getTextComponentTypeValue(component) { if (component.text !== undefined) return ['text', component.text]; else if (component.translate !== undefined) return ['translate', component.translate]; else if (component.keybind !== undefined) return ['keybind', component.keybind]; else return ['text', '']; } function getTextComponentDefaultText(component) { let [type, value] = getTextComponentTypeValue(component); //todo: why is this function being used? if (type === 'text') return value; if (type === 'translate') return formatJavaString(englishMessages[value] ?? value, ...((component.with || []).map(getTextComponentDefaultText))); if (type === 'keybind') return keybinds.find(({ code }) => code === component.keybind).default; throw new Error(`Unknown type ${type}`); } module.exports = Text;