Address second round of PR feedback (automator-improvements)

This commit is contained in:
SpectralFlame 2022-07-18 12:55:00 -05:00 committed by cyip92
parent 0c22340663
commit 458d111b67
15 changed files with 159 additions and 89 deletions

View File

@ -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) {

View File

@ -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;
};

View File

@ -912,32 +912,42 @@ simply completing more Realities.
<br>
<br>
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:
<br>
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)
<br>
- a brief introduction to the Automator
- The template creator, which allows you to generate premade script templates to accomplish certain tasks
<br>
- 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
<br>
- 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
<br>
- 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)
<br>
- a list of recently executed commands and what those commands did
<br>
- 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
<br>
<br>
You can use as many rows as you need.
<br>
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 <i>total</i> 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.
<br>
<br>
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.
<br>
<br>
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.
<br>
<br>
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.
<br>
<br>
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.
<br>
<br>
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.
<br>
<br>
Just like your entire savefile, individual Automator scripts can be imported and exported from the game.

View File

@ -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;
}
],

View File

@ -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;
}

View File

@ -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() {

View File

@ -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.
<b>
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!
</b>
</div>
<br>

View File

@ -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`;
},

View File

@ -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:
`<div class="c-block-automator-error">
<div>${this.errors.find(e => e.startLine === this.lineNumber).info}</div>
<div>${errorInfo}</div>
</div>`,
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)"
>

View File

@ -197,7 +197,7 @@ export const automatorBlocksMap = automatorBlocks.mapToObject(b => b.cmd, b => b
</p>
<br>
<p>
Inputs with a <span class="c-automator-input-optional">gray</span> color are optional, while inputs with a
Inputs with a <span class="c-automator-input-optional">brown</span> color are optional, while inputs with a
<span class="c-automator-input-required">teal</span> color are required. For more details, check the Scripting
Information pane.
</p>

View File

@ -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 {
<div class="l-panel-padding">
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.
<br>
<br>
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;
}

View File

@ -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 {
<input
v-model="aliasString"
class="c-define-textbox c-alias"
:class="{ 'l-limit-textbox' : aliasString.length === maxNameLength }"
placeholder="New constant..."
:maxlength="maxNameLength"
@focusin="handleFocus(true)"
@focusout="handleFocus(false)"
>
@ -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%;
}
</style>

View File

@ -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"
/>
<AutomatorButton
v-tooltip="'Import automator script'"
v-tooltip="importTooltip"
class="fa-file-import"
:class="{ 'c-automator__status-text--error' : !canMakeNewScript }"
@click="importScript"
/>
<div class="l-automator__script-names">

View File

@ -1,6 +1,4 @@
<script>
import { AutomatorPanels } from "./AutomatorDocs";
export default {
name: "AutomatorModeSwitch",
data() {
@ -65,38 +63,25 @@ export default {
},
toggleAutomatorMode() {
const currScript = player.reality.automator.scripts[this.currentScriptID].content;
const hasInvalidCommands = BlockAutomator.hasUnparsableCommands(currScript);
const hasInvalidCommands = BlockAutomator.hasUnparsableCommands(currScript) &&
this.automatorType === AUTOMATOR_TYPE.TEXT;
// While we normally have the player option override the modal, script deletion due to failed parsing can have a
// big enough adverse impact on the gameplay experience that we force the modal here regardless of the setting
if (hasInvalidCommands || (player.options.confirmations.switchAutomatorMode && AutomatorBackend.isRunning)) {
const blockified = AutomatorGrammar.blockifyTextAutomator(currScript);
// We explicitly pass in 0 for lostBlocks if converting from block to text since nothing is ever lost in that
// conversion direction
const lostBlocks = this.automatorType === AUTOMATOR_TYPE.TEXT
? blockified.validatedBlocks - blockified.visitedBlocks
: 0;
Modal.switchAutomatorEditorMode.show({
callBack: () => this.$recompute("currentScriptContent"),
lostBlocks: blockified.validatedBlocks - blockified.visitedBlocks,
lostBlocks,
});
} else {
const scriptID = this.currentScriptID;
Tutorial.moveOn(TUTORIAL_STATE.AUTOMATOR);
if (this.automatorType === 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;
}
// Don't use this.currentScriptContent here due to reactivity issues, but on the other hand reactively
// updating content might lead to decreased performance.
} 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;
}
this.$recompute("currentScriptContent");
AutomatorHighlighter.clearAllHighlightedLines();
AutomatorBackend.changeModes(this.currentScriptID);
}
}
}

View File

@ -137,7 +137,7 @@ export default {
</PrimaryButton>
<PrimaryButton
class="o-primary-btn--subtab-option"
onclick="Modal.studyString.show()"
onclick="Modal.studyString.show({ id: -1 })"
>
Import tree
</PrimaryButton>