Untangle the automator

This commit is contained in:
Andrei Andreev 2023-06-11 09:51:01 +02:00 committed by Andrei Andreev
parent 897364950d
commit 740d807373
16 changed files with 78 additions and 85 deletions

View File

@ -1,4 +1,5 @@
<script> <script>
import { blockifyTextAutomator } from "@/core/automator";
import ModalWrapper from "@/components/modals/ModalWrapper"; import ModalWrapper from "@/components/modals/ModalWrapper";
export default { export default {
@ -124,7 +125,7 @@ export default {
if (this.isBlock) { if (this.isBlock) {
const newTemplateBlock = { const newTemplateBlock = {
name: `Template: ${this.name}`, name: `Template: ${this.name}`,
blocks: AutomatorGrammar.blockifyTextAutomator(this.templateScript.script).blocks blocks: blockifyTextAutomator(this.templateScript.script).blocks
}; };
AutomatorData.blockTemplates.push(newTemplateBlock); AutomatorData.blockTemplates.push(newTemplateBlock);
GameUI.notify.info("Custom template block created"); GameUI.notify.info("Custom template block created");
@ -227,4 +228,4 @@ export default {
.o-load-preset-button-margin { .o-load-preset-button-margin {
margin-right: 0.3rem; margin-right: 0.3rem;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { AutomatorBackend } from "@/core/globals"; import { hasCompilationErrors } from "@/core/automator";
import ModalWrapperChoice from "@/components/modals/ModalWrapperChoice"; import ModalWrapperChoice from "@/components/modals/ModalWrapperChoice";
@ -108,7 +108,7 @@ export default {
this.importedPresets = parsed.presets; this.importedPresets = parsed.presets;
this.importedConstants = parsed.constants; this.importedConstants = parsed.constants;
this.lineCount = this.scriptContent.split("\n").length; this.lineCount = this.scriptContent.split("\n").length;
this.hasErrors = AutomatorGrammar.compile(this.scriptContent).errors.length !== 0; this.hasErrors = hasCompilationErrors(this.scriptContent);
this.isValid = true; this.isValid = true;
}, },
importSave() { importSave() {

View File

@ -2,6 +2,7 @@
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import AutomatorBlockSingleRow from "./AutomatorBlockSingleRow"; import AutomatorBlockSingleRow from "./AutomatorBlockSingleRow";
import { blockifyTextAutomator } from "@/core/automator";
export default { export default {
name: "AutomatorBlockEditor", name: "AutomatorBlockEditor",
@ -104,13 +105,13 @@ export const BlockAutomator = {
}, },
updateEditor(scriptText) { updateEditor(scriptText) {
const lines = AutomatorGrammar.blockifyTextAutomator(scriptText).blocks; const lines = blockifyTextAutomator(scriptText).blocks;
this.lines = lines; this.lines = lines;
return lines; return lines;
}, },
hasUnparsableCommands(text) { hasUnparsableCommands(text) {
const blockified = AutomatorGrammar.blockifyTextAutomator(text); const blockified = blockifyTextAutomator(text);
return blockified.validatedBlocks !== blockified.visitedBlocks; return blockified.validatedBlocks !== blockified.visitedBlocks;
}, },

View File

@ -1,4 +1,6 @@
<script> <script>
import { validateLine } from "@/core/automator";
export default { export default {
name: "AutomatorBlockSingleInput", name: "AutomatorBlockSingleInput",
props: { props: {
@ -194,10 +196,10 @@ export default {
const clone = Object.assign({}, this.b); const clone = Object.assign({}, this.b);
clone.nest = []; clone.nest = [];
lines = BlockAutomator.parseLines([clone]); lines = BlockAutomator.parseLines([clone]);
validator = AutomatorGrammar.validateLine(lines.join("\n")); validator = validateLine(lines.join("\n"));
} else { } else {
lines = BlockAutomator.parseLines([this.b]); lines = BlockAutomator.parseLines([this.b]);
validator = AutomatorGrammar.validateLine(lines[0]); validator = validateLine(lines[0]);
} }
// Yes, the odd structure of this check is intentional. Something odd happens within parseLines under certain // Yes, the odd structure of this check is intentional. Something odd happens within parseLines under certain

View File

@ -1,4 +1,6 @@
<script> <script>
import { forbiddenConstantPatterns } from "@/core/automator";
export default { export default {
name: "AutomatorDefineSingleEntry", name: "AutomatorDefineSingleEntry",
props: { props: {

View File

@ -1,4 +1,5 @@
<script> <script>
import { AUTOMATOR_TYPE } from "@/core/automator/automator-backend";
import AutomatorBlocks from "./AutomatorBlocks"; import AutomatorBlocks from "./AutomatorBlocks";
import AutomatorButton from "./AutomatorButton"; import AutomatorButton from "./AutomatorButton";
import AutomatorDataTransferPage from "./AutomatorDataTransferPage"; import AutomatorDataTransferPage from "./AutomatorDataTransferPage";
@ -121,8 +122,8 @@ export default {
created() { created() {
this.on$(GAME_EVENT.GAME_LOAD, () => this.onGameLoad()); this.on$(GAME_EVENT.GAME_LOAD, () => this.onGameLoad());
this.on$(GAME_EVENT.AUTOMATOR_SAVE_CHANGED, () => this.onGameLoad()); this.on$(GAME_EVENT.AUTOMATOR_SAVE_CHANGED, () => this.onGameLoad());
this.updateCurrentScriptID(); this.on$(GAME_EVENT.AUTOMATOR_TYPE_CHANGED, () => this.openMatchingAutomatorTypeDocs());
this.updateScriptList(); this.onGameLoad();
}, },
destroyed() { destroyed() {
this.fullScreen = false; this.fullScreen = false;
@ -154,6 +155,7 @@ export default {
onGameLoad() { onGameLoad() {
this.updateCurrentScriptID(); this.updateCurrentScriptID();
this.updateScriptList(); this.updateScriptList();
this.fixAutomatorTypeDocs();
}, },
updateScriptList() { updateScriptList() {
this.scripts = Object.values(player.reality.automator.scripts).map(script => ({ this.scripts = Object.values(player.reality.automator.scripts).map(script => ({
@ -185,6 +187,21 @@ export default {
if (!this.isBlock && AutomatorTextUI.editor) AutomatorTextUI.editor.performLint(); if (!this.isBlock && AutomatorTextUI.editor) AutomatorTextUI.editor.performLint();
}); });
}, },
fixAutomatorTypeDocs() {
const automator = player.reality.automator;
if (automator.currentInfoPane === AutomatorPanels.COMMANDS && automator.type === AUTOMATOR_TYPE.BLOCK) {
this.openMatchingAutomatorTypeDocs();
}
if (automator.currentInfoPane === AutomatorPanels.BLOCKS && automator.type === AUTOMATOR_TYPE.TEXT) {
this.openMatchingAutomatorTypeDocs();
}
},
openMatchingAutomatorTypeDocs() {
const automator = player.reality.automator;
automator.currentInfoPane = automator.type === AUTOMATOR_TYPE.BLOCK
? AutomatorPanels.BLOCKS
: AutomatorPanels.COMMANDS;
},
rename() { rename() {
this.editingName = true; this.editingName = true;
this.$nextTick(() => { this.$nextTick(() => {

View File

@ -1,4 +1,6 @@
<script> <script>
import { blockifyTextAutomator } from "@/core/automator";
export default { export default {
name: "AutomatorModeSwitch", name: "AutomatorModeSwitch",
data() { data() {
@ -68,7 +70,7 @@ export default {
(BlockAutomator.hasUnparsableCommands(currScript) || AutomatorData.currentErrors().length !== 0); (BlockAutomator.hasUnparsableCommands(currScript) || AutomatorData.currentErrors().length !== 0);
if (player.options.confirmations.switchAutomatorMode && (hasTextErrors || AutomatorBackend.isRunning)) { if (player.options.confirmations.switchAutomatorMode && (hasTextErrors || AutomatorBackend.isRunning)) {
const blockified = AutomatorGrammar.blockifyTextAutomator(currScript); const blockified = blockifyTextAutomator(currScript);
// We explicitly pass in 0 for lostBlocks if converting from block to text since nothing is ever lost in that // We explicitly pass in 0 for lostBlocks if converting from block to text since nothing is ever lost in that
// conversion direction // conversion direction

View File

@ -1,21 +1,4 @@
import { AutomatorPanels } from "@/components/tabs/automator/AutomatorDocs"; import { compile } from "./compiler";
/** @abstract */
class AutomatorCommandInterface {
constructor(id) {
AutomatorCommandInterface.all[id] = this;
}
/** @abstract */
// eslint-disable-next-line no-unused-vars
run(command) { throw new NotImplementedError(); }
}
AutomatorCommandInterface.all = [];
export function AutomatorCommand(id) {
return AutomatorCommandInterface.all[id];
}
export const AUTOMATOR_COMMAND_STATUS = Object.freeze({ export const AUTOMATOR_COMMAND_STATUS = Object.freeze({
NEXT_INSTRUCTION: 0, NEXT_INSTRUCTION: 0,
@ -150,7 +133,7 @@ export class AutomatorScript {
} }
compile() { compile() {
this._compiled = AutomatorGrammar.compile(this.text).compiled; this._compiled = compile(this.text).compiled;
} }
static create(name, content = "") { static create(name, content = "") {
@ -215,7 +198,7 @@ export const AutomatorData = {
}, },
recalculateErrors() { recalculateErrors() {
const toCheck = this.currentScriptText(); const toCheck = this.currentScriptText();
this.cachedErrors = AutomatorGrammar.compile(toCheck).errors; this.cachedErrors = compile(toCheck).errors;
this.cachedErrors.sort((a, b) => a.startLine - b.startLine); this.cachedErrors.sort((a, b) => a.startLine - b.startLine);
}, },
currentErrors() { currentErrors() {
@ -1012,18 +995,15 @@ export const AutomatorBackend = {
// This saves the script after converting it. // This saves the script after converting it.
BlockAutomator.parseTextFromBlocks(); BlockAutomator.parseTextFromBlocks();
player.reality.automator.type = AUTOMATOR_TYPE.TEXT; player.reality.automator.type = AUTOMATOR_TYPE.TEXT;
if (player.reality.automator.currentInfoPane === AutomatorPanels.BLOCKS) {
player.reality.automator.currentInfoPane = AutomatorPanels.COMMANDS;
}
} else { } else {
const toConvert = AutomatorTextUI.editor.getDoc().getValue(); const toConvert = AutomatorTextUI.editor.getDoc().getValue();
// Needs to be called to update the lines prop in the BlockAutomator object // Needs to be called to update the lines prop in the BlockAutomator object
BlockAutomator.updateEditor(toConvert); BlockAutomator.updateEditor(toConvert);
AutomatorBackend.saveScript(scriptID, toConvert); AutomatorBackend.saveScript(scriptID, toConvert);
player.reality.automator.type = AUTOMATOR_TYPE.BLOCK; player.reality.automator.type = AUTOMATOR_TYPE.BLOCK;
player.reality.automator.currentInfoPane = AutomatorPanels.BLOCKS;
} }
AutomatorHighlighter.clearAllHighlightedLines(); AutomatorHighlighter.clearAllHighlightedLines();
EventHub.ui.dispatch(GAME_EVENT.AUTOMATOR_TYPE_CHANGED);
}, },
clearEditor() { clearEditor() {

View File

@ -1,6 +1,6 @@
import { AutomatorGrammar } from "./parser"; import { lexer, tokenIds } from "./lexer";
import { AutomatorLexer } from "./lexer"; import { compile } from "./compiler";
import { parser } from "./parser";
function walkSuggestion(suggestion, prefix, output) { function walkSuggestion(suggestion, prefix, output) {
const hasAutocomplete = suggestion.$autocomplete && const hasAutocomplete = suggestion.$autocomplete &&
@ -8,14 +8,14 @@ function walkSuggestion(suggestion, prefix, output) {
const isUnlocked = suggestion.$unlocked ? suggestion.$unlocked() : true; const isUnlocked = suggestion.$unlocked ? suggestion.$unlocked() : true;
if (hasAutocomplete && isUnlocked) output.add(suggestion.$autocomplete); if (hasAutocomplete && isUnlocked) output.add(suggestion.$autocomplete);
for (const s of suggestion.categoryMatches) { for (const s of suggestion.categoryMatches) {
walkSuggestion(AutomatorLexer.tokenIds[s], prefix, output); walkSuggestion(tokenIds[s], prefix, output);
} }
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
CodeMirror.registerHelper("lint", "automato", (contents, _, editor) => { CodeMirror.registerHelper("lint", "automato", (contents, _, editor) => {
const doc = editor.getDoc(); const doc = editor.getDoc();
const errors = AutomatorGrammar.compile(contents, true).errors; const errors = compile(contents, true).errors;
return errors.map(e => ({ return errors.map(e => ({
message: e.info, message: e.info,
severity: "error", severity: "error",
@ -32,9 +32,9 @@ CodeMirror.registerHelper("hint", "anyword", editor => {
while (start && /\w/u.test(line.charAt(start - 1)))--start; while (start && /\w/u.test(line.charAt(start - 1)))--start;
const lineStart = line.slice(0, start); const lineStart = line.slice(0, start);
const currentPrefix = line.slice(start, end); const currentPrefix = line.slice(start, end);
const lineLex = AutomatorLexer.lexer.tokenize(lineStart); const lineLex = lexer.tokenize(lineStart);
if (lineLex.errors.length > 0) return undefined; if (lineLex.errors.length > 0) return undefined;
const rawSuggestions = AutomatorGrammar.parser.computeContentAssist("command", lineLex.tokens); const rawSuggestions = parser.computeContentAssist("command", lineLex.tokens);
const suggestions = new Set(); const suggestions = new Set();
for (const s of rawSuggestions) { for (const s of rawSuggestions) {
if (s.ruleStack[1] === "badCommand") continue; if (s.ruleStack[1] === "badCommand") continue;

View File

@ -1,10 +1,9 @@
import { AutomatorLexer } from "./lexer"; import { standardizeAutomatorValues, tokenMap as T } from "./lexer";
/** /**
* Note: the $ shorthand for the parser object is required by Chevrotain. Don't mess with it. * Note: the $ shorthand for the parser object is required by Chevrotain. Don't mess with it.
*/ */
const T = AutomatorLexer.tokenMap;
const presetSplitter = /name[ \t]+(.+$)/ui; const presetSplitter = /name[ \t]+(.+$)/ui;
const idSplitter = /id[ \t]+(\d)/ui; const idSplitter = /id[ \t]+(\d)/ui;

View File

@ -1,8 +1,7 @@
import { lexer, tokenMap as T } from "./lexer";
import { AutomatorCommands } from "./automator-commands"; import { AutomatorCommands } from "./automator-commands";
import { AutomatorGrammar } from "./parser"; import { parser } from "./parser";
import { AutomatorLexer } from "./lexer";
const parser = AutomatorGrammar.parser;
const BaseVisitor = parser.getBaseCstVisitorConstructorWithDefaults(); const BaseVisitor = parser.getBaseCstVisitorConstructorWithDefaults();
class Validator extends BaseVisitor { class Validator extends BaseVisitor {
@ -20,7 +19,7 @@ class Validator extends BaseVisitor {
}; };
} }
const lexResult = AutomatorLexer.lexer.tokenize(rawText); const lexResult = lexer.tokenize(rawText);
const tokens = lexResult.tokens; const tokens = lexResult.tokens;
parser.input = tokens; parser.input = tokens;
this.parseResult = parser.script(); this.parseResult = parser.script();
@ -362,7 +361,6 @@ class Validator extends BaseVisitor {
this.addError(ctx, "Missing comparison operator (<, >, <=, >=)", "Insert the appropriate comparison operator"); this.addError(ctx, "Missing comparison operator (<, >, <=, >=)", "Insert the appropriate comparison operator");
return; return;
} }
const T = AutomatorLexer.tokenMap;
if (ctx.ComparisonOperator[0].tokenType === T.OpEQ || ctx.ComparisonOperator[0].tokenType === T.EqualSign) { if (ctx.ComparisonOperator[0].tokenType === T.OpEQ || ctx.ComparisonOperator[0].tokenType === T.EqualSign) {
this.addError(ctx, "Please use an inequality comparison (>, <, >=, <=)", this.addError(ctx, "Please use an inequality comparison (>, <, >=, <=)",
"Comparisons cannot be done with equality, only with inequality operators"); "Comparisons cannot be done with equality, only with inequality operators");
@ -529,7 +527,7 @@ class Blockifier extends BaseVisitor {
} }
} }
function compile(input, validateOnly = false) { export function compile(input, validateOnly = false) {
// The lexer and codemirror choke on the last line of the script, so we pad it with an invisible newline // The lexer and codemirror choke on the last line of the script, so we pad it with an invisible newline
const script = `${input}\n `; const script = `${input}\n `;
const validator = new Validator(script); const validator = new Validator(script);
@ -542,9 +540,12 @@ function compile(input, validateOnly = false) {
compiled, compiled,
}; };
} }
AutomatorGrammar.compile = compile;
function blockifyTextAutomator(input) { export function hasCompilationErrors(input) {
return compile(input, true).errors.length !== 0;
}
export function blockifyTextAutomator(input) {
const validator = new Validator(input); const validator = new Validator(input);
const blockifier = new Blockifier(); const blockifier = new Blockifier();
const blocks = blockifier.visit(validator.parseResult); const blocks = blockifier.visit(validator.parseResult);
@ -584,11 +585,8 @@ function blockifyTextAutomator(input) {
return { blocks, validatedBlocks, visitedBlocks }; return { blocks, validatedBlocks, visitedBlocks };
} }
AutomatorGrammar.blockifyTextAutomator = blockifyTextAutomator;
function validateLine(input) { export function validateLine(input) {
const validator = new Validator(input); const validator = new Validator(input);
return validator; return validator;
} }
AutomatorGrammar.validateLine = validateLine;

View File

@ -1,5 +1,14 @@
import "./compiler";
import "./automator-codemirror"; import "./automator-codemirror";
export { AutomatorGrammar } from "./parser"; export {
export { forbiddenConstantPatterns, standardizeAutomatorValues } from "./lexer"; forbiddenConstantPatterns
} from "./lexer";
export {
blockifyTextAutomator,
hasCompilationErrors,
validateLine
} from "./compiler";
export * from "./automator-backend";
export * from "./automator-points";

View File

@ -342,7 +342,7 @@ const Pipe = createToken({ name: "Pipe", pattern: /\|/, label: "|" });
const Dash = createToken({ name: "Dash", pattern: /-/, label: "-" }); const Dash = createToken({ name: "Dash", pattern: /-/, label: "-" });
// The order here is the order the lexer looks for tokens in. // The order here is the order the lexer looks for tokens in.
const automatorTokens = [ export const automatorTokens = [
HSpace, StringLiteral, StringLiteralSingleQuote, Comment, EOL, HSpace, StringLiteral, StringLiteralSingleQuote, Comment, EOL,
ComparisonOperator, ...tokenLists.ComparisonOperator, ComparisonOperator, ...tokenLists.ComparisonOperator,
LCurly, RCurly, Comma, EqualSign, Pipe, Dash, LCurly, RCurly, Comma, EqualSign, Pipe, Dash,
@ -362,19 +362,19 @@ RCurly.LABEL = "'}'";
NumberLiteral.LABEL = "Number"; NumberLiteral.LABEL = "Number";
Comma.LABEL = "❟"; Comma.LABEL = "❟";
const lexer = new Lexer(automatorTokens, { export const lexer = new Lexer(automatorTokens, {
positionTracking: "full", positionTracking: "full",
ensureOptimizations: true ensureOptimizations: true
}); });
// The lexer uses an ID system that's separate from indices into the token array // The lexer uses an ID system that's separate from indices into the token array
const tokenIds = []; export const tokenIds = [];
for (const token of lexer.lexerDefinition) { for (const token of lexer.lexerDefinition) {
tokenIds[token.tokenTypeIdx] = token; tokenIds[token.tokenTypeIdx] = token;
} }
// We use this while building up the grammar // We use this while building up the grammar
const tokenMap = automatorTokens.mapToObject(e => e.name, e => e); export const tokenMap = automatorTokens.mapToObject(e => e.name, e => e);
const automatorCurrencyNames = tokenLists.AutomatorCurrency.map(i => i.$autocomplete.toUpperCase()); const automatorCurrencyNames = tokenLists.AutomatorCurrency.map(i => i.$autocomplete.toUpperCase());
@ -405,12 +405,3 @@ export const forbiddenConstantPatterns = lexer.lexerDefinition
.filter(p => !ignoredPatterns.includes(p.name)) .filter(p => !ignoredPatterns.includes(p.name))
.map(p => p.PATTERN.source) .map(p => p.PATTERN.source)
.flatMap(p => ((p.includes("(") || p.includes(")")) ? p : p.split("[ \\t]+"))); .flatMap(p => ((p.includes("(") || p.includes(")")) ? p : p.split("[ \\t]+")));
export const AutomatorLexer = {
lexer,
tokens: automatorTokens,
tokenIds,
tokenMap,
standardizeAutomatorValues,
forbiddenConstantPatterns,
};

View File

@ -1,14 +1,12 @@
import { EOF, Parser } from "chevrotain"; import { EOF, Parser } from "chevrotain";
import { automatorTokens, tokenMap as T } from "./lexer";
import { AutomatorCommands } from "./automator-commands"; import { AutomatorCommands } from "./automator-commands";
import { AutomatorLexer } from "./lexer";
const T = AutomatorLexer.tokenMap;
// ----------------- parser ----------------- // ----------------- parser -----------------
class AutomatorParser extends Parser { class AutomatorParser extends Parser {
constructor() { constructor() {
super(AutomatorLexer.tokens, { super(automatorTokens, {
recoveryEnabled: true, recoveryEnabled: true,
outputCst: true, outputCst: true,
nodeLocationTracking: "full", nodeLocationTracking: "full",
@ -130,10 +128,4 @@ class AutomatorParser extends Parser {
} }
} }
const parser = new AutomatorParser(); export const parser = new AutomatorParser();
export const AutomatorGrammar = {
parser,
// This field is filled in by automator-validate.js
validate: null,
};

View File

@ -100,6 +100,7 @@ window.GAME_EVENT = {
OFFLINE_CURRENCY_GAINED: "OFFLINE_CURRENCY_GAINED", OFFLINE_CURRENCY_GAINED: "OFFLINE_CURRENCY_GAINED",
SAVE_CONVERTED_FROM_PREVIOUS_VERSION: "SAVE_CONVERTED_FROM_PREVIOUS_VERSION", SAVE_CONVERTED_FROM_PREVIOUS_VERSION: "SAVE_CONVERTED_FROM_PREVIOUS_VERSION",
REALITY_FIRST_UNLOCKED: "REALITY_FIRST_UNLOCKED", REALITY_FIRST_UNLOCKED: "REALITY_FIRST_UNLOCKED",
AUTOMATOR_TYPE_CHANGED: "AUTOMATOR_TYPE_CHANGED",
AUTOMATOR_SAVE_CHANGED: "AUTOMATOR_SAVE_CHANGED", AUTOMATOR_SAVE_CHANGED: "AUTOMATOR_SAVE_CHANGED",
AUTOMATOR_CONSTANT_CHANGED: "AUTOMATOR_CONSTANT_CHANGED", AUTOMATOR_CONSTANT_CHANGED: "AUTOMATOR_CONSTANT_CHANGED",
PELLE_STRIKE_UNLOCKED: "PELLE_STRIKE_UNLOCKED", PELLE_STRIKE_UNLOCKED: "PELLE_STRIKE_UNLOCKED",

View File

@ -1,7 +1,6 @@
export * from "./glyph-effects"; export * from "./glyph-effects";
export * from "./player"; export * from "./player";
export * from "./automator/automator-backend";
export * from "./performance-stats"; export * from "./performance-stats";
export * from "./currency"; export * from "./currency";
export * from "./cache"; export * from "./cache";
@ -38,7 +37,6 @@ export * from "./celestials/pelle/game-end";
export * from "./celestials/celestials"; export * from "./celestials/celestials";
export * from "./automator"; export * from "./automator";
export * from "./automator/automator-points";
export * from "./player-progress"; export * from "./player-progress";
export * from "./modal"; export * from "./modal";