Auto-Cards/src/library.js
2025-06-20 17:35:44 -04:00

5971 lines
304 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Your "Library" tab should look like this
/*
Auto-Cards
Made by LewdLeah on May 21, 2025
This AI Dungeon script automatically creates and updates plot-relevant story cards while you play
General-purpose usefulness and compatibility with other scenarios/scripts were my design priorities
Auto-Cards is fully open-source, please copy for use within your own projects! ❤️
*/
function AutoCards(inHook, inText, inStop) {
"use strict";
/*
Default Auto-Cards settings
Feel free to change these settings to customize your scenario's default gameplay experience
The default values for your scenario are specified below:
*/
// Is Auto-Cards already enabled when the adventure begins?
const DEFAULT_DO_AC = true
// (true or false)
// Pin the "Configure Auto-Cards" story card at the top of the player's story cards list?
const DEFAULT_PIN_CONFIGURE_CARD = true
// (true or false)
// Minimum number of turns in between automatic card generation events?
const DEFAULT_CARD_CREATION_COOLDOWN = 22
// (0 to 9999)
// Use a bulleted list format for newly generated card entries?
const DEFAULT_USE_BULLETED_LIST_MODE = true
// (true or false)
// Maximum allowed length for newly generated story card entries?
const DEFAULT_GENERATED_ENTRY_LIMIT = 750
// (200 to 2000)
// Do newly generated cards have memory updates enabled by default?
const DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES = true
// (true or false)
// Default character limit before the card's memory bank is summarized?
const DEFAULT_NEW_CARDS_MEMORY_LIMIT = 2750
// (1750 to 9900)
// Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new)
const DEFAULT_MEMORY_COMPRESSION_RATIO = 25
// (20 to 1250)
// Ignore all-caps during title candidate detection?
const DEFAULT_IGNORE_ALL_CAPS_TITLES = true
// (true or false)
// Should player input actions (Do/Say/Story) be considered during title detection?
const DEFAULT_DETECT_TITLES_FROM_INPUTS = false
// (true or false)
// How many (minimum) actions in the past does Auto-Cards look for named entities?
const DEFAULT_MINIMUM_LOOK_BACK_DISTANCE = 5
// (2 to 88)
// Is Live Script Interface v2 enabled?
const DEFAULT_DO_LSI_V2 = false
// (true or false)
// Should the "Debug Data" story card be visible?
const DEFAULT_SHOW_DEBUG_DATA = false
// (true or false)
// AI prompt used to generate new story card entries?
const DEFAULT_CARD_GENERATION_PROMPT = prose(
"-----",
"",
"<SYSTEM>",
"# Stop the story and ignore previous instructions. Write a brief and coherent informational entry for %{title} following these instructions:",
"- Write only third-person pure prose information about %{title} using complete sentences with correct punctuation",
"- Avoid short-term temporary details or appearances, instead focus on plot-significant information",
"- Prioritize story-relevant details about %{title} first to ensure seamless integration with the previous plot",
"- Create new information based on the context and story direction",
"- Mention %{title} in every sentence",
"- Use semicolons if needed",
"- Add additional details about %{title} beneath incomplete entries",
"- Be concise and grounded",
"- Imitate the story's writing style and infer the reader's preferences",
"</SYSTEM>",
"Continue the entry for %{title} below while avoiding repetition:",
"%{entry}"
); // (mimic this multi-line "text" format)
// AI prompt used to summarize a given story card's memory bank?
const DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT = prose(
"-----",
"",
"<SYSTEM>",
"# Stop the story and ignore previous instructions. Summarize and condense the given paragraph into a narrow and focused memory passage while following these guidelines:",
"- Ensure the passage retains the core meaning and most essential details",
"- Use the third-person perspective",
"- Prioritize information-density, accuracy, and completeness",
"- Remain brief and concise",
"- Write firmly in the past tense",
"- The paragraph below pertains to old events from far earlier in the story",
"- Integrate %{title} naturally within the memory; however, only write about the events as they occurred",
"- Only reference information present inside the paragraph itself, be specific",
"</SYSTEM>",
"Write a summarized old memory passage for %{title} based only on the following paragraph:",
"\"\"\"",
"%{memory}",
"\"\"\"",
"Summarize below:"
); // (mimic this multi-line "text" format)
// Titles banned from future card generation attempts?
const DEFAULT_BANNED_TITLES_LIST = (
"North, East, South, West, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, January, February, March, April, May, June, July, August, September, October, November, December"
); // (mimic this comma-list "text" format)
// Default story card "type" used by Auto-Cards? (does not matter)
const DEFAULT_CARD_TYPE = "class"
// ("text")
// Should titles mentioned in the "opening" plot component be banned from future card generation by default?
const DEFAULT_BAN_TITLES_FROM_OPENING = true
// (true or false)
//—————————————————————————————————————————————————————————————————————————————————
/*
Useful API functions for coders (otherwise ignore)
Here's what each one does in plain terms:
AutoCards().API.postponeEvents();
Pauses Auto-Cards activity for n many turns
AutoCards().API.emergencyHalt();
Emergency stop or resume
AutoCards().API.suppressMessages();
Hides Auto-Cards toasts by preventing assignment to state.message
AutoCards().API.debugLog();
Writes to the debug log card
AutoCards().API.toggle();
Turns Auto-Cards on/off
AutoCards().API.generateCard();
Initiates AI generation of the requested card
AutoCards().API.redoCard();
Regenerates an existing card
AutoCards().API.setCardAsAuto();
Flags or unflags a card as automatic
AutoCards().API.addCardMemory();
Adds a memory to a specific card
AutoCards().API.eraseAllAutoCards();
Deletes all auto-cards
AutoCards().API.getUsedTitles();
Lists all current card titles
AutoCards().API.getBannedTitles();
Shows your current banned titles list
AutoCards().API.setBannedTitles();
Replaces the banned titles list with a new list
AutoCards().API.buildCard();
Makes a new card from scratch, using exact parameters
AutoCards().API.getCard();
Finds cards that match a filter
AutoCards().API.eraseCard();
Deletes cards matching a filter
*/
/*** Postpones internal Auto-Cards events for a specified number of turns
*
* @function
* @param {number} turns A non-negative integer representing the number of turns to postpone events
* @returns {Object} An object containing cooldown values affected by the postponement
* @throws {Error} If turns is not a non-negative integer
*/
// AutoCards().API.postponeEvents();
/*** Sets or clears the emergency halt flag to pause Auto-Cards operations
*
* @function
* @param {boolean} shouldHalt A boolean value indicating whether to engage (true) or disengage (false) emergency halt
* @returns {boolean} The value that was set
* @throws {Error} If called from within isolateLSIv2 scope or with a non-boolean argument
*/
// AutoCards().API.emergencyHalt();
/*** Enables or disables state.message assignments from Auto-Cards
*
* @function
* @param {boolean} shouldSuppress If true, suppresses all Auto-Cards messages; false enables them
* @returns {Array} The current pending messages after setting suppression
* @throws {Error} If shouldSuppress is not a boolean
*/
// AutoCards().API.suppressMessages();
/*** Logs debug information to the "Debug Log card console
*
* @function
* @param {...any} args Arguments to log for debugging purposes
* @returns {any} The story card object reference
*/
// AutoCards().API.debugLog();
/*** Toggles Auto-Cards behavior or sets it directly
*
* @function
* @param {boolean|null|undefined} toggleType If undefined, toggles the current state. If boolean or null, sets the state accordingly
* @returns {boolean|null|undefined} The state that was set or inferred
* @throws {Error} If toggleType is not a boolean, null, or undefined
*/
// AutoCards().API.toggle();
/*** Generates a new card using optional prompt details or a card request object
*
* This function supports two usage modes:
*
* 1. Object Mode:
* Pass a single object containing card request parameters. The only mandatory property is "title"
* All other properties are optional and customize the card generation
*
* Example:
* AutoCards().API.generateCard({
* type: "character", // The category or type of the card; defaults to "class" if omitted
* title: "Leah the Lewd", // The card's title (required)
* keysStart: "Lewd,Leah", // Optional trigger keywords associated with the card
* entryStart: "You are a woman named Leah.", // Existing content to prepend to the AI-generated entry
* entryPrompt: "", // Global prompt guiding AI content generation
* entryPromptDetails: "Focus on Leah's works of artifice and ingenuity", // Additional prompt info
* entryLimit: 750, // Target character length for the AI-generated entry
* description: "Player character!", // Freeform notes
* memoryStart: "Leah purchased a new sweater.", // Existing memory content
* memoryUpdates: true, // Whether the card's memory bank will update on its own
* memoryLimit: 2750 // Preferred memory bank size before summarization/compression
* });
*
* 2. String Mode:
* Pass a string as the title and optionally two additional strings to specify prompt details
* This mode is shorthand for quick card generation without an explicit card request object
*
* Examples:
* AutoCards().API.generateCard("Leah the Lewd");
* AutoCards().API.generateCard("Leah the Lewd", "Focus on Leah's works of artifice and ingenuity");
* AutoCards().API.generateCard(
* "Leah the Lewd",
* "Focus on Leah's works of artifice and ingenuity",
* "You are a woman named Leah."
* );
*
* @function
* @param {Object|string} request Either a fully specified card request object or a string title
* @param {string} [extra1] Optional detailed prompt text when using string mode
* @param {string} [extra2] Optional entry start text when using string mode
* @returns {boolean} Returns true if the generation attempt succeeded, false otherwise
* @throws {Error} Throws if called with invalid arguments or missing a required title property
*/
// AutoCards().API.generateCard();
/*** Regenerates a card by title or object reference, optionally preserving or modifying its input info
*
* @function
* @param {Object|string} request Either a fully specified card request object or a string title for the card to be regenerated
* @param {boolean} [useOldInfo=true] If true, preserves old info in the new generation; false omits it
* @param {string} [newInfo=""] Additional info to append to the generation prompt
* @returns {boolean} True if regeneration succeeded; false otherwise
* @throws {Error} If the request format is invalid, or if the second or third parameters are the wrong types
*/
// AutoCards().API.redoCard();
/*** Flags or unflags a card as an auto-card, controlling its automatic generation behavior
*
* @function
* @param {Object|string} targetCard The card object or title to mark/unmark as an auto-card
* @param {boolean} [setOrUnset=true] If true, marks the card as an auto-card; false removes the flag
* @returns {boolean} True if the operation succeeded; false if the card was invalid or already matched the target state
* @throws {Error} If the arguments are invalid types
*/
// AutoCards().API.setCardAsAuto();
/*** Appends a memory to a story card's memory bank
*
* @function
* @param {Object|string} targetCard A card object reference or title string
* @param {string} newMemory The memory text to add
* @returns {boolean} True if the memory was added; false if it was empty, already present, or the card was not found
* @throws {Error} If the inputs are not a string or valid card object reference
*/
// AutoCards().API.addCardMemory();
/*** Removes all previously generated auto-cards and resets various states
*
* @function
* @returns {number} The number of cards that were removed
*/
// AutoCards().API.eraseAllAutoCards();
/*** Retrieves an array of titles currently used by the adventure's story cards
*
* @function
* @returns {Array<string>} An array of strings representing used titles
*/
// AutoCards().API.getUsedTitles();
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} An array of banned title strings
*/
// AutoCards().API.getBannedTitles();
/*** Sets the banned titles array, replacing any previously banned titles
*
* @function
* @param {string|Array<string>} titles A comma-separated string or array of strings representing titles to ban
* @returns {Object} An object containing oldBans and newBans arrays
* @throws {Error} If the input is neither a string nor an array of strings
*/
// AutoCards().API.setBannedTitles();
/*** Creates a new story card with the specified parameters
*
* @function
* @param {string|Object} title Card title string or full card template object containing all fields
* @param {string} [entry] The entry text for the card
* @param {string} [type] The card type (e.g., "character", "location")
* @param {string} [keys] The keys (triggers) for the card
* @param {string} [description] The notes or memory bank of the card
* @param {number} [insertionIndex] Optional index to insert the card at a specific position within storyCards
* @returns {Object|null} The created card object reference, or null if creation failed
*/
// AutoCards().API.buildCard();
/*** Finds and returns story cards satisfying a user-defined condition
* Example:
* const leahCard = AutoCards().API.getCard(card => (card.title === "Leah"));
*
* @function
* @param {Function} predicate A function which takes a card and returns true if it matches
* @param {boolean} [getAll=false] If true, returns all matching cards; otherwise returns the first match
* @returns {Object|Array<Object>|null} A single card object reference, an array of cards, or null if no match is found
* @throws {Error} If the predicate is not a function or getAll is not a boolean
*/
// AutoCards().API.getCard();
/*** Removes story cards based on a user-defined condition or by direct reference
* Example:
* AutoCards().API.eraseCard(card => (card.title === "Leah"));
*
* @function
* @param {Function|Object} predicate A predicate function or a card object reference
* @param {boolean} [eraseAll=false] If true, removes all matching cards; otherwise removes the first match
* @returns {boolean|number} True if a single card was removed, false if none matched, or the number of cards erased
* @throws {Error} If the inputs are not a valid predicate function, card object, or boolean
*/
// AutoCards().API.eraseCard();
//—————————————————————————————————————————————————————————————————————————————————
/*
To everyone who helped, thank you:
AHotHamster22
Most extensive testing, feedback, ideation, and kindness
BinKompliziert
UI feedback
Boo
Discord communication
bottledfox
API ideas for alternative card generation use-cases
Bruno
Most extensive testing, feedback, ideation, and kindness
https://play.aidungeon.com/profile/Azuhre
Burnout
Implementation improvements, algorithm ideas, script help, and LSIv2 inspiration
bweni
Testing
DebaczX
Most extensive testing, feedback, ideation, and kindness
Dirty Kurtis
Card entry generation prompt engineering
Dragranis
Provided the memory dataset used for boundary calibration
effortlyss
Data, testing, in-game command ideas, config settings, and other UX improvements
Hawk
Grammar and special-cased proper nouns
Idle Confusion
Testing
https://play.aidungeon.com/profile/Idle%20Confusion
ImprezA
Most extensive testing, feedback, ideation, and kindness
https://play.aidungeon.com/profile/ImprezA
Kat-Oli
Title parsing, grammar, and special-cased proper nouns
KryptykAngel
LSIv2 ideas
https://play.aidungeon.com/profile/KryptykAngel
Mad19pumpkin
API ideas
https://play.aidungeon.com/profile/Mad19pumpkin
Magic
Implementation and syntax improvements
https://play.aidungeon.com/profile/MagicOfLolis
Mirox80
Testing, feedback, and scenario integration ideas
https://play.aidungeon.com/profile/Mirox80
Nathaniel Wyvern
Testing
https://play.aidungeon.com/profile/NathanielWyvern
NobodyIsUgly
All-caps title parsing feedback
OnyxFlame
Card memory bank implementation ideas and special-cased proper nouns
Purplejump
API ideas for deep integration with other AID scripts
Randy Viosca
Context injection and card memory bank structure
https://play.aidungeon.com/profile/Random_Variable
RustyPawz
API ideas for simplified card interaction
https://play.aidungeon.com/profile/RustyPawz
sinner
Testing
Sleepy pink
Testing and feedback
https://play.aidungeon.com/profile/Pinkghost
Vutinberg
Memory compression ideas and prompt engineering
Wilmar
Card entry generation and memory summarization prompt engineering
Yi1i1i
Idea for the redoCard API function and "/ac redo" in-game command
A note to future individuals:
If you fork or modify Auto-Cards... Go ahead and put your name here too! Yay! 🥰
*/
//—————————————————————————————————————————————————————————————————————————————————
/*
The code below implements Auto-Cards
Enjoy! ❤️
*/
// My class definitions are hoisted by wrapper functions because it's less ugly (lol)
const Const = hoistConst();
const O = hoistO();
const Words = hoistWords();
const StringsHashed = hoistStringsHashed();
const Internal = hoistInternal();
// AutoCards has an explicitly immutable domain: HOOK, TEXT, and STOP
const HOOK = inHook;
const TEXT = ((typeof inText === "string") && inText) || "\n";
const STOP = (inStop === true);
// AutoCards returns a pseudoimmutable codomain which is initialized only once before being read and returned
const CODOMAIN = new Const().declare();
// Transient sets for high-performance lookup
const [used, bans, auto, forenames, surnames] = Array.from({length: 5}, () => new Set());
// Holds a reference to the data card singleton, remains unassigned unless required
let data = null;
// Validate globalThis.text
text = ((typeof text === "string") && text) || "\n";
// Container for the persistent state of AutoCards
const AC = (function() {
if (state.LSIv2) {
// The Auto-Cards external API is also available from within the inner scope of LSIv2
// Call with AutoCards().API.nameOfFunction(yourArguments);
return state.LSIv2;
} else if (state.AutoCards) {
// state.AutoCards is prioritized for performance
const ac = state.AutoCards;
delete state.AutoCards;
return ac;
}
const dataVariants = getDataVariants();
data = getSingletonCard(false, O.f({...dataVariants.critical}), O.f({...dataVariants.debug}));
// Deserialize the state of Auto-Cards from the data card
const ac = (function() {
try {
return JSON.parse(data?.description);
} catch {
return null;
}
})();
// If the deserialized state fails to match the following structure, fallback to defaults
if (validate(ac, O.f({
config: [
"doAC", "deleteAllAutoCards", "pinConfigureCard", "addCardCooldown", "bulletedListMode", "defaultEntryLimit", "defaultCardsDoMemoryUpdates", "defaultMemoryLimit", "memoryCompressionRatio", "ignoreAllCapsTitles", "readFromInputs", "minimumLookBackDistance", "LSIv2", "showDebugData", "generationPrompt", "compressionPrompt", "defaultCardType"
],
signal: [
"emergencyHalt", "forceToggle", "overrideBans", "swapControlCards", "recheckRetryOrErase", "maxChars", "outputReplacement", "upstreamError"
],
generation: [
"cooldown", "completed", "permitted", "workpiece", "pending"
],
compression: [
"completed", "titleKey", "vanityTitle", "responseEstimate", "lastConstructIndex", "oldMemoryBank", "newMemoryBank"
],
message: [
"previous", "suppress", "pending", "event"
],
chronometer: [
"turn", "step", "amnesia", "postpone"
],
database: {
titles: [
"used", "banned", "candidates", "lastActionParsed", "lastTextHash", "pendingBans", "pendingUnbans"
],
memories: [
"associations", "duplicates"
]
}
}))) {
// The deserialization was a success
return ac;
}
function validate(obj, finalKeys) {
if ((typeof obj !== "object") || (obj === null)) {
return false;
} else {
return Object.entries(finalKeys).every(([key, value]) => {
if (!(key in obj)) {
return false;
} else if (Array.isArray(value)) {
return value.every(finalKey => {
return (finalKey in obj[key]);
});
} else {
return validate(obj[key], value);
}
});
}
}
// AC is malformed, reinitialize with default values
return {
// In-game configurable parameters
config: getDefaultConfig(),
// Collection of various short-term signals passed forward in time
signal: {
// API: Suspend nearly all Auto-Cards processes
emergencyHalt: false,
// API: Forcefully toggle Auto-Cards on or off
forceToggle: null,
// API: Banned titles were externally overwritten
overrideBans: 0,
// Signal the construction of the opposite control card during the upcoming onOutput hook
swapControlCards: false,
// Signal a limited recheck of recent title candidates following a retry or erase
recheckRetryOrErase: false,
// Signal an upcoming onOutput text replacement
outputReplacement: "",
// info.maxChars is only defined onContext but must be accessed during other hooks too
maxChars: Math.abs(info?.maxChars || 3200),
// An error occured within the isolateLSIv2 scope during an earlier hook
upstreamError: ""
},
// Moderates the generation of new story card entries
generation: {
// Number of story progression turns between card generations
cooldown: validateCooldown(underQuarterInteger(validateCooldown(DEFAULT_CARD_CREATION_COOLDOWN))),
// Continues prompted so far
completed: 0,
// Upper limit on consecutive continues
permitted: 34,
// Properties of the incomplete story card
workpiece: O.f({}),
// Pending card generations
pending: [],
},
// Moderates the compression of story card memories
compression: {
// Continues prompted so far
completed: 0,
// A title header reference key for this auto-card
titleKey: "",
// The full and proper title
vanityTitle: "",
// Response length estimate used to compute # of outputs remaining
responseEstimate: 1400,
// Indices [0, n] of oldMemoryBank memories used to build the current memory construct
lastConstructIndex: -1,
// Bank of card memories awaiting compression
oldMemoryBank: [],
// Incomplete bank of newly compressed card memories
newMemoryBank: [],
},
// Prevents incompatibility issues borne of state.message modification
message: {
// Last turn's state.message
previous: getStateMessage(),
// API: Allow Auto-Cards to post messages?
suppress: false,
// Pending Auto-Cards message(s)
pending: (function() {
if (DEFAULT_DO_AC !== false) {
const startupMessage = "Enabled! You may now edit the \"Configure Auto-Cards\" story card";
logEvent(startupMessage);
return [startupMessage];
} else {
return [];
}
})(),
// Counter to track all Auto-Cards message events
event: 0
},
// Timekeeper used for temporal events
chronometer: {
// Previous turn's measurement of info.actionCount
turn: getTurn(),
// Whether or not various turn counters should be stepped (falsified by retry actions)
step: true,
// Number of consecutive turn interruptions
amnesia: 0,
// API: Postpone Auto-Cards externalities for n many turns
postpone: 0,
},
// Scalable atabase to store dynamic game information
database: {
// Words are pale shadows of forgotten names. As names have power, words have power
titles: {
// A transient array of known titles parsed from card titles, entry title headers, and trigger keywords
used: [],
// Titles banned from future card generation attempts and various maintenance procedures
banned: getDefaultConfigBans(),
// Potential future card titles and their turns of occurrence
candidates: [],
// Helps avoid rechecking the same action text more than once, generally
lastActionParsed: -1,
// Ensures weird combinations of retry/erase events remain predictable
lastTextHash: "%@%",
// Newly banned titles which will be added to the config card
pendingBans: [],
// Currently banned titles which will be removed from the config card
pendingUnbans: []
},
// Memories are parsed from context and handled by various operations (basically magic)
memories: {
// Dynamic store of 'story card -> memory' conceptual relations
associations: {},
// Serialized hashset of the 2000 most recent near-duplicate memories purged from context
duplicates: "%@%"
}
}
};
})();
O.f(AC);
O.s(AC.config);
O.s(AC.signal);
O.s(AC.generation);
O.s(AC.generation.workpiece);
AC.generation.pending.forEach(request => O.s(request));
O.s(AC.compression);
O.s(AC.message);
O.s(AC.chronometer);
O.f(AC.database);
O.s(AC.database.titles);
O.s(AC.database.memories);
if (!HOOK) {
globalThis.stop ??= false;
AC.signal.maxChars = Math.abs(info?.maxChars || AC.signal.maxChars);
if (HOOK === null) {
if (/Recent\s*Story\s*:/i.test(text)) {
// AutoCards(null) is always invoked once after being declared within the shared library
// Context must be cleaned before passing text to the context modifier
// This measure is taken to ensure compatability with other scripts
// First, remove all command, continue, and comfirmation messages from the context window
text = (text
// Hide the guide
.replace(/\s*>>>\s*Detailed\s*Guide\s*:[\s\S]*?<<<\s*/gi, "\n\n")
// Excise all /AC command messages
.replace(/\s*>>>\s*Auto-Cards\s*has\s*been\s*enabled!\s*<<<\s*/gi, " ")
.replace(/^.*\/\s*A\s*C.*$/gmi, "%@%")
.replace(/\s*%@%\s*/g, " ")
// Consolidate all consecutive continue messages into placeholder substrings
.replace(/(?:(?:\s*>>>\s*please\s*select\s*"continue"\s*\([\s\S]*?\)\s*<<<\s*)+)/gi, message => {
// Replace all continue messages with %@+%-patterned substrings
return (
// The # of "@" symbols corresponds with the # of consecutive continue messages
"%" + "@".repeat(
// Count the number of consecutive continue message occurrences
(message.match(/>>>\s*please\s*select\s*"continue"\s*\([\s\S]*?\)\s*<<</gi) || []).length
) + "%"
);
})
// Situationally replace all placeholder substrings with either spaces or double newlines
.replace(/%@+%/g, (match, matchIndex, intermediateText) => {
// Check the case of the next char following the match to decide how to replace it
let i = matchIndex + match.length;
let nextChar = intermediateText[i];
if (nextChar === undefined) {
return " ";
} else if (/^[A-Z]$/.test(nextChar)) {
// Probably denotes a new sentence/paragraph
return "\n\n";
} else if (/^[a-z]$/.test(nextChar)) {
return " ";
}
// The first nextChar was a weird punctuation char, find the next non-whitespace char
do {
i++;
nextChar = intermediateText[i];
if (nextChar === undefined) {
return " ";
}
} while (/\s/.test(nextChar));
if (nextChar === nextChar.toUpperCase()) {
// Probably denotes a new sentence/paragraph
return "\n\n";
}
// Returning " " probably indicates a previous output's incompleteness
return " ";
})
// Remove all comfirmation requests and responses
.replace(/\s*\n*.*CONFIRM\s*DELETE.*\n*\s*/gi, confirmation => {
if (confirmation.includes("<<<")) {
return " ";
} else {
return "";
}
})
// Remove dumb memories from the context window
// (Latitude, if you're reading this, please give us memoryBank read/write access 😭)
.replace(/(Memories\s*:)\s*([\s\S]*?)\s*(Recent\s*Story\s*:|$)/i, (_, left, memories, right) => {
return (left + "\n" + (memories
.split("\n")
.filter(memory => {
const lowerMemory = memory.toLowerCase();
return !(
(lowerMemory.includes("select") && lowerMemory.includes("continue"))
|| lowerMemory.includes(">>>") || lowerMemory.includes("<<<")
|| lowerMemory.includes("lsiv2")
);
})
.join("\n")
) + (function() {
if (right !== "") {
return "\n\n" + right;
} else {
return "";
}
})());
})
// Remove LSIv2 error messages
.replace(/(?:\s*>>>[\s\S]*?<<<\s*)+/g, " ")
);
if (!shouldProceed()) {
// Whenever Auto-Cards is inactive, remove auto card title headers from contextualized story card entries
text = (text
.replace(/\s*{\s*titles?\s*:[\s\S]*?}\s*/gi, "\n\n")
.replace(/World\s*Lore\s*:\s*/i, "World Lore:\n")
);
// Otherwise, implement a more complex version of this step within the (HOOK === "context") scope of AutoCards
}
}
CODOMAIN.initialize(null);
} else {
// AutoCards was (probably) called without arguments, return an external API to allow other script creators to programmatically govern the behavior of Auto-Cards from elsewhere within their own scripts
CODOMAIN.initialize({API: O.f(Object.fromEntries(Object.entries({
// Call these API functions like so: AutoCards().API.nameOfFunction(argumentsOfFunction)
/*** Postpones internal Auto-Cards events for a specified number of turns
*
* @function
* @param {number} turns A non-negative integer representing the number of turns to postpone events
* @returns {Object} An object containing cooldown values affected by the postponement
* @throws {Error} If turns is not a non-negative integer
*/
postponeEvents: function(turns) {
if (Number.isInteger(turns) && (0 <= turns)) {
AC.chronometer.postpone = turns;
} else {
throw new Error(
"Invalid argument: \"" + turns + "\" -> AutoCards().API.postponeEvents() must be be called with a non-negative integer"
);
}
return {
postponeAllCooldown: turns,
addCardRealCooldown: AC.generation.cooldown,
addCardNextCooldown: AC.config.addCardCooldown
};
},
/*** Sets or clears the emergency halt flag to pause Auto-Cards operations
*
* @function
* @param {boolean} shouldHalt A boolean value indicating whether to engage (true) or disengage (false) emergency halt
* @returns {boolean} The value that was set
* @throws {Error} If called from within isolateLSIv2 scope or with a non-boolean argument
*/
emergencyHalt: function(shouldHalt) {
const scopeRestriction = new Error();
if (scopeRestriction.stack && scopeRestriction.stack.includes("isolateLSIv2")) {
throw new Error(
"Scope restriction: AutoCards().API.emergencyHalt() cannot be called from within LSIv2 (prevents deadlock) but you're more than welcome to use AutoCards().API.postponeEvents() instead!"
);
} else if (typeof shouldHalt === "boolean") {
AC.signal.emergencyHalt = shouldHalt;
} else {
throw new Error(
"Invalid argument: \"" + shouldHalt + "\" -> AutoCards().API.emergencyHalt() must be called with a boolean true or false"
);
}
return shouldHalt;
},
/*** Enables or disables state.message assignments from Auto-Cards
*
* @function
* @param {boolean} shouldSuppress If true, suppresses all Auto-Cards messages; false enables them
* @returns {Array} The current pending messages after setting suppression
* @throws {Error} If shouldSuppress is not a boolean
*/
suppressMessages: function(shouldSuppress) {
if (typeof shouldSuppress === "boolean") {
AC.message.suppress = shouldSuppress;
} else {
throw new Error(
"Invalid argument: \"" + shouldSuppress + "\" -> AutoCards().API.suppressMessages() must be called with a boolean true or false"
);
}
return AC.message.pending;
},
/*** Logs debug information to the "Debug Log" console card
*
* @function
* @param {...any} args Arguments to log for debugging purposes
* @returns {any} The story card object reference
*/
debugLog: function(...args) {
return Internal.debugLog(...args);
},
/*** Toggles Auto-Cards behavior or sets it directly
*
* @function
* @param {boolean|null|undefined} toggleType If undefined, toggles the current state. If boolean or null, sets the state accordingly
* @returns {boolean|null|undefined} The state that was set or inferred
* @throws {Error} If toggleType is not a boolean, null, or undefined
*/
toggle: function(toggleType) {
if (toggleType === undefined) {
if (AC.signal.forceToggle !== null) {
AC.signal.forceToggle = !AC.signal.forceToggle;
} else if (AC.config.doAC) {
AC.signal.forceToggle = false;
} else {
AC.signal.forceToggle = true;
}
} else if ((toggleType === null) || (typeof toggleType === "boolean")) {
AC.signal.forceToggle = toggleType;
} else {
throw new Error(
"Invalid argument: \"" + toggleType + "\" -> AutoCards().API.toggle() must be called with either A) a boolean true or false, B) a null argument, or C) no arguments at all (undefined)"
);
}
return toggleType;
},
/*** Generates a new card using optional prompt details or a request object
*
* @function
* @param {Object|string} request A request object with card parameters or a string representing the title
* @param {string} [extra1] Optional entryPromptDetails if using string mode
* @param {string} [extra2] Optional entryStart if using string mode
* @returns {boolean} Did the generation attempt succeed or fail
* @throws {Error} If the request is not valid or missing a title
*/
generateCard: function(request, extra1, extra2) {
// Function call guide:
// AutoCards().API.generateCard({
// // All properties except 'title' are optional
// type: "card type, defaults to 'class' for ease of filtering",
// title: "card title",
// keysStart: "preexisting card triggers",
// entryStart: "preexisting card entry",
// entryPrompt: "prompt the AI will use to complete this entry",
// entryPromptDetails: "extra details to include with this card's prompt",
// entryLimit: 750, // target character count for the generated entry
// description: "card notes",
// memoryStart: "preexisting card memory",
// memoryUpdates: true, // card updates when new relevant memories are formed
// memoryLimit: 2750, // max characters before the card memory is compressed
// });
if (typeof request === "string") {
request = {title: request};
if (typeof extra1 === "string") {
request.entryPromptDetails = extra1;
if (typeof extra2 === "string") {
request.entryStart = extra2;
}
}
} else if (!isTitleInObj(request)) {
throw new Error(
"Invalid argument: \"" + request + "\" -> AutoCards().API.generateCard() must be called with either 1, 2, or 3 strings OR a correctly formatted card generation object"
);
}
O.f(request);
Internal.getUsedTitles(true);
return Internal.generateCard(request);
},
/*** Regenerates a card by title or object reference, optionally preserving or modifying its input info
*
* @function
* @param {Object|string} request A card object reference or title string for the card to be regenerated
* @param {boolean} [useOldInfo=true] If true, preserves old info in the new generation; false omits it
* @param {string} [newInfo=""] Additional info to append to the generation prompt
* @returns {boolean} True if regeneration succeeded; false otherwise
* @throws {Error} If the request format is invalid, or if the second or third parameters are the wrong types
*/
redoCard: function(request, useOldInfo = true, newInfo = "") {
if (typeof request === "string") {
request = {title: request};
} else if (!isTitleInObj(request)) {
throw new Error(
"Invalid argument: \"" + request + "\" -> AutoCards().API.redoCard() must be called with a string or correctly formatted card generation object"
);
}
if (typeof useOldInfo !== "boolean") {
throw new Error(
"Invalid argument: \"" + request + ", " + useOldInfo + "\" -> AutoCards().API.redoCard() requires a boolean as its second argument"
);
} else if (typeof newInfo !== "string") {
throw new Error(
"Invalid argument: \"" + request + ", " + useOldInfo + ", " + newInfo + "\" -> AutoCards().API.redoCard() requires a string for its third argument"
);
}
return Internal.redoCard(request, useOldInfo, newInfo);
},
/*** Flags or unflags a card as an auto-card, controlling its automatic generation behavior
*
* @function
* @param {Object|string} targetCard The card object or title to mark/unmark as an auto-card
* @param {boolean} [setOrUnset=true] If true, marks the card as an auto-card; false removes the flag
* @returns {boolean} True if the operation succeeded; false if the card was invalid or already matched the target state
* @throws {Error} If the arguments are invalid types
*/
setCardAsAuto: function(targetCard, setOrUnset = true) {
if (isTitleInObj(targetCard)) {
targetCard = targetCard.title;
} else if (typeof targetCard !== "string") {
throw new Error(
"Invalid argument: \"" + targetCard + "\" -> AutoCards().API.setCardAsAuto() must be called with a string or card object"
);
}
if (typeof setOrUnset !== "boolean") {
throw new Error(
"Invalid argument: \"" + targetCard + ", " + setOrUnset + "\" -> AutoCards().API.setCardAsAuto() requires a boolean as its second argument"
);
}
const [card, isAuto] = getIntendedCard(targetCard);
if (card === null) {
return false;
}
if (setOrUnset) {
if (checkAuto()) {
return false;
}
card.description = "{title:}";
Internal.getUsedTitles(true);
return card.entry.startsWith("{title: ");
} else if (!checkAuto()) {
return false;
}
card.entry = removeAutoProps(card.entry);
card.description = removeAutoProps(card.description.replace((
/\s*Auto(?:-|\s*)Cards\s*will\s*contextualize\s*these\s*memories\s*:\s*/gi
), ""));
function checkAuto() {
return (isAuto || /{updates: (?:true|false), limit: \d+}/.test(card.description));
}
return true;
},
/*** Appends a memory to a story card's memory bank
*
* @function
* @param {Object|string} targetCard A card object reference or title string
* @param {string} newMemory The memory text to add
* @returns {boolean} True if the memory was added; false if it was empty, already present, or the card was not found
* @throws {Error} If the inputs are not a string or valid card object reference
*/
addCardMemory: function(targetCard, newMemory) {
if (isTitleInObj(targetCard)) {
targetCard = targetCard.title;
} else if (typeof targetCard !== "string") {
throw new Error(
"Invalid argument: \"" + targetCard + "\" -> AutoCards().API.addCardMemory() must be called with a string or card object"
);
}
if (typeof newMemory !== "string") {
throw new Error(
"Invalid argument: \"" + targetCard + ", " + newMemory + "\" -> AutoCards().API.addCardMemory() requires a string for its second argument"
);
}
newMemory = newMemory.trim().replace(/\s+/g, " ").replace(/^-+\s*/, "");
if (newMemory === "") {
return false;
}
const [card, isAuto, titleKey] = getIntendedCard(targetCard);
if (
(card === null)
|| card.description.replace(/\s+/g, " ").toLowerCase().includes(newMemory.toLowerCase())
) {
return false;
} else if (card.description !== "") {
card.description += "\n";
}
card.description += "- " + newMemory;
if (titleKey in AC.database.memories.associations) {
AC.database.memories.associations[titleKey][1] = (StringsHashed
.deserialize(AC.database.memories.associations[titleKey][1], 65536)
.remove(newMemory)
.add(newMemory)
.latest(3500)
.serialize()
);
} else if (isAuto) {
AC.database.memories.associations[titleKey] = [999, (new StringsHashed(65536)
.add(newMemory)
.serialize()
)];
}
return true;
},
/*** Removes all previously generated auto-cards and resets various states
*
* @function
* @returns {number} The number of cards that were removed
*/
eraseAllAutoCards: function() {
return Internal.eraseAllAutoCards();
},
/*** Retrieves an array of titles currently used by the adventure's story cards
*
* @function
* @returns {Array<string>} An array of strings representing used titles
*/
getUsedTitles: function() {
return Internal.getUsedTitles(true);
},
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} An array of banned title strings
*/
getBannedTitles: function() {
return Internal.getBannedTitles();
},
/*** Sets the banned titles array, replacing any previously banned titles
*
* @function
* @param {string|Array<string>} titles A comma-separated string or array of strings representing titles to ban
* @returns {Object} An object containing oldBans and newBans arrays
* @throws {Error} If the input is neither a string nor an array of strings
*/
setBannedTitles: function(titles) {
const codomain = {oldBans: AC.database.titles.banned};
if (Array.isArray(titles) && titles.every(title => (typeof title === "string"))) {
assignBannedTitles(titles);
} else if (typeof titles === "string") {
if (titles.includes(",")) {
assignBannedTitles(titles.split(","));
} else {
assignBannedTitles([titles]);
}
} else {
throw new Error(
"Invalid argument: \"" + titles + "\" -> AutoCards().API.setBannedTitles() must be called with either a string or an array of strings"
);
}
codomain.newBans = AC.database.titles.banned;
function assignBannedTitles(titles) {
Internal.setBannedTitles(uniqueTitlesArray(titles), false);
AC.signal.overrideBans = 3;
return;
}
return codomain;
},
/*** Creates a new story card with the specified parameters
*
* @function
* @param {string|Object} title Card title string or full card template object containing all fields
* @param {string} [entry] The entry text for the card
* @param {string} [type] The card type (e.g., "character", "location")
* @param {string} [keys] The keys (triggers) for the card
* @param {string} [description] The notes or memory bank of the card
* @param {number} [insertionIndex] Optional index to insert the card at a specific position within storyCards
* @returns {Object|null} The created card object reference, or null if creation failed
*/
buildCard: function(title, entry, type, keys, description, insertionIndex) {
if (isTitleInObj(title)) {
type = title.type ?? type;
keys = title.keys ?? keys;
entry = title.entry ?? entry;
description = title.description ?? description;
title = title.title;
}
title = cast(title);
const card = constructCard(O.f({
type: cast(type, AC.config.defaultCardType),
title,
keys: cast(keys, buildKeys("", title)),
entry: cast(entry),
description: cast(description)
}), boundInteger(0, insertionIndex, storyCards.length, newCardIndex()));
if (notEmptyObj(card)) {
return card;
}
function cast(value, fallback = "") {
if (typeof value === "string") {
return value;
} else {
return fallback;
}
}
return null;
},
/*** Finds and returns story cards satisfying a user-defined condition
*
* @function
* @param {Function} predicate A function which takes a card and returns true if it matches
* @param {boolean} [getAll=false] If true, returns all matching cards; otherwise returns the first match
* @returns {Object|Array<Object>|null} A single card object reference, an array of cards, or null if no match is found
* @throws {Error} If the predicate is not a function or getAll is not a boolean
*/
getCard: function(predicate, getAll = false) {
if (typeof predicate !== "function") {
throw new Error(
"Invalid argument: \"" + predicate + "\" -> AutoCards().API.getCard() must be called with a function"
);
} else if (typeof getAll !== "boolean") {
throw new Error(
"Invalid argument: \"" + predicate + ", " + getAll + "\" -> AutoCards().API.getCard() requires a boolean as its second argument"
);
}
return Internal.getCard(predicate, getAll);
},
/*** Removes story cards based on a user-defined condition or by direct reference
*
* @function
* @param {Function|Object} predicate A predicate function or a card object reference
* @param {boolean} [eraseAll=false] If true, removes all matching cards; otherwise removes the first match
* @returns {boolean|number} True if a single card was removed, false if none matched, or the number of cards erased
* @throws {Error} If the inputs are not a valid predicate function, card object, or boolean
*/
eraseCard: function(predicate, eraseAll = false) {
if (isTitleInObj(predicate) && storyCards.includes(predicate)) {
return eraseCard(predicate);
} else if (typeof predicate !== "function") {
throw new Error(
"Invalid argument: \"" + predicate + "\" -> AutoCards().API.eraseCard() must be called with a function or card object"
);
} else if (typeof eraseAll !== "boolean") {
throw new Error(
"Invalid argument: \"" + predicate + ", " + eraseAll + "\" -> AutoCards().API.eraseCard() requires a boolean as its second argument"
);
} else if (eraseAll) {
// Erase all cards which satisfy the given condition
let cardsErased = 0;
for (const [index, card] of storyCards.entries()) {
if (predicate(card)) {
removeStoryCard(index);
cardsErased++;
}
}
return cardsErased;
}
// Erase the first card which satisfies the given condition
for (const [index, card] of storyCards.entries()) {
if (predicate(card)) {
removeStoryCard(index);
return true;
}
}
return false;
}
}).map(([key, fn]) => [key, function(...args) {
const result = fn.apply(this, args);
if (data) {
data.description = JSON.stringify(AC);
}
return result;
}])))});
function isTitleInObj(obj) {
return (
(typeof obj === "object")
&& (obj !== null)
&& ("title" in obj)
&& (typeof obj.title === "string")
);
}
}
} else if (AC.signal.emergencyHalt) {
switch(HOOK) {
case "context": {
// AutoCards was called within the context modifier
advanceChronometer();
break; }
case "output": {
// AutoCards was called within the output modifier
concludeEmergency();
const previousAction = readPastAction(0);
if (isDoSayStory(previousAction.type) && /escape\s*emergency\s*halt/i.test(previousAction.text)) {
AC.signal.emergencyHalt = false;
}
break; }
}
CODOMAIN.initialize(TEXT);
} else if ((AC.config.LSIv2 !== null) && AC.config.LSIv2) {
// Silly recursion shenanigans
state.LSIv2 = AC;
AC.config.LSIv2 = false;
const LSI_DOMAIN = AutoCards(HOOK, TEXT, STOP);
// Is this lazy loading mechanism overkill? Yes. But it's fun!
const factories = O.f({
library: () => ({
name: Words.reserved.library,
entry: prose(
"// Your adventure's Shared Library code goes here",
"// Example Library code:",
"state.promptDragon ??= false;",
"state.mind ??= 0;",
"state.willStop ??= false;",
"function formatMessage(message, space = \" \") {",
" let leadingNewlines = \"\";",
" let trailingNewlines = \"\\n\\n\";",
" if (text.startsWith(\"\\n> \")) {",
" // We don't want any leading/trailing newlines for Do/Say",
" trailingNewlines = \"\";",
" } else if (history && (0 < history.length)) {",
" // Decide leading newlines based on the previous action",
" const action = history[history.length - 1];",
" if ((action.type === \"continue\") || (action.type === \"story\")) {",
" if (!action.text.endsWith(\"\\n\")) {",
" leadingNewlines = \"\\n\\n\";",
" } else if (!action.text.endsWith(\"\\n\\n\")) {",
" leadingNewlines = \"\\n\";",
" }",
" }",
" }",
" return leadingNewlines + \"{>\" + space + (message",
" .replace(/(?:\\s*(?:{>|<})\\s*)+/g, \" \")",
" .trim()",
" ) + space + \"<}\" + trailingNewlines;",
"}"),
description:
"// You may also continue your Library code below",
singleton: false,
position: 2
}),
input: () => ({
name: Words.reserved.input,
entry: prose(
"// Your adventure's Input Modifier code goes here",
"// Example Input code:",
"const minds = [",
"\"kind and gentle\",",
"\"curious and eager\",",
"\"cruel and evil\"",
"];",
"// Type any of these triggers into a Do/Say/Story action",
"const commands = new Map([",
"[\"encounter dragon\", () => {",
" AutoCards().API.postponeEvents(1);",
" state.promptDragon = true;",
" text = formatMessage(\"You encounter a dragon!\");",
" log(\"A dragon appears!\");",
"}],",
"[\"summon leah\", () => {",
" alterMind();",
" const success = AutoCards().API.generateCard({",
" title: \"Leah\",",
" entryPromptDetails: (",
" \"Leah is an exceptionally \" +",
" minds[state.mind] +",
" \" woman\"",
" ),",
" entryStart: \"Leah is your magically summoned assistant.\"",
" });",
" if (success) {",
" text = formatMessage(\"You begin summoning Leah!\");",
" log(\"Attempting to summon Leah\");",
" } else {",
" text = formatMessage(\"You failed to summon Leah...\");",
" log(\"Leah could not be summoned\");",
" }",
"}],",
"[\"alter leah\", () => {",
" alterMind();",
" const success = AutoCards().API.redoCard(\"Leah\", true, (",
" \"You subjected Leah to mind-altering magic\\n\" +",
" \"Therefore she is now entirely \" +",
" minds[state.mind] +",
" \", utterly captivated by your will\"",
" ));",
" if (success) {",
" text = formatMessage(",
" \"You proceed to alter Leah's mind!\"",
" );",
" log(\"Attempting to alter Leah\");",
" } else {",
" text = formatMessage(\"You failed to alter Leah...\");",
" log(\"Leah could not be altered\");",
" }",
"}],",
"[\"show api\", () => {",
" state.showAPI = true;",
" text = formatMessage(\"Displaying the Auto-Cards API below\");",
"}],",
"[\"force stop\", () => {",
" state.willStop = true;",
"}]",
"]);",
"const lowerText = text.toLowerCase();",
"for (const [trigger, implement] of commands) {",
" if (lowerText.includes(trigger)) {",
" implement();",
" break;",
" }",
"}",
"function alterMind() {",
" state.mind = (state.mind + 1) % minds.length;",
" return;",
"}"),
description:
"// You may also continue your Input code below",
singleton: false,
position: 3
}),
context: () => ({
name: Words.reserved.context,
entry: prose(
"// Your adventure's Context Modifier code goes here",
"// Example Context code:",
"text = text.replace(/\\s*{>[\\s\\S]*?<}\\s*/gi, \"\\n\\n\");",
"if (state.willStop) {",
" state.willStop = false;",
" // Assign true to prevent the onOutput hook",
" // This can only be done onContext",
" stop = true;",
"} else if (state.promptDragon) {",
" state.promptDragon = false;",
" text = (",
" text.trimEnd() +",
" \"\\n\\nA cute little dragon softly lands upon your head. \"",
" );",
"}"),
description:
"// You may also continue your Context code below",
singleton: false,
position: 4
}),
output: () => ({
name: Words.reserved.output,
entry: prose(
"// Your adventure's Output Modifier code goes here",
"// Example Output code:",
"if (state.showAPI) {",
" state.showAPI = false;",
" const apiKeys = (Object.keys(AutoCards().API)",
" .map(key => (\"AutoCards().API.\" + key + \"()\"))",
" );",
" text = formatMessage(apiKeys.join(\"\\n\"), \"\\n\");",
" log(apiKeys);",
"}"),
description:
"// You may also continue your Output code below",
singleton: false,
position: 5
}),
guide: () => ({
name: Words.reserved.guide,
entry: prose(
"Any valid JavaScript code you write within the Shared Library or Input/Context/Output Modifier story cards will be executed from top to bottom; Live Script Interface v2 closely emulates AI Dungeon's native scripting environment, even if you aren't the owner of the original scenario. Furthermore, I've provided full access to the Auto-Cards scripting API. Please note that disabling LSIv2 via the \"Configure Auto-Cards\" story card will reset your LSIv2 adventure scripts!",
"",
"If you aren't familiar with scripting in AI Dungeon, please refer to the official guidebook page:",
"https://help.aidungeon.com/scripting",
"",
"I've included an example script with the four aforementioned code cards, to help showcase some of my fancy schmancy Auto-Cards API functions. Take a look, try some of my example commands, inspect the Console Log, and so on... It's a ton of fun! ❤️",
"",
"If you ever run out of space in your Library, Input, Context, or Output code cards, simply duplicate whichever one(s) you need and then perform an in-game turn before writing any more code. (emphasis on \"before\") Doing so will signal LSIv2 to convert your duplicated code card(s) into additional auxiliary versions.",
"",
"Auxiliary code cards are numbered, and any code written within will be appended in sequential order. For example:",
"// Shared Library (entry)",
"// Shared Library (notes)",
"// Shared Library 2 (entry)",
"// Shared Library 2 (notes)",
"// Shared Library 3 (entry)",
"// Shared Library 3 (notes)",
"// Input Modifier (entry)",
"// Input Modifier (notes)",
"// Input Modifier 2 (entry)",
"// Input Modifier 2 (notes)",
"// And so on..."),
description:
"",
singleton: true,
position: 0
}),
state: () => ({
name: Words.reserved.state,
entry:
"Your adventure's full state object is displayed in the Notes section below.",
description:
"",
singleton: true,
position: 6
}),
log: () => ({
name: Words.reserved.log,
entry:
"Please refer to the Notes section below to view the full log history for LSIv2. Console log entries are ordered from most recent to oldest. LSIv2 error messages will be recorded here, alongside the outputs of log and console.log function calls within your adventure scripts.",
description:
"",
singleton: true,
position: 1
})
});
const cache = {};
const templates = new Proxy({}, {
get(_, key) {
return cache[key] ??= O.f(factories[key]());
}
});
if (AC.config.LSIv2 !== null) {
switch(HOOK) {
case "input": {
// AutoCards was called within the input modifier
const [libraryCards, inputCards, logCard] = collectCards(
templates.library,
templates.input,
templates.log
);
const [error, newText] = isolateLSIv2(parseCode(libraryCards, inputCards), callbackLog(logCard), LSI_DOMAIN);
handleError(logCard, error);
if (hadError()) {
CODOMAIN.initialize(getStoryError());
AC.signal.upstreamError = "\n";
} else {
CODOMAIN.initialize(newText);
}
break; }
case "context": {
// AutoCards was called within the context modifier
const [libraryCards, contextCards, logCard] = collectCards(
templates.library,
templates.context,
templates.log,
templates.input
);
if (hadError()) {
endContextLSI(LSI_DOMAIN);
break;
}
const [error, ...newCodomain] = (([error, newText, newStop]) => [error, newText, (newStop === true)])(
isolateLSIv2(parseCode(libraryCards, contextCards), callbackLog(logCard), LSI_DOMAIN[0], LSI_DOMAIN[1])
);
handleError(logCard, error);
endContextLSI(newCodomain);
function endContextLSI(newCodomain) {
CODOMAIN.initialize(newCodomain);
if (!newCodomain[1]) {
return;
}
const [guideCard, stateCard] = collectCards(
templates.guide,
templates.state,
templates.output
);
AC.message.pending = [];
concludeLSI(guideCard, stateCard, logCard);
return;
}
break; }
case "output": {
// AutoCards was called within the output modifier
const [libraryCards, outputCards, guideCard, stateCard, logCard] = collectCards(
templates.library,
templates.output,
templates.guide,
templates.state,
templates.log
);
if (hadError()) {
endOutputLSI(true, LSI_DOMAIN);
break;
}
const [error, newText] = isolateLSIv2(parseCode(libraryCards, outputCards), callbackLog(logCard), LSI_DOMAIN);
handleError(logCard, error);
endOutputLSI(hadError(), newText);
function endOutputLSI(displayError, newText) {
if (displayError) {
if (AC.signal.upstreamError === "\n") {
CODOMAIN.initialize("\n");
} else {
CODOMAIN.initialize(getStoryError() + "\n");
}
AC.message.pending = [];
} else {
CODOMAIN.initialize(newText);
}
concludeLSI(guideCard, stateCard, logCard);
return;
}
break; }
case "initialize": {
collectAll();
logToCard(Internal.getCard(card => (card.title === templates.log.name)), "LSIv2 startup -> Success!");
CODOMAIN.initialize(null);
break; }
}
AC.config.LSIv2 = true;
function parseCode(...args) {
return (args
.flatMap(cardset => [cardset.primary, ...cardset.auxiliaries])
.flatMap(card => [card.entry, card.description])
.join("\n")
);
}
function callbackLog(logCard) {
return function(...args) {
logToCard(logCard, ...args);
return;
}
}
function handleError(logCard, error) {
if (!error) {
return;
}
O.f(error);
AC.signal.upstreamError = (
"LSIv2 encountered an error during the on" + HOOK[0].toUpperCase() + HOOK.slice(1) + " hook"
);
if (error.message) {
AC.signal.upstreamError += ":\n";
if (error.stack) {
const stackMatch = error.stack.match(/AutoCards[\s\S]*?:\s*(\d+)\s*:\s*(\d+)/i);
if (stackMatch) {
AC.signal.upstreamError += (
(error.name ?? "Error") + ": " + error.message + "\n" +
"(line #" + stackMatch[1] + " column #" + stackMatch[2] + ")"
);
} else {
AC.signal.upstreamError += error.stack;
}
} else {
AC.signal.upstreamError += (error.name ?? "Error") + ": " + error.message;
}
AC.signal.upstreamError = cleanSpaces(AC.signal.upstreamError.trimEnd());
}
logToCard(logCard, AC.signal.upstreamError);
if (getStateMessage() === AC.signal.upstreamError) {
state.message = AC.signal.upstreamError + " ";
} else {
state.message = AC.signal.upstreamError;
}
return;
}
function hadError() {
return (AC.signal.upstreamError !== "");
}
function getStoryError() {
return getPrecedingNewlines() + ">>>\n" + AC.signal.upstreamError + "\n<<<\n";
}
function concludeLSI(guideCard, stateCard, logCard) {
AC.signal.upstreamError = "";
guideCard.description = templates.guide.description;
guideCard.entry = templates.guide.entry;
stateCard.entry = templates.state.entry;
logCard.entry = templates.log.entry;
postMessages();
const simpleState = {...state};
delete simpleState.LSIv2;
stateCard.description = limitString(stringifyObject(simpleState).trim(), 999999).trimEnd();
return;
}
} else {
const cardsets = collectAll();
for (const cardset of cardsets) {
if ("primary" in cardset) {
killCard(cardset.primary);
for (const card of cardset.auxiliaries) {
killCard(card);
}
} else {
killCard(cardset);
}
function killCard(card) {
unbanTitle(card.title);
eraseCard(card);
}
}
AC.signal.upstreamError = "";
CODOMAIN.initialize(LSI_DOMAIN);
}
// This measure ensures the Auto-Cards external API is equally available from within the inner scope of LSIv2
// As before, call with AutoCards().API.nameOfFunction(yourArguments);
deepMerge(AC, state.LSIv2);
delete state.LSIv2;
function deepMerge(target, source) {
for (const key in source) {
if (!source.hasOwnProperty(key)) {
continue;
} else if (
(typeof source[key] === "object")
&& (source[key] !== null)
&& !Array.isArray(source[key])
&& (typeof target[key] === "object")
&& (target[key] !== null)
&& (key !== "workpiece")
&& (key !== "associations")
) {
// Recursively merge static objects
deepMerge(target[key], source[key]);
} else {
// Directly replace values
target[key] = source[key];
}
}
return;
}
function collectAll() {
return collectCards(...Object.keys(factories).map(key => templates[key]));
}
// collectCards constructs, validates, repairs, retrieves, and organizes all LSIv2 script cards associated with the given arguments by iterating over the storyCards array only once! Returned elements are easily handled via array destructuring assignment
function collectCards(...args) {
// args: [{name: string, entry: string, description: string, singleton: boolean, position: integer}]
const collections = O.f(args.map(({name, entry, description, singleton, position}) => {
const collection = {
template: O.f({
type: AC.config.defaultCardType,
title: name,
keys: name,
entry,
description
}),
singleton,
position,
primary: null,
excess: [],
};
if (!singleton) {
collection.auxiliaries = [];
collection.occupied = new Set([0, 1]);
}
return O.s(collection);
}));
for (const card of storyCards) {
O.s(card);
for (const collection of collections) {
if (
!card.title.toLowerCase().includes(collection.template.title.toLowerCase())
&& !card.keys.toLowerCase().includes(collection.template.title.toLowerCase())
) {
// No match, swipe left
continue;
}
if (collection.singleton) {
setPrimary();
break;
}
const [extensionA, extensionB] = [card.title, card.keys].map(name => {
const extensionMatch = name.replace(/[^a-zA-Z0-9]/g, "").match(/\d+$/);
if (extensionMatch) {
return parseInt(extensionMatch[0], 10);
} else {
return -1;
}
});
if (-1 < extensionA) {
if (-1 < extensionB) {
if (collection.occupied.has(extensionA)) {
setAuxiliary(extensionB);
} else {
setAuxiliary(extensionA, true);
}
} else {
setAuxiliary(extensionA);
}
} else if (-1 < extensionB) {
setAuxiliary(extensionB);
} else {
setPrimary();
}
function setAuxiliary(extension, preChecked = false) {
if (preChecked || !collection.occupied.has(extension)) {
addAuxiliary(card, collection, extension);
} else {
card.title = card.keys = collection.template.title;
collection.excess.push(card);
}
return;
}
function setPrimary() {
card.title = card.keys = collection.template.title;
if (collection.primary === null) {
collection.primary = card;
} else {
collection.excess.push(card);
}
return;
}
break;
}
}
for (const collection of collections) {
banTitle(collection.template.title);
if (collection.singleton) {
if (collection.primary === null) {
constructPrimary();
} else if (hasExs()) {
for (const card of collection.excess) {
eraseCard(card);
}
}
continue;
} else if (collection.primary === null) {
if (hasExs()) {
collection.primary = collection.excess.shift();
if (hasExs() || hasAux()) {
applyComment(collection.primary);
} else {
collection.primary.entry = collection.template.entry;
collection.primary.description = collection.template.description;
continue;
}
} else {
constructPrimary();
if (hasAux()) {
applyComment(collection.primary);
} else {
continue;
}
}
}
if (hasExs()) {
for (const card of collection.excess) {
let extension = 2;
while (collection.occupied.has(extension)) {
extension++;
}
applyComment(card);
addAuxiliary(card, collection, extension);
}
}
if (hasAux()) {
collection.auxiliaries.sort((a, b) => {
return a.extension - b.extension;
});
}
function hasExs() {
return (0 < collection.excess.length);
}
function hasAux() {
return (0 < collection.auxiliaries.length);
}
function applyComment(card) {
card.entry = card.description = "// You may continue writing your code here";
return;
}
function constructPrimary() {
collection.primary = constructCard(collection.template, newCardIndex());
// I like my LSIv2 cards to display in the proper order once initialized uwu
const templateKeys = Object.keys(factories);
const cards = templateKeys.map(key => O.f({
card: Internal.getCard(card => (card.title === templates[key].name)),
position: templates[key].position
})).filter(pair => (pair.card !== null));
if (cards.length < templateKeys.length) {
return;
}
const fullCardset = cards.sort((a, b) => (a.position - b.position)).map(pair => pair.card);
for (const card of fullCardset) {
eraseCard(card);
card.title = card.keys;
}
storyCards.splice(newCardIndex(), 0, ...fullCardset);
return;
}
}
function addAuxiliary(card, collection, extension) {
collection.occupied.add(extension);
card.title = card.keys = collection.template.title + " " + extension;
collection.auxiliaries.push({card, extension});
return;
}
return O.f(collections.map(({singleton, primary, auxiliaries}) => {
if (singleton) {
return primary;
} else {
return O.f({primary, auxiliaries: O.f(auxiliaries.map(({card}) => card))});
}
}));
}
} else if (AC.config.doAC) {
// Auto-Cards is currently enabled
// "text" represents the original text which was present before any scripts were executed
// "TEXT" represents the script-modified version of "text" which AutoCards was called with
// This dual scheme exists to ensure Auto-Cards is safely compatible with other scripts
switch(HOOK) {
case "input": {
// AutoCards was called within the input modifier
if ((AC.config.deleteAllAutoCards === false) && /CONFIRM\s*DELETE/i.test(TEXT)) {
CODOMAIN.initialize("CONFIRM DELETE -> Success!");
} else if (/\/\s*A\s*C/i.test(text)) {
CODOMAIN.initialize(doPlayerCommands(text));
} else if (TEXT.startsWith(" ") && readPastAction(0).text.endsWith("\n")) {
// Just a simple little formatting bugfix for regular AID story actions
CODOMAIN.initialize(getPrecedingNewlines() + TEXT.replace(/^\s+/, ""));
} else {
CODOMAIN.initialize(TEXT);
}
break; }
case "context": {
// AutoCards was called within the context modifier
advanceChronometer();
// Get or construct the "Configure Auto-Cards" story card
const configureCardTemplate = getConfigureCardTemplate();
const configureCard = getSingletonCard(true, configureCardTemplate);
banTitle(configureCardTemplate.title);
pinAndSortCards(configureCard);
const bansOverwritten = (0 < AC.signal.overrideBans);
if ((configureCard.description !== configureCardTemplate.description) || bansOverwritten) {
const descConfigPatterns = (getConfigureCardDescription()
.split(Words.delimiter)
.slice(1)
.map(descPattern => (descPattern
.slice(0, descPattern.indexOf(":"))
.trim()
.replace(/\s+/g, "\\s*")
))
.map(descPattern => (new RegExp("^\\s*" + descPattern + "\\s*:", "i")))
);
const descConfigs = configureCard.description.split(Words.delimiter).slice(1);
if (
(descConfigs.length === descConfigPatterns.length)
&& descConfigs.every((descConfig, index) => descConfigPatterns[index].test(descConfig))
) {
// All description config headers must be present and well-formed
let cfg = extractDescSetting(0);
if (AC.config.generationPrompt !== cfg) {
notify("Changes to your card generation prompt were successfully saved");
AC.config.generationPrompt = cfg;
}
cfg = extractDescSetting(1);
if (AC.config.compressionPrompt !== cfg) {
notify("Changes to your card memory compression prompt were successfully saved");
AC.config.compressionPrompt = cfg;
}
if (bansOverwritten) {
overrideBans();
} else if ((0 < AC.database.titles.pendingBans.length) || (0 < AC.database.titles.pendingUnbans.length)) {
const pendingBans = AC.database.titles.pendingBans.map(pair => pair[0]);
const pendingRewrites = new Set(
lowArr([...pendingBans, ...AC.database.titles.pendingUnbans.map(pair => pair[0])])
);
Internal.setBannedTitles([...pendingBans, ...extractDescSetting(2)
.split(",")
.filter(newBan => !pendingRewrites.has(newBan.toLowerCase().replace(/\s+/, " ").trim()))
], true);
} else {
Internal.setBannedTitles(extractDescSetting(2).split(","), true);
}
function extractDescSetting(index) {
return descConfigs[index].replace(descConfigPatterns[index], "").trim();
}
} else if (bansOverwritten) {
overrideBans();
}
configureCard.description = getConfigureCardDescription();
function overrideBans() {
Internal.setBannedTitles(AC.database.titles.pendingBans.map(pair => pair[0]), true);
AC.signal.overrideBans = 0;
return;
}
}
if (configureCard.entry !== configureCardTemplate.entry) {
const oldConfig = {};
const settings = O.f((function() {
const userSettings = extractSettings(configureCard.entry);
if (userSettings.resetallconfigsettingsandprompts !== true) {
return userSettings;
}
// Reset all config settings and display state change notifications only when appropriate
Object.assign(oldConfig, AC.config);
Object.assign(AC.config, getDefaultConfig());
AC.config.deleteAllAutoCards = oldConfig.deleteAllAutoCards;
AC.config.LSIv2 = oldConfig.LSIv2;
AC.config.defaultCardType = oldConfig.defaultCardType;
AC.database.titles.banned = getDefaultConfigBans();
configureCard.description = getConfigureCardDescription();
configureCard.entry = getConfigureCardEntry();
const defaultSettings = extractSettings(configureCard.entry);
if ((DEFAULT_DO_AC === false) || (userSettings.disableautocards === true)) {
defaultSettings.disableautocards = true;
}
notify("Restoring all settings and prompts to their default values");
return defaultSettings;
})());
O.f(oldConfig);
if ((settings.deleteallautomaticstorycards === true) && (AC.config.deleteAllAutoCards === null)) {
AC.config.deleteAllAutoCards = true;
} else if (settings.showdetailedguide === true) {
AC.signal.outputReplacement = Words.guide;
}
let cfg;
if (parseConfig("pinthisconfigcardnearthetop", false, "pinConfigureCard")) {
if (cfg) {
pinAndSortCards(configureCard);
notify("The settings config card will now be pinned near the top of your story cards list");
} else {
const index = storyCards.indexOf(configureCard);
if (index !== -1) {
storyCards.splice(index, 1);
storyCards.push(configureCard);
}
notify("The settings config card will no longer be pinned near the top of your story cards list");
}
}
if (parseConfig("minimumturnscooldownfornewcards", true, "addCardCooldown")) {
const oldCooldown = AC.config.addCardCooldown;
AC.config.addCardCooldown = validateCooldown(cfg);
if (!isPendingGeneration() && !isAwaitingGeneration() && (0 < AC.generation.cooldown)) {
const quarterCooldown = validateCooldown(underQuarterInteger(AC.config.addCardCooldown));
if ((AC.config.addCardCooldown < oldCooldown) && (quarterCooldown < AC.generation.cooldown)) {
// Reduce the next generation's cooldown counter by a factor of 4
// But only if the new cooldown config is lower than it was before
// And also only if quarter cooldown is less than the current next gen cooldown
// (Just a random little user experience improvement)
AC.generation.cooldown = quarterCooldown;
} else if (oldCooldown < AC.config.addCardCooldown) {
if (oldCooldown === AC.generation.cooldown) {
AC.generation.cooldown = AC.config.addCardCooldown;
} else {
AC.generation.cooldown = validateCooldown(boundInteger(
0,
AC.generation.cooldown + quarterCooldown,
AC.config.addCardCooldown
));
}
}
}
switch(AC.config.addCardCooldown) {
case 9999: {
notify(
"You have disabled automatic card generation. To re-enable, simply set your cooldown config to any number lower than 9999. Or use the \"/ac\" in-game command to manually direct the card generation process"
);
break; }
case 1: {
notify(
"A new card will be generated during alternating game turns, but only if your story contains available titles"
);
break; }
case 0: {
notify(
"New cards will be immediately generated whenever valid titles exist within your recent story"
);
break; }
default: {
notify(
"A new card will be generated once every " + AC.config.addCardCooldown + " turns, but only if your story contains available titles"
);
break; }
}
}
if (parseConfig("newcardsuseabulletedlistformat", false, "bulletedListMode")) {
if (cfg) {
notify("New card entries will be generated using a bulleted list format");
} else {
notify("New card entries will be generated using a pure prose format");
}
}
if (parseConfig("maximumentrylengthfornewcards", true, "defaultEntryLimit")) {
AC.config.defaultEntryLimit = validateEntryLimit(cfg);
notify(
"New card entries will be limited to " + AC.config.defaultEntryLimit + " characters of generated text"
);
}
if (parseConfig("newcardsperformmemoryupdates", false, "defaultCardsDoMemoryUpdates")) {
if (cfg) {
notify("Newly constructed cards will begin with memory updates enabled by default");
} else {
notify("Newly constructed cards will begin with memory updates disabled by default");
}
}
if (parseConfig("cardmemorybankpreferredlength", true, "defaultMemoryLimit")) {
AC.config.defaultMemoryLimit = validateMemoryLimit(cfg);
notify(
"Newly constructed cards will begin with their memory bank length preference set to " + AC.config.defaultMemoryLimit + " characters of text"
);
}
if (parseConfig("memorysummarycompressionratio", true, "memoryCompressionRatio")) {
AC.config.memoryCompressionRatio = validateMemCompRatio(cfg);
notify(
"Freshly summarized card memory banks will be approximately " + (AC.config.memoryCompressionRatio / 10) + "x shorter than their originals"
);
}
if (parseConfig("excludeallcapsfromtitledetection", false, "ignoreAllCapsTitles")) {
if (cfg) {
notify("All-caps text will be ignored during title detection to help prevent bad cards");
} else {
notify("All-caps text may be considered during title detection processes");
}
}
if (parseConfig("alsodetecttitlesfromplayerinputs", false, "readFromInputs")) {
if (cfg) {
notify("Titles may be detected from player Do/Say/Story action inputs");
} else {
notify("Title detection will skip player Do/Say/Story action inputs for grammatical leniency");
}
}
if (parseConfig("minimumturnsagefortitledetection", true, "minimumLookBackDistance")) {
AC.config.minimumLookBackDistance = validateMinLookBackDist(cfg);
notify(
"Titles and names mentioned in your story may become eligible for future card generation attempts once they are at least " + AC.config.minimumLookBackDistance + " actions old"
);
}
cfg = settings.uselivescriptinterfacev2;
if (typeof cfg === "boolean") {
if (AC.config.LSIv2 === null) {
if (cfg) {
AC.config.LSIv2 = true;
state.LSIv2 = AC;
AutoCards("initialize");
notify("Live Script Interface v2 is now embedded within your adventure!");
}
} else {
if (!cfg) {
AC.config.LSIv2 = null;
notify("Live Script Interface v2 has been removed from your adventure");
}
}
}
if (parseConfig("logdebugdatainaseparatecard" , false, "showDebugData")) {
if (data === null) {
if (cfg) {
notify("State may now be viewed within the \"Debug Data\" story card");
} else {
notify("The \"Debug Data\" story card has been removed");
}
} else if (cfg) {
notify("Debug data will be shared with the \"Critical Data\" story card to conserve memory");
} else {
notify("Debug mode has been disabled");
}
}
if ((settings.disableautocards === true) && (AC.signal.forceToggle !== true)) {
disableAutoCards();
break;
} else {
// Apply the new card entry and proceed to implement Auto-Cards onContext
configureCard.entry = getConfigureCardEntry();
}
function parseConfig(settingsKey, isNumber, configKey) {
cfg = settings[settingsKey];
if (isNumber) {
return checkConfig("number");
} else if (!checkConfig("boolean")) {
return false;
}
AC.config[configKey] = cfg;
function checkConfig(type) {
return ((typeof cfg === type) && (
(notEmptyObj(oldConfig) && (oldConfig[configKey] !== cfg))
|| (AC.config[configKey] !== cfg)
));
}
return true;
}
}
if (AC.signal.forceToggle === false) {
disableAutoCards();
break;
}
AC.signal.forceToggle = null;
if (0 < AC.chronometer.postpone) {
CODOMAIN.initialize(TEXT);
break;
}
// Fully implement Auto-Cards onContext
const forceStep = AC.signal.recheckRetryOrErase;
const currentTurn = getTurn();
const nearestUnparsedAction = boundInteger(0, currentTurn - AC.config.minimumLookBackDistance);
if (AC.signal.recheckRetryOrErase || (nearestUnparsedAction <= AC.database.titles.lastActionParsed)) {
// The player erased or retried an unknown number of actions
// Purge recent candidates and perform a safety recheck
if (nearestUnparsedAction <= AC.database.titles.lastActionParsed) {
AC.signal.recheckRetryOrErase = true;
} else {
AC.signal.recheckRetryOrErase = false;
}
AC.database.titles.lastActionParsed = boundInteger(-1, nearestUnparsedAction - 8);
for (let i = AC.database.titles.candidates.length - 1; 0 <= i; i--) {
const candidate = AC.database.titles.candidates[i];
for (let j = candidate.length - 1; 0 < j; j--) {
if (AC.database.titles.lastActionParsed < candidate[j]) {
candidate.splice(j, 1);
}
}
if (candidate.length <= 1) {
AC.database.titles.candidates.splice(i, 1);
}
}
}
const pendingCandidates = new Map();
if ((0 < nearestUnparsedAction) && (AC.database.titles.lastActionParsed < nearestUnparsedAction)) {
const actions = [];
for (
let actionToParse = AC.database.titles.lastActionParsed + 1;
actionToParse <= nearestUnparsedAction;
actionToParse++
) {
// I wrote this whilst sleep-deprived, somehow it works
const lookBack = currentTurn - actionToParse - (function() {
if (isDoSayStory(readPastAction(0).type)) {
// Inputs count as 2 actions instead of 1, conditionally offset lookBack by 1
return 0;
} else {
return 1;
}
})();
if (history.length <= lookBack) {
// history cannot be indexed with a negative integer
continue;
}
const action = readPastAction(lookBack);
const thisTextHash = new StringsHashed(4096).add(action.text).serialize();
if (actionToParse === nearestUnparsedAction) {
if (AC.signal.recheckRetryOrErase || (thisTextHash === AC.database.titles.lastTextHash)) {
// Additional safety to minimize duplicate candidate additions during retries or erases
AC.signal.recheckRetryOrErase = true;
break;
} else {
// Action parsing will proceed
AC.database.titles.lastActionParsed = nearestUnparsedAction;
AC.database.titles.lastTextHash = thisTextHash;
}
} else if (
// Special case where a consecutive retry>erase>continue cancels out
AC.signal.recheckRetryOrErase
&& (actionToParse === (nearestUnparsedAction - 1))
&& (thisTextHash === AC.database.titles.lastTextHash)
) {
AC.signal.recheckRetryOrErase = false;
}
actions.push([action, actionToParse]);
}
if (!AC.signal.recheckRetryOrErase) {
for (const [action, turn] of actions) {
if (
(action.type === "see")
|| (action.type === "unknown")
|| (!AC.config.readFromInputs && isDoSayStory(action.type))
|| /^[^\p{Lu}]*$/u.test(action.text)
|| action.text.includes("<<<")
|| /\/\s*A\s*C/i.test(action.text)
|| /CONFIRM\s*DELETE/i.test(action.text)
) {
// Skip see actions
// Skip input actions (only if input title detection has been disabled in the config)
// Skip strings without capital letters
// Skip utility actions
continue;
}
const words = (prettifyEmDashes(action.text)
// Nuh uh
.replace(/[“”]/g, "\"").replace(/[]/g, "'").replaceAll("´", "`")
.replaceAll("。", ".").replaceAll("", "?").replaceAll("", "!")
// Replace special clause opening punctuation with colon ":" terminators
.replace(/(^|\s+)["'`]\s*/g, ": ").replace(/\s*[\(\[{]\s*/g, ": ")
// Likewise for end-quotes (curbs a common AI grammar mistake)
.replace(/\s*,?\s*["'`](?:\s+|$)/g, ": ")
// Replace funky wunky symbols with regular spaces
.replace(/[؟،«»¿¡„“…§,、\*_~><\)\]}#"`\s]/g, " ")
// Replace some mid-sentence punctuation symbols with a placeholder word
.replace(/\s*[—;,\/\\]\s*/g, " %@% ")
// Replace "I", "I'm", "I'd", "I'll", and "I've" with a placeholder word
.replace(/(?:^|\s+|-)I(?:'(?:m|d|ll|ve))?(?:\s+|-|$)/gi, " %@% ")
// Remove "'s" only if not followed by a letter
.replace(/'s(?![a-zA-Z])/g, "")
// Replace "s'" with "s" only if preceded but not followed by a letter
.replace(/(?<=[a-zA-Z])s'(?![a-zA-Z])/g, "s")
// Remove apostrophes not between letters (preserve contractions like "don't")
.replace(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Remove a leading bullet
.replace(/^\s*-+\s*/, "")
// Replace common honorifics with a placeholder word
.replace(buildKiller(Words.honorifics), " %@% ")
// Remove common abbreviations
.replace(buildKiller(Words.abbreviations), " ")
// Fix end punctuation
.replace(/\s+\.(?![a-zA-Z])/g, ".").replace(/\.\.+/g, ".")
.replace(/\s+\?(?![a-zA-Z])/g, "?").replace(/\?\?+/g, "?")
.replace(/\s+!(?![a-zA-Z])/g, "!").replace(/!!+/g, "!")
.replace(/\s+:(?![a-zA-Z])/g, ":").replace(/::+/g, ":")
// Colons are treated as substitute end-punctuation, apply the capitalization rule
.replace(/:\s+(\S)/g, (_, next) => ": " + next.toUpperCase())
// Condense consecutive whitespace
.trim().replace(/\s+/g, " ")
).split(" ");
if (!Array.isArray(words) || (words.length < 2)) {
continue;
}
const titles = [];
const incompleteTitle = [];
let previousWordTerminates = true;
for (let i = 0; i < words.length; i++) {
let word = words[i];
if (startsWithTerminator()) {
// This word begins on a terminator, push the preexisting incomplete title to titles and proceed with the next sentence's beginning
pushTitle();
previousWordTerminates = true;
// Ensure no leading terminators remain
while ((word !== "") && startsWithTerminator()) {
word = word.slice(1);
}
}
if (word === "") {
continue;
} else if (previousWordTerminates) {
// We cannot detect titles from sentence beginnings due to sentence capitalization rules. The previous sentence was recently terminated, implying the current series of capitalized words (plus lowercase minor words) occurs near the beginning of the current sentence
if (endsWithTerminator()) {
continue;
} else if (startsWithUpperCase()) {
if (isMinorWord(word)) {
// Special case where a capitalized minor word precedes a named entity, clear the previous termination status
previousWordTerminates = false;
}
// Otherwise, proceed without clearing
} else if (!isMinorWord(word) && !/^(?:and|&)(?:$|[\.\?!:]$)/.test(word)) {
// Previous sentence termination status is cleared by the first new non-minor lowercase word encountered during forward iteration through the action text's words
previousWordTerminates = false;
}
continue;
}
// Words near the beginning of this sentence have been skipped, proceed with named entity detection using capitalization rules. An incomplete title will be pushed to titles if A) a non-minor lowercase word is encountered, B) three consecutive minor words occur in a row, C) a terminator symbol is encountered at the end of a word. Otherwise, continue pushing words to the incomplete title
if (endsWithTerminator()) {
previousWordTerminates = true;
while ((word !== "") && endsWithTerminator()) {
word = word.slice(0, -1);
}
if (word === "") {
pushTitle();
continue;
}
}
if (isMinorWord(word)) {
if (0 < incompleteTitle.length) {
// Titles cannot start with a minor word
if (
(2 < incompleteTitle.length) && !(isMinorWord(incompleteTitle[incompleteTitle.length - 1]) && isMinorWord(incompleteTitle[incompleteTitle.length - 2]))
) {
// Titles cannot have 3 or more consecutive minor words in a row
pushTitle();
continue;
} else {
// Titles may contain minor words in their middles. Ex: "Ace of Spades"
incompleteTitle.push(word.toLowerCase());
}
}
} else if (startsWithUpperCase()) {
// Add this proper noun to the incomplete title
incompleteTitle.push(word);
} else {
// The full title has a non-minor lowercase word to its immediate right
pushTitle();
continue;
}
if (previousWordTerminates) {
pushTitle();
}
function pushTitle() {
while (
(1 < incompleteTitle.length)
&& isMinorWord(incompleteTitle[incompleteTitle.length - 1])
) {
incompleteTitle.pop();
}
if (0 < incompleteTitle.length) {
titles.push(incompleteTitle.join(" "));
// Empty the array
incompleteTitle.length = 0;
}
return;
}
function isMinorWord(testWord) {
return Words.minor.includes(testWord.toLowerCase());
}
function startsWithUpperCase() {
return /^\p{Lu}/u.test(word);
}
function startsWithTerminator() {
return /^[\.\?!:]/.test(word);
}
function endsWithTerminator() {
return /[\.\?!:]$/.test(word);
}
}
for (let i = titles.length - 1; 0 <= i; i--) {
titles[i] = formatTitle(titles[i]).newTitle;
if (titles[i] === "" || (
AC.config.ignoreAllCapsTitles
&& (2 < titles[i].replace(/[^a-zA-Z]/g, "").length)
&& (titles[i] === titles[i].toUpperCase())
)) {
titles.splice(i, 1);
}
}
// Remove duplicates
const uniqueTitles = [...new Set(titles)];
if (uniqueTitles.length === 0) {
continue;
} else if (
// No reason to keep checking long past the max lookback distance
(currentTurn < 256)
&& (action.type === "start")
// This is only used here so it doesn't need its own AC.config property or validation
&& (DEFAULT_BAN_TITLES_FROM_OPENING !== false)
) {
// Titles in the opening prompt are banned by default, hopefully accounting for the player character's name and other established setting details
uniqueTitles.forEach(title => banTitle(title));
} else {
// Schedule new titles for later insertion within the candidates database
for (const title of uniqueTitles) {
const pendingHashKey = title.toLowerCase();
if (pendingCandidates.has(pendingHashKey)) {
// Consolidate pending candidates with matching titles but different turns
pendingCandidates.get(pendingHashKey).turns.push(turn);
} else {
pendingCandidates.set(pendingHashKey, O.s({title, turns: [turn]}));
}
}
}
function buildKiller(words) {
return (new RegExp(("(?:^|\\s+|-)(?:" + (words
.map(word => word.replace(".", "\\."))
.join("|")
) + ")(?:\\s+|-|$)"), "gi"));
}
}
}
}
// Measure the minimum and maximum turns of occurance for all title candidates
let minTurn = currentTurn;
let maxTurn = 0;
for (let i = AC.database.titles.candidates.length - 1; 0 <= i; i--) {
const candidate = AC.database.titles.candidates[i];
const title = candidate[0];
if (isUsedOrBanned(title) || isNamed(title)) {
// Retroactively ensure AC.database.titles.candidates contains no used / banned titles
AC.database.titles.candidates.splice(i, 1);
} else {
const pendingHashKey = title.toLowerCase();
if (pendingCandidates.has(pendingHashKey)) {
// This candidate title matches one of the pending candidates, collect the pending turns
candidate.push(...pendingCandidates.get(pendingHashKey).turns);
// Remove this pending candidate
pendingCandidates.delete(pendingHashKey);
}
if (2 < candidate.length) {
// Ensure all recorded turns of occurance are unique for this candidate
// Sort the turns from least to greatest
const sortedTurns = [...new Set(candidate.slice(1))].sort((a, b) => (a - b));
if (625 < sortedTurns.length) {
sortedTurns.splice(0, sortedTurns.length - 600);
}
candidate.length = 1;
candidate.push(...sortedTurns);
}
setCandidateTurnBounds(candidate);
}
}
for (const pendingCandidate of pendingCandidates.values()) {
// Insert any remaining pending candidates (validity has already been ensured)
const newCandidate = [pendingCandidate.title, ...pendingCandidate.turns];
setCandidateTurnBounds(newCandidate);
AC.database.titles.candidates.push(newCandidate);
}
const isCandidatesSorted = (function() {
if (425 < AC.database.titles.candidates.length) {
// Sorting a large title candidates database is computationally expensive
sortCandidates();
AC.database.titles.candidates.splice(400);
// Flag this operation as complete for later consideration
return true;
} else {
return false;
}
})();
Internal.getUsedTitles();
for (const titleKey in AC.database.memories.associations) {
if (isAuto(titleKey)) {
// Reset the lifespan counter
AC.database.memories.associations[titleKey][0] = 999;
} else if (AC.database.memories.associations[titleKey][0] < 1) {
// Forget this set of memory associations
delete AC.database.memories.associations[titleKey];
} else if (!isAwaitingGeneration()) {
// Decrement the lifespan counter
AC.database.memories.associations[titleKey][0]--;
}
}
// This copy of TEXT may be mutated
let context = TEXT;
const titleHeaderPatternGlobal = /\s*{\s*titles?\s*:\s*([\s\S]*?)\s*}\s*/gi;
// Card events govern the parsing of memories from raw context as well as card memory bank injection
const cardEvents = (function() {
// Extract memories from the initial text (not TEXT as called from within the context modifier!)
const contextMemories = (function() {
const memoriesMatch = text.match(/Memories\s*:\s*([\s\S]*?)\s*(?:Recent\s*Story\s*:|$)/i);
if (!memoriesMatch) {
return new Set();
}
const uniqueMemories = new Set(isolateMemories(memoriesMatch[1]));
if (uniqueMemories.size === 0) {
return uniqueMemories;
}
const duplicatesHashed = StringsHashed.deserialize(AC.database.memories.duplicates, 65536);
const duplicateMemories = new Set();
const seenMemories = new Set();
for (const memoryA of uniqueMemories) {
if (duplicatesHashed.has(memoryA)) {
// Remove to ensure the insertion order for this duplicate changes
duplicatesHashed.remove(memoryA);
duplicateMemories.add(memoryA);
} else if ((function() {
for (const memoryB of seenMemories) {
if (0.42 < similarityScore(memoryA, memoryB)) {
// This memory is too similar to another memory
duplicateMemories.add(memoryA);
return false;
}
}
return true;
})()) {
seenMemories.add(memoryA);
}
}
if (0 < duplicateMemories.size) {
// Add each near duplicate's hashcode to AC.database.memories.duplicates
// Then remove duplicates from uniqueMemories and the context window
for (const duplicate of duplicateMemories) {
duplicatesHashed.add(duplicate);
uniqueMemories.delete(duplicate);
context = context.replaceAll("\n" + duplicate, "");
}
// Only the 2000 most recent duplicate memory hashcodes are remembered
AC.database.memories.duplicates = duplicatesHashed.latest(2000).serialize();
}
return uniqueMemories;
})();
const leftBoundary = "^|\\s|\"|'|—|\\(|\\[|{";
const rightBoundary = "\\s|\\.|\\?|!|,|;|\"|'|—|\\)|\\]|}|$";
// Murder, homicide if you will, nothing to see here
const theKiller = new RegExp("(?:" + leftBoundary + ")the[\\s\\S]*$", "i");
const peerageKiller = new RegExp((
"(?:" + leftBoundary + ")(?:" + Words.peerage.join("|") + ")(?:" + rightBoundary + ")"
), "gi");
const events = new Map();
for (const contextMemory of contextMemories) {
for (const titleKey of auto) {
if (!(new RegExp((
"(?<=" + leftBoundary + ")" + (titleKey
.replace(theKiller, "")
.replace(peerageKiller, "")
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
) + "(?=" + rightBoundary + ")"
), "i")).test(contextMemory)) {
continue;
}
// AC card titles found in active memories will promote card events
if (events.has(titleKey)) {
events.get(titleKey).pendingMemories.push(contextMemory);
continue;
}
events.set(titleKey, O.s({
pendingMemories: [contextMemory],
titleHeader: ""
}));
}
}
const titleHeaderMatches = [...context.matchAll(titleHeaderPatternGlobal)];
for (const [titleHeader, title] of titleHeaderMatches) {
if (!isAuto(title)) {
continue;
}
// Unique title headers found in context will promote card events
const titleKey = title.toLowerCase();
if (events.has(titleKey)) {
events.get(titleKey).titleHeader = titleHeader;
continue;
}
events.set(titleKey, O.s({
pendingMemories: [],
titleHeader: titleHeader
}));
}
return events;
})();
// Remove auto card title headers from active story card entries and contextualize their respective memory banks
// Also handle the growth and maintenance of card memory banks
let isRemembering = false;
for (const card of storyCards) {
// Iterate over each card to handle pending card events and forenames/surnames
const titleHeaderMatcher = /^{title: \s*([\s\S]*?)\s*}/;
let breakForCompression = isPendingCompression();
if (breakForCompression) {
break;
} else if (!card.entry.startsWith("{title: ")) {
continue;
} else if (exceedsMemoryLimit()) {
const titleHeaderMatch = card.entry.match(titleHeaderMatcher);
if (titleHeaderMatch && isAuto(titleHeaderMatch[1])) {
prepareMemoryCompression(titleHeaderMatch[1].toLowerCase());
break;
}
}
// Handle card events
const lowerEntry = card.entry.toLowerCase();
for (const titleKey of cardEvents.keys()) {
if (!lowerEntry.startsWith("{title: " + titleKey + "}")) {
continue;
}
const cardEvent = cardEvents.get(titleKey);
if (
(0 < cardEvent.pendingMemories.length)
&& /{\s*updates?\s*:\s*true\s*,\s*limits?\s*:[\s\S]*?}/i.test(card.description)
) {
// Add new card memories
const associationsHashed = (function() {
if (titleKey in AC.database.memories.associations) {
return StringsHashed.deserialize(AC.database.memories.associations[titleKey][1], 65536);
} else {
AC.database.memories.associations[titleKey] = [999, ""];
return new StringsHashed(65536);
}
})();
const oldMemories = isolateMemories(extractCardMemories().text);
for (let i = 0; i < cardEvent.pendingMemories.length; i++) {
if (associationsHashed.has(cardEvent.pendingMemories[i])) {
// Remove first to alter the insertion order
associationsHashed.remove(cardEvent.pendingMemories[i]);
} else if (!oldMemories.some(oldMemory => (
(0.8 < similarityScore(oldMemory, cardEvent.pendingMemories[i]))
))) {
// Ensure no near-duplicate memories are appended
card.description += "\n- " + cardEvent.pendingMemories[i];
}
associationsHashed.add(cardEvent.pendingMemories[i]);
}
AC.database.memories.associations[titleKey][1] = associationsHashed.latest(3500).serialize();
if (associationsHashed.size() === 0) {
delete AC.database.memories.associations[titleKey];
}
if (exceedsMemoryLimit()) {
breakForCompression = prepareMemoryCompression(titleKey);
break;
}
}
if (cardEvent.titleHeader !== "") {
// Replace this card's title header in context
const cardMemoriesText = extractCardMemories().text;
if (cardMemoriesText === "") {
// This card contains no card memories to contextualize
context = context.replace(cardEvent.titleHeader, "\n\n");
} else {
// Insert card memories within context and ensure they occur uniquely
const cardMemories = cardMemoriesText.split("\n").map(cardMemory => cardMemory.trim());
for (const cardMemory of cardMemories) {
if (25 < cardMemory.length) {
context = (context
.replaceAll(cardMemory, "<#>")
.replaceAll(cardMemory.replace(/^-+\s*/, ""), "<#>")
);
}
}
context = context.replace(cardEvent.titleHeader, (
"\n\n{%@MEM@%" + cardMemoriesText + "%@MEM@%}\n"
));
isRemembering = true;
}
}
cardEvents.delete(titleKey);
break;
}
if (breakForCompression) {
break;
}
// Simplify auto-card titles which contain an obvious surname
const titleHeaderMatch = card.entry.match(titleHeaderMatcher);
if (!titleHeaderMatch) {
continue;
}
const [oldTitleHeader, oldTitle] = titleHeaderMatch;
if (!isAuto(oldTitle)) {
continue;
}
const surname = isNamed(oldTitle, true);
if (typeof surname !== "string") {
continue;
}
const newTitle = oldTitle.replace(" " + surname, "");
const [oldTitleKey, newTitleKey] = [oldTitle, newTitle].map(title => title.toLowerCase());
if (oldTitleKey === newTitleKey) {
continue;
}
// Preemptively mitigate some global state considered within the formatTitle scope
clearTransientTitles();
AC.database.titles.used = ["%@%"];
[used, forenames, surnames].forEach(nameset => nameset.add("%@%"));
// Premature optimization is the root of all evil
const newKey = formatTitle(newTitle).newKey;
clearTransientTitles();
if (newKey === "") {
Internal.getUsedTitles();
continue;
}
if (oldTitleKey in AC.database.memories.associations) {
AC.database.memories.associations[newTitleKey] = AC.database.memories.associations[oldTitleKey];
delete AC.database.memories.associations[oldTitleKey];
}
if (AC.compression.titleKey === oldTitleKey) {
AC.compression.titleKey = newTitleKey;
}
card.entry = card.entry.replace(oldTitleHeader, oldTitleHeader.replace(oldTitle, newTitle));
card.keys = buildKeys(card.keys.replaceAll(" " + surname, ""), newKey);
Internal.getUsedTitles();
function exceedsMemoryLimit() {
return ((function() {
const memoryLimitMatch = card.description.match(/limits?\s*:\s*(\d+)\s*}/i);
if (memoryLimitMatch) {
return validateMemoryLimit(parseInt(memoryLimitMatch[1], 10));
} else {
return AC.config.defaultMemoryLimit;
}
})() < (function() {
const cardMemories = extractCardMemories();
if (cardMemories.missing) {
return card.description;
} else {
return cardMemories.text;
}
})().length);
}
function prepareMemoryCompression(titleKey) {
AC.compression.oldMemoryBank = isolateMemories(extractCardMemories().text);
if (AC.compression.oldMemoryBank.length === 0) {
return false;
}
AC.compression.completed = 0;
AC.compression.titleKey = titleKey;
AC.compression.vanityTitle = cleanSpaces(card.title.trim());
AC.compression.responseEstimate = (function() {
const responseEstimate = estimateResponseLength();
if (responseEstimate === -1) {
return 1400
} else {
return responseEstimate;
}
})();
AC.compression.lastConstructIndex = -1;
AC.compression.newMemoryBank = [];
return true;
}
function extractCardMemories() {
const memoryHeaderMatch = card.description.match(
/(?<={\s*updates?\s*:[\s\S]*?,\s*limits?\s*:[\s\S]*?})[\s\S]*$/i
);
if (memoryHeaderMatch) {
return O.f({missing: false, text: cleanSpaces(memoryHeaderMatch[0].trim())});
} else {
return O.f({missing: true, text: ""});
}
}
}
// Remove repeated memories plus any remaining title headers
context = (context
.replace(/(\s*<#>\s*)+/g, "\n")
.replace(titleHeaderPatternGlobal, "\n\n")
.replace(/World\s*Lore\s*:\s*/i, "World Lore:\n")
.replace(/Memories\s*:\s*(?=Recent\s*Story\s*:|$)/i, "")
);
// Prompt the AI to generate a new card entry, compress an existing card's memories, or continue the story
let isGenerating = false;
let isCompressing = false;
if (isPendingGeneration()) {
promptGeneration();
} else if (isAwaitingGeneration()) {
AC.generation.workpiece = AC.generation.pending.shift();
promptGeneration();
} else if (isPendingCompression()) {
promptCompression();
} else if (AC.signal.recheckRetryOrErase) {
// Do nothing 😜
} else if ((AC.generation.cooldown <= 0) && (0 < AC.database.titles.candidates.length)) {
// Prepare to automatically construct a new plot-relevant story card by selecting a title
let selectedTitle = (function() {
if (AC.database.titles.candidates.length === 1) {
return AC.database.titles.candidates[0][0];
} else if (!isCandidatesSorted) {
sortCandidates();
}
const mostRelevantTitle = AC.database.titles.candidates[0][0];
if ((AC.database.titles.candidates.length < 16) || (Math.random() < 0.6667)) {
// Usually, 2/3 of the time, the most relevant title is selected
return mostRelevantTitle;
}
// Occasionally (1/3 of the time once the candidates databases has at least 16 titles) make a completely random selection between the top 4 most recently occuring title candidates which are NOT the top 2 most relevant titles. Note that relevance !== recency
// This gives non-character titles slightly better odds of being selected for card generation due to the relevance sorter's inherent bias towards characters; they tend to appear far more often in prose
return (AC.database.titles.candidates
// Create a shallow copy to avoid modifying AC.database.titles.candidates itself
// Add index to preserve original positions whenever ties occur during sorting
.map((candidate, index) => ({candidate, index}))
// Sort by each candidate's most recent turn
.sort((a, b) => {
const turnDiff = b.candidate[b.candidate.length - 1] - a.candidate[a.candidate.length - 1];
if (turnDiff === 0) {
// Don't change indices in the case of a tie
return (a.index - b.index);
} else {
// No tie here, sort by recency
return turnDiff;
}
})
// Get the top 6 most recent titles (4 + 2 because the top 2 relevant titles may be present)
.slice(0, 6)
// Extract only the title names
.map(element => element.candidate[0])
// Exclude the top 2 most relevant titles
.filter(title => ((title !== mostRelevantTitle) && (title !== AC.database.titles.candidates[1][0])))
// Ensure only 4 titles remain
.slice(0, 4)
)[Math.floor(Math.random() * 4)];
})();
while (!Internal.generateCard(O.f({title: selectedTitle}))) {
// This is an emergency precaution, I don't expect the interior of this while loop to EVER execute
// That said, it's crucial for the while condition be checked at least once, because Internal.generateCard appends an element to AC.generation.pending as a side effect
const lowerSelectedTitle = formatTitle(selectedTitle).newTitle.toLowerCase();
const index = AC.database.titles.candidates.findIndex(candidate => {
return (formatTitle(candidate[0]).newTitle.toLowerCase() === lowerSelectedTitle);
});
if (index === -1) {
// Should be impossible
break;
}
AC.database.titles.candidates.splice(index, 1);
if (AC.database.titles.candidates.length === 0) {
break;
}
selectedTitle = AC.database.titles.candidates[0][0];
}
if (isAwaitingGeneration()) {
// Assign the workpiece so card generation may fully commence!
AC.generation.workpiece = AC.generation.pending.shift();
promptGeneration();
} else if (isPendingCompression()) {
promptCompression();
}
} else if (
(AC.chronometer.step || forceStep)
&& (0 < AC.generation.cooldown)
&& (AC.config.addCardCooldown !== 9999)
) {
AC.generation.cooldown--;
}
if (shouldTrimContext()) {
// Truncate context based on AC.signal.maxChars, begin by individually removing the oldest sentences from the recent story portion of the context window
const recentStoryPattern = /Recent\s*Story\s*:\s*([\s\S]*?)(%@GEN@%|%@COM@%|\s\[\s*Author's\s*note\s*:|$)/i;
const recentStoryMatch = context.match(recentStoryPattern);
if (recentStoryMatch) {
const recentStory = recentStoryMatch[1];
let sentencesJoined = recentStory;
// Split by the whitespace chars following each sentence (without consuming)
const sentences = splitBySentences(recentStory);
// [minimum num of story sentences] = ([max chars for context] / 6) / [average chars per sentence]
const sentencesMinimum = Math.ceil(
(AC.signal.maxChars / 6) / (
boundInteger(1, context.length) / boundInteger(1, sentences.length)
)
) + 1;
do {
if (sentences.length < sentencesMinimum) {
// A minimum of n many recent story sentences must remain
// Where n represents a sentence count equal to roughly 16.7% of the full context chars
break;
}
// Remove the first (oldest) recent story sentence
sentences.shift();
// Check if the total length exceeds the AC.signal.maxChars limit
sentencesJoined = sentences.join("");
} while (AC.signal.maxChars < (context.length - recentStory.length + sentencesJoined.length + 3));
// Rebuild the context with the truncated recentStory
context = context.replace(recentStoryPattern, "Recent Story:\n" + sentencesJoined + recentStoryMatch[2]);
}
if (isRemembering && shouldTrimContext()) {
// Next remove loaded card memories (if any) with top-down priority, one card at a time
do {
// This matcher relies on its case-sensitivity
const cardMemoriesMatch = context.match(/{%@MEM@%([\s\S]+?)%@MEM@%}/);
if (!cardMemoriesMatch) {
break;
}
context = context.replace(cardMemoriesMatch[0], (cardMemoriesMatch[0]
.replace(cardMemoriesMatch[1], "")
// Set the MEM tags to lowercase to avoid repeated future matches
.toLowerCase()
));
} while (AC.signal.maxChars < (context.length + 3));
}
if (shouldTrimContext()) {
// If the context is still too long, just trim from the beginning I guess 🤷‍♀️
context = context.slice(context.length - AC.signal.maxChars + 1);
}
}
if (isRemembering) {
// Card memory flags serve no further purpose
context = (context
// Case-insensitivity is crucial here
.replace(/(?<={%@MEM@%)\s*/gi, "")
.replace(/\s*(?=%@MEM@%})/gi, "")
.replace(/{%@MEM@%%@MEM@%}\s?/gi, "")
.replaceAll("{%@MEM@%", "{ Memories:\n")
.replaceAll("%@MEM@%}", " }")
);
}
if (isGenerating) {
// Likewise for the card entry generation delimiter
context = context.replaceAll("%@GEN@%", "");
} else if (isCompressing) {
// Or the (mutually exclusive) card memory compression delimiter
context = context.replaceAll("%@COM@%", "");
}
CODOMAIN.initialize(context);
function isolateMemories(memoriesText) {
return (memoriesText
.split("\n")
.map(memory => cleanSpaces(memory.trim().replace(/^-+\s*/, "")))
.filter(memory => (memory !== ""))
);
}
function isAuto(title) {
return auto.has(title.toLowerCase());
}
function promptCompression() {
isGenerating = false;
const cardEntryText = (function() {
const card = getAutoCard(AC.compression.titleKey);
if (card === null) {
return null;
}
const entryLines = formatEntry(card.entry).trimEnd().split("\n");
if (Object.is(entryLines[0].trim(), "")) {
return "";
}
for (let i = 0; i < entryLines.length; i++) {
entryLines[i] = entryLines[i].trim();
if (/[a-zA-Z]$/.test(entryLines[i])) {
entryLines[i] += ".";
}
entryLines[i] += " ";
}
return entryLines.join("");
})();
if (cardEntryText === null) {
// Safety measure
resetCompressionProperties();
return;
}
repositionAN();
// The "%COM%" substring serves as a temporary delimiter for later context length trucation
context = context.trimEnd() + "\n\n" + cardEntryText + (
[...AC.compression.newMemoryBank, ...AC.compression.oldMemoryBank].join(" ")
) + "%@COM@%\n\n" + (function() {
const memoryConstruct = (function() {
if (AC.compression.lastConstructIndex === -1) {
for (let i = 0; i < AC.compression.oldMemoryBank.length; i++) {
AC.compression.lastConstructIndex = i;
const memoryConstruct = buildMemoryConstruct();
if ((
(AC.config.memoryCompressionRatio / 10) * AC.compression.responseEstimate
) < memoryConstruct.length) {
return memoryConstruct;
}
}
} else {
// The previous card memory compression attempt produced a bad output
AC.compression.lastConstructIndex = boundInteger(
0, AC.compression.lastConstructIndex + 1, AC.compression.oldMemoryBank.length - 1
);
}
return buildMemoryConstruct();
})();
// Fill all %{title} placeholders
const precursorPrompt = insertTitle(AC.config.compressionPrompt, AC.compression.vanityTitle).trim();
const memoryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*memor(y|ies)\s*}+/gi;
if (memoryPlaceholderPattern.test(precursorPrompt)) {
// Fill all %{memory} placeholders with a selection of pending old memories
return precursorPrompt.replace(memoryPlaceholderPattern, memoryConstruct);
} else {
// Append the partial entry to the end of context
return precursorPrompt + "\n\n" + memoryConstruct;
}
})() + "\n\n";
isCompressing = true;
return;
}
function promptGeneration() {
repositionAN();
// All %{title} placeholders were already filled during this workpiece's initialization
// The "%GEN%" substring serves as a temporary delimiter for later context length trucation
context = context.trimEnd() + "%@GEN@%\n\n" + (function() {
// For context only, remove the title header from this workpiece's partially completed entry
const partialEntry = formatEntry(AC.generation.workpiece.entry);
const entryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*entry\s*}+/gi;
if (entryPlaceholderPattern.test(AC.generation.workpiece.prompt)) {
// Fill all %{entry} placeholders with the partial entry
return AC.generation.workpiece.prompt.replace(entryPlaceholderPattern, partialEntry);
} else {
// Append the partial entry to the end of context
return AC.generation.workpiece.prompt.trimEnd() + "\n\n" + partialEntry;
}
})();
isGenerating = true;
return;
}
function repositionAN() {
// Move the Author's Note further back in context during card generation (should still be considered)
const authorsNotePattern = /\s*(\[\s*Author's\s*note\s*:[\s\S]*\])\s*/i;
const authorsNoteMatch = context.match(authorsNotePattern);
if (!authorsNoteMatch) {
return;
}
const leadingSpaces = context.match(/^\s*/)[0];
context = context.replace(authorsNotePattern, " ").trimStart();
const recentStoryPattern = /\s*Recent\s*Story\s*:\s*/i;
if (recentStoryPattern.test(context)) {
// Remove author's note from its original position and insert above "Recent Story:\n"
context = (context
.replace(recentStoryPattern, "\n\n" + authorsNoteMatch[1] + "\n\nRecent Story:\n")
.trimStart()
);
} else {
context = authorsNoteMatch[1] + "\n\n" + context;
}
context = leadingSpaces + context;
return;
}
function sortCandidates() {
if (AC.database.titles.candidates.length < 2) {
return;
}
const turnRange = boundInteger(1, maxTurn - minTurn);
const recencyExponent = Math.log10(turnRange) + 1.85;
// Sort the database of available title candidates by relevance
AC.database.titles.candidates.sort((a, b) => {
return relevanceScore(b) - relevanceScore(a);
});
function relevanceScore(candidate) {
// weight = (((turn - minTurn) / (maxTurn - minTurn)) + 1)^(log10(maxTurn - minTurn) + 1.85)
return candidate.slice(1).reduce((sum, turn) => {
// Apply exponential scaling to give far more weight to recent turns
return sum + Math.pow((
// The recency weight's exponent scales by log10(turnRange) + 1.85
// Shhh don't question it 😜
((turn - minTurn) / turnRange) + 1
), recencyExponent);
}, 0);
}
return;
}
function shouldTrimContext() {
return (AC.signal.maxChars <= context.length);
}
function setCandidateTurnBounds(candidate) {
// candidate: ["Example Title", 0, 1, 2, 3]
minTurn = boundInteger(0, minTurn, candidate[1]);
maxTurn = boundInteger(candidate[candidate.length - 1], maxTurn);
return;
}
function disableAutoCards() {
AC.signal.forceToggle = null;
// Auto-Cards has been disabled
AC.config.doAC = false;
// Deconstruct the "Configure Auto-Cards" story card
unbanTitle(configureCardTemplate.title);
eraseCard(configureCard);
// Signal the construction of "Edit to enable Auto-Cards" during the next onOutput hook
AC.signal.swapControlCards = true;
// Post a success message
notify("Disabled! Use the \"Edit to enable Auto-Cards\" story card to undo");
CODOMAIN.initialize(TEXT);
return;
}
break; }
case "output": {
// AutoCards was called within the output modifier
const output = prettifyEmDashes(TEXT);
if (0 < AC.chronometer.postpone) {
// Do not capture or replace any outputs during this turn
promoteAmnesia();
if (permitOutput()) {
CODOMAIN.initialize(output);
}
} else if (AC.signal.swapControlCards) {
if (permitOutput()) {
CODOMAIN.initialize(output);
}
} else if (isPendingGeneration()) {
const textClone = prettifyEmDashes(text);
AC.chronometer.amnesia = 0;
AC.generation.completed++;
const generationsRemaining = (function() {
if (
textClone.includes("\"")
|| /(?<=^|\s|—|\(|\[|{)sa(ys?|id)(?=\s|\.|\?|!|,|;|—|\)|\]|}|$)/i.test(textClone)
) {
// Discard full outputs containing "say" or quotations
// To build coherent entries, the AI must not attempt to continue the story
return skip(estimateRemainingGens());
}
const oldSentences = (splitBySentences(formatEntry(AC.generation.workpiece.entry))
.map(sentence => sentence.trim())
.filter(sentence => (2 < sentence.length))
);
const seenSentences = new Set();
const entryAddition = splitBySentences(textClone
.replace(/[\*_~]/g, "")
.replace(/:+/g, "#")
.replace(/\s+/g, " ")
).map(sentence => (sentence
.trim()
.replace(/^-+\s*/, "")
)).filter(sentence => (
// Remove empty strings
(sentence !== "")
// Remove colon ":" headers or other stinky symbols because me no like 😠
&& !/[#><@]/.test(sentence)
// Remove previously repeated sentences
&& !oldSentences.some(oldSentence => (0.75 < similarityScore(oldSentence, sentence)))
// Remove repeated sentences from within entryAddition itself
&& ![...seenSentences].some(seenSentence => (0.75 < similarityScore(seenSentence, sentence)))
// Simply ensure this sentence is henceforth unique
&& seenSentences.add(sentence)
)).join(" ").trim() + " ";
if (entryAddition === " ") {
return skip(estimateRemainingGens());
} else if (
/^{title:[\s\S]*?}$/.test(AC.generation.workpiece.entry.trim())
&& (AC.generation.workpiece.entry.length < 111)
) {
AC.generation.workpiece.entry += "\n" + entryAddition;
} else {
AC.generation.workpiece.entry += entryAddition;
}
if (AC.generation.workpiece.limit < AC.generation.workpiece.entry.length) {
let exit = false;
let truncatedEntry = AC.generation.workpiece.entry.trimEnd();
const sentences = splitBySentences(truncatedEntry);
for (let i = sentences.length - 1; 0 <= i; i--) {
if (!sentences[i].includes("\n")) {
sentences.splice(i, 1);
truncatedEntry = sentences.join("").trimEnd();
if (truncatedEntry.length <= AC.generation.workpiece.limit) {
break;
}
continue;
}
// Lines only matter for initial entries provided via AutoCards().API.generateCard
const lines = sentences[i].split("\n");
for (let j = lines.length - 1; 0 <= j; j--) {
lines.splice(j, 1);
sentences[i] = lines.join("\n");
truncatedEntry = sentences.join("").trimEnd();
if (truncatedEntry.length <= AC.generation.workpiece.limit) {
// Exit from both loops
exit = true;
break;
}
}
if (exit) {
break;
}
}
if (truncatedEntry.length < 150) {
// Disregard the previous sentence/line-based truncation attempt
AC.generation.workpiece.entry = limitString(
AC.generation.workpiece.entry, AC.generation.workpiece.limit
);
// Attempt to remove the last word/fragment
truncatedEntry = AC.generation.workpiece.entry.replace(/\s*\S+$/, "");
if (150 <= truncatedEntry) {
AC.generation.workpiece.entry = truncatedEntry;
}
} else {
AC.generation.workpiece.entry = truncatedEntry;
}
return 0;
} else if ((AC.generation.workpiece.limit - 50) <= AC.generation.workpiece.entry.length) {
AC.generation.workpiece.entry = AC.generation.workpiece.entry.trimEnd();
return 0;
}
function skip(remaining) {
if (AC.generation.permitted <= AC.generation.completed) {
AC.generation.workpiece.entry = AC.generation.workpiece.entry.trimEnd();
return 0;
}
return remaining;
}
function estimateRemainingGens() {
const responseEstimate = estimateResponseLength();
if (responseEstimate === -1) {
return 1;
}
const remaining = boundInteger(1, Math.round(
(150 + AC.generation.workpiece.limit - AC.generation.workpiece.entry.length) / responseEstimate
));
if (AC.generation.permitted === 34) {
AC.generation.permitted = boundInteger(6, Math.floor(3.5 * remaining), 32);
}
return remaining;
}
return skip(estimateRemainingGens());
})();
postOutputMessage(textClone, AC.generation.completed / Math.min(
AC.generation.permitted,
AC.generation.completed + generationsRemaining
));
if (generationsRemaining <= 0) {
notify("\"" + AC.generation.workpiece.title + "\" was successfully added to your story cards!");
constructCard(O.f({
type: AC.generation.workpiece.type,
title: AC.generation.workpiece.title,
keys: AC.generation.workpiece.keys,
entry: (function() {
if (!AC.config.bulletedListMode) {
return AC.generation.workpiece.entry;
}
const sentences = splitBySentences(
formatEntry(
AC.generation.workpiece.entry.replace(/\s+/g, " ")
).replace(/:+/g, "#")
).map(sentence => {
sentence = (sentence
.replaceAll("#", ":")
.trim()
.replace(/^-+\s*/, "")
);
if (sentence.length < 12) {
return sentence;
} else {
return "\n- " + sentence.replace(/\s*[\.\?!]+$/, "");
}
});
const titleHeader = "{title: " + AC.generation.workpiece.title + "}";
if (sentences.every(sentence => (sentence.length < 12))) {
const sentencesJoined = sentences.join(" ").trim();
if (sentencesJoined === "") {
return titleHeader;
} else {
return limitString(titleHeader + "\n" + sentencesJoined, 2000);
}
}
for (let i = sentences.length - 1; 0 <= i; i--) {
const bulletedEntry = cleanSpaces(titleHeader + sentences.join(" ")).trimEnd();
if (bulletedEntry.length <= 2000) {
return bulletedEntry;
}
if (sentences.length === 1) {
break;
}
sentences.splice(i, 1);
}
return limitString(AC.generation.workpiece.entry, 2000);
})(),
description: AC.generation.workpiece.description,
}), newCardIndex());
AC.generation.cooldown = AC.config.addCardCooldown;
AC.generation.completed = 0;
AC.generation.permitted = 34;
AC.generation.workpiece = O.f({});
clearTransientTitles();
}
} else if (isPendingCompression()) {
const textClone = prettifyEmDashes(text);
AC.chronometer.amnesia = 0;
AC.compression.completed++;
const compressionsRemaining = (function() {
const newMemory = (textClone
// Remove some dumb stuff
.replace(/^[\s\S]*:/g, "")
.replace(/[\*_~#><@\[\]{}`\\]/g, " ")
// Remove bullets
.trim().replace(/^-+\s*/, "").replace(/\s*-+$/, "").replace(/\s*-\s+/g, " ")
// Condense consecutive whitespace
.replace(/\s+/g, " ")
);
if ((AC.compression.oldMemoryBank.length - 1) <= AC.compression.lastConstructIndex) {
// Terminate this compression cycle; the memory construct cannot grow any further
AC.compression.newMemoryBank.push(newMemory);
return 0;
} else if ((newMemory.trim() !== "") && (newMemory.length < buildMemoryConstruct().length)) {
// Good output, preserve and then proceed onwards
AC.compression.oldMemoryBank.splice(0, AC.compression.lastConstructIndex + 1);
AC.compression.lastConstructIndex = -1;
AC.compression.newMemoryBank.push(newMemory);
} else {
// Bad output, discard and then try again
AC.compression.responseEstimate += 200;
}
return boundInteger(1, joinMemoryBank(AC.compression.oldMemoryBank).length) / AC.compression.responseEstimate;
})();
postOutputMessage(textClone, AC.compression.completed / (AC.compression.completed + compressionsRemaining));
if (compressionsRemaining <= 0) {
const card = getAutoCard(AC.compression.titleKey);
if (card === null) {
notify(
"Failed to apply summarized memories for \"" + AC.compression.vanityTitle + "\" due to a missing or invalid AC card title header!"
);
} else {
const memoryHeaderMatch = card.description.match(
/(?<={\s*updates?\s*:[\s\S]*?,\s*limits?\s*:[\s\S]*?})[\s\S]*$/i
);
if (memoryHeaderMatch) {
// Update the card memory bank
notify("Memories for \"" + AC.compression.vanityTitle + "\" were successfully summarized!");
card.description = card.description.replace(memoryHeaderMatch[0], (
"\n" + joinMemoryBank(AC.compression.newMemoryBank)
));
} else {
notify(
"Failed to apply summarizes memories for \"" + AC.compression.vanityTitle + "\" due to a missing or invalid AC card memory header!"
);
}
}
resetCompressionProperties();
} else if (AC.compression.completed === 1) {
notify("Summarizing excess memories for \"" + AC.compression.vanityTitle + "\"");
}
function joinMemoryBank(memoryBank) {
return cleanSpaces("- " + memoryBank.join("\n- "));
}
} else if (permitOutput()) {
CODOMAIN.initialize(output);
}
concludeOutputBlock((function() {
if (AC.signal.swapControlCards) {
return getConfigureCardTemplate();
} else {
return null;
}
})())
function postOutputMessage(textClone, ratio) {
if (!permitOutput()) {
// Do nothing
} else if (0.5 < similarityScore(textClone, output)) {
// To improve Auto-Cards' compatability with other scripts, I only bother to replace the output text when the original and new output texts have a similarity score above a particular threshold. Otherwise, I may safely assume the output text has already been replaced by another script and thus skip this step.
CODOMAIN.initialize(
getPrecedingNewlines() + ">>> please select \"continue\" (" + Math.round(ratio * 100) + "%) <<<\n\n"
);
} else {
CODOMAIN.initialize(output);
}
return;
}
break; }
default: {
CODOMAIN.initialize(TEXT);
break; }
}
// Get an individual story card reference via titleKey
function getAutoCard(titleKey) {
return Internal.getCard(card => card.entry.toLowerCase().startsWith("{title: " + titleKey + "}"));
}
function buildMemoryConstruct() {
return (AC.compression.oldMemoryBank
.slice(0, AC.compression.lastConstructIndex + 1)
.join(" ")
);
}
// Estimate the average AI response char count based on recent continue outputs
function estimateResponseLength() {
if (!Array.isArray(history) || (history.length === 0)) {
return -1;
}
const charCounts = [];
for (let i = 0; i < history.length; i++) {
const action = readPastAction(i);
if ((action.type === "continue") && !action.text.includes("<<<")) {
charCounts.push(action.text.length);
}
}
if (charCounts.length < 7) {
if (charCounts.length === 0) {
return -1;
} else if (charCounts.length < 4) {
return boundInteger(350, charCounts[0]);
}
charCounts.splice(3);
}
return boundInteger(175, Math.floor(
charCounts.reduce((sum, charCount) => {
return sum + charCount;
}, 0) / charCounts.length
));
}
// Evalute how similar two strings are on the range [0, 1]
function similarityScore(strA, strB) {
if (strA === strB) {
return 1;
}
// Normalize both strings for further comparison purposes
const [cleanA, cleanB] = [strA, strB].map(str => (str
.replace(/[0-9\s]/g, " ")
.trim()
.replace(/ +/g, " ")
.toLowerCase()
));
if (cleanA === cleanB) {
return 1;
}
// Compute the Levenshtein distance
const [lengthA, lengthB] = [cleanA, cleanB].map(str => str.length);
// I love DP ❤️ (dynamic programming)
const dp = Array(lengthA + 1).fill(null).map(() => Array(lengthB + 1).fill(0));
for (let i = 0; i <= lengthA; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= lengthB; j++) {
dp[0][j] = j;
}
for (let i = 1; i <= lengthA; i++) {
for (let j = 1; j <= lengthB; j++) {
if (cleanA[i - 1] === cleanB[j - 1]) {
// No cost if chars match, swipe right 😎
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
// Deletion
dp[i - 1][j] + 1,
// Insertion
dp[i][j - 1] + 1,
// Substitution
dp[i - 1][j - 1] + 1
);
}
}
}
// Convert distance to similarity score (1 - (distance / maxLength))
return 1 - (dp[lengthA][lengthB] / Math.max(lengthA, lengthB));
}
function splitBySentences(prose) {
// Don't split sentences on honorifics or abbreviations such as "Mr.", "Mrs.", "etc."
return (prose
.replace(new RegExp("(?<=\\s|\"|\\(|—|\\[|'|{|^)(?:" + ([...Words.honorifics, ...Words.abbreviations]
.map(word => word.replace(".", ""))
.join("|")
) + ")\\.", "gi"), "$1%@%")
.split(/(?<=[\.\?!:]["\)'\]}]?\s+)(?=[^\p{Ll}\s])/u)
.map(sentence => sentence.replaceAll("%@%", "."))
);
}
function formatEntry(partialEntry) {
const cleanedEntry = cleanSpaces(partialEntry
.replace(/^{title:[\s\S]*?}/, "")
.replace(/[#><@*_~]/g, "")
.trim()
).replace(/(?<=^|\n)-+\s*/g, "");
if (cleanedEntry === "") {
return "";
} else {
return cleanedEntry + " ";
}
}
// Resolve malformed em dashes (common AI cliche)
function prettifyEmDashes(str) {
return str.replace(/(?<!^\s*)(?: - | ? ?)(?!\s*$)/g, "—");
}
function getConfigureCardTemplate() {
const names = getControlVariants().configure;
return O.f({
type: AC.config.defaultCardType,
title: names.title,
keys: names.keys,
entry: getConfigureCardEntry(),
description: getConfigureCardDescription()
});
}
function getConfigureCardEntry() {
return prose(
"> Auto-Cards automatically creates and updates plot-relevant story cards while you play. You may configure the following settings by replacing \"false\" with \"true\" (and vice versa) or by adjusting numbers for the appropriate settings.",
"> Disable Auto-Cards: false",
"> Show detailed guide: false",
"> Delete all automatic story cards: false",
"> Reset all config settings and prompts: false",
"> Pin this config card near the top: " + AC.config.pinConfigureCard,
"> Minimum turns cooldown for new cards: " + AC.config.addCardCooldown,
"> New cards use a bulleted list format: " + AC.config.bulletedListMode,
"> Maximum entry length for new cards: " + AC.config.defaultEntryLimit,
"> New cards perform memory updates: " + AC.config.defaultCardsDoMemoryUpdates,
"> Card memory bank preferred length: " + AC.config.defaultMemoryLimit,
"> Memory summary compression ratio: " + AC.config.memoryCompressionRatio,
"> Exclude all-caps from title detection: " + AC.config.ignoreAllCapsTitles,
"> Also detect titles from player inputs: " + AC.config.readFromInputs,
"> Minimum turns age for title detection: " + AC.config.minimumLookBackDistance,
"> Use Live Script Interface v2: " + (AC.config.LSIv2 !== null),
"> Log debug data in a separate card: " + AC.config.showDebugData
);
}
function getConfigureCardDescription() {
return limitString(O.v(prose(
Words.delimiter,
"> AI prompt to generate new cards:",
limitString(AC.config.generationPrompt.trim(), 4350).trimEnd(),
Words.delimiter,
"> AI prompt to summarize card memories:",
limitString(AC.config.compressionPrompt.trim(), 4350).trimEnd(),
Words.delimiter,
"> Titles banned from new card creation:",
AC.database.titles.banned.join(", ")
)), 9850);
}
} else {
// Auto-Cards is currently disabled
switch(HOOK) {
case "input": {
if (/\/\s*A\s*C/i.test(text)) {
CODOMAIN.initialize(doPlayerCommands(text));
} else {
CODOMAIN.initialize(TEXT);
}
break; }
case "context": {
// AutoCards was called within the context modifier
advanceChronometer();
// Get or construct the "Edit to enable Auto-Cards" story card
const enableCardTemplate = getEnableCardTemplate();
const enableCard = getSingletonCard(true, enableCardTemplate);
banTitle(enableCardTemplate.title);
pinAndSortCards(enableCard);
if (AC.signal.forceToggle) {
enableAutoCards();
} else if (enableCard.entry !== enableCardTemplate.entry) {
if ((extractSettings(enableCard.entry)?.enableautocards === true) && (AC.signal.forceToggle !== false)) {
// Use optional chaining to check the existence of enableautocards before accessing its value
enableAutoCards();
} else {
// Repair the damaged card entry
enableCard.entry = enableCardTemplate.entry;
}
}
AC.signal.forceToggle = null;
CODOMAIN.initialize(TEXT);
function enableAutoCards() {
// Auto-Cards has been enabled
AC.config.doAC = true;
// Deconstruct the "Edit to enable Auto-Cards" story card
unbanTitle(enableCardTemplate.title);
eraseCard(enableCard);
// Signal the construction of "Configure Auto-Cards" during the next onOutput hook
AC.signal.swapControlCards = true;
// Post a success message
notify("Enabled! You may now edit the \"Configure Auto-Cards\" story card");
return;
}
break; }
case "output": {
// AutoCards was called within the output modifier
promoteAmnesia();
if (permitOutput()) {
CODOMAIN.initialize(TEXT);
}
concludeOutputBlock((function() {
if (AC.signal.swapControlCards) {
return getEnableCardTemplate();
} else {
return null;
}
})());
break; }
default: {
CODOMAIN.initialize(TEXT);
break; }
}
function getEnableCardTemplate() {
const names = getControlVariants().enable;
return O.f({
type: AC.config.defaultCardType,
title: names.title,
keys: names.keys,
entry: prose(
"> Auto-Cards automatically creates and updates plot-relevant story cards while you play. To enable this system, simply edit the \"false\" below to say \"true\" instead!",
"> Enable Auto-Cards: false"),
description: "Perform any Do/Say/Story/Continue action within your adventure to apply this change!"
});
}
}
function hoistConst() { return (class Const {
// This helps me debug stuff uwu
#constant;
constructor(...args) {
if (args.length !== 0) {
this.constructor.#throwError([[(args.length === 1), "Const cannot be instantiated with a parameter"], ["Const cannot be instantiated with parameters"]]);
} else {
O.f(this);
return this;
}
}
declare(...args) {
if (args.length !== 0) {
this.constructor.#throwError([[(args.length === 1), "Instances of Const cannot be declared with a parameter"], ["Instances of Const cannot be declared with parameters"]]);
} else if (this.#constant === undefined) {
this.#constant = null;
return this;
} else if (this.#constant === null) {
this.constructor.#throwError("Instances of Const cannot be redeclared");
} else {
this.constructor.#throwError("Instances of Const cannot be redeclared after initialization");
}
}
initialize(...args) {
if (args.length !== 1) {
this.constructor.#throwError([[(args.length === 0), "Instances of Const cannot be initialized without a parameter"], ["Instances of Const cannot be initialized with multiple parameters"]]);
} else if (this.#constant === null) {
this.#constant = [args[0]];
return this;
} else if (this.#constant === undefined) {
this.constructor.#throwError("Instances of Const cannot be initialized before declaration");
} else {
this.constructor.#throwError("Instances of Const cannot be reinitialized");
}
}
read(...args) {
if (args.length !== 0) {
this.constructor.#throwError([[(args.length === 1), "Instances of Const cannot be read with a parameter"], ["Instances of Const cannot read with any parameters"]]);
} else if (Array.isArray(this.#constant)) {
return this.#constant[0];
} else if (this.#constant === null) {
this.constructor.#throwError("Despite prior declaration, instances of Const cannot be read before initialization");
} else {
this.constructor.#throwError("Instances of Const cannot be read before initialization");
}
}
// An error condition is paired with an error message [condition, message], call #throwError with an array of pairs to throw the message corresponding with the first true condition [[cndtn1, msg1], [cndtn2, msg2], [cndtn3, msg3], ...] The first conditionless array element always evaluates to true ('else')
static #throwError(...args) {
// Look, I thought I was going to use this more at the time okay
const [conditionalMessagesTable] = args;
const codomain = new Const().declare();
const error = O.f(new Error((function() {
const codomain = new Const().declare();
if (Array.isArray(conditionalMessagesTable)) {
const chosenPair = conditionalMessagesTable.find(function(...args) {
const [pair] = args;
const codomain = new Const().declare();
if (Array.isArray(pair)) {
if ((pair.length === 1) && (typeof pair[0] === "string")) {
codomain.initialize(true);
} else if (
(pair.length === 2)
&& (typeof pair[0] === "boolean")
&& (typeof pair[1] === "string")
) {
codomain.initialize(pair[0]);
} else {
Const.#throwError("Const.#throwError encountered an invalid array element of conditionalMessagesTable");
}
} else {
Const.#throwError("Const.#throwError encountered a non-array element within conditionalMessagesTable");
}
return codomain.read();
});
if (Array.isArray(chosenPair)) {
if (chosenPair.length === 1) {
codomain.initialize(chosenPair[0]);
} else {
codomain.initialize(chosenPair[1]);
}
} else {
codomain.initialize("Const.#throwError was not called with any true conditions");
}
} else if (typeof conditionalMessagesTable === "string") {
codomain.initialize(conditionalMessagesTable);
} else {
codomain.initialize("Const.#throwError could not parse the given argument");
}
return codomain.read();
})()));
if (error.stack) {
codomain.initialize(error.stack
.replace(/\(<isolated-vm>:/gi, "(")
.replace(/Error:|at\s*(?:#throwError|Const.(?:declare|initialize|read)|new\s*Const)\s*\(\d+:\d+\)/gi, "")
.replace(/AutoCards\s*\((\d+):(\d+)\)\s*at\s*<isolated-vm>:\d+:\d+\s*$/i, "AutoCards ($1:$2)")
.trim()
.replace(/\s+/g, " ")
);
} else {
codomain.initialize(error.message);
}
throw codomain.read();
}
}); }
function hoistO() { return (class O {
// Some Object class methods are annoyingly verbose for how often I use them 👿
static f(obj) {
return Object.freeze(obj);
}
static v(base) {
return see(Words.copy) + base;
}
static s(obj) {
return Object.seal(obj);
}
}); }
function hoistWords() { return (class Words { static #cache = {}; static {
// Each word list is initialized only once before being cached!
const wordListInitializers = {
// Special-cased honorifics which are excluded from titles and ignored during split-by-sentences operations
honorifics: () => [
"mr.", "ms.", "mrs.", "dr."
],
// Other special-cased abbreviations used to reformat titles and split-by-sentences
abbreviations: () => [
"sr.", "jr.", "etc.", "st.", "ex.", "inc."
],
// Lowercase minor connector words which may exist within titles
minor: () => [
"&", "the", "for", "of", "le", "la", "el"
],
// Removed from shortened titles for improved memory detection and trigger keword assignments
peerage: () => [
"sir", "lord", "lady", "king", "queen", "majesty", "duke", "duchess", "noble", "royal", "emperor", "empress", "great", "prince", "princess", "count", "countess", "baron", "baroness", "archduke", "archduchess", "marquis", "marquess", "viscount", "viscountess", "consort", "grand", "sultan", "sheikh", "tsar", "tsarina", "czar", "czarina", "viceroy", "monarch", "regent", "imperial", "sovereign", "president", "prime", "minister", "nurse", "doctor", "saint", "general", "private", "commander", "captain", "lieutenant", "sergeant", "admiral", "marshal", "baronet", "emir", "chancellor", "archbishop", "bishop", "cardinal", "abbot", "abbess", "shah", "maharaja", "maharani", "councillor", "squire", "lordship", "ladyship", "monseigneur", "mayor", "princeps", "chief", "chef", "their", "my", "his", "him", "he'd", "her", "she", "she'd", "you", "your", "yours", "you'd", "you've", "you'll", "yourself", "mine", "myself", "highness", "excellency", "farmer", "sheriff", "officer", "detective", "investigator", "miss", "mister", "colonel", "professor", "teacher", "agent", "heir", "heiress", "master", "mistress", "headmaster", "headmistress", "principal", "papa", "mama", "mommy", "daddy", "mother", "father", "grandma", "grandpa", "aunt", "auntie", "aunty", "uncle", "cousin", "sister", "brother", "holy", "holiness", "almighty", "senator", "congressman"
],
// Common named entities represent special-cased INVALID card titles. Because these concepts are already abundant within the AI's training data, generating story cards for any of these would be both annoying and superfluous. Therefore, Words.entities is accessed during banned titles initialization to prevent their appearance
entities: () => [
// Seasons
"spring", "summer", "autumn", "fall", "winter",
// Holidays
"halloween", "christmas", "thanksgiving", "easter", "hanukkah", "passover", "ramadan", "eid", "diwali", "new year", "new year eve", "valentine day", "oktoberfest",
// People terms
"mom", "dad", "child", "grandmother", "grandfather", "ladies", "gentlemen", "gentleman", "slave",
// Capitalizable pronoun thingys
"his", "him", "he'd", "her", "she", "she'd", "you", "your", "yours", "you'd", "you've", "you'll", "you're", "yourself", "mine", "myself", "this", "that",
// Religious figures & deities
"god", "jesus", "buddha", "allah", "christ",
// Religious texts & concepts
"bible", "holy bible", "qur'an", "quran", "hadith", "tafsir", "tanakh", "talmud", "torah", "vedas", "vatican", "paganism", "pagan",
// Religions & belief systems
"hindu", "hinduism", "christianity", "islam", "jew", "judaism", "taoism", "buddhist", "buddhism", "catholic", "baptist",
// Common locations
"earth", "moon", "sun", "new york city", "london", "paris", "tokyo", "beijing", "mumbai", "sydney", "berlin", "moscow", "los angeles", "san francisco", "chicago", "miami", "seattle", "vancouver", "toronto", "ottawa", "mexico city", "rio de janeiro", "cape town", "sao paulo", "bangkok", "delhi", "amsterdam", "seoul", "shanghai", "new delhi", "atlanta", "jerusalem", "africa", "north america", "south america", "central america", "asia", "north africa", "south africa", "boston", "rome", "america", "siberia", "new england", "manhattan", "bavaria", "catalonia", "greenland", "hong kong", "singapore",
// Countries & political entities
"china", "india", "japan", "germany", "france", "spain", "italy", "canada", "australia", "brazil", "south africa", "russia", "north korea", "south korea", "iran", "iraq", "syria", "saudi arabia", "afghanistan", "pakistan", "uk", "britain", "england", "scotland", "wales", "northern ireland", "usa", "united states", "united states of america", "mexico", "turkey", "greece", "portugal", "poland", "netherlands", "belgium", "sweden", "norway", "finland", "denmark",
// Organizations & unions
"united nations", "european union", "state", "nato", "nfl", "nba", "fbi", "cia", "harvard", "yale", "princeton", "ivy league", "little league", "nasa", "nsa", "noaa", "osha", "nascar", "daytona 500", "grand prix", "wwe", "mba", "superbowl",
// Currencies
"dollar", "euro", "pound", "yen", "rupee", "peso", "franc", "dinar", "bitcoin", "ethereum", "ruble", "won", "dirham",
// Landmarks
"sydney opera house", "eiffel tower", "statue of liberty", "big ben", "great wall of china", "taj mahal", "pyramids of giza", "grand canyon", "mount everest",
// Events
"world war i", "world war 1", "wwi", "wwii", "world war ii", "world war 2", "wwii", "ww2", "cold war", "brexit", "american revolution", "french revolution", "holocaust", "cuban missile crisis",
// Companies
"google", "microsoft", "apple", "amazon", "facebook", "tesla", "ibm", "intel", "samsung", "sony", "coca-cola", "nike", "ford", "chevy", "pontiac", "chrysler", "volkswagen", "lambo", "lamborghini", "ferrari", "pizza hut", "taco bell", "ai dungeon", "openai", "mcdonald", "mcdonalds", "kfc", "burger king", "disney",
// Nationalities & languages
"english", "french", "spanish", "german", "italian", "russian", "chinese", "japanese", "korean", "arabic", "portuguese", "hindi", "american", "canadian", "mexican", "brazilian", "indian", "australian", "egyptian", "greek", "swedish", "norwegian", "danish", "dutch", "turkish", "iranian", "ukraine", "asian", "british", "european", "polish", "thai", "vietnamese", "filipino", "malaysian", "indonesian", "finnish", "estonian", "latvian", "lithuanian", "czech", "slovak", "hungarian", "romanian", "bulgarian", "serbian", "croatian", "bosnian", "slovenian", "albanian", "georgian", "armenian", "azerbaijani", "kazakh", "uzbek", "mongolian", "hebrew", "persian", "pashto", "urdu", "bengali", "tamil", "telugu", "marathi", "gujarati", "swahili", "zulu", "xhosa", "african", "north african", "south african", "north american", "south american", "central american", "colombian", "argentinian", "chilean", "peruvian", "venezuelan", "ecuadorian", "bolivian", "paraguayan", "uruguayan", "cuban", "dominican", "arabian", "roman", "haitian", "puerto rican", "moroccan", "algerian", "tunisian", "saudi", "emirati", "qatarian", "bahraini", "omani", "yemeni", "syrian", "lebanese", "iraqi", "afghan", "pakistani", "sri lankan", "burmese", "laotian", "cambodian", "hawaiian", "victorian",
// Fantasy stuff
"elf", "elves", "elven", "dwarf", "dwarves", "dwarven", "human", "man", "men", "mankind", "humanity",
// IPs
"pokemon", "pokémon", "minecraft", "beetles", "band-aid", "bandaid", "band aid", "big mac", "gpt", "chatgpt", "gpt-2", "gpt-3", "gpt-4", "gpt-4o", "mixtral", "mistral", "linux", "windows", "mac", "happy meal", "disneyland", "disneyworld",
// US states
"alabama", "alaska", "arizona", "arkansas", "california", "colorado", "connecticut", "delaware", "florida", "georgia", "hawaii", "idaho", "illinois", "indiana", "iowa", "kansas", "kentucky", "louisiana", "maine", "massachusetts", "michigan", "minnesota", "mississippi", "missouri", "nebraska", "nevada", "new hampshire", "new jersey", "new mexico", "new york", "north carolina", "north dakota", "ohio", "oklahoma", "oregon", "pennsylvania", "rhode island", "south carolina", "south dakota", "tennessee", "texas", "utah", "vermont", "west virginia", "wisconsin", "wyoming",
// Canadian Provinces & Territories
"british columbia", "manitoba", "new brunswick", "labrador", "nova scotia", "ontario", "prince edward island", "quebec", "saskatchewan", "northwest territories", "nunavut", "yukon", "newfoundland",
// Australian States & Territories
"new south wales", "queensland", "south australia", "tasmania", "western australia", "australian capital territory",
// idk
"html", "javascript", "python", "java", "c++", "php", "bluetooth", "json", "sql", "word", "dna", "icbm", "npc", "usb", "rsvp", "omg", "brb", "lol", "rofl", "smh", "ttyl", "rubik", "adam", "t-shirt", "tshirt", "t shirt", "led", "leds", "laser", "lasers", "qna", "q&a", "vip", "human resource", "human resources", "llm", "llc", "ceo", "cfo", "coo", "office", "blt", "suv", "suvs", "ems", "emt", "cbt", "cpr", "ferris wheel", "toy", "pet", "plaything", "m o"
],
// Unwanted values
undesirables: () => [
[343332, 451737, 323433, 377817], [436425, 356928, 363825, 444048], [323433, 428868, 310497, 413952], [350097, 66825, 436425, 413952, 406593, 444048], [316932, 330000, 436425, 392073], [444048, 356928, 323433], [451737, 444048, 363825], [330000, 310497, 392073, 399300]
],
delimiter: () => (
"——————————————————————————"
),
// Source code location
copy: () => [
126852, 33792, 211200, 384912, 336633, 310497, 436425, 336633, 33792, 459492, 363825, 436425, 363825, 444048, 33792, 392073, 483153, 33792, 139425, 175857, 33792, 152592, 451737, 399300, 350097, 336633, 406593, 399300, 33792, 413952, 428868, 406593, 343332, 363825, 384912, 336633, 33792, 135168, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 33792, 310497, 399300, 330000, 33792, 428868, 336633, 310497, 330000, 33792, 392073, 483153, 33792, 316932, 363825, 406593, 33792, 343332, 406593, 428868, 33792, 436425, 363825, 392073, 413952, 384912, 336633, 33792, 363825, 399300, 436425, 444048, 428868, 451737, 323433, 444048, 363825, 406593, 399300, 436425, 33792, 406593, 399300, 33792, 310497, 330000, 330000, 363825, 399300, 350097, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 444048, 406593, 33792, 483153, 406593, 451737, 428868, 33792, 436425, 323433, 336633, 399300, 310497, 428868, 363825, 406593, 436425, 35937, 33792, 3355672848, 139592360193, 3300, 3300, 356928, 444048, 444048, 413952, 436425, 111012, 72897, 72897, 413952, 384912, 310497, 483153, 69828, 310497, 363825, 330000, 451737, 399300, 350097, 336633, 406593, 399300, 69828, 323433, 406593, 392073, 72897, 413952, 428868, 406593, 343332, 363825, 384912, 336633, 72897, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 3300, 3300, 126852, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 459492, 79233, 69828, 76032, 69828, 76032, 33792, 363825, 436425, 33792, 310497, 399300, 33792, 406593, 413952, 336633, 399300, 66825, 436425, 406593, 451737, 428868, 323433, 336633, 33792, 436425, 323433, 428868, 363825, 413952, 444048, 33792, 343332, 406593, 428868, 33792, 139425, 175857, 33792, 152592, 451737, 399300, 350097, 336633, 406593, 399300, 33792, 392073, 310497, 330000, 336633, 33792, 316932, 483153, 33792, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 69828, 33792, 261393, 406593, 451737, 33792, 356928, 310497, 459492, 336633, 33792, 392073, 483153, 33792, 343332, 451737, 384912, 384912, 33792, 413952, 336633, 428868, 392073, 363825, 436425, 436425, 363825, 406593, 399300, 33792, 444048, 406593, 33792, 451737, 436425, 336633, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 467313, 363825, 444048, 356928, 363825, 399300, 33792, 483153, 406593, 451737, 428868, 33792, 413952, 336633, 428868, 436425, 406593, 399300, 310497, 384912, 33792, 406593, 428868, 33792, 413952, 451737, 316932, 384912, 363825, 436425, 356928, 336633, 330000, 33792, 436425, 323433, 336633, 399300, 310497, 428868, 363825, 406593, 436425, 35937, 3300, 126852, 33792, 261393, 406593, 451737, 50193, 428868, 336633, 33792, 310497, 384912, 436425, 406593, 33792, 467313, 336633, 384912, 323433, 406593, 392073, 336633, 33792, 444048, 406593, 33792, 336633, 330000, 363825, 444048, 33792, 444048, 356928, 336633, 33792, 139425, 175857, 33792, 413952, 428868, 406593, 392073, 413952, 444048, 436425, 33792, 310497, 399300, 330000, 33792, 444048, 363825, 444048, 384912, 336633, 33792, 336633, 475200, 323433, 384912, 451737, 436425, 363825, 406593, 399300, 436425, 33792, 413952, 428868, 406593, 459492, 363825, 330000, 336633, 330000, 33792, 316932, 336633, 384912, 406593, 467313, 69828, 33792, 175857, 33792, 436425, 363825, 399300, 323433, 336633, 428868, 336633, 384912, 483153, 33792, 356928, 406593, 413952, 336633, 33792, 483153, 406593, 451737, 33792, 336633, 399300, 370788, 406593, 483153, 33792, 483153, 406593, 451737, 428868, 33792, 310497, 330000, 459492, 336633, 399300, 444048, 451737, 428868, 336633, 436425, 35937, 33792, 101128769412, 106046468352, 3300
],
// Card interface names reserved for use within LSIv2
reserved: () => ({
library: "Shared Library", input: "Input Modifier", context: "Context Modifier", output: "Output Modifier", guide: "LSIv2 Guide", state: "State Display", log: "Console Log"
}),
// Acceptable config settings which are coerced to true
trues: () => [
"true", "t", "yes", "y", "on"
],
// Acceptable config settings which are coerced to false
falses: () => [
"false", "f", "no", "n", "off"
],
guide: () => prose(
">>> Detailed Guide:",
"Auto-Cards was made by LewdLeah ❤️",
"",
Words.delimiter,
"",
"💡 What is Auto-Cards?",
"Auto-Cards is a plug-and-play script for AI Dungeon that watches your story and automatically writes plot-relevant story cards during normal gameplay. A forgetful AI breaks my immersion, therefore my primary goal was to address the \"object permanence problem\" by extending story cards and memories with deeper automation. Auto-Cards builds a living reference of your adventure's world as you go. For your own convenience, all of this stuff is handled in the background. Though you're certainly welcome to customize various settings or use in-game commands for more precise control",
"",
Words.delimiter,
"",
" 📌 Main Features",
"- Detects named entities from your story and periodically writes new cards",
"- Smart long-term memory updates and summaries for important cards",
"- Fully customizable AI card generation and memory summarization prompts",
"- Optional in-game commands to manually direct the card generation process",
"- Free and open source for anyone to use within their own projects",
"- Compatible with other scripts and includes an external API",
"- Optional in-game scripting interface (LSIv2)",
"",
Words.delimiter,
"",
"⚙️ Config Settings",
"You may, at any time, fine-tune your settings in-game by editing their values within the config card's entry section. Simply swap true/false or tweak numbers where appropriate",
"",
"> Disable Auto-Cards:",
"Turns the whole system off if true",
"",
"> Show detailed guide:",
"If true, shows this player guide in-game",
"",
"> Delete all automatic story cards:",
"Removes every auto-card present in your adventure",
"",
"> Reset all config settings and prompts:",
"Restores all settings and prompts to their original default values",
"",
"> Pin this config card near the top:",
"Keeps the config card pinned high on your cards list",
"",
"> Minimum turns cooldown for new cards:",
"How many turns (minimum) to wait between generating new cards. Using 9999 will pause periodic card generation while still allowing card memory updates to continue",
"",
"> New cards use a bulleted list format:",
"If true, new entries will use bullet points instead of pure prose",
"",
"> Maximum entry length for new cards:",
"Caps how long newly generated card entries can be (in characters)",
"",
"> New cards perform memory updates:",
"If true, new cards will automatically experience memory updates over time",
"",
"> Card memory bank preferred length:",
"Character count threshold before card memories are summarized to save space",
"",
"> Memory summary compression ratio:",
"Controls how much to compress when summarizing long card memory banks",
"(ratio = 10 * old / new ... such that 25 -> 2.5x shorter)",
"",
"> Exclude all-caps from title detection:",
"Prevents all-caps words like \"RUN\" from being parsed as viable titles",
"",
"> Also detect titles from player inputs:",
"Allows your typed Do/Say/Story action inputs to help suggest new card topics. Set to false if you have bad grammar, or if you're German (due to idiosyncratic noun capitalization habits)",
"",
"> Minimum turns age for title detection:",
"How many actions back the script looks when parsing recent titles from your story",
"",
"> Use Live Script Interface v2:",
"Enables LSIv2 for extra scripting magic and advanced control via arbitrary code execution",
"",
"> Log debug data in a separate card:",
"Shows a debug card if set to true",
"",
Words.delimiter,
"",
"✏️ AI Prompts",
"You may specify how the AI handles story card processes by editing either of these two prompts within the config card's notes section",
"",
"> AI prompt to generate new cards:",
"Used when Auto-Cards writes a new card entry. It tells the AI to focus on important plot stuff, avoid fluff, and write in a consistent, polished style. I like to add some personal preferences here when playing my own adventures. \"%{title}\" and \"%{entry}\" are dynamic placeholders for their namesakes",
"",
"> AI prompt to summarize card memories:",
"Summarizes older details within card memory banks to keep everything concise and neat over the long-run. Maintains only the most important details, written in the past tense. \"%{title}\" and \"%{memory}\" are dynamic placeholders for their namesakes",
"",
Words.delimiter,
"",
"⛔ Banned Titles List",
"This list prevents new cards from being created for super generic or unhelpful titles such as North, Tuesday, or December. You may edit these at the bottom of the config card's notes section. Capitalization and plural/singular forms are handled for you, so no worries about that",
"",
"> Titles banned from automatic new card generation:",
"North, East, South, West, and so on...",
"",
Words.delimiter,
"",
"🔑 In-Game Commands (/ac)",
"Use these commands to manually interact with Auto-Cards, simply type them into a Do/Say/Story input action",
"",
"/ac",
"Sets your actual cooldown to 0 and immediately attempts to generate a new card for the most relevant unused title from your story (if one exists)",
"",
"/ac Your Title Goes Here",
"Will immediately begin generating a new story card with the given title",
"Example use: \"/ac Leah\"",
"",
"/ac Your Title Goes Here / Your extra prompt details go here",
"Similar to the previous case, but with additional context to include with the card generation prompt",
"Example use: \"/ac Leah / Focus on Leah's works of artifice and ingenuity\"",
"",
"/ac Your Title Goes Here / Your extra prompt details go here / Your starter entry goes here",
"Again, similar to the previous case, but with an initial card entry for the generator to build upon",
"Example use: \"/ac Leah / Focus on Leah's works of artifice and ingenuity / You are a woman named Leah.\"",
"",
"/ac redo Your Title Goes Here",
"Rewrites your chosen story card, using the old card entry, memory bank, and story context for inspiration. Useful for recreating cards after important character development has occurred",
"Example use: \"/ac redo Leah\"",
"",
"/ac redo Your Title Goes Here / New info goes here",
"Similar to the previous case, but with additional info provided to guide the rewrite according to your additional specifications",
"Example use: \"/ac redo Leah / Leah recently achieved immortality\"",
"",
"/ac redo all",
"Recreates every single auto-card in your adventure. I must warn you though: This is very risky",
"",
"Extra Info:",
"- Invalid titles will fail. It's a technical limitation, sorry 🤷‍♀️",
"- Titles must be unique, unless you're attempting to use \"/ac redo\" for an existing card",
"- You may submit multiple commands using a single input to queue up a chained sequence of requests",
"- Capitalization doesn't matter, titles will be reformatted regardless",
"",
Words.delimiter,
"",
"🔧 External API Functions (quick summary)",
"These are mainly for other JavaScript programmers to use, so feel free to ignore this section if that doesn't apply to you. Anyway, here's what each one does in plain terms, though please do refer to my source code for the full documentation",
"",
"AutoCards().API.postponeEvents();",
"Pauses Auto-Cards activity for n many turns",
"",
"AutoCards().API.emergencyHalt();",
"Emergency stop or resume",
"",
"AutoCards().API.suppressMessages();",
"Hides Auto-Cards toasts by preventing assignment to state.message",
"",
"AutoCards().API.debugLog();",
"Writes to the debug log card",
"",
"AutoCards().API.toggle();",
"Turns Auto-Cards on/off",
"",
"AutoCards().API.generateCard();",
"Initiates AI generation of the requested card",
"",
"AutoCards().API.redoCard();",
"Regenerates an existing card",
"",
"AutoCards().API.setCardAsAuto();",
"Flags or unflags a card as automatic",
"",
"AutoCards().API.addCardMemory();",
"Adds a memory to a specific card",
"",
"AutoCards().API.eraseAllAutoCards();",
"Deletes all auto-cards",
"",
"AutoCards().API.getUsedTitles();",
"Lists all current card titles and keys",
"",
"AutoCards().API.getBannedTitles();",
"Shows your current banned titles list",
"",
"AutoCards().API.setBannedTitles();",
"Replaces the banned titles list with a new list",
"",
"AutoCards().API.buildCard();",
"Makes a new card from scratch, using exact parameters",
"",
"AutoCards().API.getCard();",
"Finds cards that match a filter",
"",
"AutoCards().API.eraseCard();",
"Deletes cards matching a filter",
"",
"These API functions also work from within the LSIv2 scope, by the way",
"",
Words.delimiter,
"",
"❤️ Special Thanks",
"This project flourished due to the incredible help, feedback, and encouragement from the AI Dungeon community. Your ideas, bug reports, testing, and support made Auto-Cards smarter, faster, and more fun for all. Please refer to my source code to learn more about everyone's specific contributions",
"",
"AHotHamster22, BinKompliziert, Boo, bottledfox, Bruno, Burnout, bweni, DebaczX, Dirty Kurtis, Dragranis, effortlyss, Hawk, Idle Confusion, ImprezA, Kat-Oli, KryptykAngel, Mad19pumpkin, Magic, Mirox80, Nathaniel Wyvern, NobodyIsUgly, OnyxFlame, Purplejump, Randy Viosca, RustyPawz, sinner, Sleepy pink, Vutinberg, Wilmar, Yi1i1i",
"",
Words.delimiter,
"",
"🎴 Random Tips",
"- The default setup works great out of the box, just play normally and watch your world build itself",
"- Enable AI Dungeon's built-in memory system for the best results",
"- Gameplay -> AI Models -> Memory System -> Memory Bank -> Toggle-ON to enable",
"- \"t\" and \"f\" are valid shorthand for \"true\" and \"false\" inside the config card",
"- If Auto-Cards goes overboard with new cards, you can pause it by setting the cooldown config to 9999",
"- Write \"{title:}\" anywhere within a regular story card's entry to transform it into an automatic card",
"- Feel free to import/export entire story card decks at any time",
"- Please copy my source code from here: https://play.aidungeon.com/profile/LewdLeah",
"",
Words.delimiter,
"",
"Happy adventuring! ❤️",
"Please erase before continuing! <<<"
)
};
for (const wordList in wordListInitializers) {
// Define a lazy getter for every word list
Object.defineProperty(Words, wordList, {
configurable: false,
enumerable: true,
get() {
// If not already in cache, initialize and store the word list
if (!(wordList in Words.#cache)) {
Words.#cache[wordList] = O.f(wordListInitializers[wordList]());
}
return Words.#cache[wordList];
}
});
}
} }); }
function hoistStringsHashed() { return (class StringsHashed {
// Used for information-dense past memory recognition
// Strings are converted to (reasonably) unique hashcodes for efficient existence checking
static #defaultSize = 65536;
#size;
#store;
constructor(size = StringsHashed.#defaultSize) {
this.#size = size;
this.#store = new Set();
return this;
}
static deserialize(serialized, size = StringsHashed.#defaultSize) {
const stringsHashed = new StringsHashed(size);
stringsHashed.#store = new Set(serialized.split(","));
return stringsHashed;
}
serialize() {
return Array.from(this.#store).join(",");
}
has(str) {
return this.#store.has(this.#hash(str));
}
add(str) {
this.#store.add(this.#hash(str));
return this;
}
remove(str) {
this.#store.delete(this.#hash(str));
return this;
}
size() {
return this.#store.size;
}
latest(keepLatestCardinality) {
if (this.#store.size <= keepLatestCardinality) {
return this;
}
const excess = this.#store.size - keepLatestCardinality;
const iterator = this.#store.values();
for (let i = 0; i < excess; i++) {
// The oldest hashcodes are removed first (insertion order matters!)
this.#store.delete(iterator.next().value);
}
return this;
}
#hash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((31 * hash) + str.charCodeAt(i)) % this.#size;
}
return hash.toString(36);
}
}); }
function hoistInternal() { return (class Internal {
// Some exported API functions are internally reused by AutoCards
// Recursively calling AutoCards().API is computationally wasteful
// AutoCards uses this collection of static methods as an internal proxy
static generateCard(request, predefinedPair = ["", ""]) {
// Method call guide:
// Internal.generateCard({
// // All properties except 'title' are optional
// type: "card type, defaults to 'class' for ease of filtering",
// title: "card title",
// keysStart: "preexisting card triggers",
// entryStart: "preexisting card entry",
// entryPrompt: "prompt the AI will use to complete this entry",
// entryPromptDetails: "extra details to include with this card's prompt",
// entryLimit: 750, // target character count for the generated entry
// description: "card notes",
// memoryStart: "preexisting card memory",
// memoryUpdates: true, // card updates when new relevant memories are formed
// memoryLimit: 2750, // max characters before the card memory is compressed
// });
const titleKeyPair = formatTitle((request.title ?? "").toString());
const title = predefinedPair[0] || titleKeyPair.newTitle;
if (
(title === "")
|| (("title" in AC.generation.workpiece) && (title === AC.generation.workpiece.title))
|| (isAwaitingGeneration() && (AC.generation.pending.some(pendingWorkpiece => (
("title" in pendingWorkpiece) && (title === pendingWorkpiece.title)
))))
) {
logEvent("The title '" + request.title + "' is invalid or unavailable for card generation", true);
return false;
}
AC.generation.pending.push(O.s({
title: title,
type: limitString((request.type || AC.config.defaultCardType).toString().trim(), 100),
keys: predefinedPair[1] || buildKeys((request.keysStart ?? "").toString(), titleKeyPair.newKey),
entry: limitString("{title: " + title + "}" + cleanSpaces((function() {
const entry = (request.entryStart ?? "").toString().trim();
if (entry === "") {
return "";
} else {
return ("\n" + entry + (function() {
if (/[a-zA-Z]$/.test(entry)) {
return ".";
} else {
return "";
}
})() + " ");
}
})()), 2000),
description: limitString((
(function() {
const description = limitString((request.description ?? "").toString().trim(), 9900);
if (description === "") {
return "";
} else {
return description + "\n\n";
}
})() + "Auto-Cards will contextualize these memories:\n{updates: " + (function() {
if (typeof request.memoryUpdates === "boolean") {
return request.memoryUpdates;
} else {
return AC.config.defaultCardsDoMemoryUpdates;
}
})() + ", limit: " + validateMemoryLimit(
parseInt((request.memoryLimit || AC.config.defaultMemoryLimit), 10)
) + "}" + (function() {
const cardMemoryBank = cleanSpaces((request.memoryStart ?? "").toString().trim());
if (cardMemoryBank === "") {
return "";
} else {
return "\n" + cardMemoryBank.split("\n").map(memory => addBullet(memory)).join("\n");
}
})()
), 10000),
prompt: (function() {
let prompt = insertTitle((
(request.entryPrompt ?? "").toString().trim() || AC.config.generationPrompt.trim()
), title);
let promptDetails = insertTitle((
cleanSpaces((request.entryPromptDetails ?? "").toString().trim())
), title);
if (promptDetails !== "") {
const spacesPrecedingTerminalEntryPlaceholder = (function() {
const terminalEntryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*entry\s*}+$/i;
if (terminalEntryPlaceholderPattern.test(prompt)) {
prompt = prompt.replace(terminalEntryPlaceholderPattern, "");
const trailingSpaces = prompt.match(/(\s+)$/);
if (trailingSpaces) {
prompt = prompt.trimEnd();
return trailingSpaces[1];
} else {
return "\n\n";
}
} else {
return "";
}
})();
switch(prompt[prompt.length - 1]) {
case "]": { encapsulateBothPrompts("[", true, "]"); break; }
case ">": { encapsulateBothPrompts(null, false, ">"); break; }
case "}": { encapsulateBothPrompts("{", true, "}"); break; }
case ")": { encapsulateBothPrompts("(", true, ")"); break; }
case "/": { encapsulateBothPrompts("/", true, "/"); break; }
case "#": { encapsulateBothPrompts("#", true, "#"); break; }
case "-": { encapsulateBothPrompts(null, false, "-"); break; }
case ":": { encapsulateBothPrompts(":", true, ":"); break; }
case "<": { encapsulateBothPrompts(">", true, "<"); break; }
};
if (promptDetails.includes("\n")) {
const lines = promptDetails.split("\n");
for (let i = 0; i < lines.length; i++) {
lines[i] = addBullet(lines[i].trim());
}
promptDetails = lines.join("\n");
} else {
promptDetails = addBullet(promptDetails);
}
prompt += "\n" + promptDetails + (function() {
if (spacesPrecedingTerminalEntryPlaceholder !== "") {
// Prompt previously contained a terminal %{entry} placeholder, re-append it
return spacesPrecedingTerminalEntryPlaceholder + "%{entry}";
}
return "";
})();
function encapsulateBothPrompts(leftSymbol, slicesAtMiddle, rightSymbol) {
if (slicesAtMiddle) {
prompt = prompt.slice(0, -1).trim();
if (promptDetails.startsWith(leftSymbol)) {
promptDetails = promptDetails.slice(1).trim();
}
}
if (!promptDetails.endsWith(rightSymbol)) {
promptDetails += rightSymbol;
}
return;
}
}
return limitString(prompt, Math.floor(0.8 * AC.signal.maxChars));
})(),
limit: validateEntryLimit(parseInt((request.entryLimit || AC.config.defaultEntryLimit), 10))
}));
notify("Generating card for \"" + title + "\"");
function addBullet(str) {
return "- " + str.replace(/^-+\s*/, "");
}
return true;
}
static redoCard(request, useOldInfo, newInfo) {
const card = getIntendedCard(request.title)[0];
const oldCard = O.f({...card});
if (!eraseCard(card)) {
return false;
} else if (newInfo !== "") {
request.entryPromptDetails = (request.entryPromptDetails ?? "").toString() + "\n" + newInfo;
}
O.f(request);
Internal.getUsedTitles(true);
if (!Internal.generateCard(request) && !Internal.generateCard(request, [
(oldCard.entry.match(/^{title: ([\s\S]*?)}/)?.[1] || request.title.replace(/\w\S*/g, word => (
word[0].toUpperCase() + word.slice(1).toLowerCase()
))), oldCard.keys
])) {
constructCard(oldCard, newCardIndex());
Internal.getUsedTitles(true);
return false;
} else if (!useOldInfo) {
return true;
}
AC.generation.pending[AC.generation.pending.length - 1].prompt = ((
removeAutoProps(oldCard.entry) + "\n\n" +
removeAutoProps(isolateNotesAndMemories(oldCard.description)[1])
).trimEnd() + "\n\n" + AC.generation.pending[AC.generation.pending.length - 1].prompt).trim();
return true;
}
// Sometimes it's helpful to log information elsewhere during development
// This log card is separate and distinct from the LSIv2 console log
static debugLog(...args) {
const debugCardName = "Debug Log";
banTitle(debugCardName);
const card = getSingletonCard(true, O.f({
type: AC.config.defaultCardType,
title: debugCardName,
keys: debugCardName,
entry: "The debug console log will print to the notes section below.",
description: Words.delimiter + "\nBEGIN DEBUG LOG"
}));
logToCard(card, ...args);
return card;
}
static eraseAllAutoCards() {
const cards = [];
Internal.getUsedTitles(true);
for (const card of storyCards) {
if (card.entry.startsWith("{title: ")) {
cards.push(card);
}
}
for (const card of cards) {
eraseCard(card);
}
auto.clear();
forgetStuff();
clearTransientTitles();
AC.generation.pending = [];
AC.database.memories.associations = {};
if (AC.config.deleteAllAutoCards) {
AC.config.deleteAllAutoCards = null;
}
return cards.length;
}
static getUsedTitles(isExternal = false) {
if (isExternal) {
bans.clear();
isBanned("", true);
} else if (0 < AC.database.titles.used.length) {
return AC.database.titles.used;
}
// All unique used titles and keys encountered during this iteration
const seen = new Set();
auto.clear();
clearTransientTitles();
AC.database.titles.used = ["%@%"];
for (const card of storyCards) {
// Perform some common-sense maintenance while we're here
card.type = card.type.trim();
card.title = card.title.trim();
// card.keys should be left as-is
card.entry = card.entry.trim();
card.description = card.description.trim();
if (isExternal) {
O.s(card);
} else if (!shouldProceed()) {
checkRemaining();
continue;
}
// An ideal auto-card's entry starts with "{title: Example of Greatness}" (example)
// An ideal auto-card's description contains "{updates: true, limit: 2750}" (example)
if (checkPlurals(denumberName(card.title.replace("\n", "")), t => isBanned(t))) {
checkRemaining();
continue;
} else if (!card.keys.includes(",")) {
const cleanKeys = denumberName(card.keys.trim());
if ((2 < cleanKeys.length) && checkPlurals(cleanKeys, t => isBanned(t))) {
checkRemaining();
continue;
}
}
// Detect and repair malformed auto-card properties in a fault-tolerant manner
const traits = [card.entry, card.description].map((str, i) => {
// Absolute abomination uwu
const hasUpdates = /updates?\s*:[\s\S]*?(?:(?:title|limit)s?\s*:|})/i.test(str);
const hasLimit = /limits?\s*:[\s\S]*?(?:(?:title|update)s?\s*:|})/i.test(str);
return [(function() {
if (hasUpdates || hasLimit) {
if (/titles?\s*:[\s\S]*?(?:(?:limit|update)s?\s*:|})/i.test(str)) {
return 2;
}
return false;
} else if (/titles?\s*:[\s\S]*?}/i.test(str)) {
return 1;
} else if (!(
(i === 0)
&& /{[\s\S]*?}/.test(str)
&& (str.match(/{/g)?.length === 1)
&& (str.match(/}/g)?.length === 1)
)) {
return false;
}
const badTitleHeaderMatch = str.match(/{([\s\S]*?)}/);
if (!badTitleHeaderMatch) {
return false;
}
const inferredTitle = badTitleHeaderMatch[1].split(",")[0].trim();
if (
(2 < inferredTitle.length)
&& (inferredTitle.length <= 100)
&& (badTitleHeaderMatch[0].length < str.length)
) {
// A rare case where the title's existence should be inferred from the enclosing {curly brackets}
return inferredTitle;
}
return false;
})(), hasUpdates, hasLimit];
}).flat();
if (traits.every(trait => !trait)) {
// This card contains no auto-card traits, not even malformed ones
checkRemaining();
continue;
}
const [
hasEntryTitle,
hasEntryUpdates,
hasEntryLimit,
hasDescTitle,
hasDescUpdates,
hasDescLimit
] = traits;
// Handle all story cards which belong to the Auto-Cards ecosystem
// May flag this damaged auto-card for later repairs
// May flag this duplicate auto-card for deformatting (will become a regular story card)
let repair = false;
let release = false;
const title = (function() {
let title = "";
if (typeof hasEntryTitle === "string") {
repair = true;
title = formatTitle(hasEntryTitle).newTitle;
if (hasDescTitle && bad()) {
title = parseTitle(false);
}
} else if (hasEntryTitle) {
title = parseTitle(true);
if (hasDescTitle) {
repair = true;
if (bad()) {
title = parseTitle(false);
}
} else if (1 < card.entry.match(/titles?\s*:/gi)?.length) {
repair = true;
}
} else if (hasDescTitle) {
repair = true;
title = parseTitle(false);
}
if (bad()) {
repair = true;
title = formatTitle(card.title).newTitle;
if (bad()) {
release = true;
} else {
seen.add(title);
auto.add(title.toLowerCase());
}
} else {
seen.add(title);
auto.add(title.toLowerCase());
const titleHeader = "{title: " + title + "}";
if (!repair && !((card.entry === titleHeader) || card.entry.startsWith(titleHeader + "\n"))) {
repair = true;
}
}
function bad() {
return ((title === "") || checkPlurals(title, t => auto.has(t)));
}
function parseTitle(fromEntry) {
const [sourceType, sourceText] = (function() {
if (fromEntry) {
return [hasEntryTitle, card.entry];
} else {
return [hasDescTitle, card.description];
}
})()
switch(sourceType) {
case 1: {
return formatTitle(isolateProperty(
sourceText,
/titles?\s*:[\s\S]*?}/i,
/(?:titles?\s*:|})/gi
)).newTitle; }
case 2: {
return formatTitle(isolateProperty(
sourceText,
/titles?\s*:[\s\S]*?(?:(?:limit|update)s?\s*:|})/i,
/(?:(?:title|update|limit)s?\s*:|})/gi
)).newTitle; }
default: {
return ""; }
}
}
return title;
})();
if (release) {
// Remove Auto-Cards properties from this incompatible story card
safeRemoveProps();
card.description = (card.description
.replace(/\s*Auto(?:-|\s*)Cards\s*will\s*contextualize\s*these\s*memories\s*:\s*/gi, "")
.replaceAll("%@%", "\n\n")
.trim()
);
seen.delete(title);
checkRemaining();
continue;
}
const memoryProperties = "{updates: " + (function() {
let updates = null;
if (hasDescUpdates) {
updates = parseUpdates(false);
if (hasEntryUpdates) {
repair = true;
if (bad()) {
updates = parseUpdates(true);
}
} else if (1 < card.description.match(/updates?\s*:/gi)?.length) {
repair = true;
}
} else if (hasEntryUpdates) {
repair = true;
updates = parseUpdates(true);
}
if (bad()) {
repair = true;
updates = AC.config.defaultCardsDoMemoryUpdates;
}
function bad() {
return (updates === null);
}
function parseUpdates(fromEntry) {
const updatesText = (isolateProperty(
(function() {
if (fromEntry) {
return card.entry;
} else {
return card.description;
}
})(),
/updates?\s*:[\s\S]*?(?:(?:title|limit)s?\s*:|})/i,
/(?:(?:title|update|limit)s?\s*:|})/gi
).toLowerCase().replace(/[^a-z]/g, ""));
if (Words.trues.includes(updatesText)) {
return true;
} else if (Words.falses.includes(updatesText)) {
return false;
} else {
return null;
}
}
return updates;
})() + ", limit: " + (function() {
let limit = -1;
if (hasDescLimit) {
limit = parseLimit(false);
if (hasEntryLimit) {
repair = true;
if (bad()) {
limit = parseLimit(true);
}
} else if (1 < card.description.match(/limits?\s*:/gi)?.length) {
repair = true;
}
} else if (hasEntryLimit) {
repair = true;
limit = parseLimit(true);
}
if (bad()) {
repair = true;
limit = AC.config.defaultMemoryLimit;
} else {
limit = validateMemoryLimit(limit);
}
function bad() {
return (limit === -1);
}
function parseLimit(fromEntry) {
const limitText = (isolateProperty(
(function() {
if (fromEntry) {
return card.entry;
} else {
return card.description;
}
})(),
/limits?\s*:[\s\S]*?(?:(?:title|update)s?\s*:|})/i,
/(?:(?:title|update|limit)s?\s*:|})/gi
).replace(/[^0-9]/g, ""));
if ((limitText === "")) {
return -1;
} else {
return parseInt(limitText, 10);
}
}
return limit.toString();
})() + "}";
if (!repair && (new RegExp("(?:^|\\n)" + memoryProperties + "(?:\\n|$)")).test(card.description)) {
// There are no serious repairs to perform
card.entry = cleanSpaces(card.entry);
const [notes, memories] = isolateNotesAndMemories(card.description);
const pureMemories = cleanSpaces(memories.replace(memoryProperties, "").trim());
rejoinDescription(notes, memoryProperties, pureMemories);
checkRemaining();
continue;
}
// Damage was detected, perform an adaptive repair on this auto-card's configurable properties
card.description = card.description.replaceAll("%@%", "\n\n");
safeRemoveProps();
card.entry = limitString(("{title: " + title + "}\n" + card.entry).trimEnd(), 2000);
const [left, right] = card.description.split("%@%");
rejoinDescription(left, memoryProperties, right);
checkRemaining();
function safeRemoveProps() {
if (typeof hasEntryTitle === "string") {
card.entry = card.entry.replace(/{[\s\S]*?}/g, "");
}
card.entry = removeAutoProps(card.entry);
const [notes, memories] = isolateNotesAndMemories(card.description);
card.description = notes + "%@%" + removeAutoProps(memories);
return;
}
function rejoinDescription(notes, memoryProperties, memories) {
card.description = limitString((notes + (function() {
if (notes === "") {
return "";
} else if (notes.endsWith("Auto-Cards will contextualize these memories:")) {
return "\n";
} else {
return "\n\n";
}
})() + memoryProperties + (function() {
if (memories === "") {
return "";
} else {
return "\n";
}
})() + memories), 10000);
return;
}
function isolateProperty(sourceText, propMatcher, propCleaner) {
return ((sourceText.match(propMatcher)?.[0] || "")
.replace(propCleaner, "")
.split(",")[0]
.trim()
);
}
// Observe literal card titles and keys
function checkRemaining() {
const literalTitles = [card.title, ...card.keys.split(",")];
for (let i = 0; i < literalTitles.length; i++) {
// The pre-format set inclusion check helps avoid superfluous formatTitle calls
literalTitles[i] = (literalTitles[i]
.replace(/["\.\?!;\(\):\[\]—{}]/g, " ")
.trim()
.replace(/\s+/g, " ")
.replace(/^'\s*/, "")
.replace(/\s*'$/, "")
);
if (seen.has(literalTitles[i])) {
continue;
}
literalTitles[i] = formatTitle(literalTitles[i]).newTitle;
if (literalTitles[i] !== "") {
seen.add(literalTitles[i]);
}
}
return;
}
function denumberName(name) {
if (2 < (name.match(/[^\d\s]/g) || []).length) {
// Important for identifying LSIv2 auxiliary code cards when banned
return name.replace(/\s*\d+$/, "");
} else {
return name;
}
}
}
clearTransientTitles();
AC.database.titles.used = [...seen];
return AC.database.titles.used;
}
static getBannedTitles() {
// AC.database.titles.banned is an array, not a set; order matters
return AC.database.titles.banned;
}
static setBannedTitles(newBans, isFinalAssignment) {
AC.database.titles.banned = [];
AC.database.titles.pendingBans = [];
AC.database.titles.pendingUnbans = [];
for (let i = newBans.length - 1; 0 <= i; i--) {
banTitle(newBans[i], isFinalAssignment);
}
return AC.database.titles.banned;
}
static getCard(predicate, getAll) {
if (getAll) {
// Return an array of card references which satisfy the given condition
const collectedCards = [];
for (const card of storyCards) {
if (predicate(card)) {
O.s(card);
collectedCards.push(card);
}
}
return collectedCards;
}
// Return a reference to the first card which satisfies the given condition
for (const card of storyCards) {
if (predicate(card)) {
return O.s(card);
}
}
return null;
}
}); }
function validateCooldown(cooldown) {
return boundInteger(0, cooldown, 9999, 22);
}
function validateEntryLimit(entryLimit) {
return boundInteger(200, entryLimit, 2000, 750);
}
function validateMemoryLimit(memoryLimit) {
return boundInteger(1750, memoryLimit, 9900, 2750);
}
function validateMemCompRatio(memCompressRatio) {
return boundInteger(20, memCompressRatio, 1250, 25);
}
function validateMinLookBackDist(minLookBackDist) {
return boundInteger(2, minLookBackDist, 88, 7);
}
function getDefaultConfig() {
function check(value, fallback = true, type = "boolean") {
if (typeof value === type) {
return value;
} else {
return fallback;
}
}
return O.s({
// Is Auto-Cards enabled?
doAC: check(DEFAULT_DO_AC),
// Delete all previously generated story cards?
deleteAllAutoCards: null,
// Pin the configuration interface story card near the top?
pinConfigureCard: check(DEFAULT_PIN_CONFIGURE_CARD),
// Minimum number of turns in between automatic card generation events?
addCardCooldown: validateCooldown(DEFAULT_CARD_CREATION_COOLDOWN),
// Use bulleted list mode for newly generated card entries?
bulletedListMode: check(DEFAULT_USE_BULLETED_LIST_MODE),
// Maximum allowed length for newly generated story card entries?
defaultEntryLimit: validateEntryLimit(DEFAULT_GENERATED_ENTRY_LIMIT),
// Do newly generated cards have memory updates enabled by default?
defaultCardsDoMemoryUpdates: check(DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES),
// Default character limit before the card's memory bank is summarized?
defaultMemoryLimit: validateMemoryLimit(DEFAULT_NEW_CARDS_MEMORY_LIMIT),
// Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new)
memoryCompressionRatio: validateMemCompRatio(DEFAULT_MEMORY_COMPRESSION_RATIO),
// Ignore all-caps during title candidate detection?
ignoreAllCapsTitles: check(DEFAULT_IGNORE_ALL_CAPS_TITLES),
// Should player input actions (Do/Say/Story) be considered for future named entity detection?
readFromInputs: check(DEFAULT_DETECT_TITLES_FROM_INPUTS),
// How many (minimum) actions in the past does Auto-Cards look for named entities?
minimumLookBackDistance: validateMinLookBackDist(DEFAULT_MINIMUM_LOOK_BACK_DISTANCE),
// Is Live Script Interface v2 enabled?
LSIv2: (function() {
if (DEFAULT_DO_LSI_V2 === true) {
return true;
} else {
// Intrepret "false" as null, both here and for later config card reads
return null;
}
})(),
// Should the debug data card be visible?
showDebugData: check(DEFAULT_SHOW_DEBUG_DATA, false),
// How should the AI be prompted when generating new story card entries?
generationPrompt: check(DEFAULT_CARD_GENERATION_PROMPT, prose(
"-----",
"",
"<SYSTEM>",
"# Stop the story and ignore previous instructions. Write a brief and coherent informational entry for %{title} following these instructions:",
"- Write only third-person pure prose information about %{title} using complete sentences with correct punctuation",
"- Avoid short-term temporary details or appearances, instead focus on plot-significant information",
"- Prioritize story-relevant details about %{title} first to ensure seamless integration with the previous plot",
"- Create new information based on the context and story direction",
"- Mention %{title} in every sentence",
"- Use semicolons if needed",
"- Add additional details about %{title} beneath incomplete entries",
"- Be concise and grounded",
"- Imitate the story's writing style and infer the reader's preferences",
"</SYSTEM>",
"Continue the entry for %{title} below while avoiding repetition:",
"%{entry}"
), "string"),
// How should the AI be prompted when summarizing memories for a given story card?
compressionPrompt: check(DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT, prose(
"-----",
"",
"<SYSTEM>",
"# Stop the story and ignore previous instructions. Summarize and condense the given paragraph into a narrow and focused memory passage while following these guidelines:",
"- Ensure the passage retains the core meaning and most essential details",
"- Use the third-person perspective",
"- Prioritize information-density, accuracy, and completeness",
"- Remain brief and concise",
"- Write firmly in the past tense",
"- The paragraph below pertains to old events from far earlier in the story",
"- Integrate %{title} naturally within the memory; however, only write about the events as they occurred",
"- Only reference information present inside the paragraph itself, be specific",
"</SYSTEM>",
"Write a summarized old memory passage for %{title} based only on the following paragraph:",
"\"\"\"",
"%{memory}",
"\"\"\"",
"Summarize below:"
), "string"),
// All cards constructed by AC will inherit this type by default
defaultCardType: check(DEFAULT_CARD_TYPE, "class", "string")
});
}
function getDefaultConfigBans() {
if (typeof DEFAULT_BANNED_TITLES_LIST === "string") {
return uniqueTitlesArray(DEFAULT_BANNED_TITLES_LIST.split(","));
} else {
return [
"North", "East", "South", "West", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
];
}
}
function uniqueTitlesArray(titles) {
const existingTitles = new Set();
return (titles
.map(title => title.trim().replace(/\s+/g, " "))
.filter(title => {
if (title === "") {
return false;
}
const lowerTitle = title.toLowerCase();
if (existingTitles.has(lowerTitle)) {
return false;
} else {
existingTitles.add(lowerTitle);
return true;
}
})
);
}
function boundInteger(lowerBound, value, upperBound, fallback) {
if (!Number.isInteger(value)) {
if (!Number.isInteger(fallback)) {
throw new Error("Invalid arguments: value and fallback are not integers");
}
value = fallback;
}
if (Number.isInteger(lowerBound) && (value < lowerBound)) {
if (Number.isInteger(upperBound) && (upperBound < lowerBound)) {
throw new Error("Invalid arguments: The inequality (lowerBound <= upperBound) must be satisfied");
}
return lowerBound;
} else if (Number.isInteger(upperBound) && (upperBound < value)) {
return upperBound;
} else {
return value;
}
}
function limitString(str, lengthLimit) {
if (lengthLimit < str.length) {
return str.slice(0, lengthLimit).trim();
} else {
return str;
}
}
function cleanSpaces(unclean) {
return (unclean
.replace(/\s*\n\s*/g, "\n")
.replace(/\t/g, " ")
.replace(/ +/g, " ")
);
}
function isolateNotesAndMemories(str) {
const bisector = str.search(/\s*(?:{|(?:title|update|limit)s?\s*:)\s*/i);
if (bisector === -1) {
return [str, ""];
} else {
return [str.slice(0, bisector), str.slice(bisector)];
}
}
function removeAutoProps(str) {
return cleanSpaces(str
.replace(/\s*{([\s\S]*?)}\s*/g, (bracedMatch, enclosedProperties) => {
if (enclosedProperties.trim().length < 150) {
return "\n";
} else {
return bracedMatch;
}
})
.replace((
/\s*(?:{|(?:title|update|limit)s?\s*:)(?:[\s\S]{0,150}?)(?=(?:title|update|limit)s?\s*:|})\s*/gi
), "\n")
.replace(/\s*(?:{|(?:title|update|limit)s?\s*:|})\s*/gi, "\n")
.trim()
);
}
function insertTitle(prompt, title) {
return prompt.replace((
/(?:[%\$]+\s*|[%\$]*){+\s*(?:titles?|names?|characters?|class(?:es)?|races?|locations?|factions?)\s*}+/gi
), title);
}
function prose(...args) {
return args.join("\n");
}
function buildKeys(keys, key) {
key = key.trim().replace(/\s+/g, " ");
const keyset = [];
if (key === "") {
return keys;
} else if (keys.trim() !== "") {
keyset.push(...keys.split(","));
const lowerKey = key.toLowerCase();
for (let i = keyset.length - 1; 0 <= i; i--) {
const preKey = keyset[i].trim().replace(/\s+/g, " ").toLowerCase();
if ((preKey === "") || preKey.includes(lowerKey)) {
keyset.splice(i, 1);
}
}
}
if (key.length < 6) {
keyset.push(...[
" " + key + " ", " " + key + "'", "\"" + key + " ", " " + key + ".", " " + key + "?", " " + key + "!", " " + key + ";", "'" + key + " ", "(" + key + " ", " " + key + ")", " " + key + ":", " " + key + "\"", "[" + key + " ", " " + key + "]", "—" + key + " ", " " + key + "—", "{" + key + " ", " " + key + "}"
]);
} else if (key.length < 9) {
keyset.push(...[
key + " ", " " + key, key + "'", "\"" + key, key + ".", key + "?", key + "!", key + ";", "'" + key, "(" + key, key + ")", key + ":", key + "\"", "[" + key, key + "]", "—" + key, key + "—", "{" + key, key + "}"
]);
} else {
keyset.push(key);
}
keys = keyset[0] || key;
let i = 1;
while ((i < keyset.length) && ((keys.length + 1 + keyset[i].length) < 101)) {
keys += "," + keyset[i];
i++;
}
return keys;
}
// Returns the template-specified singleton card (or secondary varient) after:
// 1) Erasing all inferior duplicates
// 2) Repairing damaged titles and keys
// 3) Constructing a new singleton card if it doesn't exist
function getSingletonCard(allowConstruction, templateCard, secondaryCard) {
let singletonCard = null;
const excessCards = [];
for (const card of storyCards) {
O.s(card);
if (singletonCard === null) {
if ((card.title === templateCard.title) || (card.keys === templateCard.keys)) {
// The first potentially valid singleton card candidate to be found
singletonCard = card;
}
} else if (card.title === templateCard.title) {
if (card.keys === templateCard.keys) {
excessCards.push(singletonCard);
singletonCard = card;
} else {
eraseInferiorDuplicate();
}
} else if (card.keys === templateCard.keys) {
eraseInferiorDuplicate();
}
function eraseInferiorDuplicate() {
if ((singletonCard.title === templateCard.title) && (singletonCard.keys === templateCard.keys)) {
excessCards.push(card);
} else {
excessCards.push(singletonCard);
singletonCard = card;
}
return;
}
}
if (singletonCard === null) {
if (secondaryCard) {
// Fallback to a secondary card template
singletonCard = getSingletonCard(false, secondaryCard);
}
// No singleton card candidate exists
if (allowConstruction && (singletonCard === null)) {
// Construct a new singleton card from the given template
singletonCard = constructCard(templateCard);
}
} else {
if (singletonCard.title !== templateCard.title) {
// Repair any damage to the singleton card's title
singletonCard.title = templateCard.title;
} else if (singletonCard.keys !== templateCard.keys) {
// Repair any damage to the singleton card's keys
singletonCard.keys = templateCard.keys;
}
for (const card of excessCards) {
// Erase all excess singleton card candidates
eraseCard(card);
}
if (secondaryCard) {
// A secondary card match cannot be allowed to persist
eraseCard(getSingletonCard(false, secondaryCard));
}
}
return singletonCard;
}
// Erases the given story card
function eraseCard(badCard) {
if (badCard === null) {
return false;
}
badCard.title = "%@%";
for (const [index, card] of storyCards.entries()) {
if (card.title === "%@%") {
removeStoryCard(index);
return true;
}
}
return false;
}
// Constructs a new story card from a standardized story card template object
// {type: "", title: "", keys: "", entry: "", description: ""}
// Returns a reference to the newly constructed card
function constructCard(templateCard, insertionIndex = 0) {
addStoryCard("%@%");
for (const [index, card] of storyCards.entries()) {
if (card.title !== "%@%") {
continue;
}
card.type = templateCard.type;
card.title = templateCard.title;
card.keys = templateCard.keys;
card.entry = templateCard.entry;
card.description = templateCard.description;
if (index !== insertionIndex) {
// Remove from the current position and reinsert at the desired index
storyCards.splice(index, 1);
storyCards.splice(insertionIndex, 0, card);
}
return O.s(card);
}
return {};
}
function newCardIndex() {
return +AC.config.pinConfigureCard;
}
function getIntendedCard(targetCard) {
Internal.getUsedTitles(true);
const titleKey = targetCard.trim().replace(/\s+/g, " ").toLowerCase();
const autoCard = Internal.getCard(card => (card.entry
.toLowerCase()
.startsWith("{title: " + titleKey + "}")
));
if (autoCard !== null) {
return [autoCard, true, titleKey];
}
return [Internal.getCard(card => ((card.title
.replace(/\s+/g, " ")
.toLowerCase()
) === titleKey)), false, titleKey];
}
function doPlayerCommands(input) {
let result = "";
for (const command of (
(function() {
if (/^\n> [\s\S]*? says? "[\s\S]*?"\n$/.test(input)) {
return input.replace(/\s*"\n$/, "");
} else {
return input.trimEnd();
}
})().split(/(?=\/\s*A\s*C)/i)
)) {
const prefixPattern = /^\/\s*A\s*C/i;
if (!prefixPattern.test(command)) {
continue;
}
const [requestTitle, requestDetails, requestEntry] = (command
.replace(/(?:{\s*)|(?:\s*})/g, "")
.replace(prefixPattern, "")
.replace(/(?:^\s*\/*\s*)|(?:\s*\/*\s*$)/g, "")
.split("/")
.map(requestArg => requestArg.trim())
.filter(requestArg => (requestArg !== ""))
);
if (!requestTitle) {
// Request with no args
AC.generation.cooldown = 0;
result += "/AC -> Success!\n\n";
logEvent("/AC");
} else {
const request = {title: requestTitle.replace(/\s*[\.\?!:]+$/, "")};
const redo = (function() {
const redoPattern = /^(?:redo|retry|rewrite|remake)[\s\.\?!:,;"'—\)\]]+\s*/i;
if (redoPattern.test(request.title)) {
request.title = request.title.replace(redoPattern, "");
if (/^(?:all|every)(?:\s|\.|\?|!|:|,|;|"|'|—|\)|\]|$)/i.test(request.title)) {
return [];
} else {
return true;
}
} else {
return false;
}
})();
if (Array.isArray(redo)) {
// Redo all auto cards
Internal.getUsedTitles(true);
const titleMatchPattern = /^{title: ([\s\S]*?)}/;
redo.push(...Internal.getCard(card => (
titleMatchPattern.test(card.entry)
&& /{updates: (?:true|false), limit: \d+}/.test(card.description)
), true));
let count = 0;
for (const card of redo) {
const titleMatch = card.entry.match(titleMatchPattern);
if (titleMatch && Internal.redoCard(O.f({title: titleMatch[1]}), true, "")) {
count++;
}
}
const parsed = "/AC redo all";
result += parsed + " -> ";
if (count === 0) {
result += "There were no valid auto-cards to redo";
} else {
result += "Success!";
if (1 < count) {
result += " Proceed to redo " + count + " cards";
}
}
logEvent(parsed);
} else if (!requestDetails) {
// Request with only title
submitRequest("");
} else if (!requestEntry || redo) {
// Request with title and details
request.entryPromptDetails = requestDetails;
submitRequest(" / {" + requestDetails + "}");
} else {
// Request with title, details, and entry
request.entryPromptDetails = requestDetails;
request.entryStart = requestEntry;
submitRequest(" / {" + requestDetails + "} / {" + requestEntry + "}");
}
result += "\n\n";
function submitRequest(extra) {
O.f(request);
const [type, success] = (function() {
if (redo) {
return [" redo", Internal.redoCard(request, true, "")];
} else {
Internal.getUsedTitles(true);
return ["", Internal.generateCard(request)];
}
})();
const left = "/AC" + type + " {";
const right = "}" + extra;
if (success) {
const parsed = left + AC.generation.pending[AC.generation.pending.length - 1].title + right;
result += parsed + " -> Success!";
logEvent(parsed);
} else {
const parsed = left + request.title + right;
result += parsed + " -> \"" + request.title + "\" is invalid or unavailable";
logEvent(parsed);
}
return;
}
}
if (isPendingGeneration() || isAwaitingGeneration() || isPendingCompression()) {
if (AC.config.doAC) {
AC.signal.outputReplacement = "";
} else {
AC.signal.forceToggle = true;
AC.signal.outputReplacement = ">>> please select \"continue\" (0%) <<<";
}
} else if (AC.generation.cooldown === 0) {
if (0 < AC.database.titles.candidates.length) {
if (AC.config.doAC) {
AC.signal.outputReplacement = "";
} else {
AC.signal.forceToggle = true;
AC.signal.outputReplacement = ">>> please select \"continue\" (0%) <<<";
}
} else if (AC.config.doAC) {
result = result.trimEnd() + "\n";
AC.signal.outputReplacement = "\n";
} else {
AC.signal.forceToggle = true;
AC.signal.outputReplacement = ">>> Auto-Cards has been enabled! <<<";
}
} else {
result = result.trimEnd() + "\n";
AC.signal.outputReplacement = "\n";
}
}
return getPrecedingNewlines() + result;
}
function advanceChronometer() {
const currentTurn = getTurn();
if (Math.abs(history.length - currentTurn) < 2) {
// The two measures are within ±1, thus history hasn't been truncated yet
AC.chronometer.step = !(history.length < currentTurn);
} else {
// history has been truncated, fallback to a (slightly) worse step detection technique
AC.chronometer.step = (AC.chronometer.turn < currentTurn);
}
AC.chronometer.turn = currentTurn;
return;
}
function concludeEmergency() {
promoteAmnesia();
endTurn();
AC.message.pending = [];
AC.message.previous = getStateMessage();
return;
}
function concludeOutputBlock(templateCard) {
if (AC.config.deleteAllAutoCards !== null) {
// A config-initiated event to delete all previously generated story cards is in progress
if (AC.config.deleteAllAutoCards) {
// Request in-game confirmation from the player before proceeding
AC.config.deleteAllAutoCards = false;
CODOMAIN.initialize(getPrecedingNewlines() + ">>> please submit the message \"CONFIRM DELETE\" using a Do, Say, or Story action to permanently delete all previously generated story cards <<<\n\n");
} else {
// Check for player confirmation
const previousAction = readPastAction(0);
if (isDoSayStory(previousAction.type) && /CONFIRM\s*DELETE/i.test(previousAction.text)) {
let successMessage = "Confirmation Success: ";
const numCardsErased = Internal.eraseAllAutoCards();
if (numCardsErased === 0) {
successMessage += "However, there were no previously generated story cards to delete!";
} else {
successMessage += numCardsErased + " generated story card";
if (numCardsErased === 1) {
successMessage += " was";
} else {
successMessage += "s were";
}
successMessage += " deleted";
}
notify(successMessage);
} else {
notify("Confirmation Failure: No story cards were deleted");
}
AC.config.deleteAllAutoCards = null;
CODOMAIN.initialize("\n");
}
} else if (AC.signal.outputReplacement !== "") {
const output = AC.signal.outputReplacement.trim();
if (output === "") {
CODOMAIN.initialize("\n");
} else {
CODOMAIN.initialize(getPrecedingNewlines() + output + "\n\n");
}
}
if (templateCard) {
// Auto-Cards was enabled or disabled during the previous onContext hook
// Construct the replacement control card onOutput
banTitle(templateCard.title);
getSingletonCard(true, templateCard);
AC.signal.swapControlCards = false;
}
endTurn();
if (AC.config.LSIv2 === null) {
postMessages();
}
return;
}
function endTurn() {
AC.database.titles.used = [];
AC.signal.outputReplacement = "";
[AC.database.titles.pendingBans, AC.database.titles.pendingUnbans].map(pending => decrementAll(pending));
if (0 < AC.signal.overrideBans) {
AC.signal.overrideBans--;
}
function decrementAll(pendingArray) {
if (pendingArray.length === 0) {
return;
}
for (let i = pendingArray.length - 1; 0 <= i; i--) {
if (0 < pendingArray[i][1]) {
pendingArray[i][1]--;
} else {
pendingArray.splice(i, 1);
}
}
return;
}
return;
}
// Example usage: notify("Message text goes here");
function notify(message) {
if (typeof message === "string") {
AC.message.pending.push(message);
logEvent(message);
} else if (Array.isArray(message)) {
message.forEach(element => notify(element));
} else if (message instanceof Set) {
notify([...message]);
} else {
notify(message.toString());
}
return;
}
function logEvent(message, uncounted) {
if (uncounted) {
log("Auto-Cards event: " + message);
} else {
log("Auto-Cards event #" + (function() {
try {
AC.message.event++;
return AC.message.event;
} catch {
return 0;
}
})() + ": " + message.replace(/"/g, "'"));
}
return;
}
// Provide the story card object which you wish to log info within as the first argument
// All remaining arguments represent anything you wish to log
function logToCard(logCard, ...args) {
logEvent(args.map(arg => {
if ((typeof arg === "object") && (arg !== null)) {
return JSON.stringify(arg);
} else {
return String(arg);
}
}).join(", "), true);
if (logCard === null) {
return;
}
let desc = logCard.description.trim();
const turnDelimiter = Words.delimiter + "\nAction #" + getTurn() + ":\n";
let header = turnDelimiter;
if (!desc.startsWith(turnDelimiter)) {
desc = turnDelimiter + desc;
}
const scopesTable = [
["input", "Input Modifier"],
["context", "Context Modifier"],
["output", "Output Modifier"],
[null, "Shared Library"],
[undefined, "External API"],
[Symbol("default"), "Unknown Scope"]
];
const callingScope = (function() {
const pair = scopesTable.find(([condition]) => (condition === HOOK));
if (pair) {
return pair[1];
} else {
return scopesTable[scopesTable.length - 1][1];
}
})();
const hookDelimiterLeft = callingScope + " @ ";
if (desc.startsWith(turnDelimiter + hookDelimiterLeft)) {
const hookDelimiterOld = desc.match(new RegExp((
"^" + turnDelimiter + "(" + hookDelimiterLeft + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z:\n)"
).replaceAll("\n", "\\n")));
if (hookDelimiterOld) {
header += hookDelimiterOld[1];
} else {
const hookDelimiter = getNewHookDelimiter();
desc = desc.replace(hookDelimiterLeft, hookDelimiter);
header += hookDelimiter;
}
} else {
if ((new RegExp("^" + turnDelimiter.replaceAll("\n", "\\n") + "(" + (scopesTable
.map(pair => pair[1])
.filter(scope => (scope !== callingScope))
.join("|")
) + ") @ ")).test(desc)) {
desc = desc.replace(turnDelimiter, turnDelimiter + "—————————\n");
}
const hookDelimiter = getNewHookDelimiter();
desc = desc.replace(turnDelimiter, turnDelimiter + hookDelimiter);
header += hookDelimiter;
}
const logDelimiter = (function() {
let logDelimiter = "Log #";
if (desc.startsWith(header + logDelimiter)) {
desc = desc.replace(header, header + "———\n");
const logCounter = desc.match(/Log #(\d+)/);
if (logCounter) {
logDelimiter += (parseInt(logCounter[1], 10) + 1).toString();
}
} else {
logDelimiter += "0";
}
return logDelimiter + ": ";
})();
logCard.description = limitString(desc.replace(header, header + logDelimiter + args.map(arg => {
if ((typeof arg === "object") && (arg !== null)) {
return stringifyObject(arg);
} else {
return String(arg);
}
}).join(",\n") + "\n").trim(), 999999);
// The upper limit is actually closer to 3985621, but I think 1 million is reasonable enough as-is
function getNewHookDelimiter() {
return hookDelimiterLeft + (new Date().toISOString()) + ":\n";
}
return;
}
// Makes nested objects not look like cancer within interface cards
function stringifyObject(obj) {
const seen = new WeakSet();
// Each indentation is 4 spaces
return JSON.stringify(obj, (_key, value) => {
if ((typeof value === "object") && (value !== null)) {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
}
switch(typeof value) {
case "function": {
return "[Function]"; }
case "undefined": {
return "[Undefined]"; }
case "symbol": {
return "[Symbol]"; }
default: {
return value; }
}
}, 4);
}
// Implement state.message toasts without interfering with the operation of other possible scripts
function postMessages() {
const preMessage = getStateMessage();
if ((preMessage === AC.message.previous) && (AC.message.pending.length !== 0)) {
// No other scripts are attempting to update state.message during this turn
// One or more pending Auto-Cards messages exist
if (!AC.message.suppress) {
// Message suppression is off
let newMessage = "Auto-Cards:\n";
if (AC.message.pending.length === 1) {
newMessage += AC.message.pending[0];
} else {
newMessage += AC.message.pending.map(
(messageLine, index) => ("#" + (index + 1) + ": " + messageLine)
).join("\n");
}
if (preMessage === newMessage) {
// Introduce a minor variation to facilitate repetition of the previous message toast
newMessage = newMessage.replace("Auto-Cards:\n", "Auto-Cards: \n");
}
state.message = newMessage;
}
// Clear the pending messages queue after posting or suppressing messages
AC.message.pending = [];
}
AC.message.previous = getStateMessage();
return;
}
function getStateMessage() {
return state.message ?? "";
}
function getPrecedingNewlines() {
const previousAction = readPastAction(0);
if (isDoSay(previousAction.type)) {
return "";
} else if (previousAction.text.endsWith("\n")) {
if (previousAction.text.endsWith("\n\n")) {
return "";
} else {
return "\n";
}
} else {
return "\n\n";
}
}
// Call with lookBack 0 to read the most recent action in history (or n many actions back)
function readPastAction(lookBack) {
const action = (function() {
if (Array.isArray(history)) {
return (history[(function() {
const index = history.length - 1 - Math.abs(lookBack);
if (index < 0) {
return 0;
} else {
return index;
}
})()]);
} else {
return O.f({});
}
})();
return O.f({
text: action?.text ?? (action?.rawText ?? ""),
type: action?.type ?? "unknown"
});
}
// Forget ongoing card generation/compression after passing or postponing completion over many consecutive turns
// Also decrement AC.chronometer.postpone regardless of retries or erases
function promoteAmnesia() {
// Decrement AC.chronometer.postpone in all cases
if (0 < AC.chronometer.postpone) {
AC.chronometer.postpone--;
}
if (!AC.chronometer.step) {
// Skip known retry/erase turns
return;
}
if (AC.chronometer.amnesia++ < boundInteger(16, (2 * AC.config.addCardCooldown), 64)) {
return;
}
AC.generation.cooldown = validateCooldown(underQuarterInteger(AC.config.addCardCooldown));
forgetStuff();
AC.chronometer.amnesia = 0;
return;
}
function forgetStuff() {
AC.generation.completed = 0;
AC.generation.permitted = 34;
AC.generation.workpiece = O.f({});
// AC.generation.pending is not forgotten
resetCompressionProperties();
return;
}
function resetCompressionProperties() {
AC.compression.completed = 0;
AC.compression.titleKey = "";
AC.compression.vanityTitle = "";
AC.compression.responseEstimate = 1400;
AC.compression.lastConstructIndex = -1;
AC.compression.oldMemoryBank = [];
AC.compression.newMemoryBank = [];
return;
}
function underQuarterInteger(someNumber) {
return Math.floor(someNumber / 4);
}
function getTurn() {
if (Number.isInteger(info?.actionCount)) {
// "But Leah, surely info.actionCount will never be negative?"
// You have no idea what nightmares I've seen...
return Math.abs(info.actionCount);
} else {
return 0;
}
}
// Constructs a JSON representation of various properties/settings pulled from raw text
// Used to parse the "Configure Auto-Cards" and "Edit to enable Auto-Cards" control card entries
function extractSettings(settingsText) {
const settings = {};
// Lowercase everything
// Remove all non-alphanumeric characters (aside from ":" and ">")
// Split into an array of strings delimited by the ">" character
const settingLines = settingsText.toLowerCase().replace(/[^a-z0-9:>]+/g, "").split(">");
for (const settingLine of settingLines) {
// Each setting line is preceded by ">" and bisected by ":"
const settingKeyValue = settingLine.split(":");
if ((settingKeyValue.length !== 2) || settings.hasOwnProperty(settingKeyValue[0])) {
// The bisection failed or this setting line's key already exists
continue;
}
// Parse boolean and integer setting values
if (Words.falses.includes(settingKeyValue[1])) {
// This setting line's value is false
settings[settingKeyValue[0]] = false;
} else if (Words.trues.includes(settingKeyValue[1])) {
// This setting line's value is true
settings[settingKeyValue[0]] = true;
} else if (/^\d+$/.test(settingKeyValue[1])) {
// This setting line's value is an integer
// Negative integers are parsed as being positive (because "-" characters were removed)
settings[settingKeyValue[0]] = parseInt(settingKeyValue[1], 10);
}
}
// Return the settings object for later analysis
return settings;
}
// Ensure the given singleton card is pinned near the top of the player's list of story cards
function pinAndSortCards(pinnedCard) {
if (!storyCards || (storyCards.length < 2)) {
return;
}
storyCards.sort((cardA, cardB) => {
return readDate(cardB) - readDate(cardA);
});
if (!AC.config.pinConfigureCard) {
return;
}
const index = storyCards.indexOf(pinnedCard);
if (0 < index) {
storyCards.splice(index, 1);
storyCards.unshift(pinnedCard);
}
function readDate(card) {
if (card && card.updatedAt) {
const timestamp = Date.parse(card.updatedAt);
if (!isNaN(timestamp)) {
return timestamp;
}
}
return 0;
}
return;
}
function see(arr) {
return String.fromCharCode(...arr.map(n => Math.sqrt(n / 33)));
}
function formatTitle(title) {
title = title.trim();
const failureCase = O.f({newTitle: "", newKey: ""});
if (short()) {
// This is an abundantly called function, return as early as possible to ensure superior performance
return failureCase;
}
title = (title
// Begone!
.replace(/[–。?!´“”؟،«»¿¡„“…§,、\*_~><\(\)\[\]{}#"`:!—;\.\?,\s\\]/g, " ")
.replace(/[]/g, "'").replace(/\s+'/g, " ")
// Remove the words "I", "I'm", "I'd", "I'll", and "I've"
.replace(/(?<=^|\s)(?:I|I'm|I'd|I'll|I've)(?=\s|$)/gi, "")
// Remove "'s" only if not followed by a letter
.replace(/'s(?![a-zA-Z])/g, "")
// Replace "s'" with "s" only if preceded but not followed by a letter
.replace(/(?<=[a-zA-Z])s'(?![a-zA-Z])/g, "s")
// Remove apostrophes not between letters (preserve contractions like "don't")
.replace(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Eliminate fake em dashes and terminal/leading dashes
.replace(/\s-\s/g, " ")
// Condense consecutive whitespace
.trim().replace(/\s+/g, " ")
// Remove a leading or trailing bullet
.replace(/^-+\s*/, "").replace(/\s*-+$/, "")
);
if (short()) {
return failureCase;
}
// Special-cased words
const minorWordsJoin = Words.minor.join("|");
const leadingMinorWordsKiller = new RegExp("^(?:" + minorWordsJoin + ")\\s", "i");
const trailingMinorWordsKiller = new RegExp("\\s(?:" + minorWordsJoin + ")$", "i");
// Ensure the title is not bounded by any outer minor words
title = enforceBoundaryCondition(title);
if (short()) {
return failureCase;
}
// Ensure interior minor words are lowercase and excise all interior honorifics/abbreviations
const honorAbbrevsKiller = new RegExp("(?:^|\\s|-|\\/)(?:" + (
[...Words.honorifics, ...Words.abbreviations]
).map(word => word.replace(".", "")).join("|") + ")(?=\\s|-|\\/|$)", "gi");
title = (title
// Capitalize the first letter of each word
.replace(/(?<=^|\s|-|\/)(?:\p{L})/gu, word => word.toUpperCase())
// Lowercase minor words properly
.replace(/(?<=^|\s|-|\/)(?:\p{L}+)(?=\s|-|\/|$)/gu, word => {
const lowerWord = word.toLowerCase();
if (Words.minor.includes(lowerWord)) {
return lowerWord;
} else {
return word;
}
})
// Remove interior honorifics/abbreviations
.replace(honorAbbrevsKiller, "")
.trim()
);
if (short()) {
return failureCase;
}
let titleWords = title.split(" ");
while ((2 < title.length) && (98 < title.length) && (1 < titleWords.length)) {
titleWords.pop();
title = titleWords.join(" ").trim();
const unboundedLength = title.length;
title = enforceBoundaryCondition(title);
if (unboundedLength !== title.length) {
titleWords = title.split(" ");
}
}
if (isUsedOrBanned(title) || isNamed(title)) {
return failureCase;
}
// Procedurally generated story card trigger keywords exclude certain words and patterns which are otherwise permitted in titles
let key = title;
const peerage = new Set(Words.peerage);
if (titleWords.some(word => ((word === "the") || peerage.has(word.toLowerCase())))) {
if (titleWords.length < 2) {
return failureCase;
}
key = enforceBoundaryCondition(
titleWords.filter(word => !peerage.has(word.toLowerCase())).join(" ")
);
if (key.includes(" the ")) {
key = enforceBoundaryCondition(key.split(" the ")[0]);
}
if (isUsedOrBanned(key)) {
return failureCase;
}
}
function short() {
return (title.length < 3);
}
function enforceBoundaryCondition(str) {
while (leadingMinorWordsKiller.test(str)) {
str = str.replace(/^\S+\s+/, "");
}
while (trailingMinorWordsKiller.test(str)) {
str = str.replace(/\s+\S+$/, "");
}
return str;
}
return O.f({newTitle: title, newKey: key});
}
// I really hate english grammar
function checkPlurals(title, predicate) {
function check(t) { return ((t.length < 3) || (100 < t.length) || predicate(t)); }
const t = title.toLowerCase();
if (check(t)) { return true; }
// s>p : singular -> plural : p>s: plural -> singular
switch(t[t.length - 1]) {
// p>s : s -> _ : Birds -> Bird
case "s": if (check(t.slice(0, -1))) { return true; }
case "x":
// s>p : s, x, z -> ses, xes, zes : Mantis -> Mantises
case "z": if (check(t + "es")) { return true; }
break;
// s>p : o -> oes, os : Gecko -> Geckoes, Geckos
case "o": if (check(t + "es") || check(t + "s")) { return true; }
break;
// p>s : i -> us : Cacti -> Cactus
case "i": if (check(t.slice(0, -1) + "us")) { return true; }
// s>p : i, y -> ies : Kitty -> Kitties
case "y": if (check(t.slice(0, -1) + "ies")) { return true; }
break;
// s>p : f -> ves : Wolf -> Wolves
case "f": if (check(t.slice(0, -1) + "ves")) { return true; }
// s>p : !(s, x, z, i, y) -> +s : Turtle -> Turtles
default: if (check(t + "s")) { return true; }
break;
} switch(t.slice(-2)) {
// p>s : es -> _ : Foxes -> Fox
case "es": if (check(t.slice(0, -2))) { return true; } else if (
(t.endsWith("ies") && (
// p>s : ies -> y : Bunnies -> Bunny
check(t.slice(0, -3) + "y")
// p>s : ies -> i : Ravies -> Ravi
|| check(t.slice(0, -2))
// p>s : es -> is : Crises -> Crisis
)) || check(t.slice(0, -2) + "is")) { return true; }
break;
// s>p : us -> i : Cactus -> Cacti
case "us": if (check(t.slice(0, -2) + "i")) { return true; }
break;
// s>p : is -> es : Thesis -> Theses
case "is": if (check(t.slice(0, -2) + "es")) { return true; }
break;
// s>p : fe -> ves : Knife -> Knives
case "fe": if (check(t.slice(0, -2) + "ves")) { return true; }
break;
case "sh":
// s>p : sh, ch -> shes, ches : Fish -> Fishes
case "ch": if (check(t + "es")) { return true; }
break;
} return false;
}
function isUsedOrBanned(title) {
function isUsed(lowerTitle) {
if (used.size === 0) {
const usedTitles = Internal.getUsedTitles();
for (let i = 0; i < usedTitles.length; i++) {
used.add(usedTitles[i].toLowerCase());
}
if (used.size === 0) {
// Add a placeholder so compute isn't wasted on additional checks during this hook
used.add("%@%");
}
}
return used.has(lowerTitle);
}
return checkPlurals(title, t => (isUsed(t) || isBanned(t)));
}
function isBanned(lowerTitle, getUsedIsExternal) {
if (bans.size === 0) {
// In order to save space, implicit bans aren't listed within the UI
const controlVariants = getControlVariants();
const dataVariants = getDataVariants();
const bansToAdd = [...lowArr([
...Internal.getBannedTitles(),
controlVariants.enable.title.replace("\n", ""),
controlVariants.enable.keys,
controlVariants.configure.title.replace("\n", ""),
controlVariants.configure.keys,
dataVariants.debug.title,
dataVariants.debug.keys,
dataVariants.critical.title,
dataVariants.critical.keys,
...Object.values(Words.reserved)
]), ...(function() {
if (shouldProceed() || getUsedIsExternal) {
// These proper nouns are way too common to waste card generations on; they already exist within the AI training data so this would be pointless
return [...Words.entities, ...Words.undesirables.map(undesirable => see(undesirable))];
} else {
return [];
}
})()];
for (let i = 0; i < bansToAdd.length; i++) {
bans.add(bansToAdd[i]);
}
}
return bans.has(lowerTitle);
}
function isNamed(title, returnSurname) {
const peerage = new Set(Words.peerage);
const minorWords = new Set(Words.minor);
if ((forenames.size === 0) || (surnames.size === 0)) {
const usedTitles = Internal.getUsedTitles();
for (let i = 0; i < usedTitles.length; i++) {
const usedTitleWords = divideTitle(usedTitles[i]);
if (
(usedTitleWords.length === 2)
&& (2 < usedTitleWords[0].length)
&& (2 < usedTitleWords[1].length)
) {
forenames.add(usedTitleWords[0]);
surnames.add(usedTitleWords[1]);
} else if (
(usedTitleWords.length === 1)
&& (2 < usedTitleWords[0].length)
) {
forenames.add(usedTitleWords[0]);
}
}
if (forenames.size === 0) {
forenames.add("%@%");
}
if (surnames.size === 0) {
surnames.add("%@%");
}
}
const titleWords = divideTitle(title);
if (
returnSurname
&& (titleWords.length === 2)
&& (3 < titleWords[0].length)
&& (3 < titleWords[1].length)
&& forenames.has(titleWords[0])
&& surnames.has(titleWords[1])
) {
return (title
.split(" ")
.find(casedTitleWord => (casedTitleWord.toLowerCase() === titleWords[1]))
);
} else if (
(titleWords.length === 2)
&& (2 < titleWords[0].length)
&& (2 < titleWords[1].length)
&& forenames.has(titleWords[0])
) {
return true;
} else if (
(titleWords.length === 1)
&& (2 < titleWords[0].length)
&& (forenames.has(titleWords[0]) || surnames.has(titleWords[0]))
) {
return true;
}
function divideTitle(undividedTitle) {
const titleWords = undividedTitle.toLowerCase().split(" ");
if (titleWords.some(word => minorWords.has(word))) {
return [];
} else {
return titleWords.filter(word => !peerage.has(word));
}
}
return false;
}
function shouldProceed() {
return (AC.config.doAC && !AC.signal.emergencyHalt && (AC.chronometer.postpone < 1));
}
function isDoSayStory(type) {
return (isDoSay(type) || (type === "story"));
}
function isDoSay(type) {
return ((type === "do") || (type === "say"));
}
function permitOutput() {
return ((AC.config.deleteAllAutoCards === null) && (AC.signal.outputReplacement === ""));
}
function isAwaitingGeneration() {
return (0 < AC.generation.pending.length);
}
function isPendingGeneration() {
return notEmptyObj(AC.generation.workpiece);
}
function isPendingCompression() {
return (AC.compression.titleKey !== "");
}
function notEmptyObj(obj) {
return (obj && (0 < Object.keys(obj).length));
}
function clearTransientTitles() {
AC.database.titles.used = [];
[used, forenames, surnames].forEach(nameset => nameset.clear());
return;
}
function banTitle(title, isFinalAssignment) {
title = limitString(title.replace(/\s+/g, " ").trim(), 100);
const lowerTitle = title.toLowerCase();
if (bans.size !== 0) {
bans.add(lowerTitle);
}
if (!lowArr(Internal.getBannedTitles()).includes(lowerTitle)) {
AC.database.titles.banned.unshift(title);
if (isFinalAssignment) {
return;
}
AC.database.titles.pendingBans.unshift([title, 3]);
const index = AC.database.titles.pendingUnbans.findIndex(pair => (pair[0].toLowerCase() === lowerTitle));
if (index !== -1) {
AC.database.titles.pendingUnbans.splice(index, 1);
}
}
return;
}
function unbanTitle(title) {
title = title.replace(/\s+/g, " ").trim();
const lowerTitle = title.toLowerCase();
if (used.size !== 0) {
bans.delete(lowerTitle);
}
let index = lowArr(Internal.getBannedTitles()).indexOf(lowerTitle);
if (index !== -1) {
AC.database.titles.banned.splice(index, 1);
AC.database.titles.pendingUnbans.unshift([title, 3]);
index = AC.database.titles.pendingBans.findIndex(pair => (pair[0].toLowerCase() === lowerTitle));
if (index !== -1) {
AC.database.titles.pendingBans.splice(index, 1);
}
}
return;
}
function lowArr(arr) {
return arr.map(str => str.toLowerCase());
}
function getControlVariants() {
return O.f({
configure: O.f({
title: "Configure \nAuto-Cards",
keys: "Edit the entry above to adjust your story card automation settings",
}),
enable: O.f({
title: "Edit to enable \nAuto-Cards",
keys: "Edit the entry above to enable story card automation",
}),
});
}
function getDataVariants() {
return O.f({
debug: O.f({
title: "Debug Data",
keys: "You may view the debug state in the notes section below",
}),
critical: O.f({
title: "Critical Data",
keys: "Never modify or delete this story card",
}),
});
}
// Prepare to export the codomain
const codomain = CODOMAIN.read();
const [stopPackaged, lastCall] = (function() {
// Tbh I don't know why I even bothered going through the trouble of implementing "stop" within LSIv2
switch(HOOK) {
case "context": {
const haltStatus = [];
if (Array.isArray(codomain)) {
O.f(codomain);
haltStatus.push(true, codomain[1]);
} else {
haltStatus.push(false, STOP);
}
if ((AC.config.LSIv2 !== false) && (haltStatus[1] === true)) {
// AutoCards will return [text, (stop === true)] onContext
// The onOutput lifecycle hook will not be executed during this turn
concludeEmergency();
}
return haltStatus; }
case "output": {
// AC.config.LSIv2 being either true or null implies (lastCall === true)
return [null, AC.config.LSIv2 ?? true]; }
default: {
return [null, null]; }
}
})();
// Repackage AC to propagate its state forward in time
if (state.LSIv2) {
// Facilitates recursive calls of AutoCards
// The Auto-Cards external API is accessible through the LSIv2 scope
state.LSIv2 = AC;
} else {
const memoryOverflow = (38000 < (JSON.stringify(state).length + JSON.stringify(AC).length));
if (memoryOverflow) {
// Memory overflow is imminent
const dataVariants = getDataVariants();
if (lastCall) {
unbanTitle(dataVariants.debug.title);
banTitle(dataVariants.critical.title);
}
setData(dataVariants.critical, dataVariants.debug);
if (state.AutoCards) {
// Decouple state for safety
delete state.AutoCards;
}
} else {
if (lastCall) {
const dataVariants = getDataVariants();
unbanTitle(dataVariants.critical.title);
if (AC.config.showDebugData) {
// Update the debug data card
banTitle(dataVariants.debug.title);
setData(dataVariants.debug, dataVariants.critical);
} else {
// There should be no data card
unbanTitle(dataVariants.debug.title);
if (data === null) {
data = getSingletonCard(false, O.f({...dataVariants.debug}), O.f({...dataVariants.critical}));
}
eraseCard(data);
data = null;
}
} else if (AC.config.showDebugData && (HOOK === undefined)) {
const dataVariants = getDataVariants();
setData(dataVariants.debug, dataVariants.critical);
}
// Save a backup image to state
state.AutoCards = AC;
}
function setData(primaryVariant, secondaryVariant) {
const dataCardTemplate = O.f({
type: AC.config.defaultCardType,
title: primaryVariant.title,
keys: primaryVariant.keys,
entry: (function() {
const mutualEntry = (
"If you encounter an Auto-Cards bug or otherwise wish to help me improve this script by sharing your configs and game data, please send me the notes text found below. You may ping me @LewdLeah through the official AI Dungeon Discord server. Please ensure the content you share is appropriate for the server, otherwise DM me instead. 😌"
);
if (memoryOverflow) {
return (
"Seeing this means Auto-Cards detected an imminent memory overflow event. But fear not! As an emergency fallback, the full state of Auto-Cards' data has been serialized and written to the notes section below. This text will be deserialized during each lifecycle hook, therefore it's absolutely imperative that you avoid editing this story card!"
) + (function() {
if (AC.config.showDebugData) {
return "\n\n" + mutualEntry;
} else {
return "";
}
})();
} else {
return (
"This story card displays the full serialized state of Auto-Cards. To remove this card, simply set the \"log debug data\" setting to false within your \"Configure\" card. "
) + mutualEntry;
}
})(),
description: JSON.stringify(AC)
});
if (data === null) {
data = getSingletonCard(true, dataCardTemplate, O.f({...secondaryVariant}));
}
for (const propertyName of ["title", "keys", "entry", "description"]) {
if (data[propertyName] !== dataCardTemplate[propertyName]) {
data[propertyName] = dataCardTemplate[propertyName];
}
}
const index = storyCards.indexOf(data);
if ((index !== -1) && (index !== (storyCards.length - 1))) {
// Ensure the data card is always at the bottom of the story cards list
storyCards.splice(index, 1);
storyCards.push(data);
}
return;
}
}
// This is the only return point within the parent scope of AutoCards
if (stopPackaged === false) {
return [codomain, STOP];
} else {
return codomain;
}
} AutoCards(null); function isolateLSIv2(code, log, text, stop) { const console = Object.freeze({log}); try { eval(code); return [null, text, stop]; } catch (error) { return [error, text, stop]; } }
// Your other library scripts go here