UNPKG

@sarahisweird/hmoog

Version:

Out-of-game automation for Hackmud

217 lines (216 loc) 8.27 kB
import { getShellPath, removeColors, waitMs } from './utils.js'; import * as native from '@sarahisweird/hmoog-native'; import FileWatcher from './fileWatcher.js'; import { readFile } from 'node:fs/promises'; import { ACTIVATING_HARDLINE_MESSAGE, FAILURE_MESSAGE, FLUSH_MESSAGE, GREATER_THAN_ENCODED, HARDLINE_ACTIVE_MESSAGE, HARDLINE_ALREADY_ACTIVE_MESSAGE, HARDLINE_DISCONNECTED_MESSAGE, HARDLINE_RECALIBRATING_MESSAGE, LESS_THAN_ENCODED, NO_HARDLINES_AVAILABLE_MESSAGE, SUCCESS_MESSAGE } from './constants.js'; import { AnsiConverter } from './terminal/ansi_converter.js'; const sendCommand = async (command) => { if (!native.sendKeystrokes(command + '\n')) return false; await waitMs(50); return true; }; export class HmOog { shellPath; fileWatcher; ansiOptions; lastCommand; isHardlineActive = false; constructor(options) { const defaultedOptions = { shellPath: getShellPath(), ansiOptions: {}, ...options, }; this.shellPath = defaultedOptions.shellPath; this.ansiOptions = defaultedOptions.ansiOptions; this.fileWatcher = new FileWatcher(this.shellPath); } async init() { if (!native.init()) { throw new Error('Failed to initialize hmoog-native!'); } native.sendMouseClick(100, 100, false); native.sendEscape(); await this.#flush(); } async run(command, timeout = 0, retry = true) { let data = null; let didReallyTimeout = false; if (timeout) { waitMs(timeout).then(() => didReallyTimeout = true); } while (data === null) { native.sendEscape(); await waitMs(500); if (!await sendCommand(command)) { throw new Error('Failed to send command via hmoog-native.'); } await waitMs(500); this.lastCommand = command; data = await this.#flush(timeout); this.lastCommand = undefined; if (data) break; if (didReallyTimeout) { console.warn(`Execution of command timed out after ${timeout}ms.`); return null; } if (!retry) { console.warn('Couldn\'t get a result for some reason!'); native.sendEscape(); return null; } } return this.#postProcess(command, data); } async enterHardline() { await sendCommand('kernel.hardline'); await waitMs(50); const lines = await this.#flush(3000); if (lines) { const result = this.#postProcess('kernel.hardline', lines); const cooldown = this.#getHardlineCooldown(result); console.log(cooldown); if (cooldown < 0) return 0; if (cooldown > 0) return cooldown; } await waitMs(10000); for (let i = 0; i < 12; i++) { await sendCommand('0123456789'); } await waitMs(15000); // Ensure that if we didn't manage to send the flush beforehand, it doesn't // sit in the shell still. native.sendKeystrokes('\n'); this.isHardlineActive = true; return 0; } /** * Alias for {@link exitHardline}. */ async enterRecon() { return this.exitHardline(); } async exitHardline() { const exitCommand = 'kernel.hardline { dc: true }'; await sendCommand(exitCommand); await waitMs(5000); const data = await this.#flush(); const result = this.#postProcess(exitCommand, data); const success = result.colored.raw.includes(HARDLINE_DISCONNECTED_MESSAGE); if (success) { this.isHardlineActive = false; } return success; } isInHardline() { return this.isHardlineActive; } #getHardlineCooldown(result) { const lines = result.colored.lines; if (lines.includes(ACTIVATING_HARDLINE_MESSAGE)) return 0; let cooldownMessage; const notAvailableIndex = lines.findLastIndex(line => line.includes(NO_HARDLINES_AVAILABLE_MESSAGE)); const recalibratingIndex = lines.findLastIndex(line => line.includes(HARDLINE_RECALIBRATING_MESSAGE)); const alreadyActiveIndex = lines.findLastIndex(line => line.includes(HARDLINE_ALREADY_ACTIVE_MESSAGE)); if (alreadyActiveIndex !== -1) { return -1; } else if (notAvailableIndex !== -1) { cooldownMessage = lines[notAvailableIndex] .substring(NO_HARDLINES_AVAILABLE_MESSAGE.length); } else if (recalibratingIndex !== -1) { cooldownMessage = lines[recalibratingIndex] .substring(HARDLINE_RECALIBRATING_MESSAGE.length); } else { // Tentatively going to assume that this is means it succeeded, but we didn't flush return 0; } const secondsString = cooldownMessage.substring(1).split(' ')[0].replace('s', ''); return (parseInt(secondsString) + 1) * 1000; } #postProcess(command, lines) { let success; if (lines.indexOf(SUCCESS_MESSAGE) !== -1) { success = true; } else if (lines.indexOf(FAILURE_MESSAGE) !== -1) { success = false; } const lastCommandIndex = lines.findLastIndex(line => removeColors(line) === `>>${command}`); let commandLine = ''; if (lastCommandIndex !== -1) { commandLine = lines[lastCommandIndex]; lines = lines.slice(lastCommandIndex + 1); } const rawText = lines.join('\n'); const uncoloredText = removeColors(rawText) .replaceAll(LESS_THAN_ENCODED, '<') .replaceAll(GREATER_THAN_ENCODED, '>'); const uncoloredLines = uncoloredText.split('\n'); const ansiCommand = AnsiConverter.convertFromShellText(commandLine, this.ansiOptions); const ansiText = AnsiConverter.convertFromShellText(rawText, this.ansiOptions); const ansiLines = ansiText.split('\n'); return { success: success, colored: { command: commandLine, raw: rawText, lines: lines, }, uncolored: { command: `>>${command}`, raw: uncoloredText, lines: uncoloredLines, }, ansi: { command: ansiCommand, raw: ansiText, lines: ansiLines, }, }; } async #flush(timeout = 0) { let didTimeout = false; let didFlush = false; this.fileWatcher.waitForChange().then(() => didFlush = true); if (timeout <= 0) timeout = 10000; waitMs(timeout).then(() => didTimeout = true); await sendCommand('flush'); while (!didTimeout && !didFlush) { native.sendKeystrokes('\n'); await waitMs(50); } if (didTimeout) return null; return await this.#readShell(); } async #readShell() { const contents = await readFile(this.shellPath, { encoding: 'utf-8' }); const lines = contents.split('\n'); const lastHardlineDisconnectedIndex = lines.lastIndexOf(HARDLINE_DISCONNECTED_MESSAGE); const lastHardlineActiveIndex = lines.lastIndexOf(HARDLINE_ACTIVE_MESSAGE); if (lastHardlineDisconnectedIndex > lastHardlineActiveIndex) { this.isHardlineActive = false; } if (!this.lastCommand) return lines; const enteredCommand = this.lastCommand .replaceAll('<', LESS_THAN_ENCODED) .replaceAll('>', GREATER_THAN_ENCODED); const lastCommandIndex = lines.findLastIndex(line => removeColors(line) === `>>${enteredCommand}`); if (lastCommandIndex === -1) return null; const lastFlushIndex = lines.lastIndexOf(FLUSH_MESSAGE); if (lastFlushIndex < lastCommandIndex) return null; return lines.slice(lastCommandIndex, lastFlushIndex); } }