@sarahisweird/hmoog
Version:
Out-of-game automation for Hackmud
219 lines (218 loc) • 8.03 kB
JavaScript
;
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;