Adventure + Scenario Exporter

Export any adventure or scenario to a local file.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @version      1.4.0
// @name         Adventure + Scenario Exporter
// @description  Export any adventure or scenario to a local file.
// @author       Magic <[email protected]>
// @supportURL   https://github.com/magicoflolis/userscriptrepo/issues
// @namespace    https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon
// @homepageURL  https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAIuSURBVHhe7di/Li1RFMfxk0iuiKtC0N5GoxaNQrSi8QJa9zXETYSKF9BSqUlEpziJB6AQfzoSuaXm8BvzdWQyM87Y48zs2Wd9khPMXnvt9dsZiWgZY4wxxpiKdTqdCb4dTK+iS9jhx8ETXUBEl3DBo8FC/ne6hFMeDwYF/kX2Lj1bZzl8CrtK7gSWwxf93pM5hZKwkTXPLGXhImguysKk1/+YnLkoDRMZv6RL2qc8LAq2S8ae2BIWshXClnAo03McrRi2hUGv/l9yFcbW5lP4GTJ9C9ubTeFTf/MXRYtmI4sTWjQXOVy1adNMevVfCOJqlFYJej7Ot/5S+IM4gztapXy15gXNNxoNWYYuMPOfpVoaYn2DR/6JBixD4W5olaK1I8r8fAs04BrzOaNVJko+LPPYHwzmTBc4R6sULU/FVZ9Y8oOG/81cTrT/mlaZtP5EaRdLftA8h/FYbmiTi7IEXcoSy/VjJicK8kibTFq/ojRBz88pqR8zOVGQe9qkaHkkrspGWf2Yp4xhWiWwlouy+jFPWUO0e6c3o+ef05TWT8OeMVMp6vOgL+34p944vn4a/A8zVUZn/ud4PzBXZXQBWxztBw20yWyV4Fi/MFslONIvegvmma+vdM4kR/pHw90yZ1+o/yVH+YtZ+4Ij/Me8P4rWzcHcpem1v6Nl85DBmcKv0Kq5FGKbPIVpzwnbw6FQe+TLpZp/lIdNQaf1WdRnQZ8xHhtjjDHGGFOJVusN4nlaFWZwR4AAAAAASUVORK5CYII=
// @license      MIT
// @compatible     chrome
// @compatible     firefox
// @compatible     edge
// @compatible     opera
// @compatible     safari
// @connect     api.aidungeon.com
// @connect     api-beta.aidungeon.com
// @connect     api-alpha.aidungeon.com
// @connect     api-internal.aidungeon.com
// @connect     play.aidungeon.com
// @connect     beta.aidungeon.com
// @connect     alpha.aidungeon.com
// @connect     internal.aidungeon.com
// @grant     unsafeWindow
// @grant     GM_addElement
// @grant     GM_info
// @grant     GM_registerMenuCommand
// @grant     GM.addElement
// @grant     GM.info
// @grant     GM.registerMenuCommand
// @match     https://play.aidungeon.com/*
// @match     https://beta.aidungeon.com/*
// @match     https://alpha.aidungeon.com/*
// @match     https://internal.aidungeon.com/*
// @match     https://api.aidungeon.com/graphql
// @match     https://api-beta.aidungeon.com/graphql
// @match     https://api-alpha.aidungeon.com/graphql
// @match     https://api-internal.aidungeon.com/graphql
// @noframes
// @run-at     document-start
// ==/UserScript==
(() => {
'use strict';
/******************************************************************************/
const inIframe = (() => {
  try {
    return window.self !== window.top;
  } catch {
    return true;
  }
})();
if (inIframe) return;
let userjs = self.userjs;
/**
 * Skip plain text documents, based on uBlock Origin
 *
 * [vapi.js](https://github.com/gorhill/uBlock/blob/68962453ff6eec7ff109615a738beb8699b9844a/platform/common/vapi.js#L35)
 */
if (
  (document instanceof Document ||
    (document instanceof XMLDocument && document.createElement('div') instanceof HTMLDivElement)) &&
  /^text\/html|^application\/(xhtml|xml)/.test(document.contentType || '') === true &&
  (self.userjs instanceof Object === false || userjs.UserJS !== true)
) {
  userjs = self.userjs = { UserJS: true };
} else {
  console.error('[%cAID Script+%c] %cERROR','color: rgb(69, 91, 106);','','color: rgb(249, 24, 128);', `MIME type is not a document, got "${document.contentType || ''}"`);
}
if (!(typeof userjs === 'object' && userjs.UserJS)) return;
/** [i18n directory](https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon/src/_locales) */
const translations = {
 "en": {
  "aiVersionsJSON": "Export Instructions (JSON)",
  "aiVersionsTXT": "Export Text Instructions (TXT)",
  "export_in": "Export in",
  "Export": "Export",
  "Import": "Import"
 }
};
/** [CSS directory](https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon/src/sass) */
const main_css = `mujs-main {
  min-height: var(--t-size-8, 64px);
  margin-top: -1px;
  border-top-color: var(--c-coreA3, rgba(219, 241, 255, 0.22));
  pointer-events: auto !important;
  border-top-style: solid;
  border-top-width: 1px;
  flex-direction: row;
  justify-content: space-between;
  gap: var(--t-space-3, 24px);
  align-items: center;
  position: relative;
  flex-shrink: 0;
  min-width: 0px;
  box-sizing: border-box;
  flex-basis: auto;
  display: flex;
  color: var(--color, rgb(255, 255, 255));
}
mujs-main[data-section=play] {
  display: contents;
}
mujs-main[data-section=play] .mujs-inp {
  color: rgb(239, 68, 68);
  border-color: rgb(223, 49, 38);
  background-color: rgba(223, 49, 38, 0.1);
}
mujs-main[data-section=play] .mujs-inp:hover {
  background-color: rgba(223, 49, 38, 0.2) !important;
  border-color: rgba(223, 49, 38, 0.1) !important;
}

mujs-elem {
  font-family: VoyageIcons;
  cursor: pointer;
  display: flex;
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: auto;
  flex-direction: row;
  align-items: center;
  min-width: 0px;
  min-height: 0px;
  position: relative;
}
mujs-elem.mujs-inp {
  border-width: 0px !important;
}
mujs-elem[data-section=play] {
  padding-left: var(--t-space-2);
  padding-right: var(--t-space-2);
  border-radius: var(--t-radius-1);
  gap: var(--t-space-1);
  border-color: var(--borderColor);
  background-color: rgba(199, 231, 255, 0.13);
  height: var(--t-size-5);
  overflow: hidden;
  border-style: solid;
  border-width: 1px;
  justify-content: center;
  box-sizing: border-box;
  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px 0px;
}
mujs-elem[data-section=play]:hover {
  box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 0px 0px !important;
}
mujs-elem[data-section=play]:not(.mujs-inp):hover {
  background-color: var(--backgroundHover) !important;
  border-color: var(--borderColorHover) !important;
}
mujs-elem[data-section=preview] {
  gap: var(--t-space-3);
  box-sizing: border-box;
  list-style-image: none;
  list-style-position: outside;
  list-style-type: none;
  padding-bottom: var(--t-space-2);
  padding-top: var(--t-space-2);
  pointer-events: auto;
}
mujs-elem[data-section=preview] span[data-section] {
  font-size: var(--f-size-3, 16px);
  font-weight: 500;
}
mujs-elem[data-section=preview] p {
  font-size: var(--f-size-2, 14px);
}
mujs-elem p {
  font-size: var(--f-size-1, 12px);
}
mujs-elem span[data-section] {
  font-family: IBMPlexSans;
  text-transform: uppercase;
  font-size: var(--f-size-2, 14px);
}`;
/******************************************************************************/
// #region Console
class con extends null {
  static #title = '[%cAID Script%c]';
  static #color = 'color: rgb(69, 91, 106);';
  /**
   * @param {unknown[]} msg
   */
  static dbg(...msg) {
    const dt = new Date();
    console.debug(
      `${con.#title} %cDBG`,
      con.#color,
      '',
      'color: rgb(255, 212, 0);',
      `[${dt.getHours()}:${('0' + dt.getMinutes()).slice(-2)}:${('0' + dt.getSeconds()).slice(-2)}]`,
      ...msg
    );
  }
  /**
   * @param {unknown[]} msg
   */
  static err(...msg) {
    console.error(`${con.#title} %cERROR`, con.#color, '', 'color: rgb(249, 24, 128);', ...msg);
    const t = con.#title.replace(/%c/g, '');
    for (const e of msg.filter((i) => i instanceof Error)) {
      if ('cause' in e) con.alert(`${t} (${e.cause}) ${e.message}`);
    }
  }
  /**
   * @param {unknown[]} msg
   */
  static info(...msg) {
    console.info(`${con.#title} %cINF`, con.#color, '', 'color: rgb(0, 186, 124);', ...msg);
  }
  /**
   * @param {unknown[]} msg
   */
  static log(...msg) {
    console.log(`${con.#title} %cLOG`, con.#color, '', 'color: rgb(219, 160, 73);', ...msg);
  }
  static alert(message) {
    if (typeof window.alert !== 'undefined') window.alert(message);
  }
}
// #endregion
// #region Utilities
/**
 * Check if we are running on a mobile device or tablet.
 * @returns {boolean}
 */
const isMobile = (() => {
  try {
    const { userAgent, userAgentData } = navigator;
    const { platform, mobile } = userAgentData ? Object(userAgentData) : {};
    return (
      /Mobile|Tablet/.test(userAgent ? String(userAgent) : '') ||
      Boolean(mobile) ||
      /Android|Apple/.test(platform ? String(platform) : '')
    );
  } catch (ex) {
    if (ex instanceof Error) ex.cause = 'getUAData';
    con.err(ex);
  }
  return false;
})();
/**
 * @template {PropertyKey} K
 * @template T
 * @param {Iterable<T>} items
 * @param {(item: T, index: number) => K} keySelector
 * @returns {Partial<Record<K, T[]>>}
 */
const groupBy = function (items, keySelector) {
  if ('groupBy' in Object) {
    return Object.groupBy(items, keySelector);
  }
  /** [Object.groupBy polyfill](https://gist.github.com/gtrabanco/7c97bd41aa74af974fa935bfb5044b6e) */
  return Array.from(items).reduce((acc, item, index) => {
    const key = keySelector(item, index);
    acc[key] ??= [];
    acc[key].push(item);
    return acc;
  }, /** @type {Partial<Record<K, T[]>>} */ ({}));
};
/**
 * Transform parameter into string
 * @template O
 * @param {O} obj
 * @returns {string}
 */
const objToStr = (obj) => {
  try {
    return Object.prototype.toString.call(obj).match(/\[object (.*)\]/)?.[1] || '';
  } catch {
    return '';
  }
};
/**
 * @template O
 * @param {O} obj
 * @returns {obj is () => unknown}
 */
const isFN = (obj) => /Function/.test(objToStr(obj));
/**
 * @template O
 * @param {O} obj
 * @returns {obj is HTMLElement}
 */
const isHTML = (obj) => /HTML/.test(objToStr(obj));
/**
 * @template O
 * @param {O} obj
 * @returns {obj is Document | Element | HTMLElement | Node}
 */
const isDOM = (obj) => /Document|Element|HTML/.test(objToStr(obj));
/**
 * Transform target into Array
 * @template T
 * @template {Record<string, boolean>} A
 * @param {T | null} [target] - The target to normalize into an array
 * @param {A} [args]
 * @param {Document | Element | HTMLElement | null} [root]
 * @returns {T extends null | undefined ? [] : T extends readonly unknown[] ? T : T extends Document | Element | HTMLElement ? [T] : T extends string ? A extends { split: true } ? string[] : typeof root extends Document | Element | HTMLElement ? Element[] : [T] : A extends { entries: true } ? T extends Record<infer K, infer V> ? Array<[K extends string ? K : string, V]> : Array<[string, unknown]> : A extends { keys: true } ? T extends Record<infer K, unknown> ? Array<K extends string ? K : string> : T extends Set<unknown> | Map<infer K, unknown> ? K[] : string[] : A extends { values: true } ? T extends Record<string, infer V> ? V[] : T extends Set<infer V> | Map<unknown, infer V> ? V[] : unknown[] : T extends Iterable<infer U> ? U[] : unknown[]}
 */
const toArray = (target, args, root) => {
  args = Object.assign({}, args);
  if (target == null) return [];
  if (Array.isArray(target)) return target;
  if (isDOM(target)) return Array.of(target);
  /** @type {keyof typeof args | undefined} */
  const method = ['split', 'entries', 'keys', 'values'].find((key) => args[key]);
  if (typeof target === 'string') {
    if (isDOM(root)) {
      const q = root.querySelectorAll(target);
      return Array.from(q);
    }
    return method === 'split' ? [...target] : [target];
  }
  if (method != null) {
    const s = objToStr(target);
    const m = method === 'split' ? 'keys' : method;
    if (/Object/.test(s)) {
      if (Object[m]) return Array.from(Object[m](target));
    } else if (/Set|Map/.test(s)) {
      /** @type {Set<unknown> | Map<unknown, unknown>} */
      const prim = target;
      if (prim[m]) return Array.from(prim[m]());
    }
  }
  return Array.from(target);
};
/**
 * Parameter is `JSON Object`
 * @template O
 * @param {O} obj
 * @returns {obj is Record<PropertyKey, unknown>}
 */
const isObj = (obj) => /Object/.test(objToStr(obj));
/**
 * Parameter is `null` or `undefined`
 * @template O
 * @param {O} obj
 * @returns {obj is (null | undefined)}
 */
const isNull = (obj) => Object.is(obj, null) || Object.is(obj, undefined);
/**
 * Parameter is Blank
 * @template O
 * @param {O} obj
 * @returns {boolean}
 */
const isBlank = (obj) => {
  if (typeof obj === 'string') return Object.is(obj.replaceAll('\0', '').trim(), '');
  return Object.is(toArray(obj, { split: true }).length, 0);
};
/**
 * Parameter is Empty
 * @template O
 * @param {O} obj
 * @returns {boolean}
 */
const isEmpty = (obj) => isNull(obj) || isBlank(obj);
/**
 * Parameter is Array
 * @template O
 * @param {O} obj
 * @returns {obj is any[]}
 */
const isArr = (obj) => Array.isArray(obj);
/**
 * @param {string} [str]
 * @returns {RegExpExecArray | []}
 */
const getUrlInfo = (str = location.pathname) => /(adventure|scenario)\/([^/]+)/.exec(str) ?? [];
/**
 * Returns the first element that is a descendant of node that matches selectors.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
 * @template {HTMLElement} E
 * @template {string} S
 * @param {S} selectors
 * @param {E} root
 */
const qs = (selectors, root) => {
  try {
    return (root || document).querySelector(selectors);
  } catch (ex) {
    con.err(ex);
    return null;
  }
};
/**
 * Create a MutationObserver for the element.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MutationObserver)
 * @template {Node} E
 * @param {E} element
 * @param {MutationCallback} listener
 * @param {MutationObserverInit} options
 * @returns {MutationObserver}
 */
const observe = (element, listener, options = { subtree: true, childList: true }) => {
  const observer = new MutationObserver(listener);
  observer.observe(element, options);
  listener.call(element, [], observer);
  return observer;
};
/**
 * Create an event listener for the element.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
 * @template {keyof HTMLElementEventMap} T
 * @template {Element} E
 * @param {E | E[]} el
 * @param {T} type
 * @param {(this: E, event: HTMLElementEventMap[T]) => *} listener
 * @param {boolean | AddEventListenerOptions} [options]
 */
const ael = (el, type, listener, options) => {
  try {
    /** @type {unknown} */
    const _listener = listener;
    for (const elem of toArray(el).filter(isHTML)) {
      if (isMobile && type === 'click') {
        elem.addEventListener('touchstart', _listener, options);
        continue;
      }
      elem.addEventListener(type, _listener, options);
    }
  } catch (ex) {
    if (ex instanceof Error) ex.cause = 'ael';
    con.err(ex);
  }
};
/**
 * Creates an instance of the element for the specified tag.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElement)
 * @template {keyof HTMLElementTagNameMap} T
 * @template {HTMLElementTagNameMap[T]} E
 * @template {{ [P in keyof E]: E[P] }} Attr
 * @param {T} tagName - The name of an element.
 * @param {Attr | string | string[]} [cname] - A className for the element.
 * @param {Attr | string} [attrs] - Set attributes for the element.
 * @returns {E}
 * @see {@link document.createElement}
 */
const make = (tagName, cname, attrs) => {
  const el = document.createElement(tagName);
  /**
   * @template {Element} E
   * @param {E} elem - HTMLElement
   * @param {string | string[]} str - Class string(s)
   */
  const addClass = (elem, str) => {
    const arr = (Array.isArray(str) ? str : typeof str === 'string' ? str.split(' ') : []).filter(
      (s) => !isEmpty(s)
    );
    return !isEmpty(arr) && elem.classList.add(...arr);
  };
  /**
   * Set attributes for an element
   * @template {Element} Elem
   * @param {Elem} elem - HTMLElement
   * @param {Elem[keyof Elem]} attr - Attributes for this HTMLElement
   */
  const formAttrs = (elem, attr = {}) => {
    if (elem == null) return elem;
    for (const [key, value] of Object.entries(attr)) {
      if (isObj(value)) {
        formAttrs(elem[key], value);
      } else if (isFN(value)) {
        if (/^on/.test(key)) {
          elem[key] = value;
          continue;
        }
        ael(elem, key, value);
      } else if (/^class/i.test(key)) {
        addClass(elem, value);
      } else if (elem.tagName === 'A' && /^(download|type)/i.test(key)) {
        elem.setAttribute(key, value);
      } else {
        elem[key] = value;
      }
    }
    return elem;
  };
  if ((typeof cname === 'string' || Array.isArray(cname)) && !isEmpty(cname)) addClass(el, cname);
  if (typeof attrs === 'string' && !isEmpty(attrs)) el.textContent = attrs;
  formAttrs(el, (isObj(cname) && cname) || (isObj(attrs) && attrs) || {});
  return el;
};
// #endregion
// #region i18n
class Language extends null {
  static i18nMap = new Map(Object.entries(translations));
  /**
   * @param { string | Date | number } str
   */
  static toDate(str) {
    return new Intl.DateTimeFormat(navigator.language).format(
      typeof str === 'string' ? new Date(str) : str
    );
  }
  /**
   * @param { number | bigint } number
   */
  static toNumber(number) {
    return new Intl.NumberFormat(navigator.language).format(number);
  }
  /**
   * @param {string} key
   * @returns {string}
   */
  static i18n$(key) {
    try {
      const c = Language.i18nMap.get(Language.current) ?? Language.i18nMap.get('en');
      return c?.[key] ?? 'INVALID KEY';
    } catch (e) {
      con.err(e);
      return 'ERROR';
    }
  }
  static get current() {
    const [current = 'en'] = navigator.language.split('-');
    if (!Language.i18nMap.has(current)) return 'en';
    return current;
  }
}
const { i18n$ } = Language;
// #endregion
/**
 * Check if we are executing in a UserScript evironment.
 * @see {@link GM}
 */
const isGM = typeof GM !== 'undefined';
/**
 * Get the Window interface
 * @see {@link unsafeWindow}
 * @see {@link window}
 */
const win = (typeof unsafeWindow !== 'undefined' && unsafeWindow) || window;
/**
 * @see {@link GM_info}
 */
const $info = (() => {
  if (isGM) {
    if (isObj(GM.info)) {
      return GM.info;
    } else if (isObj(GM_info)) {
      return GM_info;
    }
  }
  return {
    script: {
      icon: '',
      name: 'Adventure + Scenario Exporter',
      namespace: 'https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon',
      updateURL:
        'https://github.com/magicoflolis/userscriptrepo/raw/refs/heads/master/userscripts/AIDungeon/dist/main-userjs.user.js',
      version: 'Bookmarklet',
      bugs: 'https://github.com/magicoflolis/userscriptrepo/issues'
    }
  };
})();
/**
 * @param {string} css - CSS to inject
 * @param {string} [name] - Name of stylesheet
 * @param {boolean} [useMake] - Use {@link make} instead of {@link GM_addElement}
 */
const loadCSS = (css, name, useMake) => {
  if (userjs.stylesheet) return /** @type {NonNullable<typeof sty>} */ (userjs.stylesheet);
  if (useMake == null) useMake = !isGM;
  if (typeof name !== 'string' || isEmpty(name)) name = 'CSS';
  if (typeof css !== 'string')
    throw new Error('"css" must be a typeof "string"', { cause: 'loadCSS' });
  /** @type {?HTMLStyleElement} */
  let sty = null;
  if (isBlank(css)) throw new Error(`"${name}" contains empty CSS string`, { cause: 'loadCSS' });
  const parent = document.head ?? document.body ?? document.documentElement;
  if (useMake) {
    sty = make('style', {
      textContent: css,
      dataset: {
        insertedBy: $info.script.name,
        role: name
      }
    });
    parent.appendChild(sty);
    userjs.stylesheet = sty;
    return sty;
  }
  /** @type {typeof GM_addElement | undefined} */
  const fn =
    (typeof GM.addElement !== 'undefined' && isFN(GM.addElement) && GM.addElement) ||
    (typeof GM_addElement !== 'undefined' && isFN(GM_addElement) && GM_addElement);
  if (!isFN(fn)) return loadCSS(css, name, true);
  sty = fn(parent, 'style', { textContent: css });
  if (/Element/.test(objToStr(sty))) {
    sty.dataset.insertedBy = $info.script.name;
    sty.dataset.role = name;
    userjs.stylesheet = sty;
    return sty;
  }
  return loadCSS(css, name, true);
};
/**
 * Internal network framework
 */
const Network = {
  /**
   * Create a fetch request
   *
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Fetch_API)
   * @template {string | object | Blob | ArrayBuffer | Document | Response} T
   * @template {RequestInfo | URL} U
   * @template {Request["method"]} M
   * @template {VMScriptResponseType} RT
   * @template {VMScriptGMXHRDetails<T> | RequestInit} D
   * @param {U} url
   * @param {M} method
   * @param {RT} responseType
   * @param {D} data
   * @returns {Promise<T>}
   */
  async req(url, method = 'GET', responseType = 'json', data) {
    if (url == null) throw new TypeError('"url" must be a type of request.');
    if (typeof method !== 'string') throw new TypeError(`"${method}" must be a type of string.`);
    if (typeof responseType !== 'string')
      throw new TypeError(`"${responseType}" must be a type of string.`);
    method = method.toUpperCase().replaceAll(/\W/g, '');
    responseType = responseType.toLowerCase().replaceAll(/\W/g, '');
    data = Object.assign({}, { method }, data);
    const r = await fetch(url, data);
    if (!r.ok) throw new Error(r);
    const check = (s = 'text') => (isFN(r[s]) ? r[s]() : r);
    let resp = r;
    if (/arraybuffer/.test(responseType)) {
      resp = check('arrayBuffer');
    } else if (/json/.test(responseType)) {
      resp = check('json');
    } else if (/text/.test(responseType)) {
      resp = check('text');
    } else if (/blob/.test(responseType)) {
      resp = check('blob');
    } else if (/formdata/.test(responseType)) {
      resp = check('formData');
    } else if (/clone/.test(responseType)) {
      resp = check('clone');
    } else if (/document/.test(responseType)) {
      const respTxt = check('text');
      /** @type {?DOMParser} */
      let domParser;
      if (typeof DOMParser !== 'undefined') domParser = new DOMParser();
      /**
       * @param {string} txt
       */
      const fn = (txt) => (domParser ? domParser.parseFromString(txt, 'text/html') : txt);
      if (respTxt instanceof Promise) {
        resp = fn(await respTxt);
      } else {
        resp = fn(respTxt);
      }
    }
    return resp;
  },
  /**
   * @template {{loaded: number; total: number}} E
   * @param {E} evt
   */
  prog(evt) {
    return Object.is(evt.total, 0) ? '0%' : `${+((evt.loaded / evt.total) * 100).toFixed(2)}%`;
  }
};
/**
 * @template { {url: unknown; filename?: string; type?: string} } D
 * @param {D} details
 */
const doDownloadProcess = (details) => {
  if (!isObj(details) || !details.url) return;
  details.url = `data:text/plain;charset=utf-8,${encodeURIComponent(isObj(details.url) ? JSON.stringify(details.url, null, ' ') : details.url)}`;
  make('a', {
    download: details.filename || 'file',
    href: details.url,
    type: details.type || 'text/plain'
  }).dispatchEvent(new MouseEvent('click'));
};
/**
 * Internal {@link GM_registerMenuCommand} framework
 */
const Command = {
  cmds: new Set(),
  register(text, command) {
    if (!isGM || this.cmds.has(command)) return;
    if (isFN(command)) this.cmds.add(command);
    if (isFN(GM.registerMenuCommand)) {
      GM.registerMenuCommand(text, command);
    } else if (isFN(GM_registerMenuCommand)) {
      GM_registerMenuCommand(text, command);
    }
  }
};
/**
 * Grab the users firebase token
 * @param {boolean} [yoga]
 * @returns {Promise<string>}
 */
const getToken = (yoga) => {
  return new Promise((resolve, reject) => {
    /**
     * @template S
     * @param {S} str
     */
    const format = (str) => {
      if (yoga) {
        const textarea = qs('.graphiql-editor:nth-child(2) > .CodeMirror textarea');
        if (textarea) {
          textarea.value = `{\n\x20\x20"Authorization": "firebase ${str}"\n}`;
        }
      }
      return str;
    };
    if (userjs.accessToken !== undefined) {
      resolve(format(userjs.accessToken));
    }
    const dbReq = win.indexedDB.open('firebaseLocalStorageDb');
    dbReq.onerror = reject;
    dbReq.onsuccess = (event) => {
      try {
        const transaction = event.target.result.transaction(['firebaseLocalStorage'], 'readwrite');
        const objectStore = transaction.objectStore('firebaseLocalStorage');
        const allKeys = objectStore.getAllKeys();
        allKeys.onerror = reject;
        allKeys.onsuccess = (evt) => {
          const key = evt.target.result.find((r) => r.includes('firebase:authUser:'));
          objectStore.get(key).onsuccess = (e) => {
            const { value } = e.target.result;
            const accessToken = value.stsTokenManager.accessToken;
            resolve(format(accessToken));
          };
        };
      } catch {
        Network.req(
          'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyCnvo_XFPmAabrDkOKBRpbivp5UH8r_3mg',
          'POST',
          'json',
          {
            body: JSON.stringify({ returnSecureToken: true })
          }
        )
          .then((r) => {
            if (isObj(r) && 'idToken' in r && !isEmpty(r.idToken)) {
              resolve(format(r.idToken));
            } else {
              resolve('');
            }
          })
          .catch(reject);
      }
    };
  });
};
//#region fromGraphQL
/**
 * @type { import("../typings/types.d.ts").fromGraphQL }
 */
const fromGraphQL = async (type, shortId) => {
  const resp = {
    data: {}
  };
  try {
    /**
     * @template { import("../typings/types.d.ts").Templates } T
     * @type { T }
     */
    const template = {
      Custom: {
        body: shortId
      },
      GetAdventure: {
        body: {
          query:
            '{\nadventure: adventureState(shortId: "{{shortId}}") {\nid\nshortId\nscenarioId\ndetails\ninstructions\ntitle\ndescription\ntags\ngameState\nactionCount\ncontentType\ndeletedAt\nthirdPerson\nmemory\nauthorsNote\nimage\nactionWindow(limit: 1000000, offset: 0, desc: true) {\nimageText\nid\ntext\ntype\nimageUrl\nshareUrl\nadventureId\ndecisionId\nundoneAt\ndeletedAt\ncreatedAt\n}\nstoryCards {\nid\n... on StoryCard {\nid\ntype\nkeys\nvalue\ntitle\nuseForCharacterCreation\ndescription\ndeletedAt\n}\n}\n}\n}'
        }
      },
      GetScenario: {
        body: {
          query:
            '{\nscenario(shortId: "{{shortId}}") {\nid\nshortId\ntitle\ndescription\nprompt\nmemory\nauthorsNote\ndetails\nimage\nisOwner\nallowComments\ntags\nthirdPerson\ntype\ncontentType\ncontentRating\nparentScenarioId\noptions {\nid\nshortId\ntitle\nprompt\nparentScenarioId\ndeletedAt\n}\ngameCodeSharedLibrary\ngameCodeOnInput\ngameCodeOnOutput\ngameCodeOnModelContext\nstoryCards {\nid\n... on StoryCard {\nid\ntype\nkeys\nvalue\ntitle\nuseForCharacterCreation\ndescription\ndeletedAt\n}\n}\n}\n}'
        }
      },
      GetScenarioScripting: {
        body: {
          query:
            '{\nscenario(shortId: "{{shortId}}") {\nid\nshortId\ntitle\ndescription\nimage\ngameCodeSharedLibrary\ngameCodeOnInput\ngameCodeOnOutput\ngameCodeOnModelContext\nrecentScriptLogs\nlastModelContext\n}}'
        }
      },
      aiVersions: {
        body: {
          query:
            'query {\naiVisibleVersions {\nsuccess\nmessage\naiVisibleVersions {\n...AllDetails\n}\nvisibleTextVersions {\n...AllDetails\n}\nvisibleImageVersions {\n...AllDetails\n}\n}\n}\nfragment AllDetails on AiVisibleVersion {\nid\ntype\nversionName\naiDetails\naiSettings\naccess\nrelease\navailable\ninstructions\nengineNameEngine {\nengineName\navailable\navailableSettings\n}\n}'
        }
      },
      importStoryCards: {
        headers: {
          'x-gql-operation-name': 'ImportStoryCards'
        },
        body: {
          operationName: 'ImportStoryCards',
          variables: {
            input: shortId
          },
          query:
            'mutation ImportStoryCards($input: ImportStoryCardsInput!) {\nimportStoryCards(input: $input) {\nsuccess\nmessage\n}\n}'
        }
      },
      UpdateScenario: {
        headers: {
          'x-gql-operation-name': 'UpdateScenario'
        },
        body: {
          operationName: 'UpdateScenario',
          variables: {
            input: shortId
          },
          query:
            'mutation UpdateScenario($input: ScenarioInput) {\nupdateScenario(input: $input) {\nscenario {\nid\ntitle\ndescription\nprompt\nmemory\nauthorsNote\ntags\nnsfw\ncontentRating\ncontentRatingLockedAt\ncontentRatingLockedMessage\npublished\nthirdPerson\nallowComments\nunlisted\nimage\nuploadId\ntype\ndetails\neditedAt\n__typename\n}\nmessage\nsuccess\n__typename\n}}'
        }
      },
      UpdateOptionTitle: {
        headers: {
          'x-gql-operation-name': 'UpdateOptionTitle'
        },
        body: {
          operationName: 'UpdateOptionTitle',
          variables: {
            input: shortId
          },
          query:
            'mutation UpdateOptionTitle($input: ScenarioInput) {\n updateScenario(input: $input) {\n scenario {\n id\n shortId\n title\n prompt\n parentScenarioId\n deletedAt\n __typename\n }\n message\n success\n __typename\n }}'
        }
      },
      UpdateScenarioScripts: {
        headers: {
          'x-gql-operation-name': 'UpdateScenarioScripts'
        },
        body: {
          operationName: 'UpdateScenarioScripts',
          variables: shortId,
          query:
            'mutation UpdateScenarioScripts($shortId: String, $gameCode: JSONObject) {\n updateScenarioScripts(shortId: $shortId, gameCode: $gameCode) {\n success\n message\n scenario {\n id\n gameCodeSharedLibrary\n gameCodeOnInput\n gameCodeOnOutput\n gameCodeOnModelContext\n __typename\n }\n __typename\n }}'
        }
      },
      UpdateAdventureState: {
        headers: {
          'x-gql-operation-name': 'UpdateAdventureState'
        },
        body: {
          operationName: 'UpdateAdventureState',
          variables: {
            input: shortId
          },
          query:
            'mutation UpdateAdventureState($input: AdventureStateInput) {\n updateAdventureState(input: $input) {\n adventure {\n id\n details\n editedAt\n __typename\n }\n message\n success\n __typename\n }}'
        }
      },
      UpdateAdventurePlot: {
        headers: {
          'x-gql-operation-name': 'UpdateAdventurePlot'
        },
        body: {
          operationName: 'UpdateAdventurePlot',
          variables: {
            input: shortId
          },
          query:
            'mutation UpdateAdventurePlot($input: AdventurePlotInput) {\n updateAdventurePlot(input: $input) {\n adventure {\n id\n thirdPerson\n memory\n authorsNote\n editedAt\n __typename\n }\n message\n success\n __typename\n }}'
        }
      },
      UpdateAdventureDetails: {
        body: {
          operationName: 'UpdateAdventureDetails',
          variables: {
            input: shortId
          },
          query:
            'mutation UpdateAdventureDetails($input: AdventureDetailsInput) {\n updateAdventureDetails(input: $input) {\n adventure {\n id\n title\n description\n image\n uploadId\n tags\n nsfw\n contentRating\n contentRatingLockedAt\n contentRatingLockedMessage\n allowComments\n editedAt\n __typename\n }\n message\n success\n __typename\n }\n}'
        }
      },
      MainMenuViewCreateOptions: {
        body: {
          operationName: 'MainMenuViewCreateOptions',
          variables: shortId,
          query:
            'mutation MainMenuViewCreateOptions($title: String, $shortId: String, $count: Int) { createScenarioOptions(title: $title, shortId: $shortId, count: $count) { scenarios { shortId } success message } }'
        }
      },
      MainMenuViewDeleteScenario: {
        body: {
          operationName: 'MainMenuViewDeleteScenario',
          variables: shortId,
          query:
            'mutation MainMenuViewDeleteScenario($shortId: String) { deleteScenario(shortId: $shortId) { scenario {\n id\n shortId\n title\n parentScenarioId\n deletedAt\n __typename  }  success  message  __typename }}'
        }
      },
      NewMenuCreateScenario: {
        body: {
          operationName: 'NewMenuCreateScenario',
          variables: {
            // { prompt: '', title: '' }
            input: shortId
          },
          query:
            'mutation NewMenuCreateScenario($input: ScenarioInput) { createScenario(input: $input) { success message scenario { id shortId title prompt authorsNote ...CardSearchable __typename } __typename}}fragment CardSearchable on Searchable { publishTable id contentType publicId shortId title description image tags voteCount published unlisted publishedAt createdAt isOwner editedAt deletedAt blockedAt saveCount commentCount userId contentRating user { id isMember profile { id title thumbImageUrl __typename } __typename } ... on Adventure { actionCount userJoined unlisted playerCount contentResponses { userVote isSaved isDisliked __typename } __typename } ... on Scenario { adventuresPlayed contentResponses { userVote isSaved isDisliked __typename } __typename } __typename }'
        }
      },
      UpdateScenarioUpload: {
        body: {
          variables: shortId,
          query:
            'mutation UpdateScenarioUpload($shortId: String!, $uploadId: String!) {\n updateScenarioUpload(shortId: $shortId, uploadId: $uploadId) {\n scenario {\n id\n uploadId\n image\n __typename\n }\n message\n success\n __typename\n }\n}'
        }
      },
      // {
      // 	"autoGenerate": true,
      // 	"contentType": "scenario",
      // 	"description": "",
      // 	"id": "2134213",
      // 	"includeStorySummary": false,
      // 	"instructions": "",
      // 	"keys": "",
      // 	"shortId": "",
      // 	"storyInformation": "",
      // 	"temperature": 1,
      // 	"title": "",
      // 	"type": "character",
      // 	"useForCharacterCreation": true,
      // 	"value": ""
      // }
      UseCreateStoryCard: {
        body: {
          operationName: 'UseCreateStoryCard',
          variables: {
            input: shortId
          },
          query:
            'mutation UseCreateStoryCard($input: CreateStoryCardInput!) {\n createStoryCard(input: $input) {\n success\n message\n storyCard {\n id\n type\n title\n description\n keys\n value\n useForCharacterCreation\n updatedAt\n __typename\n }\n __typename\n }\n}'
        }
      },
      // {
      // 	"contentType": "scenario",
      // 	"description": "",
      // 	"id": "345345",
      // 	"includeStorySummary": true,
      // 	"instructions": "",
      // 	"keys": "",
      // 	"shortId": "",
      // 	"storyInformation": "",
      // 	"temperature": 1,
      // 	"title": "",
      // 	"type": "character",
      // 	"useForCharacterCreation": false,
      // 	"value": ""
      // }
      GenerateStoryCard: {
        body: {
          operationName: 'GenerateStoryCard',
          variables: {
            input: shortId
          },
          query:
            'mutation GenerateStoryCard($input: GenerateStoryCardInput!) {\n generateStoryCard(input: $input) {\n code\n success\n message\n storyCard {\n id\n type\n title\n description\n keys\n value\n useForCharacterCreation\n updatedAt\n __typename\n }\n __typename\n }\n}'
        }
      },
      // Manually create/save Story Card
      // 	{
      // 		"contentType": "scenario",
      // 		"description": "",
      // 		"id": "508647468",
      // 		"keys": ",
      // 		"shortId": "",
      // 		"title": "",
      // 		"type": "tesasdasd",
      // 		"useForCharacterCreation": false,
      // 		"value": "instructions"
      // 	}
      UseAutoSaveStoryCard: {
        body: {
          operationName: 'UseAutoSaveStoryCard',
          variables: {
            input: shortId
          },
          query:
            'mutation UseAutoSaveStoryCard($input: UpdateStoryCardInput!) {\n updateStoryCard(input: $input) {\n success\n message\n storyCard {\n id\n type\n title\n description\n keys\n value\n useForCharacterCreation\n updatedAt\n __typename\n }\n __typename\n }\n}'
        }
      },
      // Same as UseAutoSaveStoryCard, newer?
      SaveQueueStoryCard: {
        body: {
          operationName: 'SaveQueueStoryCard',
          variables: {
            input: shortId
          },
          query:
            'mutation SaveQueueStoryCard($input: UpdateStoryCardInput!) {\n updateStoryCard(input: $input) {\n success\n message\n storyCard {\n id\n type\n title\n description\n keys\n value\n useForCharacterCreation\n updatedAt\n __typename\n }\n __typename\n }\n}'
        }
      }
    };
    if (/scenario/.test(type)) {
      type = 'GetScenario';
    } else if (/adventure/.test(type)) {
      type = 'GetAdventure';
    }
    if (!template[type]) return resp;
    for (const v of Object.values(template)) {
      v.load = () => {
        template[type].headers ??= {};
        template[type].body ??= {};
        const { headers, body } = v;
        if (body.operationName) {
          template[type].body.variables ??= {};
        }
        body.query = body.query.replace(/\{\{shortId\}\}/g, shortId);
        return { headers, body: JSON.stringify(body) };
      };
    }
    const sel = template[type].load();
    const accessToken = await getToken();
    const headers = {
      Accept: 'application/graphql-response+json, application/json, multipart/mixed',
      authorization: `firebase ${accessToken}`,
      'content-type': 'application/json',
      'Sec-GPC': '1',
      'Sec-Fetch-Dest': 'empty',
      'Sec-Fetch-Mode': 'cors',
      'Sec-Fetch-Site': 'same-site'
    };
    /** @type {"api.aidungeon.com" | "api-beta.aidungeon.com" | "api-alpha.aidungeon.com" | "api-internal.aidungeon.com"} */
    const GRAPHQL_HOST = win?.__NEXT_DATA__?.runtimeConfig?.GRAPHQL_HOST || 'api.aidungeon.com';
    /** @type { Record<string, unknown> } */
    const req = await Network.req(`https://${GRAPHQL_HOST}/graphql`, 'POST', 'json', {
      headers: {
        ...headers,
        ...sel.headers
      },
      body: sel.body
    });
    Object.assign(resp, req);
    return resp;
  } catch (ex) {
    con.err(ex);
  }
  return resp;
};
//#endregion
/**
 * @param {string} language
 * @param {string} [content]
 */
const codeBlock = (language, content) => {
  return content === undefined
    ? `\`\`\`\n${language}\n\`\`\``
    : `\`\`\`${language}\n${content}\n\`\`\``;
};
const add = (s = '', l = 50, template = '-') => {
  let base = '';
  while (base.length < l / 2) {
    base += template;
  }
  base += s;
  while (base.length < l * 2) {
    base += template;
  }
  return base;
};
/**
 * @param {string} fileFormat
 * @param {'Export'|'Import'} type
 * @param {import("../typings/types.d.ts").fromPath} content
 */
const startDownload = async (fileFormat = 'json', type = 'Export', content = {}) => {
  const elem = qs(`mujs-elem[data-file-format="${fileFormat}"] > span[data-type="${type}"]`);
  if (isNull(elem)) return;
  const [, contentType, shortId] = getUrlInfo();
  if (!contentType)
    throw new Error('Navigate to an adventure or scenario first!', { cause: 'startDownload' });
  const world = await fromGraphQL(contentType, shortId).catch(con.err);
  if (isNull(world.data)) return;
  const worldData = world.data.adventure ?? world.data.scenario;
  if (isNull(worldData)) return;
  /**
   * @param {import("../typings/types.d.ts").StoryCard[]} sc
   */
  const transformCards = (sc) =>
    sc
      .filter((op) => op.deletedAt == null)
      .map(({ type, keys, value, title, description, useForCharacterCreation }) => {
        return {
          type,
          keys,
          value,
          title,
          description,
          useForCharacterCreation
        };
      });
  if (type === 'Import') {
    if (isEmpty(content)) throw new Error('"content" field is empty', { cause: 'startDownload' });
    if (worldData.isOwner === false)
      throw new Error(`You are not the owner of this ${contentType}.`, {
        cause: worldData.title ?? 'startDownload'
      });
    const r = content?.data.adventure ?? content?.data.scenario;
    /** @type {unknown[]} */
    const fetchRecords = [];
    const $add = (type, data) => fetchRecords.push(fromGraphQL(type, data).catch(con.err));
    const update = {
      adventureState: {
        shortId,
        details: r.details ?? content.data.adventureState?.details
      },
      adventureDetails: {
        shortId,
        allowComments: r.allowComments,
        contentRating: r.contentRating,
        description: r.description,
        image: r.image,
        tags: r.tags,
        title: r.title
      },
      adventurePlot: {
        shortId,
        memory: r.memory,
        authorsNote: r.authorsNote,
        thirdPerson: r.thirdPerson
      },
      scenario: {
        shortId,
        title: r.title,
        description: r.description,
        prompt: r.prompt,
        memory: r.memory,
        authorsNote: r.authorsNote,
        tags: r.tags,
        contentRating: r.contentRating,
        thirdPerson: r.thirdPerson,
        allowComments: r.allowComments,
        image: r.image,
        type: r.type,
        details: r.details
      },
      scripting: {
        shortId,
        gameCode: {
          onInput: r.gameCodeOnInput,
          onModelContext: r.gameCodeOnModelContext,
          onOutput: r.gameCodeOnOutput,
          sharedLibrary: r.gameCodeSharedLibrary
        }
      },
      storyCards: {
        contentType,
        shortId,
        storyCards: []
      }
    };
    const customQuery = (obj, execute = false) => {
      const custom = {
        variables: {},
        queryBody: [],
        queryParams: [],
        query: ''
      };
      const templates = {
        adventureState:
          'updateAdventureState(input: $adventureState) {\nadventure {\nid\ndetails\neditedAt\n__typename\n}\nmessage\nsuccess\n}',
        adventureDetails:
          'updateAdventureDetails (input: $adventureDetails) {\nadventure {\nid\ntitle\ndescription\nimage\nuploadId\ntags\nnsfw\ncontentRating\ncontentRatingLockedAt\ncontentRatingLockedMessage\nallowComments\neditedAt\n}\nmessage\nsuccess\n}',
        adventurePlot:
          'updateAdventurePlot(input: $adventurePlot) {\nadventure {\nid\nthirdPerson\nmemory\nauthorsNote\neditedAt\n}\n message\n success\n}',
        scenario:
          'updateScenario(input: $scenario) {\nscenario {\nid\ntitle\ndescription\n}\nmessage\nsuccess\n}',
        scripting: '',
        storyCards: 'importStoryCards(input: $storyCards) {\nsuccess\nmessage\n}'
      };
      const templateParams = {
        adventureState: 'AdventureStateInput',
        adventureDetails: 'AdventureDetailsInput',
        adventurePlot: 'AdventurePlotInput',
        scenario: 'ScenarioInput',
        scripting: '',
        storyCards: 'ImportStoryCardsInput!'
      };
      for (const [k, v] of Object.entries(obj)) {
        if (isEmpty(templates[k])) continue;
        custom.variables[k] = v;
        custom.queryBody.push(`_${k}: ${templates[k]}`);
        custom.queryParams.push(`$${k}: ${templateParams[k]}`);
      }
      custom.query = `mutation UserJSUpdate(${custom.queryParams.join(',')}) { ${custom.queryBody.join('\n')} }`;
      delete custom.queryBody;
      delete custom.queryParams;
      if (execute) {
        return fromGraphQL('Custom', custom).catch(con.err);
      }
      $add('Custom', custom);
    };
    if (isArr(r.storyCards) && !isBlank(r.storyCards)) {
      update.storyCards.storyCards = transformCards(r.storyCards);
    }
    if (contentType === 'scenario') {
      delete update.adventureState;
      delete update.adventureDetails;
      delete update.adventurePlot;
      $add('UpdateScenarioScripts', update.scripting);
      const gotIds = new Set();
      /**
       * @template {import("../typings/types.d.ts").aidDataList['scenario']['scenario']} Root
       * @param {Root} root
       * @param {string} $shortId
       */
      const createOptions = async (root, $shortId) => {
        root = Object.assign({}, root);
        if (gotIds.has(root.id)) return;
        gotIds.add(root.id);
        elem.textContent = `Importing: ${root.title}`;
        const Options = root.options.filter((op) => op.deletedAt == null);
        if (!isBlank(Options) && root.type === 'multipleChoice') {
          const g = groupBy(Options, (option) => option.parentScenarioId || root.id);
          const arr = g[root.id] || [];
          if (arr.length < 2) return;
          const createScenarioOptions = (
            await fromGraphQL('MainMenuViewCreateOptions', {
              shortId: $shortId,
              count: arr.length
            })
          ).data.createScenarioOptions;
          /** @type { { id: string; userId: string; shortId: string; title: string; prompt: string | null; parentScenarioId: string | null; deletedAt: string | null; __typename: "Scenario"; }[] } */
          const scenarios = createScenarioOptions.scenarios;
          for (let i = 0; i < scenarios.length; i++) {
            const thisWorld = scenarios[i];
            const option = arr[i];
            const obj = {
              scenario: {
                shortId: thisWorld.shortId,
                title: option.title || root.title,
                description: option.description || root.description,
                prompt: option.prompt || root.prompt,
                memory: option.memory || root.memory,
                authorsNote: option.authorsNote || root.authorsNote,
                tags: option.tags || r.tags,
                contentRating: option.contentRating || root.contentRating,
                thirdPerson: option.thirdPerson,
                allowComments: option.allowComments || root.allowComments,
                image: option.image || root.image,
                type: option.type || root.type,
                details: option.details || root.details || r.details
              },
              scripting: {
                shortId: thisWorld.shortId,
                gameCode: {
                  onInput: option.gameCodeOnInput || root.gameCodeOnInput,
                  onModelContext: option.gameCodeOnModelContext || root.gameCodeOnModelContext,
                  onOutput: option.gameCodeOnOutput || root.gameCodeOnOutput,
                  sharedLibrary: option.gameCodeSharedLibrary || root.gameCodeSharedLibrary
                }
              },
              storyCards: {
                contentType,
                shortId: thisWorld.shortId,
                storyCards: []
              }
            };
            elem.textContent = `Importing: ${option.title}`;
            if (isArr(option.storyCards) && !isBlank(option.storyCards)) {
              obj.storyCards.storyCards = transformCards(option.storyCards);
            }
            await customQuery(obj, true);
            $add('UpdateScenarioScripts', obj.scripting);
            if (option.type === 'multipleChoice') {
              const subWorlds = g[option.id] || [];
              if (subWorlds.length > 0) {
                await createOptions(
                  {
                    ...option,
                    options: [...new Set([...Options, ...subWorlds])].filter(
                      (op) => op.deletedAt == null
                    )
                  },
                  thisWorld.shortId
                );
              }
            }
          }
        }
      };
      await createOptions(r, shortId);
    } else if (contentType === 'adventure') {
      delete update.scenario;
      delete update.scripting;
    }
    customQuery(update);
    const records = await Promise.allSettled(fetchRecords);
    const msgs = ['Page reload required!'];
    for (const {
      value: { data }
    } of records) {
      for (const key of Object.keys(data)) {
        const resp = data[key];
        if (resp.success === false)
          throw new Error(`Failed to import "${key}"`, { cause: 'startDownload' });
        msgs.push(`${resp.message} - ${key}`);
      }
    }
    con.alert(msgs.join('\n\n'));
    return;
  }
  if (isArr(worldData.options)) {
    const subData = [];
    for (const opt of worldData.options.filter((o) => o.shortId !== shortId)) {
      elem.textContent = `Exporting: ${opt.title}`;
      const r = await fromGraphQL(contentType, opt.shortId).catch(con.err);
      if (!r.data) {
        con.err(r);
        continue;
      }
      const d = r.data.adventure ?? r.data.scenario;
      if (typeof d.deletedAt === 'string') continue;
      const subWorld = {
        id: d.id,
        type: d.type,
        title: d.title,
        description: d.description,
        prompt: d.prompt,
        memeory: d.memeory,
        authorsNote: d.authorsNote,
        thirdPerson: d.thirdPerson,
        details: d.details,
        parentScenarioId: d.parentScenarioId
      };
      if (isArr(d.storyCards) && !isBlank(d.storyCards)) {
        subWorld.storyCards = transformCards(d.storyCards);
      }
      if (d.gameCodeSharedLibrary) {
        subWorld.gameCodeSharedLibrary = d.gameCodeSharedLibrary;
      }
      if (d.gameCodeOnInput) {
        subWorld.gameCodeOnInput = d.gameCodeOnInput;
      }
      if (d.gameCodeOnOutput) {
        subWorld.gameCodeOnOutput = d.gameCodeOnOutput;
      }
      if (d.gameCodeOnModelContext) {
        subWorld.gameCodeOnModelContext = d.gameCodeOnModelContext;
      }
      if (isArr(d.options)) {
        subWorld.options = d.options
          .filter((op) => op.deletedAt == null)
          .map(({ id, title, prompt, parentScenarioId }) => {
            return { id, title, prompt, parentScenarioId };
          });
      }
      subData.push(subWorld);
    }
    worldData.options = subData;
  }
  const arr = [];
  let str;
  if (fileFormat === 'txt') {
    /**
     * @param {import("../typings/types.d.ts").StoryCard[]} sc
     */
    const storycards = (sc) => {
      const a = [];
      const count = {
        loaded: 0,
        total: sc.length * 1000
      };
      for (const card of sc) {
        const c = [];
        let title = '';
        for (const [k, v] of Object.entries(card)) {
          if (isEmpty(v)) continue;
          if (k === 'keys') {
            c.push(`TRIGGERS: ${v}`);
          } else if (k === 'value') {
            count.loaded += v.length;
            const p = Network.prog({
              loaded: v.length,
              total: 1000
            });
            c.push(`ENTRY (${v.length}/1000=${p}): ${v}`);
          } else if (k === 'description') {
            c.push(`NOTES: ${v}`);
          } else if (k === 'type') {
            c.push(`${k.toUpperCase()}: ${v}`);
          } else if (k === 'prompt') {
            c.push(v);
          } else if (k === 'title') {
            title = v;
          }
        }
        a.push(`${title ? `${title} ` : ''}[\n  ${c.join('\n  ')}\n]`);
      }
      return {
        ...count,
        percent: Network.prog(count),
        cards: a.join('\n')
      };
    };
    /**
     * @param { import("../typings/types.d.ts").actionWindow[] } actions
     */
    const actionWindow = (actions) => {
      const a = [];
      for (const action of actions) {
        const c = [];
        for (const [k, v] of Object.entries(action)) {
          if (isNull(v) || isBlank(v)) continue;
          if (k === 'text') {
            c.push(v);
          } else if (k === 'type') {
            c.push(`${add(v.toUpperCase(), 25, '=')}\n`);
          }
        }
        a.push(c.join('\n'));
      }
      return a.join('\n');
    };
    for (const [k, v] of Object.entries(worldData)) {
      if (isEmpty(v)) continue;
      if (/title|description|prompt/.test(k)) {
        arr.push(`${add(k.toUpperCase())}\n${v}`);
      } else if (/memory/.test(k)) {
        arr.push(`${add('PLOT ESSENTIALS')}\n${v}`);
      } else if (/authorsNote/.test(k)) {
        arr.push(`${add("AUTHOR'S NOTE")}\n${v}`);
      } else if (/storyCards/.test(k)) {
        const sc = storycards(v);
        arr.push(
          `${add(`STORY CARDS [${v.length} cards | ${sc.loaded}/${sc.total}=${sc.percent}]`)}\n${sc.cards}`
        );
      } else if (/actionWindow/.test(k)) {
        arr.push(`${add('ACTIONS')}\n${actionWindow(v)}`);
      } else if (/options/.test(k)) {
        arr.push(`${add('OPTIONS')}\n${storycards(v).cards}`);
      } else if (/details/.test(k)) {
        for (const [key, value] of Object.entries(v)) {
          if (isEmpty(value)) continue;
          if (/instructions/.test(key)) {
            arr.push(`${add('AI Instructions')}\n${JSON.stringify(value, null, ' ')}`);
          } else {
            arr.push(`${add(key.toUpperCase())}\n${value}`);
          }
        }
      } else if (/gameCodeSharedLibrary/.test(k)) {
        arr.push(`${add('SCRIPTING LIBRARY')}\n${v}`);
      } else if (/gameCodeOnInput/.test(k)) {
        arr.push(`${add('SCRIPTS INPUT')}\n${v}`);
      } else if (/gameCodeOnOutput/.test(k)) {
        arr.push(`${add('SCRIPTS OUTPUT')}\n${v}`);
      } else if (/gameCodeOnModelContext/.test(k)) {
        arr.push(`${add('SCRIPTS CONTEXT')}\n${v}`);
      } else if (/lastModelContext/.test(k)) {
        arr.push(`${add('SCRIPTS MODEL CONTEXT')}\n${v}`);
      } else if (/recentScriptLogs/.test(k)) {
        arr.push(`${add('SCRIPT LOGS')}\n${v.join('\n')}`);
      }
    }
    str = arr.join('\n');
  } else if (fileFormat === 'md' || fileFormat === 'html') {
    const fragment = fileFormat === 'html' ? document.createDocumentFragment() : null;
    const mkSection = (innerHTML = '') => {
      const d = make('details', 'flex');
      d.appendChild(make('summary', { innerHTML }));
      return d;
    };
    class Section {
      /** @type {HTMLDetailsElement} */
      elem;
      constructor(str = '') {
        this.elem = mkSection(str);
      }
      /**
       * @param  {...string} nodes
       */
      add(...nodes) {
        if (fragment) {
          const div = make('div', 'flex');
          for (const n of nodes) {
            const textarea = make('textarea', {
              placeholder: n,
              defaultValue: n,
              value: n
            });
            div.appendChild(textarea);
          }
          this.elem.appendChild(div);
        } else {
          for (const n of nodes) {
            this.elem.innerHTML += `\n\n${codeBlock('txt', n)}\n\n`;
          }
        }

        return this;
      }
      addObj(obj = {}, elem = this.elem) {
        if (fragment) {
          const div = make('div', 'flex');
          for (const [k, v] of Object.entries(obj)) {
            if (isEmpty(v)) continue;
            const container = make('div', 'flex', {
              style: 'flex-direction: row;'
            });
            if (isObj(v)) {
              this.addObj(v, container);
            } else if (isArr(v)) {
              if (k === 'storyCards') {
                const arr = v.filter((op) => op.deletedAt == null);
                const section = new Section(`Story Cards (${arr.length})`);
                for (const card of arr) {
                  new Section(`${card.title} (${card.type})`)
                    .addStoryCard(card)
                    .close(section.elem);
                }
                section.close(container);
              } else if (k === 'options') {
                const arr = v.filter((op) => op.deletedAt == null);
                const section = new Section(`Options (${arr.length})`);
                for (const opt of arr) {
                  delete opt.options;
                  delete opt.details;
                  new Section(opt.title).addObj(opt).close(section.elem);
                }
                section.close(container);
              } else {
                for (const val of v) this.addObj(val, container);
              }
            } else {
              container.appendChild(
                make('span', {
                  textContent: k
                })
              );
              const e = make('textarea', {
                placeholder: v,
                defaultValue: v,
                value: v
              });
              container.appendChild(e);
            }
            div.appendChild(container);
          }
          elem.appendChild(div);
        }
        return this;
      }
      /**
       *
       * @param {import("../typings/types.d.ts").storyCard} card
       */
      addStoryCard(card) {
        if (fragment) {
          const div = make('div', 'flex');
          for (const [k, v] of Object.entries(card)) {
            if (k === 'deletedAt' || k === 'useForCharacterCreation') continue;
            const container = make('div', 'flex', {
              style: 'flex-direction: row;'
            });
            const span = make('span', {
              textContent: k
            });
            const e = make(/value|keys|description/i.test(k) ? 'textarea' : 'input', {
              placeholder: v,
              defaultValue: v,
              value: v
            });
            container.append(span, e);
            div.appendChild(container);
          }
          this.elem.appendChild(div);
        } else {
          this.add(card.value, card.keys, card.description);
        }
        return this;
      }
      /**
       * @param {HTMLElement} [parent]
       */
      close(parent) {
        if (fragment) {
          (parent || fragment).append(this.elem);
        } else {
          arr.push(this.elem.outerHTML);
        }
      }
    }
    const closeDiv = make('div', 'flex');
    new Section(`Information (${worldData.title})`)
      .addObj({
        ID: worldData.id,
        Title: worldData.title,
        Description: worldData.description,
        Prompt: worldData.prompt,
        "Author's Note": worldData.authorsNote,
        Memory: worldData.memory
      })
      .close(closeDiv);
    if (!isEmpty(worldData.options)) {
      const o = new Section('Options');
      for (const opt of worldData.options.filter((op) => op.deletedAt == null)) {
        delete opt.options;
        delete opt.details;
        new Section(opt.title).addObj(opt).close(o.elem);
      }
      o.close(closeDiv);
    }
    if (!isEmpty(worldData.gameState)) {
      new Section('Game State')
        .addObj({ state: JSON.stringify(worldData.gameState, null, ' ') })
        .close(closeDiv);
    }
    if (
      [
        worldData.gameCodeSharedLibrary,
        worldData.gameCodeOnInput,
        worldData.gameCodeOnOutput,
        worldData.gameCodeOnModelContext
      ].find((i) => !isEmpty(i))
    ) {
      new Section('Scripts')
        .addObj({
          SharedLibrary: worldData.gameCodeSharedLibrary,
          Input: worldData.gameCodeOnInput,
          Output: worldData.gameCodeOnOutput,
          Context: worldData.gameCodeOnModelContext
        })
        .close(closeDiv);
    }
    if (!isEmpty(worldData.storyCards)) {
      const section = new Section(`Story Cards (${worldData.storyCards.length})`);
      for (const card of worldData.storyCards) {
        new Section(`${card.title} (${card.type})`).addStoryCard(card).close(section.elem);
      }
      section.close(closeDiv);
    }
    if (!isEmpty(worldData.actionWindow)) {
      const section = new Section(`Actions (${worldData.actionWindow.length})`);
      for (const action of worldData.actionWindow.filter((op) => op.deletedAt == null)) {
        const createdAt = new Date(action.createdAt);
        new Section(`${createdAt.toLocaleString(navigator.language)} ${action.type.toUpperCase()}`)
          .add(action.text, `# Discord TimeStamp\n<t:${+createdAt}></t:${+createdAt}>`)
          .close(section.elem);
      }
      section.close(closeDiv);
    }
    if (fragment) {
      const style = make('style', {
        textContent:
          'details { cursor: pointer; } .flex { display: flex; gap: 1em; flex-wrap: wrap; flex-direction: column; }'
      });
      fragment.append(closeDiv, style);
      const serializer = new XMLSerializer();
      str = serializer.serializeToString(fragment);
    } else {
      str = arr.join('\n');
    }
  }
  doDownloadProcess({
    url: fileFormat === 'json' ? world : str,
    filename: `${worldData.title}_${worldData.shortId}.${contentType}.${fileFormat}`
  });
};
/**
 * @template {HTMLElement} P
 * @param {P} parent
 * @param {string} section
 * @param {string} fileFormat
 * @param {'Export'|'Import'} type
 */
const inject = (parent, section = 'play', fileFormat = 'json', type = 'Export') => {
  if (!parent) return;
  if (section === 'play' && type === 'Export') {
    parent.style = 'flex-direction: column;';
  }
  const o = {
    Export: {
      class: 'mujs-btn',
      color: 't_coreA1',
      text: 'w_export'
    },
    Import: {
      class: 'mujs-inp',
      color: 't_redA',
      text: 'w_import'
    }
  };
  if (qs(`.${o[type].class}[data-file-format="${fileFormat}"]`)) return;
  /**
   * @type { HTMLInputElement }
   */
  let inpJSON;
  const [, contentType] = getUrlInfo();
  const btn = make('mujs-elem', o[type].class, {
    dataset: {
      fileFormat,
      section
    }
  });
  const textContent = `${i18n$(type)} ${contentType} (${fileFormat.toUpperCase()})`;
  const txt = make('span', {
    textContent,
    dataset: {
      section,
      type
    }
  });
  const ico = make('p', {
    textContent: o[type].text
  });
  const s = make('mujs-main', {
    dataset: {
      section
    }
  });
  const resetText = () => {
    txt.innerHTML = textContent;
  };
  ico.setAttribute('importantforaccessibility', 'no');
  ico.setAttribute('aria-hidden', 'true');
  btn.append(ico, txt);
  btn.onclick = async (evt) => {
    evt.preventDefault();
    if (type === 'Export') {
      txt.textContent = `Exporting (${fileFormat.toUpperCase()})...`;
      await startDownload(fileFormat, type).catch(con.err);
      resetText();
    } else if (type === 'Import' && inpJSON) {
      txt.textContent = `Importing ${contentType}...`;
      inpJSON.click();
    }
  };
  s.append(btn);
  parent.appendChild(s);
  if (type === 'Import') {
    inpJSON = make('input', {
      type: 'file',
      accept: '.json',
      style: 'display: none;'
    });
    ael(inpJSON, 'cancel', resetText);
    ael(inpJSON, 'change', function () {
      if (this.files == null) return;
      const file = this.files[0];
      if (file === undefined || file.name === '') return resetText();
      const fr = new FileReader();
      fr.onload = function () {
        if (typeof this.result === 'string') {
          const content = JSON.parse(this.result);
          startDownload(fileFormat, type, content).catch(con.err).finally(resetText);
        } else {
          resetText();
        }
      };
      fr.onerror = resetText;
      fr.readAsText(file);
    });
    parent.appendChild(inpJSON);
  }
};

/**
 * @template F
 * @param { (this: F, doc: Document) => * } onDomReady
 */
const loadDOM = (onDomReady) => {
  if (typeof onDomReady === 'function') {
    if (document.readyState === 'interactive' || document.readyState === 'complete') {
      onDomReady.call(/** @type {F} */ (undefined), document);
    } else {
      document.addEventListener(
        'DOMContentLoaded',
        (evt) =>
          onDomReady.call(/** @type {F} */ (undefined), /** @type {Document} */ (evt.target)),
        {
          once: true
        }
      );
    }
  }
};

if (!('userjs' in win)) {
  Object.assign(win, {
    userjs: {
      getToken,
      fromGraphQL,
      doDownloadProcess
    }
  });
}

loadDOM((doc) => {
  try {
    if ([window.location, doc].filter((i) => i == null).length > 0) {
      throw new Error(
        '"window.location" or "document" is null, reload the webpage or use a different one',
        {
          cause: 'loadDOM'
        }
      );
    }
    if (location.hostname.startsWith('api-') && location.pathname === '/graphql') {
      return getToken(true);
    }
    if (isNull(loadCSS(main_css, 'primary-stylesheet')))
      throw new Error('Failed to initialize script!', { cause: 'loadCSS' });
    const fileFormats = ['json', 'txt', 'html'];
    const ignoreTags = new Set(['br', 'head', 'link', 'meta', 'script', 'style']);
    observe(doc, (mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;
          if (ignoreTags.has(node.localName)) continue;
          if (node.parentElement === null) continue;
          if (!(node instanceof HTMLElement)) continue;
          let n = qs('div[aria-label="Content stats"]', node);
          if (n) {
            for (const f of fileFormats) inject(n, 'preview', f);
            inject(n, 'preview', 'json', 'Import');
          }
          const p = toArray('p[aria-level="2"]', {}, node).find((elem) =>
            elem.textContent.includes('story card management')
          );
          if (p) {
            n = p.parentElement;
            for (const f of fileFormats) inject(n, 'play', f);
            inject(n, 'play', 'json', 'Import');
          }
        }
      }
    });
    Command.register(i18n$('aiVersionsJSON'), async () => {
      try {
        const o = await fromGraphQL('aiVersions');
        if (!o.data.aiVisibleVersions)
          throw new Error('failed to load', {
            cause: `${i18n$('aiVersionsJSON')} - aiVersions`
          });
        doDownloadProcess({
          url: o,
          filename: `Instructions-${Date.now()}.json`
        });
      } catch (e) {
        con.err(e);
      }
    });
    Command.register(i18n$('aiVersionsTXT'), async () => {
      try {
        const o = await fromGraphQL('aiVersions');
        const root = o.data.aiVisibleVersions;
        if (!root)
          throw new Error('failed to load', {
            cause: `${i18n$('aiVersionsTXT')} - aiVersions`
          });
        /** @type {string[]} */
        const arr = [];
        const $line = (v, end = '\n') => {
          arr.push(`${v}${end}`);
        };
        for (const v of root.visibleTextVersions) {
          $line(add(v.aiDetails.title));
          $line(v.instructions, '\n\n');
        }
        doDownloadProcess({
          url: arr.join('\n'),
          filename: `Text_Versions-${Date.now()}.txt`
        });
      } catch (e) {
        con.err(e);
      }
    });
    for (const f of fileFormats) {
      Command.register(`${i18n$('export_in')} (${f.toUpperCase()})`, async () => {
        await startDownload(f).catch(con.err);
      });
    }
  } catch (ex) {
    con.err(ex);
  }
});

})();