Adventure + Scenario Exporter

Export any adventure or scenario to a local file.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
  }
});

})();