diff --git a/javascripts/core/automator/automator-backend.js b/javascripts/core/automator/automator-backend.js index db361cd8e..7b34d5f0e 100644 --- a/javascripts/core/automator/automator-backend.js +++ b/javascripts/core/automator/automator-backend.js @@ -1,3 +1,5 @@ +import { AutomatorPanels } from "../../../src/components/tabs/automator/AutomatorDocs"; + /** @abstract */ class AutomatorCommandInterface { constructor(id) { @@ -180,6 +182,12 @@ export const AutomatorData = { MAX_ALLOWED_SCRIPT_CHARACTERS: 10000, MAX_ALLOWED_TOTAL_CHARACTERS: 60000, + MAX_ALLOWED_SCRIPT_NAME_LENGTH: 15, + MAX_ALLOWED_SCRIPT_COUNT: 20, + MAX_ALLOWED_CONSTANT_NAME_LENGTH: 20, + // Note that a study string with ALL studies in unshortened form without duplicated studies is ~230 characters + MAX_ALLOWED_CONSTANT_VALUE_LENGTH: 250, + MAX_ALLOWED_CONSTANT_COUNT: 30, scriptIndex() { return player.reality.automator.state.editorScript; @@ -428,7 +436,7 @@ export const AutomatorBackend = { step() { if (this.stack.isEmpty) return false; - for (let steps = 0; steps < 100; steps++) { + for (let steps = 0; steps < 100 && !this.hasJustCompleted; steps++) { switch (this.runCurrentCommand()) { case AUTOMATOR_COMMAND_STATUS.SAME_INSTRUCTION: return true; @@ -442,14 +450,22 @@ export const AutomatorBackend = { case AUTOMATOR_COMMAND_STATUS.SKIP_INSTRUCTION: this.nextCommand(); } + + // We need to break out of the loop if the last commands are all SKIP_INSTRUCTION, or else it'll start + // trying to execute from an undefined stack if it isn't set to automatically repeat + if (!this.stack.top) this.hasJustCompleted = true; } // This should in practice never happen by accident due to it requiring 100 consecutive commands that don't do // anything (looping a smaller group of no-ops will instead trigger the loop check every tick). Nevertheless, // better to not have an explicit infinite loop so that the game doesn't hang if the player decides to be funny - // and input 3000 comments in a row - GameUI.notify.error("Automator halted - too many consecutive no-ops detected"); - AutomatorData.logCommandEvent("Automator halted due to excessive no-op commands", this.currentLineNumber); + // and input 3000 comments in a row. If hasJustCompleted is true, then we actually broke out because the end of + // the script has no-ops and we just looped through them, and therefore shouldn't show these messages + if (!this.hasJustCompleted) { + GameUI.notify.error("Automator halted - too many consecutive no-ops detected"); + AutomatorData.logCommandEvent("Automator halted due to excessive no-op commands", this.currentLineNumber); + } + this.stop(); return false; }, @@ -612,6 +628,26 @@ export const AutomatorBackend = { this.reset(this.stack._data[0].commands); }, + changeModes(scriptID) { + Tutorial.moveOn(TUTORIAL_STATE.AUTOMATOR); + if (player.reality.automator.type === AUTOMATOR_TYPE.BLOCK) { + // 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.fromText(toConvert); + AutomatorBackend.saveScript(scriptID, toConvert); + player.reality.automator.type = AUTOMATOR_TYPE.BLOCK; + player.reality.automator.currentInfoPane = AutomatorPanels.BLOCKS; + } + AutomatorHighlighter.clearAllHighlightedLines(); + }, + stack: { _data: [], push(commands) { diff --git a/javascripts/core/automator/compiler.js b/javascripts/core/automator/compiler.js index 9542150ed..39d5ec6ae 100644 --- a/javascripts/core/automator/compiler.js +++ b/javascripts/core/automator/compiler.js @@ -525,6 +525,10 @@ import { AutomatorLexer } from "./lexer"; foundChildren += nestedCommands ? nestedCommands.map(c => validatedCount(c) + 1).reduce((sum, val) => sum + val, 0) : 0; + + // Trailing newlines get turned into a command with a single EOF argument; we return -1 because one level up + // on the recursion this looks like an otherwise valid command and would be counted as such + if (key === "EOF") return -1; } return foundChildren; }; diff --git a/javascripts/core/secret-formula/h2p.js b/javascripts/core/secret-formula/h2p.js index 9b8e40aff..3f6ff0502 100644 --- a/javascripts/core/secret-formula/h2p.js +++ b/javascripts/core/secret-formula/h2p.js @@ -912,32 +912,42 @@ simply completing more Realities.

The Automator uses a scripting language that allows you to automate nearly the entire game. -The interface has two panes, a script pane on the left where you enter the commands to automate the game, and a -multiple function pane on the right. +The interface has two panes, a script pane on the left where you enter the commands to automate the game and a +pane on the right which has multiple panels which do many different things. The panels on the right side include:
-These functions include: +- The command list, with information on all the commands available to you (some may not be available until you have + unlocked certain things)
-- a brief introduction to the Automator +- The template creator, which allows you to generate premade script templates to accomplish certain tasks
-- the command list, with information on all the commands available to you +- All errors in the current Automator script, as well as possible suggestions on how to fix them; the button for + this panel will light up if you have any errors in your current script
-- the template creator, which allows you to fill in premade templates to suit your own purposes +- All recently executed commands, what those commands did, and how recently they were executed
-- a list of all errors in the current Automator script +- A constant definition panel where you can define as shorthand for values within the automator (eg. special numbers + or certain Time Study trees)
-- a list of recently executed commands and what those commands did -
-- if you are in the Block mode of the Automator, the command blocks used to write the script +- If you are in the block mode of the Automator, there will also be a panel for the command blocks used to write the + script

-You can use as many rows as you need. -
-Some commands are gated behind unlocks, which will only become visible once you have unlocked them. +There are a few limitations to scripts in order to reduce lag and prevent save file size from getting too large. +Individual scripts are limited to a maximum of ${formatInt(AutomatorData.MAX_ALLOWED_SCRIPT_CHARACTERS)} +characters each, and all your scripts together cannot exceed a total character count of +${formatInt(AutomatorData.MAX_ALLOWED_TOTAL_CHARACTERS)}. Any changes made to scripts while above these limits +will not be saved if you refresh the page.

-You are able to create new scripts by clicking on the dropdown, and then clicking the "Create New..." option. -To rename a script, click the pencil next to the dropdown. Scripts are automatically saved as you edit them. -You can create as many scripts as you want. +You are able to create new scripts by clicking on the dropdown, and then clicking the "Create new script..." option. +To rename a script, click the pencil next to the dropdown and edit the name to whatever you with the script to be +called (max ${formatInt(AutomatorData.MAX_ALLOWED_SCRIPT_NAME_LENGTH)} characters). You are allowed to create a +maximum of ${formatInt(AutomatorData.MAX_ALLOWED_SCRIPT_COUNT)} scripts. +
+
+Scripts are automatically saved as you edit them, but are not saved to your game save until the global autosave timer +(ie. "Time since last save") triggers a full game save. If you make changes to scripts right before closing the game, +you should wait until the game saves afterwards in order to not lose your changes.

If you want a larger workspace, you can press the button in the top right corner of the documentation pane of the @@ -946,11 +956,15 @@ panes if you want more room to write your script or read documentation.

By pressing the top-right button on the script pane, you can switch to block mode, which may be more approachable if -you are unfamiliar with programming. To enter commands in block mode, drag the box for the relevant command from the -documentation pane into the script pane and drop it where you want the command to go. Commands can be freely -rearranged by dragging the blocks around if needed. Clicking the top-right button in block mode will switch back to -text mode, and switching between block and text mode will automatically translate your script as well. -Note that scripts can only be converted into block mode if they have no errors! +you are unfamiliar with programming. To enter commands in block mode, select the command block pane on the right and +drag the box for the relevant command into the script pane and drop it where you want the command to go. Commands can be +freely rearranged by dragging the blocks around if needed. +
+
+Clicking the top-right button in block mode will switch back to text mode, and switching between block and text mode +will automatically translate your script as well. If you have a script in text mode which has errors, the Automator +may not be able to figure out what blocks to convert the lines with errors into. This may result in part of your +script being lost if you attempt to convert a text script with errors into a bock script.

Just like your entire savefile, individual Automator scripts can be imported and exported from the game. diff --git a/javascripts/core/storage/dev-migrations.js b/javascripts/core/storage/dev-migrations.js index e1a62cfad..c58300842 100644 --- a/javascripts/core/storage/dev-migrations.js +++ b/javascripts/core/storage/dev-migrations.js @@ -1447,6 +1447,15 @@ GameStorage.devMigrations = { } player.reality.automator.scripts[key].content = lines.join("\n"); } + + // Migrate IDs for all saves made during wave 3 testing, to prevent odd overwriting behavior on importing + const newScripts = {}; + const oldScriptKeys = Object.keys(player.reality.automator.scripts); + for (let newID = 1; newID <= oldScriptKeys.length; newID++) { + newScripts[newID] = player.reality.automator.scripts[oldScriptKeys[newID - 1]]; + newScripts[newID].id = newID; + } + player.reality.automator.scripts = newScripts; } ], diff --git a/public/stylesheets/automator.css b/public/stylesheets/automator.css index fd050ecb9..b218e8bbd 100644 --- a/public/stylesheets/automator.css +++ b/public/stylesheets/automator.css @@ -14,7 +14,7 @@ --color-blockmator-block-background: #f5f5f5; --color-blockmator-block-command: #401090; --color-blockmator-block-required: #50aaaa; - --color-blockmator-block-optional: #bbbbbb; + --color-blockmator-block-optional: #684700; --color-blockmator-editor-background: white; } @@ -31,7 +31,7 @@ --color-blockmator-block-background: #000115; --color-blockmator-block-command: #a142ff; --color-blockmator-block-required: #005050; - --color-blockmator-block-optional: #555555; + --color-blockmator-block-optional: #684700; --color-blockmator-editor-background: black; } diff --git a/src/components/modals/StudyStringModal.vue b/src/components/modals/StudyStringModal.vue index 4272bcd7d..724853377 100644 --- a/src/components/modals/StudyStringModal.vue +++ b/src/components/modals/StudyStringModal.vue @@ -25,10 +25,9 @@ export default { }; }, computed: { - // This modal is used by both study importing and preset editing but only has a prop actually passed in when - // editing (which is the preset index). Needs to be an undefined check because index can be zero + // This modal is used by both study importing and preset editing, but is given an id of -1 when importing isImporting() { - return this.id === undefined; + return this.id === -1; }, // This represents the state reached from importing into an empty tree importedTree() { diff --git a/src/components/modals/SwitchAutomatorEditorModal.vue b/src/components/modals/SwitchAutomatorEditorModal.vue index a5fd852ec..c116a6cec 100644 --- a/src/components/modals/SwitchAutomatorEditorModal.vue +++ b/src/components/modals/SwitchAutomatorEditorModal.vue @@ -38,19 +38,7 @@ export default { this.isCurrentlyBlocks = player.reality.automator.type === AUTOMATOR_TYPE.BLOCK; }, toggleAutomatorMode() { - const scriptID = this.currentScriptID; - Tutorial.moveOn(TUTORIAL_STATE.AUTOMATOR); - if (this.isCurrentlyBlocks) { - // This saves the script after converting it. - BlockAutomator.parseTextFromBlocks(); - player.reality.automator.type = AUTOMATOR_TYPE.TEXT; - } else { - const toConvert = AutomatorTextUI.editor.getDoc().getValue(); - // Needs to be called to update the lines prop in the BlockAutomator object - BlockAutomator.fromText(toConvert); - AutomatorBackend.saveScript(scriptID, toConvert); - player.reality.automator.type = AUTOMATOR_TYPE.BLOCK; - } + AutomatorBackend.changeModes(this.currentScriptID); this.callback?.(); } } @@ -73,7 +61,8 @@ export default { commands. Warning: If these errors are caused by malformed loops or IFs, this may end up deleting large portions of - your script! Changing editor modes currently will delete {{ quantifyInt("block", lostBlocks) }}! + your script! Changing editor modes currently will cause {{ quantifyInt("line", lostBlocks) }} of code to be + lost!
diff --git a/src/components/tabs/automator/AutomatorBlockEditor.vue b/src/components/tabs/automator/AutomatorBlockEditor.vue index 553ff3044..bdef8d6a5 100644 --- a/src/components/tabs/automator/AutomatorBlockEditor.vue +++ b/src/components/tabs/automator/AutomatorBlockEditor.vue @@ -24,6 +24,7 @@ export default { }, mounted() { BlockAutomator.initialize(); + AutomatorData.recalculateErrors(); BlockAutomator.editor.scrollTo(0, BlockAutomator.previousScrollPosition); BlockAutomator.gutter.style.bottom = `${BlockAutomator.editor.scrollTop}px`; }, diff --git a/src/components/tabs/automator/AutomatorBlockSingleInput.vue b/src/components/tabs/automator/AutomatorBlockSingleInput.vue index 364417dcc..a479b90bc 100644 --- a/src/components/tabs/automator/AutomatorBlockSingleInput.vue +++ b/src/components/tabs/automator/AutomatorBlockSingleInput.vue @@ -234,7 +234,6 @@ export default { } this.recalculateErrorCount(); }, - // This gets called whenever blocks are changed, but we also need to halt execution if the currently visible script // is also the one being run recalculateErrorCount() { @@ -246,10 +245,17 @@ export default { }, errorTooltip() { if (!this.hasError || this.suppressTooltip) return undefined; + + // We want to keep the verbose error info for the error panel, but we need to shorten it for the tooltips here + // The problematic errors all seem to have the same format, which we can explicitly modify + let errorInfo = this.errors.find(e => e.startLine === this.lineNumber).info; + errorInfo = errorInfo + .replaceAll("\n", "") + .replace(/Expecting: one of these possible Token sequences:.*but found: (.*)/ui, "Unexpected input format: $1"); return { content: `
-
${this.errors.find(e => e.startLine === this.lineNumber).info}
+
${errorInfo}
`, html: true, trigger: "manual", @@ -298,7 +304,7 @@ export default { v-model="textContents" v-tooltip="errorTooltip()" :class="textInputClassObject()" - @keyup="validateInput()" + @keyup="changeBlock()" @focusin="handleFocus(true)" @focusout="handleFocus(false)" > diff --git a/src/components/tabs/automator/AutomatorBlocks.vue b/src/components/tabs/automator/AutomatorBlocks.vue index 50398f3ab..ac6601ec5 100644 --- a/src/components/tabs/automator/AutomatorBlocks.vue +++ b/src/components/tabs/automator/AutomatorBlocks.vue @@ -197,7 +197,7 @@ export const automatorBlocksMap = automatorBlocks.mapToObject(b => b.cmd, b => b


- Inputs with a gray color are optional, while inputs with a + Inputs with a brown color are optional, while inputs with a teal color are required. For more details, check the Scripting Information pane.

diff --git a/src/components/tabs/automator/AutomatorDefinePage.vue b/src/components/tabs/automator/AutomatorDefinePage.vue index aa4104c79..f15eaabfe 100644 --- a/src/components/tabs/automator/AutomatorDefinePage.vue +++ b/src/components/tabs/automator/AutomatorDefinePage.vue @@ -13,7 +13,13 @@ export default { }, computed: { maxConstantCount() { - return 30; + return AutomatorData.MAX_ALLOWED_CONSTANT_COUNT; + }, + maxNameLength() { + return AutomatorData.MAX_ALLOWED_CONSTANT_NAME_LENGTH; + }, + maxValueLength() { + return AutomatorData.MAX_ALLOWED_CONSTANT_VALUE_LENGTH; }, }, methods: { @@ -29,7 +35,8 @@ export default {
This panel allows you to define case-sensitive constant values which can be used in place of numbers or Time Study import strings. These definitions are shared across all of your scripts and are limited to a maximum of - {{ maxConstantCount }} defined constants. + {{ maxConstantCount }} defined constants. Additionally, constant names and values are limited to lengths of + {{ maxNameLength }} and {{ maxValueLength }} characters respectively.

As a usage example, defining @@ -59,6 +66,7 @@ export default { display: flex; flex-direction: column; border: solid 0.1rem var(--color-automator-docs-font); + border-radius: var(--var-border-radius, 0.5rem); padding: 0.5rem; margin-top: 1rem; } diff --git a/src/components/tabs/automator/AutomatorDefineSingleEntry.vue b/src/components/tabs/automator/AutomatorDefineSingleEntry.vue index 6dc3e1b25..72cb57c95 100644 --- a/src/components/tabs/automator/AutomatorDefineSingleEntry.vue +++ b/src/components/tabs/automator/AutomatorDefineSingleEntry.vue @@ -14,6 +14,14 @@ export default { valueString: "", }; }, + computed: { + maxNameLength() { + return AutomatorData.MAX_ALLOWED_CONSTANT_NAME_LENGTH; + }, + maxValueLength() { + return AutomatorData.MAX_ALLOWED_CONSTANT_VALUE_LENGTH; + }, + }, created() { this.aliasString = this.constant; this.oldAlias = this.aliasString; @@ -34,7 +42,6 @@ export default { return matchObj ? matchObj[0] === this.aliasString : false; }); - if (this.aliasString.length >= 20) return "Constant name must be shorter than 20 characters"; if (!isValidName) return "Constant name must be alphanumeric and cannot start with a number"; if (alreadyExists) return "You have already defined a constant with this name"; if (hasCommandConflict) return "Constant name conflicts with a command key word"; @@ -43,11 +50,9 @@ export default { const isNumber = this.valueString.match(/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/u); // Note: Does not do validation for studies existing - const isStudyString = this.valueString.match(/^\d{2,3}(,\d{2,3})*(\|\d\d?)?$/u); + const isStudyString = TimeStudyTree.isValidImportString(this.valueString); if (!isNumber && !isStudyString) return "Constant value must either be a number or Time Study string"; - // Note that a study string with ALL studies is ~230 characters - if (this.valueString.length >= 250) return "Constant value must be shorter than 250 characters"; return null; }, errorTooltip() { @@ -85,7 +90,9 @@ export default { @@ -100,7 +107,9 @@ export default { v-if="aliasString" v-model="valueString" class="c-define-textbox c-value" + :class="{ 'l-limit-textbox' : valueString && valueString.length === maxValueLength }" placeholder="Value for constant..." + :maxlength="maxValueLength" @focusin="handleFocus(true)" @focusout="handleFocus(false)" > @@ -115,6 +124,9 @@ export default { } .o-arrow-padding { + display: flex; + flex-direction: column; + justify-content: center; padding: 0 1rem; } @@ -133,11 +145,16 @@ export default { background: var(--color-automator-error-background); } +.l-limit-textbox { + border-style: dotted; + border-color: var(--color-automator-error-outline); +} + .c-alias { - width: 14rem; + min-width: 14.5rem; } .c-value { - width: 30rem; + width: 100%; } diff --git a/src/components/tabs/automator/AutomatorDocs.vue b/src/components/tabs/automator/AutomatorDocs.vue index 056e634c3..36d4d6fca 100644 --- a/src/components/tabs/automator/AutomatorDocs.vue +++ b/src/components/tabs/automator/AutomatorDocs.vue @@ -63,7 +63,7 @@ export default { }, nameTooltip() { return this.isNameTooLong - ? `Names cannot be longer than ${formatInt(this.maxScriptNameLength)} characters!` + ? `Names cannot be longer than ${formatInt(AutomatorData.MAX_ALLOWED_SCRIPT_NAME_LENGTH)} characters!` : ""; }, currentScriptID: { @@ -89,14 +89,14 @@ export default { maxTotalChars() { return AutomatorData.MAX_ALLOWED_TOTAL_CHARACTERS; }, - maxScriptNameLength() { - return 15; - }, maxScriptCount() { - return 20; + return AutomatorData.MAX_ALLOWED_SCRIPT_COUNT; }, panelEnum() { return AutomatorPanels; + }, + importTooltip() { + return this.canMakeNewScript ? "Import automator script" : "You have too many scripts to import another!"; } }, watch: { @@ -138,6 +138,7 @@ export default { } }, importScript() { + if (!this.canMakeNewScript) return; Modal.importScript.show(); }, onGameLoad() { @@ -201,7 +202,7 @@ export default { let newName = ""; if (trimmed.length === 2 && trimmed[1].length > 0) newName = trimmed[1]; - if (newName.length > this.maxScriptNameLength) { + if (newName.length > AutomatorData.MAX_ALLOWED_SCRIPT_NAME_LENGTH) { this.isNameTooLong = true; return; } @@ -290,8 +291,9 @@ export default { @click="exportScript" />
diff --git a/src/components/tabs/automator/AutomatorModeSwitch.vue b/src/components/tabs/automator/AutomatorModeSwitch.vue index 9d194beae..0738c94c9 100644 --- a/src/components/tabs/automator/AutomatorModeSwitch.vue +++ b/src/components/tabs/automator/AutomatorModeSwitch.vue @@ -1,6 +1,4 @@