STscript improvements (see below)

Add /while loop
Add escaping of macros in sub-commands
Add /input prompt
This commit is contained in:
Cohee 2023-11-24 12:49:14 +02:00
parent 55607ee847
commit ad8709842b
2 changed files with 112 additions and 32 deletions

View File

@ -25,6 +25,7 @@ import {
this_chid,
setCharacterName,
generateRaw,
callPopup,
} from "../script.js";
import { getMessageTimeStamp } from "./RossAscends-mods.js";
import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, selected_group } from "./group-chats.js";
@ -33,7 +34,7 @@ import { addEphemeralStoppingString, chat_styles, power_user } from "./power-use
import { autoSelectPersona } from "./personas.js";
import { getContext } from "./extensions.js";
import { hideChatMessage, unhideChatMessage } from "./chats.js";
import { stringToRange } from "./utils.js";
import { delay, stringToRange } from "./utils.js";
import { registerVariableCommands } from "./variables.js";
export {
executeSlashCommands,
@ -166,6 +167,8 @@ parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="mono
parser.addCommand('abort', abortCallback, [], ' aborts the slash command batch execution', true, true);
parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] (search value) performs a fuzzy match of the provided search using the provided list of value and passes the closest match to the next command through the pipe.', true, true);
parser.addCommand('pass', (_, arg) => arg, [], '<span class="monospace">(text)</span> passes the text to the next command through the pipe.', true, true);
parser.addCommand('delay', delayCallback, ['wait', 'sleep'], '<span class="monospace">(milliseconds)</span> delays the next command in the pipe by the specified number of milliseconds.', true, true);
parser.addCommand('input', inputCallback, ['prompt'], '<span class="monospace">(prompt)</span> shows a popup with the provided prompt and passes the user input to the next command through the pipe.', true, true);
registerVariableCommands();
const NARRATOR_NAME_KEY = 'narrator_name';
@ -177,6 +180,28 @@ function abortCallback() {
throw new Error('/abort command executed');
}
async function delayCallback(_, amount) {
if (!amount) {
console.warn('WARN: No amount provided for /delay command');
return;
}
amount = Number(amount);
if (isNaN(amount)) {
amount = 0;
}
await delay(amount);
}
async function inputCallback(_, prompt) {
// Do not remove this delay, otherwise the prompt will not show up
await delay(1);
const result = await callPopup(prompt || '', 'input');
await delay(1);
return result || '';
}
function fuzzyCallback(args, value) {
if (!value) {
console.warn('WARN: No argument provided for /fuzzy command');
@ -1042,9 +1067,11 @@ async function executeSlashCommands(text, unescape = false) {
return false;
}
// Unescape the pipe character
// Unescape the pipe character and macro braces
if (unescape) {
text = text.replace(/\\\|/g, '|');
text = text.replace(/\\\{/g, '{');
text = text.replace(/\\\}/g, '}');
}
// Hack to allow multi-line slash commands
@ -1085,8 +1112,8 @@ async function executeSlashCommands(text, unescape = false) {
if (typeof result.args === 'object') {
for (const [key, value] of Object.entries(result.args)) {
if (typeof value === 'string') {
if (pipeResult && /{{pipe}}/i.test(value)) {
result.args[key] = value.replace(/{{pipe}}/i, pipeResult);
if (/{{pipe}}/i.test(value)) {
result.args[key] = value.replace(/{{pipe}}/i, pipeResult || '');
}
result.args[key] = substituteParams(value.trim());
@ -1094,8 +1121,8 @@ async function executeSlashCommands(text, unescape = false) {
}
}
if (pipeResult && typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult);
if (typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult || '');
}
pipeResult = await result.command.callback(result.args, unnamedArg);

View File

@ -9,7 +9,7 @@ function getLocalVariable(name) {
const localVariable = chat_metadata?.variables[name];
return localVariable || '';
return isNaN(Number(localVariable)) ? (localVariable || '') : Number(localVariable);
}
function setLocalVariable(name, value) {
@ -25,7 +25,7 @@ function setLocalVariable(name, value) {
function getGlobalVariable(name) {
const globalVariable = extension_settings.variables.global[name];
return globalVariable || '';
return isNaN(Number(globalVariable)) ? (globalVariable || '') : Number(globalVariable);
}
function setGlobalVariable(name, value) {
@ -130,25 +130,80 @@ function listVariablesCallback() {
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
}
async function ifCallback(args, command) {
// Resultion order: numeric literal, local variable, global variable, string literal
const a = isNaN(Number(args.a)) ? (getLocalVariable(args.a) || getGlobalVariable(args.a) || args.a || '') : Number(args.a);
const b = isNaN(Number(args.b)) ? (getLocalVariable(args.b) || getGlobalVariable(args.b) || args.b || '') : Number(args.b);
const rule = args.rule;
async function whileCallback(args, command) {
const MAX_LOOPS = 100;
const isGuardOff = ['off', 'false', '0'].includes(args.guard?.toLowerCase());
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
if (!rule) {
toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command');
return '';
for (let i = 0; i < iterations; i++) {
const { a, b, rule } = parseBooleanOperands(args);
const result = evalBoolean(rule, a, b);
if (result && command) {
await executeSubCommands(command);
} else {
break;
}
}
}
async function ifCallback(args, command) {
const { a, b, rule } = parseBooleanOperands(args);
const result = evalBoolean(rule, a, b);
if (result && command) {
return await executeSubCommands(command);
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
return await executeSubCommands(args.else);
}
if ((typeof a === 'number' && isNaN(a)) || (typeof a === 'string' && a === '')) {
toastr.warning('The first operand must be a number, string or a variable name for the /if command.', 'Invalid /if command');
return '';
}
function parseBooleanOperands(args) {
// Resultion order: numeric literal, local variable, global variable, string literal
function getOperand(operand) {
const operandNumber = Number(operand);
const operandLocalVariable = getLocalVariable(operand);
const operandGlobalVariable = getGlobalVariable(operand);
const stringLiteral = String(operand);
if (!isNaN(operandNumber)) {
return operandNumber;
}
if (operandLocalVariable !== undefined && operandLocalVariable !== null && operandLocalVariable !== '') {
return operandLocalVariable;
}
if (operandGlobalVariable !== undefined && operandGlobalVariable !== null && operandGlobalVariable !== '') {
return operandGlobalVariable;
}
return stringLiteral || '';
}
const left = getOperand(args.a || args.left || args.first || args.x);
const right = getOperand(args.b || args.right || args.second || args.y);
const rule = args.rule;
if ((typeof left === 'number' && isNaN(left)) || (typeof left === 'string' && left === '')) {
toastr.warning('The left operand must be a number, string or a variable name.', 'Invalid command');
throw new Error('Invalid command.');
}
return { a: left, b: right, rule };
}
function evalBoolean(rule, a, b) {
if (!rule) {
toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
throw new Error('Invalid command.');
}
let result = false;
if (typeof a === 'string') {
if (typeof a === 'string' && typeof b !== 'number') {
const aString = String(a).toLowerCase();
const bString = String(b).toLowerCase();
@ -166,14 +221,17 @@ async function ifCallback(args, command) {
result = aString !== bString;
break;
default:
toastr.warning('Unknown rule for the /if command for type string.', 'Invalid /if command');
return '';
toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
throw new Error('Invalid command.');
}
} else if (typeof a === 'number') {
const aNumber = Number(a);
const bNumber = Number(b);
switch (rule) {
case 'not':
result = !aNumber;
break;
case 'gt':
result = aNumber > bNumber;
break;
@ -193,18 +251,12 @@ async function ifCallback(args, command) {
result = aNumber !== bNumber;
break;
default:
toastr.warning('Unknown rule for the /if command for type number.', 'Invalid /if command');
return '';
toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
throw new Error('Invalid command.');
}
}
if (result && command) {
return await executeSubCommands(command);
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
return await executeSubCommands(args.else);
}
return '';
return result;
}
async function executeSubCommands(command) {
@ -234,5 +286,6 @@ export function registerVariableCommands() {
registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> set a global variable value and pass it down the pipe, e.g. <tt>/setglobalvar key=color green</tt>', true, true);
registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '<span class="monospace">(key)</span> get a global variable value and pass it down the pipe, e.g. <tt>/getglobalvar height</tt>', true, true);
registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> add a value to a global variable and pass the result down the pipe, e.g. <tt>/addglobalvar score 10</tt>', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison else="(alt.command)" "(command)"</span> compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison else="(alt.command)" "(command)"</span> compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
registerSlashCommand('while', whileCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison "(command)"</span> compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/while a=i a=10 rule=let "/addvar i 1"</tt> adds 1 to the value of "i" until it reaches 10. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
}