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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
<script>
import { validateLine } from "@/core/automator";
export default {
name: "AutomatorBlockSingleInput",
props: {
@ -194,10 +196,10 @@ export default {
const clone = Object.assign({}, this.b);
clone.nest = [];
lines = BlockAutomator.parseLines([clone]);
validator = AutomatorGrammar.validateLine(lines.join("\n"));
validator = validateLine(lines.join("\n"));
} else {
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

View File

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

View File

@ -1,4 +1,5 @@
<script>
import { AUTOMATOR_TYPE } from "@/core/automator/automator-backend";
import AutomatorBlocks from "./AutomatorBlocks";
import AutomatorButton from "./AutomatorButton";
import AutomatorDataTransferPage from "./AutomatorDataTransferPage";
@ -121,8 +122,8 @@ export default {
created() {
this.on$(GAME_EVENT.GAME_LOAD, () => this.onGameLoad());
this.on$(GAME_EVENT.AUTOMATOR_SAVE_CHANGED, () => this.onGameLoad());
this.updateCurrentScriptID();
this.updateScriptList();
this.on$(GAME_EVENT.AUTOMATOR_TYPE_CHANGED, () => this.openMatchingAutomatorTypeDocs());
this.onGameLoad();
},
destroyed() {
this.fullScreen = false;
@ -154,6 +155,7 @@ export default {
onGameLoad() {
this.updateCurrentScriptID();
this.updateScriptList();
this.fixAutomatorTypeDocs();
},
updateScriptList() {
this.scripts = Object.values(player.reality.automator.scripts).map(script => ({
@ -185,6 +187,21 @@ export default {
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() {
this.editingName = true;
this.$nextTick(() => {

View File

@ -1,4 +1,6 @@
<script>
import { blockifyTextAutomator } from "@/core/automator";
export default {
name: "AutomatorModeSwitch",
data() {
@ -68,7 +70,7 @@ export default {
(BlockAutomator.hasUnparsableCommands(currScript) || AutomatorData.currentErrors().length !== 0);
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
// conversion direction

View File

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

View File

@ -1,6 +1,6 @@
import { AutomatorGrammar } from "./parser";
import { AutomatorLexer } from "./lexer";
import { lexer, tokenIds } from "./lexer";
import { compile } from "./compiler";
import { parser } from "./parser";
function walkSuggestion(suggestion, prefix, output) {
const hasAutocomplete = suggestion.$autocomplete &&
@ -8,14 +8,14 @@ function walkSuggestion(suggestion, prefix, output) {
const isUnlocked = suggestion.$unlocked ? suggestion.$unlocked() : true;
if (hasAutocomplete && isUnlocked) output.add(suggestion.$autocomplete);
for (const s of suggestion.categoryMatches) {
walkSuggestion(AutomatorLexer.tokenIds[s], prefix, output);
walkSuggestion(tokenIds[s], prefix, output);
}
}
// eslint-disable-next-line no-unused-vars
CodeMirror.registerHelper("lint", "automato", (contents, _, editor) => {
const doc = editor.getDoc();
const errors = AutomatorGrammar.compile(contents, true).errors;
const errors = compile(contents, true).errors;
return errors.map(e => ({
message: e.info,
severity: "error",
@ -32,9 +32,9 @@ CodeMirror.registerHelper("hint", "anyword", editor => {
while (start && /\w/u.test(line.charAt(start - 1)))--start;
const lineStart = line.slice(0, start);
const currentPrefix = line.slice(start, end);
const lineLex = AutomatorLexer.lexer.tokenize(lineStart);
const lineLex = lexer.tokenize(lineStart);
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();
for (const s of rawSuggestions) {
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.
*/
const T = AutomatorLexer.tokenMap;
const presetSplitter = /name[ \t]+(.+$)/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 { AutomatorGrammar } from "./parser";
import { AutomatorLexer } from "./lexer";
import { parser } from "./parser";
const parser = AutomatorGrammar.parser;
const BaseVisitor = parser.getBaseCstVisitorConstructorWithDefaults();
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;
parser.input = tokens;
this.parseResult = parser.script();
@ -362,7 +361,6 @@ class Validator extends BaseVisitor {
this.addError(ctx, "Missing comparison operator (<, >, <=, >=)", "Insert the appropriate comparison operator");
return;
}
const T = AutomatorLexer.tokenMap;
if (ctx.ComparisonOperator[0].tokenType === T.OpEQ || ctx.ComparisonOperator[0].tokenType === T.EqualSign) {
this.addError(ctx, "Please use an inequality comparison (>, <, >=, <=)",
"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
const script = `${input}\n `;
const validator = new Validator(script);
@ -542,9 +540,12 @@ function compile(input, validateOnly = false) {
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 blockifier = new Blockifier();
const blocks = blockifier.visit(validator.parseResult);
@ -584,11 +585,8 @@ function blockifyTextAutomator(input) {
return { blocks, validatedBlocks, visitedBlocks };
}
AutomatorGrammar.blockifyTextAutomator = blockifyTextAutomator;
function validateLine(input) {
export function validateLine(input) {
const validator = new Validator(input);
return validator;
}
AutomatorGrammar.validateLine = validateLine;

View File

@ -1,5 +1,14 @@
import "./compiler";
import "./automator-codemirror";
export { AutomatorGrammar } from "./parser";
export { forbiddenConstantPatterns, standardizeAutomatorValues } from "./lexer";
export {
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: "-" });
// The order here is the order the lexer looks for tokens in.
const automatorTokens = [
export const automatorTokens = [
HSpace, StringLiteral, StringLiteralSingleQuote, Comment, EOL,
ComparisonOperator, ...tokenLists.ComparisonOperator,
LCurly, RCurly, Comma, EqualSign, Pipe, Dash,
@ -362,19 +362,19 @@ RCurly.LABEL = "'}'";
NumberLiteral.LABEL = "Number";
Comma.LABEL = "❟";
const lexer = new Lexer(automatorTokens, {
export const lexer = new Lexer(automatorTokens, {
positionTracking: "full",
ensureOptimizations: true
});
// 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) {
tokenIds[token.tokenTypeIdx] = token;
}
// 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());
@ -405,12 +405,3 @@ export const forbiddenConstantPatterns = lexer.lexerDefinition
.filter(p => !ignoredPatterns.includes(p.name))
.map(p => p.PATTERN.source)
.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 { automatorTokens, tokenMap as T } from "./lexer";
import { AutomatorCommands } from "./automator-commands";
import { AutomatorLexer } from "./lexer";
const T = AutomatorLexer.tokenMap;
// ----------------- parser -----------------
class AutomatorParser extends Parser {
constructor() {
super(AutomatorLexer.tokens, {
super(automatorTokens, {
recoveryEnabled: true,
outputCst: true,
nodeLocationTracking: "full",
@ -130,10 +128,4 @@ class AutomatorParser extends Parser {
}
}
const parser = new AutomatorParser();
export const AutomatorGrammar = {
parser,
// This field is filled in by automator-validate.js
validate: null,
};
export const parser = new AutomatorParser();

View File

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

View File

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