/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
| @@ -71,6 +71,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom | |||
| 71 | import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; | 71 | import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; |
| 72 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; | 72 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; |
| 73 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; | 73 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; |
| 74 | import { getTagsList } from './tags.js'; | ||
| 74 | export { | 75 | export { |
| 75 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, | 76 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, |
| 76 | }; | 77 | }; |
| @@ -174,6 +175,65 @@ export function initDefaultSlashCommands() { | |||
| 174 | `, | 175 | `, |
| 175 | })); | 176 | })); |
| 176 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 177 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 178 | name: 'char-find', | ||
| 179 | aliases: ['findchar'], | ||
| 180 | callback: (args, name) => { | ||
| 181 | if (typeof name !== 'string') throw new Error('name must be a string'); | ||
| 182 | if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array'); | ||
| 183 | |||
| 184 | const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent) }); | ||
| 185 | return char?.avatar ?? ''; | ||
| 186 | }, | ||
| 187 | returns: 'the avatar key (unique identifier) of the character', | ||
| 188 | namedArgumentList: [ | ||
| 189 | SlashCommandNamedArgument.fromProps({ | ||
| 190 | name: 'tag', | ||
| 191 | description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.', | ||
| 192 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 193 | enumProvider: commonEnumProviders.tags('assigned'), | ||
| 194 | acceptsMultiple: true, | ||
| 195 | }), | ||
| 196 | SlashCommandNamedArgument.fromProps({ | ||
| 197 | name: 'preferCurrent', | ||
| 198 | description: 'Prefer current character or characters in a group, if multiple characters match', | ||
| 199 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 200 | defaultValue: 'true', | ||
| 201 | }), | ||
| 202 | ], | ||
| 203 | unnamedArgumentList: [ | ||
| 204 | SlashCommandArgument.fromProps({ | ||
| 205 | description: 'Character name', | ||
| 206 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 207 | enumProvider: commonEnumProviders.characters('character'), | ||
| 208 | forceEnum: false, | ||
| 209 | }), | ||
| 210 | ], | ||
| 211 | helpString: ` | ||
| 212 | <div> | ||
| 213 | Searches for a character and returns its avatar key. | ||
| 214 | </div> | ||
| 215 | <div> | ||
| 216 | This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name | ||
| 217 | if you have multiple characters with the same name. | ||
| 218 | </div> | ||
| 219 | <div> | ||
| 220 | <strong>Example:</strong> | ||
| 221 | <ul> | ||
| 222 | <li> | ||
| 223 | <pre><code>/char-find name="Chloe"</code></pre> | ||
| 224 | Returns the avatar key for "Chloe". | ||
| 225 | </li> | ||
| 226 | <li> | ||
| 227 | <pre><code>/search name="Chloe" tag="friend"</code></pre> | ||
| 228 | Returns the avatar key for the character "Chloe" that is tagged with "friend". | ||
| 229 | This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else, | ||
| 230 | so you can actually select the character you are looking for. | ||
| 231 | </li> | ||
| 232 | </ul> | ||
| 233 | </div> | ||
| 234 | `, | ||
| 235 | })); | ||
| 236 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 177 | name: 'sendas', | 237 | name: 'sendas', |
| 178 | callback: sendMessageAs, | 238 | callback: sendMessageAs, |
| 179 | namedArgumentList: [ | 239 | namedArgumentList: [ |
| @@ -3117,41 +3177,93 @@ async function setNarratorName(_, text) { | |||
| 3117 | } | 3177 | } |
| 3118 | 3178 | ||
| 3119 | /** | 3179 | /** |
| 3180 | * Checks if an argument is a string array (or undefined), and if not, throws an error | ||
| 3181 | * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check | ||
| 3182 | * @param {string} name The name of the argument for the error message | ||
| 3183 | * @param {object} [options={}] - The optional arguments | ||
| 3184 | * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined | ||
| 3185 | * @throws {Error} If the argument is not an array | ||
| 3186 | * @returns {string[]} | ||
| 3187 | */ | ||
| 3188 | export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) { | ||
| 3189 | if (arg === undefined) { | ||
| 3190 | if (allowUndefined) return undefined; | ||
| 3191 | throw new Error(`Argument "${name}" is undefined, but must be a string array`); | ||
| 3192 | } | ||
| 3193 | if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`); | ||
| 3194 | if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`); | ||
| 3195 | return arg; | ||
| 3196 | } | ||
| 3197 | |||
| 3198 | /** | ||
| 3199 | * Checks if an argument is a string or closure array (or undefined), and if not, throws an error | ||
| 3200 | * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check | ||
| 3201 | * @param {string} name The name of the argument for the error message | ||
| 3202 | * @param {object} [options={}] - The optional arguments | ||
| 3203 | * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined | ||
| 3204 | * @throws {Error} If the argument is not an array of strings or closures | ||
| 3205 | * @returns {(string|SlashCommandClosure)[]} | ||
| 3206 | */ | ||
| 3207 | export function validateArrayArg(arg, name, { allowUndefined = true } = {}) { | ||
| 3208 | if (arg === undefined) { | ||
| 3209 | if (allowUndefined) return []; | ||
| 3210 | throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`); | ||
| 3211 | } | ||
| 3212 | if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`); | ||
| 3213 | if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`); | ||
| 3214 | return arg; | ||
| 3215 | } | ||
| 3216 | |||
| 3217 | /** | ||
| 3120 | * Finds a character by name, with optional filtering and precedence for avatars | 3218 | * Finds a character by name, with optional filtering and precedence for avatars |
| 3121 | * @param {string} name - The name to search for | ||
| 3122 | * @param {object} [options={}] - The options for the search | 3219 | * @param {object} [options={}] - The options for the search |
| 3220 | * @param {string?} [options.name=null] - The name to search for | ||
| 3123 | * @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar | 3221 | * @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar |
| 3124 | * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive | 3222 | * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive |
| 3125 | * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by | 3223 | * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by |
| 3126 | * @param {any?} [options.preferCurrentChar=null] - The current character to prefer | 3224 | * @param {boolean} [options.preferCurrentChar=false] - Whether to prefer the current character(s) |
| 3225 | * @param {boolean} [options.quiet=false] - Whether to suppress warnings | ||
| 3127 | * @returns {any?} - The found character or null if not found | 3226 | * @returns {any?} - The found character or null if not found |
| 3128 | */ | 3227 | */ |
| 3129 | export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) { | 3228 | export function findChar({ name = null, allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) { |
| 3130 | const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name; | 3229 | const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name); |
| 3230 | |||
| 3231 | // Get the current character(s) | ||
| 3232 | const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]]; | ||
| 3131 | 3233 | ||
| 3132 | // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence | 3234 | // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence |
| 3133 | if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) { | 3235 | if (preferCurrentChar && !filteredByTags) { |
| 3134 | return preferCurrentChar; | 3236 | const preferredChar = currentChars.find(matches); |
| 3237 | if (preferredChar) { | ||
| 3238 | return preferredChar; | ||
| 3239 | } | ||
| 3135 | } | 3240 | } |
| 3136 | 3241 | ||
| 3137 | // Filter characters by tags if provided | 3242 | // Filter characters by tags if provided |
| 3138 | let filteredCharacters = characters; | 3243 | let filteredCharacters = characters; |
| 3139 | if (filteredByTags) { | 3244 | if (filteredByTags) { |
| 3140 | filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag))); | 3245 | filteredCharacters = characters.filter(char => { |
| 3246 | const charTags = getTagsList(char.avatar, false); | ||
| 3247 | return filteredByTags.every(tagName => charTags.some(x => x.name == tagName)); | ||
| 3248 | }); | ||
| 3141 | } | 3249 | } |
| 3142 | 3250 | ||
| 3143 | // If allowAvatar is true, search by avatar first | 3251 | // If allowAvatar is true, search by avatar first |
| 3144 | if (allowAvatar) { | 3252 | if (allowAvatar && name) { |
| 3145 | const characterByAvatar = filteredCharacters.find(char => char.avatar === name); | 3253 | const characterByAvatar = filteredCharacters.find(char => char.avatar === name); |
| 3146 | if (characterByAvatar) { | 3254 | if (characterByAvatar) { |
| 3147 | return characterByAvatar; | 3255 | return characterByAvatar; |
| 3148 | } | 3256 | } |
| 3149 | } | 3257 | } |
| 3150 | 3258 | ||
| 3151 | // Search for a matching character by name | 3259 | // Search for matching characters by name |
| 3152 | let character = filteredCharacters.find(matches); | 3260 | const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters; |
| 3261 | if (matchingCharacters.length > 1) { | ||
| 3262 | if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`); | ||
| 3263 | else console.warn(`Multiple characters found for name "${name}". Returning the first match.`); | ||
| 3264 | } | ||
| 3153 | 3265 | ||
| 3154 | return character; | 3266 | return matchingCharacters[0] || null; |
| 3155 | } | 3267 | } |
| 3156 | 3268 | ||
| 3157 | export async function sendMessageAs(args, text) { | 3269 | export async function sendMessageAs(args, text) { |
| @@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js'; | |||
| 15 | * _abortController:SlashCommandAbortController, | 15 | * _abortController:SlashCommandAbortController, |
| 16 | * _debugController:SlashCommandDebugController, | 16 | * _debugController:SlashCommandDebugController, |
| 17 | * _hasUnnamedArgument:boolean, | 17 | * _hasUnnamedArgument:boolean, |
| 18 | * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[], | 18 | * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined, |
| 19 | * }} NamedArguments | 19 | * }} NamedArguments |
| 20 | */ | 20 | */ |
| 21 | 21 | ||
| 22 | /** | 22 | /** |
| 23 | * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments | 23 | * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments |
| 24 | * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]}} NamedArgumentsCapture | 24 | * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture |
| 25 | */ | 25 | */ |
| 26 | 26 | ||
| 27 | /** | 27 | /** |
| @@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol | |||
| 2 | import { extension_settings } from '../extensions.js'; | 2 | import { extension_settings } from '../extensions.js'; |
| 3 | import { getGroupMembers, groups } from '../group-chats.js'; | 3 | import { getGroupMembers, groups } from '../group-chats.js'; |
| 4 | import { power_user } from '../power-user.js'; | 4 | import { power_user } from '../power-user.js'; |
| 5 | import { searchCharByName, getTagsList, tags } from '../tags.js'; | 5 | import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js'; |
| 6 | import { world_names } from '../world-info.js'; | 6 | import { world_names } from '../world-info.js'; |
| 7 | import { SlashCommandClosure } from './SlashCommandClosure.js'; | 7 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 8 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; | 8 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; |
| @@ -182,6 +182,18 @@ export const commonEnumProviders = { | |||
| 182 | personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)), | 182 | personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)), |
| 183 | 183 | ||
| 184 | /** | 184 | /** |
| 185 | * All possible tags, or only those that have been assigned | ||
| 186 | * | ||
| 187 | * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show | ||
| 188 | * @returns {() => SlashCommandEnumValue[]} | ||
| 189 | */ | ||
| 190 | tags: (mode = 'all') => () => { | ||
| 191 | let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set(); | ||
| 192 | return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id))) | ||
| 193 | .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); | ||
| 194 | }, | ||
| 195 | |||
| 196 | /** | ||
| 185 | * All possible tags for a given char/group entity | 197 | * All possible tags for a given char/group entity |
| 186 | * | 198 | * |
| 187 | * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show | 199 | * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show |