Merge branch 'staging' into parser-followup-2

This commit is contained in:
LenAnderson
2024-07-14 19:18:52 -04:00
7 changed files with 389 additions and 170 deletions

View File

@@ -110,7 +110,9 @@ function renderAlternativeTokensView() {
// scroll past long prior context // scroll past long prior context
if (prefix) { if (prefix) {
view.find('.logprobs_output_token').first()[0].scrollIntoView(); const element = view.find('.logprobs_output_token').first();
const scrollOffset = element.offset().top - element.parent().offset().top;
element.parent().scrollTop(scrollOffset);
} }
} }

View File

@@ -464,6 +464,7 @@ export function evaluateMacros(content, env) {
content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? '')); content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? ''));
content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? '')); content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? ''));
content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? '')); content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? ''));
content = content.replace(/{{reverse\:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join(''));
content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');

View File

@@ -135,6 +135,7 @@ export class Popup {
/** @type {Promise<any>} */ #promise; /** @type {Promise<any>} */ #promise;
/** @type {(result: any) => any} */ #resolver; /** @type {(result: any) => any} */ #resolver;
/** @type {boolean} */ #isClosingPrevented;
/** /**
* Constructs a new Popup object with the given text content, type, inputValue, and options * Constructs a new Popup object with the given text content, type, inputValue, and options
@@ -334,10 +335,21 @@ export class Popup {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
await this.complete(POPUP_RESULT.CANCELLED); await this.complete(POPUP_RESULT.CANCELLED);
window.removeEventListener('cancel', cancelListenerBound);
}; };
const cancelListenerBound = cancelListener.bind(this); this.dlg.addEventListener('cancel', cancelListener.bind(this));
this.dlg.addEventListener('cancel', cancelListenerBound);
// Don't ask me why this is needed. I don't get it. But we have to keep it.
// We make sure that the modal on its own doesn't hide. Dunno why, if onClosing is triggered multiple times through the cancel event, and stopped,
// it seems to just call 'close' on the dialog even if the 'cancel' event was prevented.
// So here we just say that close should not happen if it was prevented.
const closeListener = async (evt) => {
if (this.#isClosingPrevented) {
evt.preventDefault();
evt.stopPropagation();
this.dlg.showModal();
}
};
this.dlg.addEventListener('close', closeListener.bind(this));
const keyListener = async (evt) => { const keyListener = async (evt) => {
switch (evt.key) { switch (evt.key) {
@@ -366,16 +378,16 @@ export class Popup {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult); const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
// Call complete on the popup. Make sure that we handle `onClosing` cancels correctly and don't remove the listener then.
await this.complete(result); await this.complete(result);
window.removeEventListener('keydown', keyListenerBound);
break; break;
} }
} }
}; };
const keyListenerBound = keyListener.bind(this); this.dlg.addEventListener('keydown', keyListener.bind(this));
this.dlg.addEventListener('keydown', keyListenerBound);
} }
/** /**
@@ -445,9 +457,11 @@ export class Popup {
* - popup with `POPUP_TYPE.INPUT` will return the input value - or `false` on negative and `null` on cancelled * - popup with `POPUP_TYPE.INPUT` will return the input value - or `false` on negative and `null` on cancelled
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value * - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
* *
* <b>IMPORTANT:</b> If the popup closing was cancelled via the `onClosing` handler, the return value will be `Promise<undefined>`.
*
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value) * @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
* *
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed. * @returns {Promise<string|number|boolean|undefined?>} A promise that resolves with the value of the popup when it is completed. <b>Returns `undefined` if the closing action was cancelled.</b>
*/ */
async complete(result) { async complete(result) {
// In all cases besides INPUT the popup value should be the result // In all cases besides INPUT the popup value should be the result
@@ -481,8 +495,16 @@ export class Popup {
if (this.onClosing) { if (this.onClosing) {
const shouldClose = this.onClosing(this); const shouldClose = this.onClosing(this);
if (!shouldClose) return; if (!shouldClose) {
this.#isClosingPrevented = true;
// Set values back if we cancel out of closing the popup
this.value = undefined;
this.result = undefined;
this.inputResults = undefined;
return undefined;
}
} }
this.#isClosingPrevented = false;
Popup.util.lastResult = { value, result, inputResults: this.inputResults }; Popup.util.lastResult = { value, result, inputResults: this.inputResults };
this.#hide(); this.#hide();

View File

@@ -28,6 +28,7 @@
<li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li> <li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
<li><tt>&lcub;&lcub;currentSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> <li><tt>&lcub;&lcub;currentSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
<li><tt>&lcub;&lcub;lastSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> <li><tt>&lcub;&lcub;lastSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
<li><tt>&lcub;&lcub;reverse:(content)&rcub;&rcub;</tt> <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> <span data-i18n="help_macros_25">you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</span></li> <li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> <span data-i18n="help_macros_25">you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</span></li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> <span data-i18n="help_macros_26">the current time</span></li> <li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> <span data-i18n="help_macros_26">the current time</span></li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> <span data-i18n="help_macros_27">the current date</span></li> <li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> <span data-i18n="help_macros_27">the current date</span></li>

View File

@@ -107,6 +107,7 @@ const METADATA_KEY = 'world_info';
const DEFAULT_DEPTH = 4; const DEFAULT_DEPTH = 4;
const DEFAULT_WEIGHT = 100; const DEFAULT_WEIGHT = 100;
const MAX_SCAN_DEPTH = 1000; const MAX_SCAN_DEPTH = 1000;
const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
// Typedef area // Typedef area
/** /**
@@ -123,6 +124,7 @@ const MAX_SCAN_DEPTH = 1000;
* @property {number} [sticky] The sticky value of the entry * @property {number} [sticky] The sticky value of the entry
* @property {number} [cooldown] The cooldown of the entry * @property {number} [cooldown] The cooldown of the entry
* @property {number} [delay] The delay of the entry * @property {number} [delay] The delay of the entry
* @property {string[]} [decorators] Array of decorators for the entry
*/ */
/** /**
@@ -169,7 +171,7 @@ class WorldInfoBuffer {
#skew = 0; #skew = 0;
/** /**
* @type {number} The starting depth of the global scan depth. Incremented by "min activations" feature to not repeat scans. When > 0 it means a complete scan was done up to #startDepth already, and `advanceScanPosition` was called. * @type {number} The starting depth of the global scan depth.
*/ */
#startDepth = 0; #startDepth = 0;
@@ -222,12 +224,12 @@ class WorldInfoBuffer {
} }
if (depth < 0) { if (depth < 0) {
console.error(`Invalid WI scan depth ${depth}. Must be >= 0`); console.error(`[WI] Invalid WI scan depth ${depth}. Must be >= 0`);
return ''; return '';
} }
if (depth > MAX_SCAN_DEPTH) { if (depth > MAX_SCAN_DEPTH) {
console.warn(`Invalid WI scan depth ${depth}. Truncating to ${MAX_SCAN_DEPTH}`); console.warn(`[WI] Invalid WI scan depth ${depth}. Truncating to ${MAX_SCAN_DEPTH}`);
depth = MAX_SCAN_DEPTH; depth = MAX_SCAN_DEPTH;
} }
@@ -301,10 +303,17 @@ class WorldInfoBuffer {
} }
/** /**
* Increments skew and sets startDepth to previous depth. * Checks if the recursion buffer is not empty.
* @returns {boolean} Returns true if the recursion buffer is not empty, otherwise false
*/ */
advanceScanPosition() { hasRecurse() {
this.#startDepth = this.getDepth(); return this.#recurseBuffer.length > 0;
}
/**
* Increments skew to advance the scan range.
*/
advanceScan() {
this.#skew++; this.#skew++;
} }
@@ -436,7 +445,7 @@ class WorldInfoTimedEffects {
const key = this.#getEntryKey(entry); const key = this.#getEntryKey(entry);
const effect = this.#getEntryTimedEffect('cooldown', entry, true); const effect = this.#getEntryTimedEffect('cooldown', entry, true);
chat_metadata.timedWorldInfo.cooldown[key] = effect; chat_metadata.timedWorldInfo.cooldown[key] = effect;
console.log(`Adding cooldown entry ${key} on ended sticky: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`); console.log(`[WI] Adding cooldown entry ${key} on ended sticky: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
// Set the cooldown immediately for this evaluation // Set the cooldown immediately for this evaluation
this.#buffer.cooldown.push(entry); this.#buffer.cooldown.push(entry);
}, },
@@ -447,10 +456,10 @@ class WorldInfoTimedEffects {
* @param {WIScanEntry} entry Entry that ended cooldown * @param {WIScanEntry} entry Entry that ended cooldown
*/ */
'cooldown': (entry) => { 'cooldown': (entry) => {
console.debug('Cooldown ended for entry', entry.uid); console.debug('[WI] Cooldown ended for entry', entry.uid);
}, },
'delay': () => {}, 'delay': () => { },
}; };
/** /**
@@ -537,11 +546,11 @@ class WorldInfoTimedEffects {
/** @type {[string, WITimedEffect][]} */ /** @type {[string, WITimedEffect][]} */
const effects = Object.entries(chat_metadata.timedWorldInfo[type]); const effects = Object.entries(chat_metadata.timedWorldInfo[type]);
for (const [key, value] of effects) { for (const [key, value] of effects) {
console.log(`Processing ${type} entry ${key}`, value); console.log(`[WI] Processing ${type} entry ${key}`, value);
const entry = this.#entries.find(x => String(this.#getEntryHash(x)) === String(value.hash)); const entry = this.#entries.find(x => String(this.#getEntryHash(x)) === String(value.hash));
if (this.#chat.length <= Number(value.start) && !value.protected) { if (this.#chat.length <= Number(value.start) && !value.protected) {
console.log(`Removing ${type} entry ${key} from timedWorldInfo: chat not advanced`, value); console.log(`[WI] Removing ${type} entry ${key} from timedWorldInfo: chat not advanced`, value);
delete chat_metadata.timedWorldInfo[type][key]; delete chat_metadata.timedWorldInfo[type][key];
continue; continue;
} }
@@ -549,7 +558,7 @@ class WorldInfoTimedEffects {
// Missing entries (they could be from another character's lorebook) // Missing entries (they could be from another character's lorebook)
if (!entry) { if (!entry) {
if (this.#chat.length >= Number(value.end)) { if (this.#chat.length >= Number(value.end)) {
console.log(`Removing ${type} entry from timedWorldInfo: entry not found and interval passed`, entry); console.log(`[WI] Removing ${type} entry from timedWorldInfo: entry not found and interval passed`, entry);
delete chat_metadata.timedWorldInfo[type][key]; delete chat_metadata.timedWorldInfo[type][key];
} }
continue; continue;
@@ -557,13 +566,13 @@ class WorldInfoTimedEffects {
// Ignore invalid entries (not configured for timed effects) // Ignore invalid entries (not configured for timed effects)
if (!entry[type]) { if (!entry[type]) {
console.log(`Removing ${type} entry from timedWorldInfo: entry not ${type}`, entry); console.log(`[WI] Removing ${type} entry from timedWorldInfo: entry not ${type}`, entry);
delete chat_metadata.timedWorldInfo[type][key]; delete chat_metadata.timedWorldInfo[type][key];
continue; continue;
} }
if (this.#chat.length >= Number(value.end)) { if (this.#chat.length >= Number(value.end)) {
console.log(`Removing ${type} entry from timedWorldInfo: ${type} interval passed`, entry); console.log(`[WI] Removing ${type} entry from timedWorldInfo: ${type} interval passed`, entry);
delete chat_metadata.timedWorldInfo[type][key]; delete chat_metadata.timedWorldInfo[type][key];
if (typeof onEnded === 'function') { if (typeof onEnded === 'function') {
onEnded(entry); onEnded(entry);
@@ -572,7 +581,7 @@ class WorldInfoTimedEffects {
} }
buffer.push(entry); buffer.push(entry);
console.log(`Timed effect "${type}" applied to entry`, entry); console.log(`[WI] Timed effect "${type}" applied to entry`, entry);
} }
} }
@@ -588,7 +597,7 @@ class WorldInfoTimedEffects {
if (this.#chat.length < entry.delay) { if (this.#chat.length < entry.delay) {
buffer.push(entry); buffer.push(entry);
console.log('Timed effect "delay" applied to entry', entry); console.log('[WI] Timed effect "delay" applied to entry', entry);
} }
} }
@@ -635,7 +644,7 @@ class WorldInfoTimedEffects {
const effect = this.#getEntryTimedEffect(type, entry, false); const effect = this.#getEntryTimedEffect(type, entry, false);
chat_metadata.timedWorldInfo[type][key] = effect; chat_metadata.timedWorldInfo[type][key] = effect;
console.log(`Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`); console.log(`[WI] Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
} }
} }
@@ -667,7 +676,7 @@ class WorldInfoTimedEffects {
if (newState) { if (newState) {
const effect = this.#getEntryTimedEffect(type, entry, false); const effect = this.#getEntryTimedEffect(type, entry, false);
chat_metadata.timedWorldInfo[type][key] = effect; chat_metadata.timedWorldInfo[type][key] = effect;
console.log(`Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`); console.log(`[WI] Adding ${type} entry ${key}: start=${effect.start}, end=${effect.end}, protected=${effect.protected}`);
} }
} }
@@ -3419,13 +3428,12 @@ async function createNewWorldInfo(worldName, { interactive = false } = {}) {
async function getCharacterLore() { async function getCharacterLore() {
const character = characters[this_chid]; const character = characters[this_chid];
const name = character?.name; const name = character?.name;
/** @type {Set<string>} */
let worldsToSearch = new Set(); let worldsToSearch = new Set();
const baseWorldName = character?.data?.extensions?.world; const baseWorldName = character?.data?.extensions?.world;
if (baseWorldName) { if (baseWorldName) {
worldsToSearch.add(baseWorldName); worldsToSearch.add(baseWorldName);
} else {
console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`);
} }
// TODO: Maybe make the utility function not use the window context? // TODO: Maybe make the utility function not use the window context?
@@ -3435,40 +3443,48 @@ async function getCharacterLore() {
worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]); worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
} }
if (!worldsToSearch.size) {
return [];
}
let entries = []; let entries = [];
for (const worldName of worldsToSearch) { for (const worldName of worldsToSearch) {
if (selected_world_info.includes(worldName)) { if (selected_world_info.includes(worldName)) {
console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`); console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);
continue; continue;
} }
if (chat_metadata[METADATA_KEY] === worldName) { if (chat_metadata[METADATA_KEY] === worldName) {
console.debug(`Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`); console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`);
continue; continue;
} }
const data = await loadWorldInfoData(worldName); const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : []; const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
entries = entries.concat(newEntries); entries = entries.concat(newEntries);
if (!newEntries.length) {
console.debug(`[WI] Character ${name}'s world ${worldName} could not be found or is empty`);
}
} }
console.debug(`Character ${name} lore (${Array.from(worldsToSearch)}) has ${entries.length} world info entries`); console.debug(`[WI] Character ${name}'s lore has ${entries.length} world info entries`, [...worldsToSearch]);
return entries; return entries;
} }
async function getGlobalLore() { async function getGlobalLore() {
if (!selected_world_info) { if (!selected_world_info?.length) {
return []; return [];
} }
let entries = []; let entries = [];
for (const worldName of selected_world_info) { for (const worldName of selected_world_info) {
const data = await loadWorldInfoData(worldName); const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: worldName })) : []; const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
entries = entries.concat(newEntries); entries = entries.concat(newEntries);
} }
console.debug(`Global world info has ${entries.length} entries`); console.debug(`[WI] Global world info has ${entries.length} entries`, selected_world_info);
return entries; return entries;
} }
@@ -3481,14 +3497,14 @@ async function getChatLore() {
} }
if (selected_world_info.includes(chatWorld)) { if (selected_world_info.includes(chatWorld)) {
console.debug(`Chat world ${chatWorld} is already activated in global world info! Skipping...`); console.debug(`[WI] Chat world ${chatWorld} is already activated in global world info! Skipping...`);
return []; return [];
} }
const data = await loadWorldInfoData(chatWorld); const data = await loadWorldInfoData(chatWorld);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(x => ({ ...x, world: chatWorld })) : []; const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: chatWorld, ...rest })) : [];
console.debug(`Chat lore has ${entries.length} entries`); console.debug(`[WI] Chat lore has ${entries.length} entries`, [chatWorld]);
return entries; return entries;
} }
@@ -3512,7 +3528,7 @@ export async function getSortedEntries() {
entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)]; entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];
break; break;
default: default:
console.error('Unknown WI insertion strategy: ', world_info_character_strategy, 'defaulting to evenly'); console.error('[WI] Unknown WI insertion strategy:', world_info_character_strategy, 'defaulting to evenly');
entries = [...globalLore, ...characterLore].sort(sortFn); entries = [...globalLore, ...characterLore].sort(sortFn);
break; break;
} }
@@ -3520,7 +3536,13 @@ export async function getSortedEntries() {
// Chat lore always goes first // Chat lore always goes first
entries = [...chatLore.sort(sortFn), ...entries]; entries = [...chatLore.sort(sortFn), ...entries];
console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`); // Parse decorators
entries = entries.map((entry) => {
const [decorators, content] = parseDecorators(entry.content);
return { ...entry, decorators, content };
});
console.debug(`[WI] Found ${entries.length} world lore entries. Sorted by strategy`, Object.entries(world_info_insertion_strategy).find((x) => x[1] === world_info_character_strategy));
// Need to deep clone the entries to avoid modifying the cached data // Need to deep clone the entries to avoid modifying the cached data
return structuredClone(entries); return structuredClone(entries);
@@ -3531,6 +3553,62 @@ export async function getSortedEntries() {
} }
} }
/**
* Parse decorators from worldinfo content
* @param {string} content The content to parse
* @returns {[string[],string]} The decorators found in the content and the content without decorators
*/
function parseDecorators(content) {
/**
* Check if the decorator is known
* @param {string} data string to check
* @returns {boolean} true if the decorator is known
*/
const isKnownDecorator = (data) => {
if (data.startsWith('@@@')) {
data = data.substring(1);
}
for (let i = 0; i < KNOWN_DECORATORS.length; i++) {
if (data.startsWith(KNOWN_DECORATORS[i])) {
return true;
}
}
return false;
};
if (content.startsWith('@@')) {
let newContent = content;
const splited = content.split('\n');
let decorators = [];
let fallbacked = false;
for (let i = 0; i < splited.length; i++) {
if (splited[i].startsWith('@@')) {
if (splited[i].startsWith('@@@') && !fallbacked) {
continue;
}
if (isKnownDecorator(splited[i])) {
decorators.push(splited[i].startsWith('@@@') ? splited[i].substring(1) : splited[i]);
fallbacked = false;
}
else {
fallbacked = true;
}
} else {
newContent = splited.slice(i).join('\n');
break;
}
}
return [decorators, newContent];
}
return [[], content];
}
/** /**
* Performs a scan on the chat and returns the world info activated. * Performs a scan on the chat and returns the world info activated.
* @param {string[]} chat The chat messages to scan, in reverse order. * @param {string[]} chat The chat messages to scan, in reverse order.
@@ -3543,6 +3621,8 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
const context = getContext(); const context = getContext();
const buffer = new WorldInfoBuffer(chat); const buffer = new WorldInfoBuffer(chat);
console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages) ---`);
// Combine the chat // Combine the chat
// Add the depth or AN if enabled // Add the depth or AN if enabled
@@ -3566,11 +3646,11 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
let budget = Math.round(world_info_budget * maxContext / 100) || 1; let budget = Math.round(world_info_budget * maxContext / 100) || 1;
if (world_info_budget_cap > 0 && budget > world_info_budget_cap) { if (world_info_budget_cap > 0 && budget > world_info_budget_cap) {
console.debug(`Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`); console.debug(`[WI] Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`);
budget = world_info_budget_cap; budget = world_info_budget_cap;
} }
console.debug(`Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`); console.debug(`[WI] Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`);
const sortedEntries = await getSortedEntries(); const sortedEntries = await getSortedEntries();
const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries); const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries);
@@ -3580,21 +3660,48 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
} }
console.debug(`[WI] --- SEARCHING ENTRIES (on ${sortedEntries.length} entries) ---`);
while (scanState) { while (scanState) {
// Track how many times the loop has run. May be useful for debugging. // Track how many times the loop has run. May be useful for debugging.
// eslint-disable-next-line no-unused-vars
count++; count++;
console.debug(`[WI] Loop #${count}. Search state`, Object.entries(scan_state).find(x => x[1] === scanState));
// Until decided otherwise, we set the loop to stop scanning after this
let nextScanState = scan_state.NONE;
// Loop and find all entries that can activate here
let activatedNow = new Set(); let activatedNow = new Set();
for (let entry of sortedEntries) { for (let entry of sortedEntries) {
// Logging preparation
let headerLogged = false;
function log(...args) {
if (!headerLogged) {
console.debug(`[WI] Entry ${entry.uid}`, `from '${entry.world}' processing`, entry);
headerLogged = true;
}
console.debug(`[WI] Entry ${entry.uid}`, ...args);
}
// Already processed, considered and then skipped entries should still be skipped
if (failedProbabilityChecks.has(entry) || allActivatedEntries.has(entry)) {
continue;
}
if (entry.disable == true) {
log('disabled');
continue;
}
// Check if this entry applies to the character or if it's excluded // Check if this entry applies to the character or if it's excluded
if (entry.characterFilter && entry.characterFilter?.names?.length > 0) { if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {
const nameIncluded = entry.characterFilter.names.includes(getCharaFilename()); const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded; const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded;
if (filtered) { if (filtered) {
console.debug(`WI entry ${entry.uid} filtered out by character`); log('filtered out by character');
continue; continue;
} }
} }
@@ -3611,7 +3718,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag; const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag;
if (filtered) { if (filtered) {
console.debug(`WI entry ${entry.uid} filtered out by tag`); log('filtered out by tag');
continue; continue;
} }
} }
@@ -3623,186 +3730,253 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
const isDelay = timedEffects.isEffectActive('delay', entry); const isDelay = timedEffects.isEffectActive('delay', entry);
if (isDelay) { if (isDelay) {
console.debug(`WI entry ${entry.uid} suppressed by delay`, entry); log('suppressed by delay');
continue; continue;
} }
if (isCooldown && !isSticky) { if (isCooldown && !isSticky) {
console.debug(`WI entry ${entry.uid} suppressed by cooldown`, entry); log('suppressed by cooldown');
continue;
}
if (failedProbabilityChecks.has(entry)) {
continue;
}
if (allActivatedEntries.has(entry) || entry.disable == true) {
continue; continue;
} }
// Only use checks for recursion flags if the scan step was activated by recursion // Only use checks for recursion flags if the scan step was activated by recursion
if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) { if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) {
console.debug(`WI entry ${entry.uid} suppressed by delay until recursion`, entry); log('suppressed by delay until recursion');
continue; continue;
} }
if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) { if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) {
console.debug(`WI entry ${entry.uid} suppressed by exclude recursion`, entry); log('suppressed by exclude recursion');
continue; continue;
} }
if (entry.constant || buffer.isExternallyActivated(entry) || isSticky) { if (entry.decorators.includes('@@activate')) {
log('activated by @@activate decorator');
activatedNow.add(entry); activatedNow.add(entry);
continue; continue;
} }
if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing if (entry.decorators.includes('@@dont_activate')) {
// If selectiveLogic isn't found, assume it's AND, only do this once per entry log('suppressed by @@dont_activate decorator');
const selectiveLogic = entry.selectiveLogic ?? 0; continue;
}
primary: for (let key of entry.key) { // Now do checks for immediate activations
const substituted = substituteParams(key); if (entry.constant) {
const textToScan = buffer.get(entry, scanState); log('activated because of constant');
activatedNow.add(entry);
continue;
}
if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) { if (buffer.isExternallyActivated(entry)) {
console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`); log('externally activated');
activatedNow.add(entry);
continue;
}
//selective logic begins if (isSticky) {
if ( log('activated because active sticky');
entry.selective && //all entries are selective now activatedNow.add(entry);
Array.isArray(entry.keysecondary) && //always true continue;
entry.keysecondary.length //ignore empties }
) {
console.debug(`WI UID:${entry.uid} found. Checking logic: ${entry.selectiveLogic}`);
let hasAnyMatch = false;
let hasAllMatch = true;
secondary: for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
const hasSecondaryMatch = secondarySubstituted && buffer.matchKeys(textToScan, secondarySubstituted.trim(), entry);
console.debug(`WI UID:${entry.uid}: Filtering for secondary keyword - "${secondarySubstituted}".`);
if (hasSecondaryMatch) { if (!Array.isArray(entry.key) || !entry.key.length) {
hasAnyMatch = true; log('has no keys defined, skipped');
} continue;
}
if (!hasSecondaryMatch) { // Cache the text to scan before the loop, it won't change its content
hasAllMatch = false; const textToScan = buffer.get(entry, scanState);
}
// Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya) // PRIMARY KEYWORDS
// If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass let primaryKeyMatch = entry.key.find(key => {
if ((selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) || (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch)) { const substituted = substituteParams(key);
// Differ both logic statements in the debugger return substituted && buffer.matchKeys(textToScan, substituted.trim(), entry);
if (selectiveLogic === world_info_logic.AND_ANY) { });
console.debug(`(AND ANY Check) Activating WI Entry ${entry.uid}. Found match for word: ${substituted} ${secondarySubstituted}`);
} else {
console.debug(`(NOT ALL Check) Activating WI Entry ${entry.uid}. Found match for word "${substituted}" without secondary keyword: ${secondarySubstituted}`);
}
activatedNow.add(entry);
break secondary;
}
}
// Handle NOT ANY logic if (!primaryKeyMatch) {
if (selectiveLogic === world_info_logic.NOT_ANY && !hasAnyMatch) { // Don't write logs for simple no-matches
console.debug(`(NOT ANY Check) Activating WI Entry ${entry.uid}, no secondary keywords found.`); continue;
activatedNow.add(entry); }
}
// Handle AND ALL logic const hasSecondaryKeywords = (
if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) { entry.selective && //all entries are selective now
console.debug(`(AND ALL Check) Activating WI Entry ${entry.uid}, all secondary keywords found.`); Array.isArray(entry.keysecondary) && //always true
activatedNow.add(entry); entry.keysecondary.length //ignore empties
} );
} else {
// Handle cases where secondary is empty if (!hasSecondaryKeywords) {
console.debug(`WI UID ${entry.uid}: Activated without filter logic.`); // Handle cases where secondary is empty
activatedNow.add(entry); log('activated by primary key match', primaryKeyMatch);
break primary; activatedNow.add(entry);
} continue;
}
// SECONDARY KEYWORDS
const selectiveLogic = entry.selectiveLogic ?? 0; // If selectiveLogic isn't found, assume it's AND, only do this once per entry
log('Entry with primary key match', primaryKeyMatch, 'has secondary keywords. Checking with logic logic', Object.entries(world_info_logic).find(x => x[1] === entry.selectiveLogic));
/** @type {() => boolean} */
function matchSecondaryKeys() {
let hasAnyMatch = false;
let hasAllMatch = true;
for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
const hasSecondaryMatch = secondarySubstituted && buffer.matchKeys(textToScan, secondarySubstituted.trim(), entry);
if (hasSecondaryMatch) hasAnyMatch = true;
if (!hasSecondaryMatch) hasAllMatch = false;
// Simplified AND ANY / NOT ALL if statement. (Proper fix for PR#1356 by Bronya)
// If AND ANY logic and the main checks pass OR if NOT ALL logic and the main checks do not pass
if (selectiveLogic === world_info_logic.AND_ANY && hasSecondaryMatch) {
log('activated. (AND ANY) Found match secondary keyword', secondarySubstituted);
return true;
}
if (selectiveLogic === world_info_logic.NOT_ALL && !hasSecondaryMatch) {
log('activated. (NOT ALL) Found not matching secondary keyword', secondarySubstituted);
return true;
} }
} }
// Handle NOT ANY logic
if (selectiveLogic === world_info_logic.NOT_ANY && !hasAnyMatch) {
log('activated. (NOT ANY) No secondary keywords found', entry.keysecondary);
return true;
}
// Handle AND ALL logic
if (selectiveLogic === world_info_logic.AND_ALL && hasAllMatch) {
log('activated. (AND ALL) All secondary keywords found', entry.keysecondary);
return true;
}
return false;
} }
const matched = matchSecondaryKeys();
if (!matched) {
log('skipped. Secondary keywords not satisfied', entry.keysecondary);
continue;
}
// Success logging was already done inside the function, so just add the entry
activatedNow.add(entry);
continue;
} }
scanState = world_info_recursive && activatedNow.size > 0 ? scan_state.RECURSION : scan_state.NONE; console.debug(`[WI] Search done. Found ${activatedNow.size} possible entries.`);
const newEntries = [...activatedNow] const newEntries = [...activatedNow]
.sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b)); .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
let newContent = ''; let newContent = '';
const textToScanTokens = await getTokenCountAsync(allActivatedText); const textToScanTokens = await getTokenCountAsync(allActivatedText);
const probabilityChecksBefore = failedProbabilityChecks.size;
filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState); filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState);
console.debug('-- PROBABILITY CHECKS BEGIN --'); console.debug('[WI] --- PROBABILITY CHECKS ---');
for (const entry of newEntries) { for (const entry of newEntries) {
const rollValue = Math.random() * 100; function verifyProbability() {
// If we don't need to roll, it's always true
if (entry.useProbability && rollValue > entry.probability) { if (!entry.useProbability || entry.probability === 100) {
const isSticky = timedEffects.isEffectActive('sticky', entry); console.debug(`WI entry ${entry.uid} does not use probability`);
if (!isSticky) { return true;
console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`);
failedProbabilityChecks.add(entry);
continue;
} }
} else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`); }
const isSticky = timedEffects.isEffectActive('sticky', entry);
if (isSticky) {
console.debug(`WI entry ${entry.uid} is sticky, does not need to re-roll probability`);
return true;
}
const rollValue = Math.random() * 100;
if (rollValue <= entry.probability) {
console.debug(`WI entry ${entry.uid} passed probability check of ${entry.probability}%`);
return true;
}
failedProbabilityChecks.add(entry);
return false;
}
const success = verifyProbability();
if (!success) {
console.debug(`WI entry ${entry.uid} failed probability check, removing from activated entries`, entry);
continue;
}
// Substitute macros inline, for both this checking and also future processing // Substitute macros inline, for both this checking and also future processing
entry.content = substituteParams(entry.content); entry.content = substituteParams(entry.content);
newContent += `${entry.content}\n`; newContent += `${entry.content}\n`;
if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) { if ((textToScanTokens + (await getTokenCountAsync(newContent))) >= budget) {
console.debug('WI budget reached, stopping');
if (world_info_overflow_alert) { if (world_info_overflow_alert) {
console.log('Alerting'); console.warn(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info'); toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
} else {
console.debug(`[WI] budget of ${budget} reached, stopping after ${allActivatedEntries.size} entries`);
} }
scanState = scan_state.NONE;
token_budget_overflowed = true; token_budget_overflowed = true;
break; break;
} }
allActivatedEntries.add(entry); allActivatedEntries.add(entry);
console.debug('WI entry activated:', entry); console.debug(`[WI] Entry ${entry.uid} activation successful, adding to prompt`, entry);
} }
const probabilityChecksAfter = failedProbabilityChecks.size; const successfulNewEntries = newEntries.filter(x => !failedProbabilityChecks.has(x));
const successfulNewEntriesForRecursion = successfulNewEntries.filter(x => !x.preventRecursion);
if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) { if (!newEntries.length) {
console.debug('WI probability checks failed for all activated entries, stopping'); console.debug('[WI] No new entries activated, stopping');
scanState = scan_state.NONE; } else if (!successfulNewEntries.length) {
console.debug('[WI] Probability checks failed for all activated entries, stopping');
} else {
console.debug(`[WI] Successfully activated ${successfulNewEntries.length} new entries to prompt. ${allActivatedEntries.size} total entries activated.`, successfulNewEntries);
} }
if (newEntries.length === 0) { // After processing and rolling entries is done, see if we should continue with normal recursion
console.debug('No new entries activated, stopping'); if (world_info_recursive && !token_budget_overflowed && successfulNewEntriesForRecursion.length) {
scanState = scan_state.NONE; nextScanState = scan_state.RECURSION;
} }
// If we are inside min activations scan, and we have recursive buffer, we should do a recursive scan before increasing the buffer again
// There might be recurse-trigger-able entries that match the buffer, so we need to check that
if (world_info_recursive && !token_budget_overflowed && scanState === scan_state.MIN_ACTIVATIONS && buffer.hasRecurse()) {
nextScanState = scan_state.RECURSION;
}
// If scanning is planned to stop, but min activations is set and not satisfied, check if we should continue
const minActivationsNotSatisfied = world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations);
if (!nextScanState && !token_budget_overflowed && minActivationsNotSatisfied) {
console.debug('[WI] --- MIN ACTIVATIONS CHECK ---');
let over_max = (
world_info_min_activations_depth_max > 0 &&
buffer.getDepth() > world_info_min_activations_depth_max
) || (buffer.getDepth() > chat.length);
if (!over_max) {
console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), advancing depth to ${buffer.getDepth() + 1} and checking again`);
nextScanState = scan_state.MIN_ACTIVATIONS; // loop
buffer.advanceScan();
} else {
console.debug(`[WI] Min activations not reached (${allActivatedEntries.size}/${world_info_min_activations}), but reached on of depth. Stopping`);
}
}
// Final check if we should really continue scan, and extend the current WI recurse buffer
scanState = nextScanState;
if (scanState) { if (scanState) {
const text = newEntries const text = successfulNewEntriesForRecursion
.filter(x => !failedProbabilityChecks.has(x))
.filter(x => !x.preventRecursion)
.map(x => x.content).join('\n'); .map(x => x.content).join('\n');
buffer.addRecurse(text); buffer.addRecurse(text);
allActivatedText = (text + '\n' + allActivatedText); allActivatedText = (text + '\n' + allActivatedText);
} }
// world_info_min_activations
if (!scanState && !token_budget_overflowed) {
if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) {
let over_max = (
world_info_min_activations_depth_max > 0 &&
buffer.getDepth() > world_info_min_activations_depth_max
) || (buffer.getDepth() > chat.length);
if (!over_max) {
scanState = scan_state.MIN_ACTIVATIONS; // loop
buffer.advanceScanPosition();
}
}
}
} }
console.debug('[WI] --- BUILDING PROMPT ---');
// Forward-sorted list of entries for joining // Forward-sorted list of entries for joining
const WIBeforeEntries = []; const WIBeforeEntries = [];
const WIAfterEntries = []; const WIAfterEntries = [];
@@ -3818,7 +3992,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true }); const content = getRegexedString(entry.content, regex_placement.WORLD_INFO, { depth: regexDepth, isMarkdown: false, isPrompt: true });
if (!content) { if (!content) {
console.debug('Skipping adding WI entry to prompt due to empty content:', entry); console.debug(`[WI] Entry ${entry.uid}`, 'skipped adding to prompt due to empty content', entry);
return; return;
} }
@@ -3876,6 +4050,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) {
buffer.resetExternalEffects(); buffer.resetExternalEffects();
timedEffects.cleanUp(); timedEffects.cleanUp();
console.log(`[WI] Adding ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries));
console.debug('[WI] --- DONE ---');
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries }; return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries };
} }
@@ -3890,13 +4067,13 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
for (const [key, group] of Object.entries(groups)) { for (const [key, group] of Object.entries(groups)) {
// Group scoring is disabled both globally and for the group entries // Group scoring is disabled both globally and for the group entries
if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) { if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) {
console.debug(`Skipping group scoring for group '${key}'`); console.debug(`[WI] Skipping group scoring for group '${key}'`);
continue; continue;
} }
const scores = group.map(entry => buffer.getScore(entry, scanState)); const scores = group.map(entry => buffer.getScore(entry, scanState));
const maxScore = Math.max(...scores); const maxScore = Math.max(...scores);
console.debug(`Group '${key}' max score: ${maxScore}`); console.debug(`[WI] Group '${key}' max score:`, maxScore);
//console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] }))); //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] })));
for (let i = 0; i < group.length; i++) { for (let i = 0; i < group.length; i++) {
@@ -3907,7 +4084,7 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
} }
if (scores[i] < maxScore) { if (scores[i] < maxScore) {
console.debug(`Removing score loser from inclusion group '${key}' entry '${group[i].uid}'`, group[i]); console.debug(`[WI] Entry ${group[i].uid}`, `removed as score loser from inclusion group '${key}'`, group[i]);
removeEntry(group[i]); removeEntry(group[i]);
group.splice(i, 1); group.splice(i, 1);
scores.splice(i, 1); scores.splice(i, 1);
@@ -3925,7 +4102,8 @@ function filterGroupsByScoring(groups, buffer, removeEntry, scanState) {
* @param {number} scanState The current scan state * @param {number} scanState The current scan state
*/ */
function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) { function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) {
console.debug('-- INCLUSION GROUP CHECKS BEGIN --'); console.debug('[WI] --- INCLUSION GROUP CHECKS ---');
const grouped = newEntries.filter(x => x.group).reduce((acc, item) => { const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {
item.group.split(/,\s*/).filter(x => x).forEach(group => { item.group.split(/,\s*/).filter(x => x).forEach(group => {
if (!acc[group]) { if (!acc[group]) {
@@ -3937,7 +4115,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
}, {}); }, {});
if (Object.keys(grouped).length === 0) { if (Object.keys(grouped).length === 0) {
console.debug('No inclusion groups found'); console.debug('[WI] No inclusion groups found');
return; return;
} }
@@ -3948,7 +4126,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
continue; continue;
} }
if (logging) console.debug(`Removing loser from inclusion group '${entry.group}' entry '${entry.uid}'`, entry); if (logging) console.debug(`[WI] Entry ${entry.uid}`, `removed as loser from inclusion group '${entry.group}'`, entry);
removeEntry(entry); removeEntry(entry);
} }
} }
@@ -3956,24 +4134,24 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
filterGroupsByScoring(grouped, buffer, removeEntry, scanState); filterGroupsByScoring(grouped, buffer, removeEntry, scanState);
for (const [key, group] of Object.entries(grouped)) { for (const [key, group] of Object.entries(grouped)) {
console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group); console.debug(`[WI] Checking inclusion group '${key}' with ${group.length} entries`, group);
if (Array.from(allActivatedEntries).some(x => x.group === key)) { if (Array.from(allActivatedEntries).some(x => x.group === key)) {
console.debug(`Skipping inclusion group check, group already activated '${key}'`); console.debug(`[WI] Skipping inclusion group check, group '${key}' was already activated`);
// We need to forcefully deactivate all other entries in the group // We need to forcefully deactivate all other entries in the group
removeAllBut(group, null, false); removeAllBut(group, null, false);
continue; continue;
} }
if (!Array.isArray(group) || group.length <= 1) { if (!Array.isArray(group) || group.length <= 1) {
console.debug('Skipping inclusion group check, only one entry'); console.debug('[WI] Skipping inclusion group check, only one entry');
continue; continue;
} }
// Check for group prio // Check for group prio
const prios = group.filter(x => x.groupOverride).sort(sortFn); const prios = group.filter(x => x.groupOverride).sort(sortFn);
if (prios.length) { if (prios.length) {
console.debug(`Activated inclusion group '${key}' with by prio winner entry '${prios[0].uid}'`, prios[0]); console.debug(`[WI] Entry ${prios[0].uid}`, `activated as prio winner from inclusion group '${key}'`, prios[0]);
removeAllBut(group, prios[0]); removeAllBut(group, prios[0]);
continue; continue;
} }
@@ -3988,14 +4166,14 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanSt
currentWeight += (entry.groupWeight ?? DEFAULT_WEIGHT); currentWeight += (entry.groupWeight ?? DEFAULT_WEIGHT);
if (rollValue <= currentWeight) { if (rollValue <= currentWeight) {
console.debug(`Activated inclusion group '${key}' with roll winner entry '${entry.uid}'`, entry); console.debug(`[WI] Entry ${entry.uid}`, `activated as roll winner from inclusion group '${key}'`, entry);
winner = entry; winner = entry;
break; break;
} }
} }
if (!winner) { if (!winner) {
console.debug(`Failed to activate inclusion group '${key}', no winner found`); console.debug(`[WI] Failed to activate inclusion group '${key}', no winner found`);
continue; continue;
} }

View File

@@ -23,9 +23,21 @@ const write = (image, data) => {
} }
} }
// Add new chunks before the IEND chunk // Add new v2 chunk before the IEND chunk
const base64EncodedData = Buffer.from(data, 'utf8').toString('base64'); const base64EncodedData = Buffer.from(data, 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData)); chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData));
// Try adding v3 chunk before the IEND chunk
try {
//change v2 format to v3
const v3Data = JSON.parse(data);
v3Data.spec = 'chara_card_v3';
v3Data.spec_version = '3.0';
const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64');
chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData));
} catch (error) { }
const newBuffer = Buffer.from(encode(chunks)); const newBuffer = Buffer.from(encode(chunks));
return newBuffer; return newBuffer;
}; };

View File

@@ -408,6 +408,9 @@ function charaFormatData(data, directories) {
//_.set(char, 'data.extensions.avatar', 'none'); //_.set(char, 'data.extensions.avatar', 'none');
//_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); //_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
// V3 fields
_.set(char, 'data.group_only_greetings', data.group_only_greetings ?? []);
if (data.world) { if (data.world) {
try { try {
const file = readWorldInfoFile(directories, data.world, false); const file = readWorldInfoFile(directories, data.world, false);