/char-find command to get a specific unique char
- findChar utility function that does the heavy lifting of finding a specific char based on conditions - Log/warn if multiple characters match - Validation function for named args that should be arrays
This commit is contained in:
parent
20a82fb242
commit
0be48c567a
|
@ -71,6 +71,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
|
||||||
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
|
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
|
||||||
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
||||||
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
|
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
|
||||||
|
import { getTagsList } from './tags.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
||||||
};
|
};
|
||||||
|
@ -173,6 +174,65 @@ export function initDefaultSlashCommands() {
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'char-find',
|
||||||
|
aliases: ['findchar'],
|
||||||
|
callback: (args, name) => {
|
||||||
|
if (typeof name !== 'string') throw new Error('name must be a string');
|
||||||
|
if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
|
||||||
|
|
||||||
|
const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent) });
|
||||||
|
return char?.avatar ?? '';
|
||||||
|
},
|
||||||
|
returns: 'the avatar key (unique identifier) of the character',
|
||||||
|
namedArgumentList: [
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'tag',
|
||||||
|
description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
enumProvider: commonEnumProviders.tags('assigned'),
|
||||||
|
acceptsMultiple: true,
|
||||||
|
}),
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'preferCurrent',
|
||||||
|
description: 'Prefer current character or characters in a group, if multiple characters match',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
defaultValue: 'true',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'Character name',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
enumProvider: commonEnumProviders.characters('character'),
|
||||||
|
forceEnum: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Searches for a character and returns its avatar key.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name
|
||||||
|
if you have multiple characters with the same name.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/char-find name="Chloe"</code></pre>
|
||||||
|
Returns the avatar key for "Chloe".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<pre><code>/search name="Chloe" tag="friend"</code></pre>
|
||||||
|
Returns the avatar key for the character "Chloe" that is tagged with "friend".
|
||||||
|
This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
|
||||||
|
so you can actually select the character you are looking for.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'sendas',
|
name: 'sendas',
|
||||||
callback: sendMessageAs,
|
callback: sendMessageAs,
|
||||||
|
@ -3116,42 +3176,94 @@ async function setNarratorName(_, text) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an argument is a string array (or undefined), and if not, throws an error
|
||||||
|
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
|
||||||
|
* @param {string} name The name of the argument for the error message
|
||||||
|
* @param {object} [options={}] - The optional arguments
|
||||||
|
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
|
||||||
|
* @throws {Error} If the argument is not an array
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
|
||||||
|
if (arg === undefined) {
|
||||||
|
if (allowUndefined) return undefined;
|
||||||
|
throw new Error(`Argument "${name}" is undefined, but must be a string array`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
|
||||||
|
if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an argument is a string or closure array (or undefined), and if not, throws an error
|
||||||
|
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
|
||||||
|
* @param {string} name The name of the argument for the error message
|
||||||
|
* @param {object} [options={}] - The optional arguments
|
||||||
|
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
|
||||||
|
* @throws {Error} If the argument is not an array of strings or closures
|
||||||
|
* @returns {(string|SlashCommandClosure)[]}
|
||||||
|
*/
|
||||||
|
export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
|
||||||
|
if (arg === undefined) {
|
||||||
|
if (allowUndefined) return [];
|
||||||
|
throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
|
||||||
|
if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a character by name, with optional filtering and precedence for avatars
|
* Finds a character by name, with optional filtering and precedence for avatars
|
||||||
* @param {string} name - The name to search for
|
|
||||||
* @param {object} [options={}] - The options for the search
|
* @param {object} [options={}] - The options for the search
|
||||||
|
* @param {string?} [options.name=null] - The name to search for
|
||||||
* @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar
|
* @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar
|
||||||
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
|
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
|
||||||
* @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
|
* @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
|
||||||
* @param {any?} [options.preferCurrentChar=null] - The current character to prefer
|
* @param {boolean} [options.preferCurrentChar=false] - Whether to prefer the current character(s)
|
||||||
|
* @param {boolean} [options.quiet=false] - Whether to suppress warnings
|
||||||
* @returns {any?} - The found character or null if not found
|
* @returns {any?} - The found character or null if not found
|
||||||
*/
|
*/
|
||||||
export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) {
|
export function findChar({ name = null, allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) {
|
||||||
const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name;
|
const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
|
||||||
|
|
||||||
|
// Get the current character(s)
|
||||||
|
const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]];
|
||||||
|
|
||||||
// If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence
|
// If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence
|
||||||
if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) {
|
if (preferCurrentChar && !filteredByTags) {
|
||||||
return preferCurrentChar;
|
const preferredChar = currentChars.find(matches);
|
||||||
|
if (preferredChar) {
|
||||||
|
return preferredChar;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter characters by tags if provided
|
// Filter characters by tags if provided
|
||||||
let filteredCharacters = characters;
|
let filteredCharacters = characters;
|
||||||
if (filteredByTags) {
|
if (filteredByTags) {
|
||||||
filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag)));
|
filteredCharacters = characters.filter(char => {
|
||||||
|
const charTags = getTagsList(char.avatar, false);
|
||||||
|
return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If allowAvatar is true, search by avatar first
|
// If allowAvatar is true, search by avatar first
|
||||||
if (allowAvatar) {
|
if (allowAvatar && name) {
|
||||||
const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
|
const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
|
||||||
if (characterByAvatar) {
|
if (characterByAvatar) {
|
||||||
return characterByAvatar;
|
return characterByAvatar;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for a matching character by name
|
// Search for matching characters by name
|
||||||
let character = filteredCharacters.find(matches);
|
const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
|
||||||
|
if (matchingCharacters.length > 1) {
|
||||||
|
if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
|
||||||
|
else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
|
||||||
|
}
|
||||||
|
|
||||||
return character;
|
return matchingCharacters[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendMessageAs(args, text) {
|
export async function sendMessageAs(args, text) {
|
||||||
|
|
|
@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
* _abortController:SlashCommandAbortController,
|
* _abortController:SlashCommandAbortController,
|
||||||
* _debugController:SlashCommandDebugController,
|
* _debugController:SlashCommandDebugController,
|
||||||
* _hasUnnamedArgument:boolean,
|
* _hasUnnamedArgument:boolean,
|
||||||
* [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[],
|
* [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
|
||||||
* }} NamedArguments
|
* }} NamedArguments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
|
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
|
||||||
* @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]}} NamedArgumentsCapture
|
* @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
|
||||||
import { extension_settings } from '../extensions.js';
|
import { extension_settings } from '../extensions.js';
|
||||||
import { getGroupMembers, groups } from '../group-chats.js';
|
import { getGroupMembers, groups } from '../group-chats.js';
|
||||||
import { power_user } from '../power-user.js';
|
import { power_user } from '../power-user.js';
|
||||||
import { searchCharByName, getTagsList, tags } from '../tags.js';
|
import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
|
||||||
import { world_names } from '../world-info.js';
|
import { world_names } from '../world-info.js';
|
||||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
|
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
|
||||||
|
@ -181,6 +181,18 @@ export const commonEnumProviders = {
|
||||||
*/
|
*/
|
||||||
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
|
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All possible tags, or only those that have been assigned
|
||||||
|
*
|
||||||
|
* @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
|
||||||
|
* @returns {() => SlashCommandEnumValue[]}
|
||||||
|
*/
|
||||||
|
tags: (mode = 'all') => () => {
|
||||||
|
let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
|
||||||
|
return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
|
||||||
|
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All possible tags for a given char/group entity
|
* All possible tags for a given char/group entity
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue