Allow create new world books via `/getpersonabook` and `/getcharbook` (#4597) * feat: allow create new world books via /getpersonabook and /getcharbook Refactor character world info management into dedicated functions Extract character world assignment logic from UI handlers into reusable functions Add support for automatic lorebook creation in slash commands Improve code maintainability by centralizing world info operations * refactor: update boolean enum provider from onOffToggle to trueFalse in world info commands * fix: improve world info name uniqueness check using getUniqueName utility * Unset current persona WI on file deletion --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -46,6 +46,8 @@ import { | ||
| 46 | 46 | wi_anchor_position, |
| 47 | 47 | world_info_include_names, |
| 48 | 48 | initWorldInfo, |
| 49 | + charUpdatePrimaryWorld, | |
| 50 | + charSetAuxWorlds, | |
| 49 | 51 | } from './scripts/world-info.js'; |
| 50 | 52 | |
| 51 | 53 | import { |
| @@ -8151,61 +8153,16 @@ async function openCharacterWorldPopup() { | ||
| 8151 | 8153 | const selectedValue = $(this).val(); |
| 8152 | 8154 | const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN; |
| 8153 | 8155 | const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; |
| 8154 | - const previousValue = $('#character_world').val(); | |
| 8156 | + await charUpdatePrimaryWorld(name); | |
| 8155 | - $('#character_world').val(name); | |
| 8156 | - | |
| 8157 | - console.debug('Character world selected:', name); | |
| 8158 | - | |
| 8159 | - if (menu_type == 'create') { | |
| 8160 | - create_save.world = name; | |
| 8161 | - } else { | |
| 8162 | - if (previousValue && !name) { | |
| 8163 | - try { | |
| 8164 | - // Dirty hack to remove embedded lorebook from character JSON data. | |
| 8165 | - const data = JSON.parse(String($('#character_json_data').val())); | |
| 8166 | - | |
| 8167 | - if (data?.data?.character_book) { | |
| 8168 | - data.data.character_book = undefined; | |
| 8169 | - } | |
| 8170 | - | |
| 8171 | - $('#character_json_data').val(JSON.stringify(data)); | |
| 8172 | - toastr.info(t`Embedded lorebook will be removed from this character.`); | |
| 8173 | - } catch { | |
| 8174 | - console.error('Failed to parse character JSON data.'); | |
| 8175 | - } | |
| 8176 | - } | |
| 8177 | - | |
| 8178 | - await createOrEditCharacter(); | |
| 8179 | - } | |
| 8180 | - | |
| 8181 | - setWorldInfoButtonClass(undefined, !!name); | |
| 8182 | 8157 | } |
| 8183 | 8158 | |
| 8184 | 8159 | function handleExtrasWorldSelect(evt) { |
| 8185 | - const selectedValues = $(this).val(); | |
| 8160 | + const el = evt?.currentTarget ?? this; | |
| 8186 | 8161 | const selectedWorldsselectedValues = Array$(el).isArrayval(selectedValues) ? selectedValues : []; |
| 8187 | 8162 | letconst charLoreselected = world_infoArray.charLoreisArray(selectedValues) ?? selectedValues : []; |
| 8188 | - const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter(Boolean); | |
| 8163 | + const fileName = getCharaFilename(null, {}); | |
| 8189 | 8164 | const existingCharIndexnextList = charLoreselected.findIndex(map(e)i => eworld_names[i]).name === fileNamefilter(Boolean); |
| 8190 | - | |
| 8165 | + charSetAuxWorlds(fileName, nextList); | |
| 8191 | - if (menu_type == 'create') { | |
| 8192 | - create_save.extra_books = tempExtraBooks; | |
| 8193 | - return; | |
| 8194 | - } | |
| 8195 | - | |
| 8196 | - if (existingCharIndex === -1) { | |
| 8197 | - // Add record only if at least 1 lorebook is selected. | |
| 8198 | - if (tempExtraBooks.length > 0) { | |
| 8199 | - charLore.push({ name: fileName, extraBooks: tempExtraBooks }); | |
| 8200 | - } | |
| 8201 | - } else if (tempExtraBooks.length === 0) { | |
| 8202 | - charLore.splice(existingCharIndex, 1); | |
| 8203 | - } else { | |
| 8204 | - charLore[existingCharIndex].extraBooks = tempExtraBooks; | |
| 8205 | - } | |
| 8206 | - | |
| 8207 | - Object.assign(world_info, { charLore: charLore }); | |
| 8208 | - saveSettingsDebounced(); | |
| 8209 | 8166 | } |
| 8210 | 8167 | |
| 8211 | 8168 | // --- Populate Dropdowns --- |
| @@ -8328,7 +8285,7 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) { | ||
| 8328 | 8285 | * Creates or edits a character based on the form data. |
| 8329 | 8286 | * @param {Event} [e] Event that triggered the function call. |
| 8330 | 8287 | */ |
| 8331 | 8288 | export async function createOrEditCharacter(e) { |
| 8332 | 8289 | $('#rm_info_avatar').html(''); |
| 8333 | 8290 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); |
| 8334 | 8291 | formData.set('fav', String(fav_ch_checked)); |
| @@ -1219,7 +1219,7 @@ function onPersonaDescriptionPositionInput() { | ||
| 1219 | 1219 | $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH); |
| 1220 | 1220 | } |
| 1221 | 1221 | |
| 1222 | 1222 | export function getOrCreatePersonaDescriptor() { |
| 1223 | 1223 | let object = power_user.persona_descriptions[user_avatar]; |
| 1224 | 1224 | |
| 1225 | 1225 | if (!object) { |
| @@ -289,6 +289,15 @@ export function removeFromArray(array, item) { | ||
| 289 | 289 | } |
| 290 | 290 | |
| 291 | 291 | /** |
| 292 | + * Normalizes an array by removing duplicates, trimming strings, and filtering out empty values. | |
| 293 | + * @param {any[]} arr - The array to normalize. | |
| 294 | + * @returns {any[]} The normalized array. | |
| 295 | + */ | |
| 296 | +export function normalizeArray(arr) { | |
| 297 | + return [...new Set((arr ?? []).map(s => typeof s === 'string' ? s.trim() : s).filter(Boolean))]; | |
| 298 | +} | |
| 299 | + | |
| 300 | +/** | |
| 292 | 301 | * Checks if a string only contains digits. |
| 293 | 302 | * @param {string} str The string to check. |
| 294 | 303 | * @returns {boolean} True if the string only contains digits, false otherwise. |
| @@ -620,7 +629,7 @@ export function isElementInViewport(el) { | ||
| 620 | 629 | /** |
| 621 | 630 | * Returns a name that is unique among the names that exist. |
| 622 | 631 | * @param {string} name The name to check. |
| 623 | 632 | * @param {{ (yname: anystring): boolean; }} exists Function to check if name exists. |
| 624 | 633 | * @returns {string} A unique name. |
| 625 | 634 | */ |
| 626 | 635 | export function getUniqueName(name, exists) { |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles, create_save, createOrEditCharacter, name1 } from '../script.js'; |
| 4 | 4 | import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique, equalsIgnoreCaseAndAccents, uuidv4, normalizeArray, getUniqueName } from './utils.js'; |
| 5 | 5 | import { extension_settings, getContext } from './extensions.js'; |
| 6 | 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; |
| 7 | 7 | import { isMobile } from './RossAscends-mods.js'; |
| @@ -22,6 +22,7 @@ import { StructuredCloneMap } from './util/StructuredCloneMap.js'; | ||
| 22 | 22 | import { renderTemplateAsync } from './templates.js'; |
| 23 | 23 | import { t } from './i18n.js'; |
| 24 | 24 | import { accountStorage } from './util/AccountStorage.js'; |
| 25 | +import { getOrCreatePersonaDescriptor, setPersonaDescription, user_avatar } from './personas.js'; | |
| 25 | 26 | |
| 26 | 27 | export const world_info_insertion_strategy = { |
| 27 | 28 | evenly: 0, |
| @@ -1041,39 +1042,69 @@ function registerWorldInfoSlashCommands() { | ||
| 1041 | 1042 | |
| 1042 | 1043 | /** |
| 1043 | 1044 | * Gets the name of the persona-bound lorebook. |
| 1044 | - * @returns {string} The name of the persona-bound lorebook | |
| 1045 | + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments | |
| 1046 | + * @param {string} _unnamedArg not used | |
| 1047 | + * @returns {Promise<string>} The name of the persona-bound lorebook | |
| 1045 | 1048 | */ |
| 1046 | 1049 | async function getPersonaBookCallback({ name, create }, _unnamedArg) { |
| 1047 | 1050 | returnlet bookName = power_user.persona_description_lorebook || ''; |
| 1051 | + if (bookName) { | |
| 1052 | + return bookName; | |
| 1053 | + } | |
| 1054 | + | |
| 1055 | + if (isTrueBoolean(String(create))) { | |
| 1056 | + const newName = await createWorldWithName(name, `Persona Book ${name1}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1057 | + power_user.persona_description_lorebook = newName; | |
| 1058 | + setPersonaDescription(); | |
| 1059 | + saveSettingsDebounced(); | |
| 1060 | + return newName; | |
| 1061 | + } | |
| 1062 | + | |
| 1063 | + return ''; | |
| 1048 | 1064 | } |
| 1049 | 1065 | |
| 1050 | 1066 | /** |
| 1051 | 1067 | * Gets the name of the character-bound lorebook. |
| 1052 | 1068 | * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments |
| 1053 | 1069 | * @param {string} namecharacterIdentifier Character name |
| 1054 | 1070 | * @returns {Promise<string>} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string |
| 1055 | 1071 | */ |
| 1056 | 1072 | async function getCharBookCallback({ type, name, create }, namecharacterIdentifier) { |
| 1057 | 1073 | const context = getContext(); |
| 1058 | 1074 | if (context.groupId && !namecharacterIdentifier) throw new Error('This command is not available in groups without providing a character name'); |
| 1059 | 1075 | type = String(type ?? '').trim().toLowerCase() || 'primary'; |
| 1060 | 1076 | namecharacterIdentifier = String(namecharacterIdentifier ?? '') || context.characters[context.characterId]?.avatar || null; |
| 1061 | 1077 | const character = findChar({ name: characterIdentifier }); |
| 1062 | 1078 | if (!character) { |
| 1063 | 1079 | toastr.error(t`Character not found.`); |
| 1064 | 1080 | return ''; |
| 1065 | 1081 | } |
| 1066 | 1082 | const books = []; |
| 1067 | 1083 | if (type === 'all' || type === 'primary' && character.data?.extensions?.world) { |
| 1068 | 1084 | books.push(character.data?.extensions?.world); |
| 1069 | 1085 | } |
| 1070 | 1086 | if (type === 'all' || type === 'additional') { |
| 1071 | 1087 | const fileName = getCharaFilename(context.characters.indexOf(character)); |
| 1072 | 1088 | const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); |
| 1073 | 1089 | if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) { |
| 1074 | 1090 | books.push(...extraCharLore.extraBooks.filter(onlyUnique).filter(Boolean)); |
| 1075 | 1091 | } |
| 1076 | 1092 | } |
| 1093 | + | |
| 1094 | + if (isTrueBoolean(String(create)) && books.length === 0) { | |
| 1095 | + const newName = await createWorldWithName(name, `Character Book ${character.name}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1096 | + // Also assign the book now - additional if requested, otherwise as primary | |
| 1097 | + if (type === 'additional') { | |
| 1098 | + await charUpdateAddAuxWorld(character.avatar, newName); | |
| 1099 | + } | |
| 1100 | + else { | |
| 1101 | + await charUpdatePrimaryWorld(newName); | |
| 1102 | + } | |
| 1103 | + // Refresh UI, if needed | |
| 1104 | + setWorldInfoButtonClass(this_chid); | |
| 1105 | + books.push(newName); | |
| 1106 | + } | |
| 1107 | + | |
| 1077 | 1108 | return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean)); |
| 1078 | 1109 | } |
| 1079 | 1110 | |
| @@ -1094,10 +1125,23 @@ function registerWorldInfoSlashCommands() { | ||
| 1094 | 1125 | return chat_metadata[METADATA_KEY]; |
| 1095 | 1126 | } |
| 1096 | 1127 | |
| 1097 | - const name = (() => { | |
| 1128 | + if (isFalseBoolean(String(args.create))) { | |
| 1129 | + return ''; | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + const name = await createWorldWithName(args.name, `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1133 | + | |
| 1134 | + chat_metadata[METADATA_KEY] = name; | |
| 1135 | + await saveMetadata(); | |
| 1136 | + $('.chat_lorebook_button').addClass('world_set'); | |
| 1137 | + return name; | |
| 1138 | + } | |
| 1139 | + | |
| 1140 | + async function createWorldWithName(possibleName = undefined, fallbackName = undefined) { | |
| 1141 | + let newName = (() => { | |
| 1098 | 1142 | // Use the provided name if it's not in use |
| 1099 | 1143 | if (typeof args.namepossibleName === 'string') { |
| 1100 | 1144 | const name = String(args.namepossibleName); |
| 1101 | 1145 | if (world_names.includes(name)) { |
| 1102 | 1146 | throw new Error('This World Info file name is already in use'); |
| 1103 | 1147 | } |
| @@ -1105,14 +1149,14 @@ function registerWorldInfoSlashCommands() { | ||
| 1105 | 1149 | } |
| 1106 | 1150 | |
| 1107 | 1151 | // Replace non-alphanumeric characters with underscores, cut to 64 characters |
| 1108 | - return `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); | |
| 1152 | + return fallbackName ?? `Lorebook (${uuidv4()})`; | |
| 1109 | 1153 | })(); |
| 1110 | - await createNewWorldInfo(name); | |
| 1111 | 1154 | |
| 1112 | - chat_metadata[METADATA_KEY] = name; | |
| 1155 | + // Make sure the name is unique | |
| 1113 | - await saveMetadata(); | |
| 1156 | + newName = getUniqueName(newName, world_names.includes.bind(world_names)); | |
| 1114 | - $('.chat_lorebook_button').addClass('world_set'); | |
| 1157 | + | |
| 1115 | - return name; | |
| 1158 | + await createNewWorldInfo(newName); | |
| 1159 | + return newName; | |
| 1116 | 1160 | } |
| 1117 | 1161 | |
| 1118 | 1162 | async function findBookEntryCallback(args, value) { |
| @@ -1549,6 +1593,15 @@ function registerWorldInfoSlashCommands() { | ||
| 1549 | 1593 | isRequired: false, |
| 1550 | 1594 | acceptsMultiple: false, |
| 1551 | 1595 | }), |
| 1596 | + SlashCommandNamedArgument.fromProps({ | |
| 1597 | + name: 'create', | |
| 1598 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1599 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1600 | + isRequired: false, | |
| 1601 | + acceptsMultiple: false, | |
| 1602 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1603 | + defaultValue: 'true', | |
| 1604 | + }), | |
| 1552 | 1605 | ], |
| 1553 | 1606 | aliases: ['getchatlore', 'getchatwi'], |
| 1554 | 1607 | })); |
| @@ -1563,6 +1616,25 @@ function registerWorldInfoSlashCommands() { | ||
| 1563 | 1616 | name: 'getpersonabook', |
| 1564 | 1617 | callback: getPersonaBookCallback, |
| 1565 | 1618 | returns: 'lorebook name', |
| 1619 | + | |
| 1620 | + namedArgumentList: [ | |
| 1621 | + SlashCommandNamedArgument.fromProps({ | |
| 1622 | + name: 'name', | |
| 1623 | + description: 'lorebook name if creating a new one, will be auto-generated otherwise', | |
| 1624 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1625 | + isRequired: false, | |
| 1626 | + acceptsMultiple: false, | |
| 1627 | + }), | |
| 1628 | + SlashCommandNamedArgument.fromProps({ | |
| 1629 | + name: 'create', | |
| 1630 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1631 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1632 | + isRequired: false, | |
| 1633 | + acceptsMultiple: false, | |
| 1634 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1635 | + defaultValue: 'false', | |
| 1636 | + }), | |
| 1637 | + ], | |
| 1566 | 1638 | helpString: 'Get a name of the current persona-bound lorebook and pass it down the pipe. Returns empty string if persona lorebook is not set.', |
| 1567 | 1639 | aliases: ['getpersonalore', 'getpersonawi'], |
| 1568 | 1640 | })); |
| @@ -1578,6 +1650,22 @@ function registerWorldInfoSlashCommands() { | ||
| 1578 | 1650 | enumList: ['primary', 'additional', 'all'], |
| 1579 | 1651 | defaultValue: 'primary', |
| 1580 | 1652 | }), |
| 1653 | + SlashCommandNamedArgument.fromProps({ | |
| 1654 | + name: 'name', | |
| 1655 | + description: 'lorebook name if creating a new one, will be auto-generated otherwise', | |
| 1656 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1657 | + isRequired: false, | |
| 1658 | + acceptsMultiple: false, | |
| 1659 | + }), | |
| 1660 | + SlashCommandNamedArgument.fromProps({ | |
| 1661 | + name: 'create', | |
| 1662 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1663 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1664 | + isRequired: false, | |
| 1665 | + acceptsMultiple: false, | |
| 1666 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1667 | + defaultValue: 'false', | |
| 1668 | + }), | |
| 1581 | 1669 | ], |
| 1582 | 1670 | unnamedArgumentList: [ |
| 1583 | 1671 | SlashCommandArgument.fromProps({ |
| @@ -3995,6 +4083,16 @@ export async function deleteWorldInfo(worldInfoName) { | ||
| 3995 | 4083 | } |
| 3996 | 4084 | } |
| 3997 | 4085 | |
| 4086 | + if (power_user.persona_description_lorebook === worldInfoName) { | |
| 4087 | + power_user.persona_description_lorebook = ''; | |
| 4088 | + if (power_user.personas[user_avatar]) { | |
| 4089 | + const object = getOrCreatePersonaDescriptor(); | |
| 4090 | + object.lorebook = ''; | |
| 4091 | + } | |
| 4092 | + $('#persona_lore_button').toggleClass('world_set', false); | |
| 4093 | + saveSettingsDebounced(); | |
| 4094 | + } | |
| 4095 | + | |
| 3998 | 4096 | return true; |
| 3999 | 4097 | } |
| 4000 | 4098 | |
| @@ -5636,6 +5734,90 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid, { deleteOr | ||
| 5636 | 5734 | } |
| 5637 | 5735 | } |
| 5638 | 5736 | |
| 5737 | + | |
| 5738 | +/** | |
| 5739 | + * Updates the primary world info linked to a character. | |
| 5740 | + * Can also unset it to null. | |
| 5741 | + * @param {string} name - The name of the world info to link to the character. | |
| 5742 | + */ | |
| 5743 | +export async function charUpdatePrimaryWorld(name) { | |
| 5744 | + const previousValue = $('#character_world').val(); | |
| 5745 | + $('#character_world').val(name); | |
| 5746 | + | |
| 5747 | + console.debug('Character world selected:', name); | |
| 5748 | + | |
| 5749 | + if (menu_type == 'create') { | |
| 5750 | + create_save.world = name; | |
| 5751 | + return; | |
| 5752 | + } | |
| 5753 | + | |
| 5754 | + if (previousValue && !name) { | |
| 5755 | + try { | |
| 5756 | + // Dirty hack to remove embedded lorebook from character JSON data. | |
| 5757 | + const data = JSON.parse(String($('#character_json_data').val())); | |
| 5758 | + | |
| 5759 | + if (data?.data?.character_book) { | |
| 5760 | + data.data.character_book = undefined; | |
| 5761 | + } | |
| 5762 | + | |
| 5763 | + $('#character_json_data').val(JSON.stringify(data)); | |
| 5764 | + toastr.info(t`Embedded lorebook will be removed from this character.`); | |
| 5765 | + } catch { | |
| 5766 | + console.error('Failed to parse character JSON data.'); | |
| 5767 | + } | |
| 5768 | + } | |
| 5769 | + | |
| 5770 | + await createOrEditCharacter(); | |
| 5771 | + | |
| 5772 | + setWorldInfoButtonClass(undefined, !!name); | |
| 5773 | +} | |
| 5774 | + | |
| 5775 | +/** | |
| 5776 | + * Adds one or more auxiliary world books to a character. | |
| 5777 | + * @param {string} characterKey - The key of the character to add auxiliary world books to | |
| 5778 | + * @param {string|string[]} nameOrNames - The name or names of the auxiliary world books to add | |
| 5779 | + */ | |
| 5780 | +export async function charUpdateAddAuxWorld(characterKey, nameOrNames) { | |
| 5781 | + const fileName = getCharaFilename(null, { manualAvatarKey: characterKey }); | |
| 5782 | + const toAdd = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames]; | |
| 5783 | + updateAuxBooks(fileName, curr => [...curr, ...toAdd]); | |
| 5784 | +} | |
| 5785 | + | |
| 5786 | +/** | |
| 5787 | + * Replaces the entire list of auxiliary world books for a character. | |
| 5788 | + * @param {string} fileName - The filename of the character to update | |
| 5789 | + * @param {string[]} books - The new list of auxiliary world books to replace the existing list with | |
| 5790 | + */ | |
| 5791 | +export function charSetAuxWorlds(fileName, books) { | |
| 5792 | + updateAuxBooks(fileName, _ => Array.isArray(books) ? books : []); | |
| 5793 | +} | |
| 5794 | + | |
| 5795 | +function updateAuxBooks(fileName, computeNext) { | |
| 5796 | + if (!fileName) return; | |
| 5797 | + | |
| 5798 | + if (menu_type === 'create') { | |
| 5799 | + const current = create_save.extra_books ?? []; | |
| 5800 | + create_save.extra_books = normalizeArray(computeNext(current)); | |
| 5801 | + return; // no debounced save in create flow | |
| 5802 | + } | |
| 5803 | + | |
| 5804 | + const charLore = world_info.charLore ?? []; | |
| 5805 | + const idx = charLore.findIndex(e => e.name === fileName); | |
| 5806 | + const current = idx !== -1 ? (charLore[idx].extraBooks ?? []) : []; | |
| 5807 | + const next = normalizeArray(computeNext(current)); | |
| 5808 | + | |
| 5809 | + if (next.length === 0) { | |
| 5810 | + if (idx !== -1) charLore.splice(idx, 1); | |
| 5811 | + } else if (idx === -1) { | |
| 5812 | + charLore.push({ name: fileName, extraBooks: next }); | |
| 5813 | + } else { | |
| 5814 | + charLore[idx] = { ...charLore[idx], extraBooks: next }; | |
| 5815 | + } | |
| 5816 | + | |
| 5817 | + Object.assign(world_info, { charLore }); | |
| 5818 | + saveSettingsDebounced(); | |
| 5819 | +} | |
| 5820 | + | |
| 5639 | 5821 | export function initWorldInfo() { |
| 5640 | 5822 | $('#world_info').on('mousedown change', async function (e) { |
| 5641 | 5823 | // If there's no world names, don't do anything |