UNPKG

@sarahisweird/hmoog

Version:

Out-of-game automation for Hackmud

219 lines (218 loc) 8.03 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HmOog = void 0; const hmoog_native_1 = __importDefault(require("@sarahisweird/hmoog-native")); const fileWatcher_1 = __importDefault(require("./fileWatcher")); const node_crypto_1 = require("node:crypto"); const promises_1 = require("node:fs/promises"); const utils_1 = require("./utils"); const errors_1 = require("./errors"); const types_1 = require("./types"); const constants_1 = require("./constants"); /** The class that manages OOG activity. */ class HmOog { /** * @param options Initialisation options - see {@link OogOptions} */ constructor(options) { this.didInit = false; this.unprocessedLines = []; const defaultedOptions = { shellPath: (0, utils_1.getShellPath)(), shouldFocusShell: true, ...(options || {}), }; if (!hmoog_native_1.default.init()) { throw new errors_1.OogInitializationError('Could not initialize native module!'); } this.shellPath = defaultedOptions.shellPath; this.shouldFocusShell = defaultedOptions.shouldFocusShell; this.fileWatcher = new fileWatcher_1.default(defaultedOptions.shellPath); } /** * Initialize the OOG. Must be called before any other methods! */ async init() { if (this.shouldFocusShell) { hmoog_native_1.default.sendMouseClick(100, 100, false); } await this.updateShell(); this.consumeLines(); // Discard old output this.didInit = true; } /** * Wait until the shell is flushed. * * @returns why the shell was flushed */ async waitForFlush() { this.assertDidInit(); await this.fileWatcher.waitForChange(); return await this.updateShell(); } ; /** * Wait until the shell is flushed by a `flush` command. */ async waitForCommandFlush() { while (await this.waitForFlush() !== types_1.FlushReason.COMMAND) { } } /** * Runs the `flush` command and waits until it successfully flushed the shell. * * @remarks * The flush operation may be delayed by another program that's currently being executed, * in which case this function only returns when both the program and the flush command ran. */ async flush() { this.assertDidInit(); let didCommandFlush = false; this.waitForCommandFlush().then(() => { didCommandFlush = true; }); await this.sendRaw('flush'); while (!didCommandFlush) { hmoog_native_1.default.sendKeystrokes('\n'); await (0, utils_1.waitMs)(50); } // For some reason, the delay sometimes isn't big enough. This should fix it. await (0, utils_1.waitMs)(100); } /** * Sends a command to Hackmud and processes the result. * * @param command The command to send * @returns The result of the command * @throws TypeError if the command contains newlines * @throws OogExecutionError if the command couldn't be typed into Hackmud */ async runCommand(command) { if (!await this.sendRaw(command)) throw new errors_1.OogExecutionError('Could not send command to Hackmud!'); await this.flush(); const lines = this.consumeLines(); const echoedCommand = lines.length > 0 ? (0, utils_1.removeColors)(lines.shift()).slice(2) : undefined; if (echoedCommand !== command) throw new errors_1.OogExecutionError('Could not find the command that was sent!'); let success; if ([constants_1.SUCCESS_MESSAGE, constants_1.FAILURE_MESSAGE].includes(lines[0])) { success = lines.shift() == constants_1.SUCCESS_MESSAGE; } const rawText = lines.join('\n'); const rawUncoloredText = (0, utils_1.removeColors)(rawText); const uncoloredLines = rawUncoloredText.split('\n'); return { command: command, success: success, output: { colored: { raw: rawText, lines: lines, }, uncolored: { raw: rawUncoloredText, lines: uncoloredLines, }, }, }; } /** * Sends a command to Hackmud. * * @param command The command to send * @returns whether the command could be typed into Hackmud * @throws TypeError if the command contains newlines */ async sendRaw(command) { if (command.trim() === '') throw new TypeError('Command cannot be empty!'); if (command.includes('\n')) throw new TypeError('Commands cannot contain newlines!'); if (!hmoog_native_1.default.sendKeystrokes(command + '\n')) return false; await (0, utils_1.waitMs)(20); return true; } /** * Removes and returns all shell lines yet to be processed by other methods. * * @returns a list of the raw shell output */ consumeLines() { const lines = this.unprocessedLines; this.unprocessedLines = []; return lines; } /** * Small helper method to check if {@link init} has been run. * * @private * @throws OogNotInitializedError if not initialized */ assertDidInit() { if (!this.didInit) throw new errors_1.OogNotInitializedError(); } /** * Sends a special flag, so we can find where we last left off. * * @remarks * * The reason we need to do this is that we can't tell apart old stuff from new stuff * in every case. If the outputs have been exactly the same, we'd need to guesswork. * This completely negates the need for guessing! * * @private */ async placeShellFlag() { this.lastShellFlag = (0, node_crypto_1.randomUUID)().toString(); await this.sendRaw(`# ${this.lastShellFlag}`); } /** * Helper method that searches for the previous {@link placeShellFlag shell flag} and removes everything * before it, including the flag itself and its error output. * * @private * @param lines The lines to process */ removeOldLines(lines) { if (!this.lastShellFlag) return lines; const lastLineIndex = lines.findIndex(line => line.includes(this.lastShellFlag)); if (this.didInit && lastLineIndex === -1) throw new Error('Couldn\'t merge shell histories!'); return lines.slice(lastLineIndex + 3); } /** * Reads the actual shell.txt file and puts yet unprocessed lines into {@link unprocessedLines}. * * Removes *some* junk output like any `flush` executions. * * @private * @returns The reason for the last shell flush */ async updateShell() { const newContents = await (0, promises_1.readFile)(this.shellPath, { encoding: 'utf8' }); const lines = newContents.split('\n'); let newLines = this.removeOldLines(lines); if (newLines.length === 0) return types_1.FlushReason.AUTO; const flushReason = newLines[newLines.length - 1] === constants_1.FLUSH_MESSAGE ? types_1.FlushReason.COMMAND : types_1.FlushReason.AUTO; if (flushReason === types_1.FlushReason.COMMAND) { (0, utils_1.popAssert)(newLines, constants_1.FLUSH_MESSAGE); // If the last command also was a flush, this will be empty. if (newLines.length > 0) { (0, utils_1.popAssert)(newLines, ''); } } this.unprocessedLines.push(...newLines); await this.placeShellFlag(); return flushReason; } } exports.HmOog = HmOog;