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>

a65f708fd81817783a6a50e1ba2d9448ab4cc03d

Wolfsblvt <wolfsblvt@gmail.com>

Signed
4 files changed, +227 -79Showing whitespace changes
public/script.js+11 -54
@@ -46,6 +46,8 @@ import {
46 wi_anchor_position,46 wi_anchor_position,
47 world_info_include_names,47 world_info_include_names,
48 initWorldInfo,48 initWorldInfo,
49 charUpdatePrimaryWorld,
50 charSetAuxWorlds,
49} from './scripts/world-info.js';51} from './scripts/world-info.js';
5052
51import {53import {
@@ -8151,61 +8153,16 @@ async function openCharacterWorldPopup() {
8151 const selectedValue = $(this).val();8153 const selectedValue = $(this).val();
8152 const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN;8154 const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN;
8153 const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';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 }8157 }
81808158
8181 setWorldInfoButtonClass(undefined, !!name);8159 function handleExtrasWorldSelect(evt) {
8182 }8160 const el = evt?.currentTarget ?? this;
81838161 const selectedValues = $(el).val();
8184 function handleExtrasWorldSelect() {8162 const selected = Array.isArray(selectedValues) ? selectedValues : [];
8185 const selectedValues = $(this).val();8163 const fileName = getCharaFilename(null, {});
8186 const selectedWorlds = Array.isArray(selectedValues) ? selectedValues : [];8164 const nextList = selected.map(i => world_names[i]).filter(Boolean);
8187 let charLore = world_info.charLore ?? [];8165 charSetAuxWorlds(fileName, nextList);
8188 const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter(Boolean);
8189 const existingCharIndex = charLore.findIndex((e) => e.name === fileName);
8190
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 }
82108167
8211 // --- Populate Dropdowns ---8168 // --- Populate Dropdowns ---
@@ -8328,7 +8285,7 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) {
8328 * Creates or edits a character based on the form data.8285 * Creates or edits a character based on the form data.
8329 * @param {Event} [e] Event that triggered the function call.8286 * @param {Event} [e] Event that triggered the function call.
8330 */8287 */
8331async function createOrEditCharacter(e) {8288export async function createOrEditCharacter(e) {
8332 $('#rm_info_avatar').html('');8289 $('#rm_info_avatar').html('');
8333 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));8290 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
8334 formData.set('fav', String(fav_ch_checked));8291 formData.set('fav', String(fav_ch_checked));
public/scripts/personas.js+1 -1
@@ -1219,7 +1219,7 @@ function onPersonaDescriptionPositionInput() {
1219 $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);1219 $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
1220}1220}
12211221
1222function getOrCreatePersonaDescriptor() {1222export function getOrCreatePersonaDescriptor() {
1223 let object = power_user.persona_descriptions[user_avatar];1223 let object = power_user.persona_descriptions[user_avatar];
12241224
1225 if (!object) {1225 if (!object) {
public/scripts/utils.js+10 -1
@@ -289,6 +289,15 @@ export function removeFromArray(array, item) {
289}289}
290290
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 */
296export function normalizeArray(arr) {
297 return [...new Set((arr ?? []).map(s => typeof s === 'string' ? s.trim() : s).filter(Boolean))];
298}
299
300/**
292 * Checks if a string only contains digits.301 * Checks if a string only contains digits.
293 * @param {string} str The string to check.302 * @param {string} str The string to check.
294 * @returns {boolean} True if the string only contains digits, false otherwise.303 * @returns {boolean} True if the string only contains digits, false otherwise.
@@ -620,7 +629,7 @@ export function isElementInViewport(el) {
620/**629/**
621 * Returns a name that is unique among the names that exist.630 * Returns a name that is unique among the names that exist.
622 * @param {string} name The name to check.631 * @param {string} name The name to check.
623 * @param {{ (y: any): boolean; }} exists Function to check if name exists.632 * @param {{ (name: string): boolean; }} exists Function to check if name exists.
624 * @returns {string} A unique name.633 * @returns {string} A unique name.
625 */634 */
626export function getUniqueName(name, exists) {635export function getUniqueName(name, exists) {
public/scripts/world-info.js+205 -23
@@ -1,7 +1,7 @@
1import { Fuse } from '../lib.js';1import { Fuse } from '../lib.js';
22
3import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';3import { 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';
4import { 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 } from './utils.js';4import { 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';
5import { extension_settings, getContext } from './extensions.js';5import { extension_settings, getContext } from './extensions.js';
6import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';6import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
7import { isMobile } from './RossAscends-mods.js';7import { isMobile } from './RossAscends-mods.js';
@@ -22,6 +22,7 @@ import { StructuredCloneMap } from './util/StructuredCloneMap.js';
22import { renderTemplateAsync } from './templates.js';22import { renderTemplateAsync } from './templates.js';
23import { t } from './i18n.js';23import { t } from './i18n.js';
24import { accountStorage } from './util/AccountStorage.js';24import { accountStorage } from './util/AccountStorage.js';
25import { getOrCreatePersonaDescriptor, setPersonaDescription, user_avatar } from './personas.js';
2526
26export const world_info_insertion_strategy = {27export const world_info_insertion_strategy = {
27 evenly: 0,28 evenly: 0,
@@ -1041,39 +1042,69 @@ function registerWorldInfoSlashCommands() {
10411042
1042 /**1043 /**
1043 * Gets the name of the persona-bound lorebook.1044 * Gets the name of the persona-bound lorebook.
1044 * @returns {string} The name of the persona-bound lorebook1045 * @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 function getPersonaBookCallback() {1049 async function getPersonaBookCallback({ name, create }, _unnamedArg) {
1047 return power_user.persona_description_lorebook || '';1050 let 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 }
10491065
1050 /**1066 /**
1051 * Gets the name of the character-bound lorebook.1067 * Gets the name of the character-bound lorebook.
1052 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments1068 * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
1053 * @param {string} name Character name1069 * @param {string} characterIdentifier Character name
1054 * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string1070 * @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 function getCharBookCallback({ type }, name) {1072 async function getCharBookCallback({ type, name, create }, characterIdentifier) {
1057 const context = getContext();1073 const context = getContext();
1058 if (context.groupId && !name) throw new Error('This command is not available in groups without providing a character name');1074 if (context.groupId && !characterIdentifier) throw new Error('This command is not available in groups without providing a character name');
1059 type = String(type ?? '').trim().toLowerCase() || 'primary';1075 type = String(type ?? '').trim().toLowerCase() || 'primary';
1060 name = String(name ?? '') || context.characters[context.characterId]?.avatar || null;1076 characterIdentifier = String(characterIdentifier ?? '') || context.characters[context.characterId]?.avatar || null;
1061 const character = findChar({ name });1077 const character = findChar({ name: characterIdentifier });
1062 if (!character) {1078 if (!character) {
1063 toastr.error(t`Character not found.`);1079 toastr.error(t`Character not found.`);
1064 return '';1080 return '';
1065 }1081 }
1066 const books = [];1082 const books = [];
1067 if (type === 'all' || type === 'primary') {1083 if (type === 'all' || type === 'primary' && character.data?.extensions?.world) {
1068 books.push(character.data?.extensions?.world);1084 books.push(character.data.extensions.world);
1069 }1085 }
1070 if (type === 'all' || type === 'additional') {1086 if (type === 'all' || type === 'additional') {
1071 const fileName = getCharaFilename(context.characters.indexOf(character));1087 const fileName = getCharaFilename(context.characters.indexOf(character));
1072 const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);1088 const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
1073 if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) {1089 if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) {
1074 books.push(...extraCharLore.extraBooks);1090 books.push(...extraCharLore.extraBooks.filter(onlyUnique).filter(Boolean));
1091 }
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);
1075 }1102 }
1103 // Refresh UI, if needed
1104 setWorldInfoButtonClass(this_chid);
1105 books.push(newName);
1076 }1106 }
1107
1077 return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean));1108 return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean));
1078 }1109 }
10791110
@@ -1094,10 +1125,23 @@ function registerWorldInfoSlashCommands() {
1094 return chat_metadata[METADATA_KEY];1125 return chat_metadata[METADATA_KEY];
1095 }1126 }
10961127
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 // Use the provided name if it's not in use1142 // Use the provided name if it's not in use
1099 if (typeof args.name === 'string') {1143 if (typeof possibleName === 'string') {
1100 const name = String(args.name);1144 const name = String(possibleName);
1101 if (world_names.includes(name)) {1145 if (world_names.includes(name)) {
1102 throw new Error('This World Info file name is already in use');1146 throw new Error('This World Info file name is already in use');
1103 }1147 }
@@ -1105,14 +1149,14 @@ function registerWorldInfoSlashCommands() {
1105 }1149 }
11061150
1107 // Replace non-alphanumeric characters with underscores, cut to 64 characters1151 // 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);
11111154
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 }
11171161
1118 async function findBookEntryCallback(args, value) {1162 async function findBookEntryCallback(args, value) {
@@ -1549,6 +1593,15 @@ function registerWorldInfoSlashCommands() {
1549 isRequired: false,1593 isRequired: false,
1550 acceptsMultiple: false,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 aliases: ['getchatlore', 'getchatwi'],1606 aliases: ['getchatlore', 'getchatwi'],
1554 }));1607 }));
@@ -1563,6 +1616,25 @@ function registerWorldInfoSlashCommands() {
1563 name: 'getpersonabook',1616 name: 'getpersonabook',
1564 callback: getPersonaBookCallback,1617 callback: getPersonaBookCallback,
1565 returns: 'lorebook name',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 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.',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 aliases: ['getpersonalore', 'getpersonawi'],1639 aliases: ['getpersonalore', 'getpersonawi'],
1568 }));1640 }));
@@ -1578,6 +1650,22 @@ function registerWorldInfoSlashCommands() {
1578 enumList: ['primary', 'additional', 'all'],1650 enumList: ['primary', 'additional', 'all'],
1579 defaultValue: 'primary',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 unnamedArgumentList: [1670 unnamedArgumentList: [
1583 SlashCommandArgument.fromProps({1671 SlashCommandArgument.fromProps({
@@ -3995,6 +4083,16 @@ export async function deleteWorldInfo(worldInfoName) {
3995 }4083 }
3996 }4084 }
39974085
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 return true;4096 return true;
3999}4097}
40004098
@@ -5636,6 +5734,90 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid, { deleteOr
5636 }5734 }
5637}5735}
56385736
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 */
5743export 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 */
5780export 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 */
5791export function charSetAuxWorlds(fileName, books) {
5792 updateAuxBooks(fileName, _ => Array.isArray(books) ? books : []);
5793}
5794
5795function 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
5639export function initWorldInfo() {5821export function initWorldInfo() {
5640 $('#world_info').on('mousedown change', async function (e) {5822 $('#world_info').on('mousedown change', async function (e) {
5641 // If there's no world names, don't do anything5823 // If there's no world names, don't do anything