import { get } from "svelte/store"; import { CharEmotion, selectedCharID } from "../stores.svelte"; import { type character, type customscript, type groupChat, type Database, getDatabase, getCurrentCharacter, getCurrentChat } from "../storage/database.svelte"; import { downloadFile } from "../globalApi.svelte"; import { alertError, alertNormal } from "../alert"; import { language } from "src/lang"; import { selectSingleFile } from "../util"; import { assetRegex, type CbsConditions, risuChatParser as risuChatParserOrg, type simpleCharacterArgument } from "../parser.svelte"; import { getModuleAssets, getModuleRegexScripts, getModuleTriggers } from "./modules"; import { HypaProcesser } from "./memory/hypamemory"; import { runLuaEditTrigger } from "./lua"; import { pluginV2 } from "../plugins/plugins"; import { runTrigger } from "./triggers"; const dreg = /{{data}}/g const randomness = /\|\|\|/g export type ScriptMode = 'editinput'|'editoutput'|'editprocess'|'editdisplay' type pScript = { script: customscript, order: number actions: string[] } export async function processScript(char:character|groupChat, data:string, mode:ScriptMode, cbsConditions:CbsConditions = {}){ return (await processScriptFull(char, data, mode, -1, cbsConditions)).data } export function exportRegex(s?:customscript[]){ let db = getDatabase() const script = s ?? db.globalscript const data = Buffer.from(JSON.stringify({ type: 'regex', data: script }), 'utf-8') downloadFile(`regexscript_export.json`,data) alertNormal(language.successExport) } export async function importRegex(o?:customscript[]):Promise{ o = o ?? [] const filedata = (await selectSingleFile(['json'])).data if(!filedata){ return o } let db = getDatabase() try { const imported= JSON.parse(Buffer.from(filedata).toString('utf-8')) if(imported.type === 'regex' && imported.data){ const datas:customscript[] = imported.data const script = o for(const data of datas){ script.push(data) } return o } else{ alertError("File invaid or corrupted") } } catch (error) { alertError(`${error}`) } return o } let bestMatchCache = new Map() let processScriptCache = new Map() function generateScriptCacheKey(scripts: customscript[], data: string, mode: ScriptMode, chatID = -1, cbsConditions: CbsConditions = {}) { let hash = data + '|||' + mode + '|||'; for (const script of scripts) { if(script.type !== mode){ continue } hash += `${script.flag?.includes('') ? risuChatParser(script.in, { chatID: chatID, cbsConditions }) : script.in}|||${risuChatParser(script.out, { chatID: chatID, cbsConditions})}|||${script.flag ?? ''}|||${script.ableFlag ? 1 : 0}`; } return hash; } function cacheScript(hash:string, result:string){ processScriptCache.set(hash, result) if(processScriptCache.size > 1000){ processScriptCache.delete(processScriptCache.keys().next().value) } } function getScriptCache(hash:string){ return processScriptCache.get(hash) } export function resetScriptCache(){ processScriptCache = new Map() } export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1, cbsConditions:CbsConditions = {}){ let db = getDatabase() const originalData = data let emoChanged = false data = await runLuaEditTrigger(char, mode, data) if(mode === 'editdisplay'){ const currentChar = getCurrentCharacter() if(currentChar.type !== 'group'){ try{ const perf = performance.now() const d = await runTrigger(currentChar, 'display', { chat: getCurrentChat(), displayMode: true, displayData: data }) data = d?.displayData ?? data console.log('Trigger time', performance.now() - perf) } catch(e){ console.error(e) } } } if(pluginV2[mode].size > 0){ for(const plugin of pluginV2[mode]){ const res = await plugin(data) if(res !== null && res !== undefined){ data = res } } } data = risuChatParser(data, { chatID: chatID, cbsConditions }) const scripts = (db.presetRegex ?? []).concat(char.customscript).concat(getModuleRegexScripts()) const hash = generateScriptCacheKey(scripts, data, mode, chatID, cbsConditions) const cached = getScriptCache(hash) if(cached){ return {data: cached, emoChanged: false} } if(scripts.length === 0){ cacheScript(hash, data) return {data, emoChanged} } function executeScript(pscript:pScript){ const script = pscript.script if(script.in === ''){ return } if(script.type === mode){ let outScript2 = script.out.replaceAll("$n", "\n") let outScript = outScript2.replace(dreg, "$&") let flag = 'g' if(script.ableFlag){ flag = script.flag || 'g' } if(outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom') || pscript.actions.includes('move_top') || pscript.actions.includes('move_bottom')){ flag = flag.replace('g', '') //temperary fix } if(outScript.endsWith('>') && !pscript.actions.includes('no_end_nl')){ outScript += '\n' } //remove unsupported flag flag = flag.trim().replace(/[^dgimsuvy]/g, '') //remove repeated flags flag = flag.split('').filter((v, i, a) => a.indexOf(v) === i).join('') if(flag.length === 0){ flag = 'u' } let input = script.in if(pscript.actions.includes('cbs')){ input = risuChatParser(input, { chatID: chatID, cbsConditions }) } const reg = new RegExp(input, flag) if(outScript.startsWith('@@') || pscript.actions.length > 0){ if(reg.test(data)){ if(outScript.startsWith('@@emo ')){ const emoName = script.out.substring(6).trim() let charemotions = get(CharEmotion) let tempEmotion = charemotions[char.chaId] if(!tempEmotion){ tempEmotion = [] } if(tempEmotion.length > 4){ tempEmotion.splice(0, 1) } if(char.type !== 'simple'){ for(const emo of char.emotionImages){ if(emo[0] === emoName){ const emos:[string, string,number] = [emo[0], emo[1], Date.now()] tempEmotion.push(emos) charemotions[char.chaId] = tempEmotion CharEmotion.set(charemotions) emoChanged = true break } } } } else if((outScript.startsWith('@@inject') || pscript.actions.includes('inject')) && chatID !== -1){ const selchar = db.characters[get(selectedCharID)] selchar.chats[selchar.chatPage].message[chatID].data = data data = data.replace(reg, "") } else if( outScript.startsWith('@@move_top') || outScript.startsWith('@@move_bottom') || pscript.actions.includes('move_top') || pscript.actions.includes('move_bottom') ){ const isGlobal = flag.includes('g') const matchAll = isGlobal ? data.matchAll(reg) : [data.match(reg)] data = data.replace(reg, "") for(const matched of matchAll){ if(matched){ const inData = matched[0] let out = outScript.replace('@@move_top ', '').replace('@@move_bottom ', '') .replace(/(?{ const index = parseInt(v.substring(1)) if(index < matched.length){ return matched[index] } return v }) .replace(/\$\&/g, inData) .replace(/(?]+)>/g, (v) => { const groupName = parseInt(v.substring(2, v.length - 1)) if(matched.groups && matched.groups[groupName]){ return matched.groups[groupName] } return v }) if(outScript.startsWith('@@move_top') || pscript.actions.includes('move_top')){ data = out + '\n' +data } else{ data = data + '\n' + out } } } } else{ data = risuChatParser(data.replace(reg, outScript), { chatID: chatID, cbsConditions }) } } else{ if((outScript.startsWith('@@repeat_back') || pscript.actions.includes('repeat_back')) && chatID !== -1){ const v = outScript.split(' ', 2)[1] const selchar = db.characters[get(selectedCharID)] const chat = selchar.chats[selchar.chatPage] let lastChat = chat.fmIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[chat.fmIndex] let pointer = chatID - 1 while(pointer >= 0){ if(chat.message[pointer].role === chat.message[chatID].role){ lastChat = chat.message[pointer].data break } pointer-- } const r = lastChat.match(reg) if(!v){ data = data + r[0] } else if(r[0]){ switch(v){ case 'end': data = data + r[0] break case 'start': data = r[0] + data break case 'end_nl': data = data + "\n" + r[0] break case 'start_nl': data = r[0] + "\n" + data break } } } } } else{ data = risuChatParser(data.replace(reg, outScript), { chatID: chatID, cbsConditions }) } } } let parsedScripts:pScript[] = [] let orderChanged = false for (const script of scripts){ if(script.ableFlag && script.flag?.includes('<')){ const rregex = /<(.+?)>/g const scriptData = safeStructuredClone(script) let order = 0 const actions:string[] = [] scriptData.flag = scriptData.flag?.replace(rregex, (v:string, p1:string) => { const meta = p1.split(',').map((v) => v.trim()) for(const m of meta){ if(m.startsWith('order ')){ order = parseInt(m.substring(6)) orderChanged = true } else{ actions.push(m) } } return '' }) parsedScripts.push({ script: scriptData, order, actions }) continue } parsedScripts.push({ script, order: 0, actions: [] }) } if(orderChanged){ parsedScripts.sort((a, b) => b.order - a.order) //sort by order } for (const script of parsedScripts){ try { executeScript(script) } catch (error) { console.error(error) } } if(db.dynamicAssets && (char.type === 'simple' || char.type === 'character') && char.additionalAssets && char.additionalAssets.length > 0){ if(!db.dynamicAssetsEditDisplay && mode === 'editdisplay'){ cacheScript(hash, data) return {data, emoChanged} } const assetNames = char.additionalAssets.map((v) => v[0]) const moduleAssets = getModuleAssets() if(moduleAssets.length > 0){ for(const asset of moduleAssets){ assetNames.push(asset[0]) } } const processer = new HypaProcesser() await processer.addText(assetNames) const matches = data.matchAll(assetRegex) for(const match of matches){ const type = match[1] const assetName = match[2] const cacheKey = char.chaId + '::' + assetName if(type !== 'emotion' && type !== 'source'){ if(bestMatchCache.has(cacheKey)){ data = data.replaceAll(match[0], `{{${type}::${bestMatchCache.get(cacheKey)}}}`) } else if(!assetNames.includes(assetName)){ const searched = await processer.similaritySearch(assetName) const bestMatch = searched[0] if(bestMatch){ data = data.replaceAll(match[0], `{{${type}::${bestMatch}}}`) bestMatchCache.set(cacheKey, bestMatch) } } } } } cacheScript(hash, data) return {data, emoChanged} } const rgx = /(?:{{|<)(.+?)(?:}}|>)/gm export const risuChatParser = risuChatParserOrg