🏠 返回首頁 

Greasy Fork is available in English.

Adventure + Scenario Exporter

Export any adventure or scenario to a local file.

  1. // ==UserScript==
  2. // @version 1.0.1
  3. // @name Adventure + Scenario Exporter
  4. // @description Export any adventure or scenario to a local file.
  5. // @author Magic <magicoflolis@tuta.io>
  6. // @supportURL https://github.com/magicoflolis/userscriptrepo/issues
  7. // @namespace https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon
  8. // @homepageURL https://github.com/magicoflolis/userscriptrepo/tree/master/userscripts/AIDungeon
  9. // @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=
  10. // @license MIT
  11. // @compatible chrome
  12. // @compatible firefox
  13. // @compatible edge
  14. // @compatible opera
  15. // @compatible safari
  16. // @connect play.aidungeon.com
  17. // @connect beta.aidungeon.com
  18. // @grant unsafeWindow
  19. // @grant GM_registerMenuCommand
  20. // @grant GM.registerMenuCommand
  21. // @match https://play.aidungeon.com/*
  22. // @match https://beta.aidungeon.com/*
  23. // @noframes
  24. // @run-at document-start
  25. // ==/UserScript==
  26. (() => {
  27. 'use strict';
  28. /******************************************************************************/
  29. const inIframe = (() => {
  30. try {
  31. return window.self !== window.top;
  32. } catch (e) {
  33. return true;
  34. }
  35. })();
  36. if (inIframe) {
  37. return;
  38. }
  39. let userjs = self.userjs;
  40. /**
  41. * Skip text/plain documents, based on uBlock Origin `vapi.js` file
  42. *
  43. * [source code](https://github.com/gorhill/uBlock/blob/68962453ff6eec7ff109615a738beb8699b9844a/platform/common/vapi.js#L35)
  44. */
  45. if (
  46. (document instanceof Document ||
  47. (document instanceof XMLDocument && document.createElement('div') instanceof HTMLDivElement)) &&
  48. /^text\/html|^application\/(xhtml|xml)/.test(document.contentType || '') === true &&
  49. (self.userjs instanceof Object === false || userjs.UserJS !== true)
  50. ) {
  51. userjs = self.userjs = { UserJS: true };
  52. } else {
  53. 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 || ''}"`);
  54. }
  55. if (!(typeof userjs === 'object' && userjs.UserJS)) {
  56. return;
  57. }
  58. /******************************************************************************/
  59. // #region Console
  60. const con = {
  61. title: '[%cAID Script%c]',
  62. color: 'color: rgb(69, 91, 106);',
  63. dbg(...msg) {
  64. const dt = new Date();
  65. console.debug(
  66. `${con.title} %cDBG`,
  67. con.color,
  68. '',
  69. 'color: rgb(255, 212, 0);',
  70. `[${dt.getHours()}:${('0' + dt.getMinutes()).slice(-2)}:${('0' + dt.getSeconds()).slice(-2)}]`,
  71. ...msg
  72. );
  73. },
  74. err(...msg) {
  75. console.error(`${con.title} %cERROR`, con.color, '', 'color: rgb(249, 24, 128);', ...msg);
  76. const a = typeof alert !== 'undefined' && alert;
  77. const t = con.title.replace(/%c/g, '');
  78. for (const ex of msg) {
  79. if (typeof ex === 'object' && 'cause' in ex && a) {
  80. a(`${t} (${ex.cause}) ${ex.message}`);
  81. }
  82. }
  83. },
  84. info(...msg) {
  85. console.info(`${con.title} %cINF`, con.color, '', 'color: rgb(0, 186, 124);', ...msg);
  86. },
  87. log(...msg) {
  88. console.log(`${con.title} %cLOG`, con.color, '', 'color: rgb(219, 160, 73);', ...msg);
  89. }
  90. };
  91. const { err } = con;
  92. // #endregion
  93. // #region Constants
  94. const isMobile = (() => {
  95. try {
  96. if (navigator) {
  97. const { userAgent, userAgentData } = navigator;
  98. const { platform, mobile } = userAgentData ? Object(userAgentData) : {};
  99. return (
  100. /Mobile|Tablet/.test(userAgent ? String(userAgent) : '') ||
  101. Boolean(mobile) ||
  102. /Android|Apple/.test(platform ? String(platform) : '')
  103. );
  104. }
  105. } catch (ex) {
  106. ex.cause = 'getUAData';
  107. err(ex);
  108. }
  109. return false;
  110. })();
  111. const win = unsafeWindow ?? window;
  112. const isGM = typeof GM !== 'undefined';
  113. // #endregion
  114. // #region Validators
  115. const objToStr = (obj) => Object.prototype.toString.call(obj);
  116. // const isElem = (obj) => /Element/.test(objToStr(obj));
  117. const isHTML = (obj) => /object HTML/.test(objToStr(obj));
  118. const isObj = (obj) => /Object/.test(objToStr(obj));
  119. const isFN = (obj) => /Function/.test(objToStr(obj));
  120. /**
  121. * @type { import("../typings/shared.d.ts").isNull }
  122. */
  123. const isNull = (obj) => {
  124. return Object.is(obj, null) || Object.is(obj, undefined);
  125. };
  126. /**
  127. * @type { import("../typings/shared.d.ts").isBlank }
  128. */
  129. const isBlank = (obj) => {
  130. return (
  131. (typeof obj === 'string' && Object.is(obj.trim(), '')) ||
  132. ((obj instanceof Set || obj instanceof Map) && Object.is(obj.size, 0)) ||
  133. (Array.isArray(obj) && Object.is(obj.length, 0)) ||
  134. (isObj(obj) && Object.is(Object.keys(obj).length, 0))
  135. );
  136. };
  137. /**
  138. * @type { import("../typings/shared.d.ts").isEmpty }
  139. */
  140. const isEmpty = (obj) => {
  141. return isNull(obj) || isBlank(obj);
  142. };
  143. // #endregion
  144. // #region Utilities
  145. /**
  146. * @type { import("../typings/shared.d.ts").qs }
  147. */
  148. const qs = (selector, root) => {
  149. try {
  150. return (root || document).querySelector(selector);
  151. } catch (ex) {
  152. err(ex);
  153. }
  154. return null;
  155. };
  156. /**
  157. * @type { import("../typings/shared.d.ts").normalizeTarget }
  158. */
  159. const normalizeTarget = (target, toQuery = true, root) => {
  160. if (Object.is(target, null) || Object.is(target, undefined)) {
  161. return [];
  162. }
  163. if (Array.isArray(target)) {
  164. return target;
  165. }
  166. if (typeof target === 'string') {
  167. return toQuery ? Array.from((root || document).querySelectorAll(target)) : Array.of(target);
  168. }
  169. if (/object HTML/.test(Object.prototype.toString.call(target))) {
  170. return Array.of(target);
  171. }
  172. return Array.from(target);
  173. };
  174. /**
  175. * @type { import("../typings/shared.d.ts").observe }
  176. */
  177. const observe = (element, listener, options = { subtree: true, childList: true }) => {
  178. const observer = new MutationObserver(listener);
  179. observer.observe(element, options);
  180. listener.call(element, [], observer);
  181. return observer;
  182. };
  183. /**
  184. * @type { import("../typings/shared.d.ts").ael }
  185. */
  186. const ael = (el, type, listener, options = {}) => {
  187. try {
  188. for (const elem of normalizeTarget(el).filter(isHTML)) {
  189. if (isMobile && type === 'click') {
  190. elem.addEventListener('touchstart', listener, options);
  191. continue;
  192. }
  193. elem.addEventListener(type, listener, options);
  194. }
  195. } catch (ex) {
  196. ex.cause = 'ael';
  197. err(ex);
  198. }
  199. };
  200. /**
  201. * @type { import("../typings/shared.d.ts").make }
  202. */
  203. const make = (tagName, cname, attrs) => {
  204. let el;
  205. try {
  206. /**
  207. * @param {HTMLElement} elem
  208. * @param {string|string[]} str
  209. */
  210. const addClass = (elem, str) => {
  211. const arr = (Array.isArray(str) ? str : typeof str === 'string' ? str.split(' ') : []).filter(
  212. (s) => !isEmpty(s)
  213. );
  214. return !isEmpty(arr) && elem.classList.add(...arr);
  215. };
  216. /**
  217. * @type { import("../typings/shared.d.ts").formAttrs }
  218. */
  219. const formAttrs = (elem, attr) => {
  220. for (const [key, value] of Object.entries(attr)) {
  221. if (typeof value === 'object') {
  222. formAttrs(elem[key], value);
  223. } else if (isFN(value)) {
  224. if (/^on/.test(key)) {
  225. elem[key] = value;
  226. continue;
  227. }
  228. ael(elem, key, value);
  229. } else if (/^class/i.test(key)) {
  230. addClass(elem, value);
  231. } else {
  232. elem[key] = value;
  233. }
  234. }
  235. return elem;
  236. };
  237. el = document.createElement(tagName);
  238. if ((typeof cname === 'string' || Array.isArray(cname)) && !isEmpty(cname)) addClass(el, cname);
  239. if (typeof attrs === 'string' && !isEmpty(attrs)) el.textContent = attrs;
  240. formAttrs(el, isObj(cname) ? cname : isObj(attrs) ? attrs : {});
  241. } catch (ex) {
  242. if (ex instanceof DOMException) throw new Error(`${ex.name}: ${ex.message}`, { cause: 'make' });
  243. ex.cause = 'make';
  244. err(ex);
  245. }
  246. return el;
  247. };
  248. //#endregion
  249. /**
  250. * @type { import("../typings/shared.d.ts").Network }
  251. */
  252. const Network = {
  253. async req(url, method = 'GET', responseType = 'json', data) {
  254. if (isEmpty(url)) throw new Error('"url" parameter is empty');
  255. data = Object.assign({}, data);
  256. method = this.bscStr(method, false);
  257. responseType = this.bscStr(responseType);
  258. const params = {
  259. method,
  260. ...data
  261. };
  262. return new Promise((resolve, reject) => {
  263. fetch(url, params)
  264. .then((response_1) => {
  265. if (!response_1.ok) reject(response_1);
  266. const check = (str_2 = 'text') => {
  267. return isFN(response_1[str_2]) ? response_1[str_2]() : response_1;
  268. };
  269. if (responseType.match(/buffer/)) {
  270. resolve(check('arrayBuffer'));
  271. } else if (responseType.match(/json/)) {
  272. resolve(check('json'));
  273. } else if (responseType.match(/text/)) {
  274. resolve(check('text'));
  275. } else if (responseType.match(/blob/)) {
  276. resolve(check('blob'));
  277. } else if (responseType.match(/formdata/)) {
  278. resolve(check('formData'));
  279. } else if (responseType.match(/clone/)) {
  280. resolve(check('clone'));
  281. } else if (responseType.match(/document/)) {
  282. const respTxt = check('text');
  283. const domParser = new DOMParser();
  284. if (respTxt instanceof Promise) {
  285. respTxt.then((txt) => {
  286. const doc = domParser.parseFromString(txt, 'text/html');
  287. resolve(doc);
  288. });
  289. } else {
  290. const doc = domParser.parseFromString(respTxt, 'text/html');
  291. resolve(doc);
  292. }
  293. } else {
  294. resolve(response_1);
  295. }
  296. })
  297. .catch(reject);
  298. });
  299. },
  300. bscStr(str = '', lowerCase = true) {
  301. return str[lowerCase ? 'toLowerCase' : 'toUpperCase']().replaceAll(/\W/g, '');
  302. }
  303. };
  304. const doDownloadProcess = (details) => {
  305. if (!details.url) {
  306. return;
  307. }
  308. const a = make('a');
  309. a.href = details.url;
  310. a.setAttribute('download', details.filename || '');
  311. a.setAttribute('type', 'text/plain');
  312. a.dispatchEvent(new MouseEvent('click'));
  313. };
  314. const Command = {
  315. cmds: new Set(),
  316. register(text, command) {
  317. if (!isGM) {
  318. return;
  319. }
  320. if (isFN(command)) {
  321. if (this.cmds.has(command)) {
  322. return;
  323. }
  324. this.cmds.add(command);
  325. }
  326. if (isFN(GM.registerMenuCommand)) {
  327. GM.registerMenuCommand(text, command);
  328. } else if (isFN(GM_registerMenuCommand)) {
  329. GM_registerMenuCommand(text, command);
  330. }
  331. }
  332. };
  333. /**
  334. * @returns {Promise<string>}
  335. */
  336. const getToken = () => {
  337. return new Promise((resolve, reject) => {
  338. if (userjs.accessToken !== undefined) resolve(userjs.accessToken);
  339. const dbReq = win.indexedDB.open('firebaseLocalStorageDb');
  340. dbReq.onerror = reject;
  341. dbReq.onsuccess = (event) => {
  342. const transaction = event.target.r###lt.transaction(['firebaseLocalStorage'], 'readwrite');
  343. const objectStore = transaction.objectStore('firebaseLocalStorage');
  344. const allKeys = objectStore.getAllKeys();
  345. allKeys.onerror = reject;
  346. allKeys.onsuccess = (evt) => {
  347. const key = evt.target.r###lt.find((r) => r.includes('firebase:authUser:'));
  348. objectStore.get(key).onsuccess = (evt) => {
  349. const { value } = evt.target.r###lt;
  350. userjs.accessToken = value.stsTokenManager.accessToken;
  351. resolve(userjs.accessToken);
  352. };
  353. };
  354. };
  355. });
  356. };
  357. class dataStructure {
  358. /** @type { import("../typings/types.d.ts").dataStructure<this["token"]>["_headers"] } */
  359. _headers;
  360. /** @type {string} */
  361. token;
  362. /** @type {string} */
  363. operationName;
  364. /** @type {{[key: string]: any}} */
  365. variables;
  366. /** @type {string} */
  367. query;
  368. constructor(accessToken) {
  369. this.token = accessToken;
  370. }
  371. get headers() {
  372. return this._headers;
  373. }
  374. set headers(data) {
  375. this._headers = {
  376. authorization: `firebase ${this.token}`,
  377. 'content-type': 'application/json',
  378. 'x-gql-operation-name': data,
  379. 'Sec-GPC': '1',
  380. 'Sec-Fetch-Dest': 'empty',
  381. 'Sec-Fetch-Mode': 'cors',
  382. 'Sec-Fetch-Site': 'same-site',
  383. Priority: 'u=4'
  384. };
  385. }
  386. get body() {
  387. return JSON.stringify({
  388. operationName: this.operationName,
  389. variables: this.variables,
  390. query: this.query
  391. });
  392. }
  393. set body(data) {
  394. this.operationName = data.operationName;
  395. this.variables = data.variables ?? {};
  396. this.query = data.query;
  397. }
  398. format() {
  399. return {
  400. headers: this.headers,
  401. referrer: 'https://play.aidungeon.com/',
  402. body: this.body
  403. };
  404. }
  405. }
  406. /**
  407. * @type { import("../typings/types.d.ts").fromGraphQL }
  408. */
  409. const fromGraphQL = async (type, shortId) => {
  410. const resp = {
  411. data: {}
  412. };
  413. try {
  414. /** @type { import("../typings/types.d.ts").Templates } */
  415. const template = {
  416. adventure: {
  417. headers: {
  418. 'x-gql-operation-name': 'GetGameplayAdventure'
  419. },
  420. body: {
  421. operationName: 'GetGameplayAdventure',
  422. variables: { shortId, limit: 10000, desc: true },
  423. query:
  424. 'query GetGameplayAdventure($shortId: String, $limit: Int, $offset: Int, $desc: Boolean) {\n adventure(shortId: $shortId) {\n id\n publicId\n shortId\n scenarioId\n instructions\n title\n description\n tags\n nsfw\n isOwner\n userJoined\n gameState\n actionCount\n contentType\n createdAt\n showComments\n commentCount\n allowComments\n voteCount\n userVote\n editedAt\n published\n unlisted\n deletedAt\n saveCount\n isSaved\n user {\n id\n isCurrentUser\n isMember\n profile {\n id\n title\n thumbImageUrl\n __typename\n }\n __typename\n }\n shortCode\n thirdPerson\n imageStyle\n memory\n authorsNote\n image\n actionWindow(limit: $limit, offset: $offset, desc: $desc) {\n id\n imageText\n ...ActionSubscriptionAction\n __typename\n }\n allPlayers {\n ...PlayerSubscriptionPlayer\n __typename\n }\n storyCards {\n id\n ...StoryCard\n __typename\n }\n __typename\n }\n}\n\nfragment ActionSubscriptionAction on Action {\n id\n text\n type\n imageUrl\n shareUrl\n imageText\n adventureId\n decisionId\n undoneAt\n deletedAt\n createdAt\n logId\n __typename\n}\n\nfragment PlayerSubscriptionPlayer on Player {\n id\n userId\n characterName\n isTypingAt\n user {\n id\n isMember\n profile {\n id\n title\n thumbImageUrl\n __typename\n }\n __typename\n }\n createdAt\n deletedAt\n blockedAt\n __typename\n}\n\nfragment StoryCard on StoryCard {\n id\n type\n keys\n value\n title\n useForCharacterCreation\n description\n updatedAt\n deletedAt\n __typename\n}'
  425. }
  426. },
  427. adventureDetails: {
  428. body: {
  429. operationName: 'GetAdventureDetails',
  430. variables: { shortId },
  431. query:
  432. 'query GetAdventureDetails($shortId: String) {\n adventureState(shortId: $shortId) {\n id\n details\n __typename\n }\n}'
  433. }
  434. },
  435. scenario: {
  436. headers: {
  437. 'x-gql-operation-name': 'GetScenario'
  438. },
  439. body: {
  440. operationName: 'GetScenario',
  441. variables: { shortId },
  442. query:
  443. 'query GetScenario($shortId: String) {\n scenario(shortId: $shortId) {\n id\n contentType\n createdAt\n editedAt\n publicId\n shortId\n title\n description\n prompt\n memory\n authorsNote\n image\n isOwner\n published\n unlisted\n allowComments\n showComments\n commentCount\n voteCount\n userVote\n saveCount\n storyCardCount\n isSaved\n tags\n adventuresPlayed\n thirdPerson\n nsfw\n contentRating\n contentRatingLockedAt\n contentRatingLockedMessage\n tags\n type\n details\n parentScenario {\n id\n shortId\n title\n __typename\n }\n user {\n isCurrentUser\n isMember\n profile {\n title\n thumbImageUrl\n __typename\n }\n __typename\n }\n options {\n id\n userId\n shortId\n title\n prompt\n parentScenarioId\n deletedAt\n __typename\n }\n storyCards {\n id\n ...StoryCard\n __typename\n }\n ...CardSearchable\n __typename\n }\n}\n\nfragment CardSearchable on Searchable {\n id\n contentType\n publicId\n shortId\n title\n description\n image\n tags\n userVote\n voteCount\n published\n unlisted\n publishedAt\n createdAt\n isOwner\n editedAt\n deletedAt\n blockedAt\n isSaved\n saveCount\n commentCount\n userId\n contentRating\n user {\n id\n isMember\n profile {\n id\n title\n thumbImageUrl\n __typename\n }\n __typename\n }\n ... on Adventure {\n actionCount\n userJoined\n playPublicId\n unlisted\n playerCount\n __typename\n }\n ... on Scenario {\n adventuresPlayed\n __typename\n }\n __typename\n}\n\nfragment StoryCard on StoryCard {\n id\n type\n keys\n value\n title\n useForCharacterCreation\n description\n updatedAt\n deletedAt\n __typename\n}'
  444. }
  445. },
  446. aiVersions: {
  447. headers: {
  448. 'x-gql-operation-name': 'GetAiVersions'
  449. },
  450. body: {
  451. operationName: 'GetAiVersions',
  452. variables: {},
  453. query:
  454. 'query GetAiVersions {\n aiVisibleVersions {\n success\n message\n aiVisibleVersions {\n id\n type\n versionName\n aiDetails\n aiSettings\n access\n release\n available\n instructions\n engineNameEngine {\n engineName\n available\n availableSettings\n __typename\n }\n __typename\n }\n visibleTextVersions {\n id\n type\n versionName\n aiDetails\n aiSettings\n access\n release\n available\n instructions\n engineNameEngine {\n engineName\n available\n availableSettings\n __typename\n }\n __typename\n }\n visibleImageVersions {\n id\n type\n versionName\n aiDetails\n aiSettings\n access\n release\n available\n instructions\n engineNameEngine {\n engineName\n available\n availableSettings\n __typename\n }\n __typename\n }\n __typename\n }\n}'
  455. }
  456. }
  457. };
  458. const sel = template[type];
  459. if (!sel) {
  460. return resp;
  461. }
  462. const accessToken = await getToken();
  463. const ds = new dataStructure(accessToken);
  464. ds.headers = sel.headers;
  465. ds.body = sel.body;
  466. const req = await Network.req('https://api.aidungeon.com/graphql', 'POST', 'json', ds.format());
  467. if (/adventure/.test(type)) {
  468. ds.body = template['adventureDetails'];
  469. const state = await Network.req(
  470. 'https://api.aidungeon.com/graphql',
  471. 'POST',
  472. 'json',
  473. ds.format()
  474. );
  475. Object.assign(resp.data, { ...req.data, ...state.data });
  476. } else {
  477. Object.assign(resp, req);
  478. }
  479. return resp;
  480. } catch (ex) {
  481. err(ex);
  482. }
  483. return resp;
  484. };
  485. const codeBlock = (language, content) => {
  486. return content === undefined
  487. ? `\`\`\`\n${language}\n\`\`\``
  488. : `\`\`\`${language}\n${content}\n\`\`\``;
  489. };
  490. const add = (s = '', l = 50, template = '-') => {
  491. let base = '';
  492. while (base.length < l / 2) {
  493. base += template;
  494. }
  495. base += s;
  496. while (base.length < l * 2) {
  497. base += template;
  498. }
  499. return base;
  500. };
  501. const startDownload = async (fileFormat = 'json') => {
  502. const p = /\/(adventure|scenario)\/([\w-]+)\/.+(\/)?/.exec(location.pathname);
  503. if (p === null) throw new Error('Navigate to an adventure or scenario first!', { cause: 'startDownload' });
  504. /**
  505. * @type { import("../typings/types.d.ts").fromPath }
  506. */
  507. const obj = await fromGraphQL(p[1], p[2]);
  508. const root = obj.data.adventure ?? obj.data.scenario;
  509. const arr = [];
  510. let str;
  511. if (fileFormat === 'txt') {
  512. /**
  513. * @param { import("../typings/types.d.ts").storyCard[] } cards
  514. */
  515. const storycards = (cards) => {
  516. const a = [];
  517. for (const card of cards) {
  518. const c = [];
  519. let title = '';
  520. for (const [k, v] of Object.entries(card)) {
  521. if (isEmpty(v)) continue;
  522. if (k === 'keys') {
  523. c.push(`TRIGGERS: ${v}`);
  524. } else if (k === 'value') {
  525. c.push(`ENTRY (${v.length}/1000): ${v}`);
  526. } else if (k === 'description') {
  527. c.push(`NOTES: ${v}`);
  528. } else if (k === 'type') {
  529. c.push(`${k.toUpperCase()}: ${v}`);
  530. } else if (k === 'prompt') {
  531. c.push(v);
  532. } else if (k === 'title') {
  533. title = v;
  534. }
  535. }
  536. a.push(`${title ? `${title} ` : ''}[\n ${c.join('\n ')}\n]`);
  537. }
  538. return a.join('\n');
  539. };
  540. /**
  541. * @param { import("../typings/types.d.ts").actionWindow[] } actions
  542. */
  543. const actionWindow = (actions) => {
  544. const a = [];
  545. for (const action of actions) {
  546. const c = [];
  547. for (const [k, v] of Object.entries(action)) {
  548. if (isEmpty(v)) continue;
  549. if (k === 'text') {
  550. c.push(v);
  551. } else if (k === 'type') {
  552. c.push(`${add(v.toUpperCase(), 25, '=')}\n`);
  553. }
  554. }
  555. a.push(c.join('\n'));
  556. }
  557. return a.join('\n');
  558. };
  559. for (const [k, v] of Object.entries(root)) {
  560. if (isEmpty(v)) continue;
  561. if (/title|description|prompt/.test(k)) {
  562. arr.push(`${add(k.toUpperCase())}\n${v}`);
  563. } else if (/memory/.test(k)) {
  564. arr.push(`${add('PLOT ESSENTIALS')}\n${v}`);
  565. } else if (/authorsNote/.test(k)) {
  566. arr.push(`${add("AUTHOR'S NOTE")}\n${v}`);
  567. } else if (/storyCards/.test(k)) {
  568. arr.push(`${add('STORY CARDS')}\n${storycards(v)}`);
  569. } else if (/actionWindow/.test(k)) {
  570. arr.push(`${add('ACTIONS')}\n${actionWindow(v)}`);
  571. } else if (/options/.test(k)) {
  572. arr.push(`${add('OPTIONS')}\n${storycards(v)}`);
  573. } else if (/details/.test(k)) {
  574. for (const [key, value] of Object.entries(v)) {
  575. if (isEmpty(value)) continue;
  576. if (/instructions/.test(key)) {
  577. arr.push(`${add('AI Instructions')}\n${JSON.stringify(value, null, ' ')}`);
  578. } else {
  579. arr.push(`${add(key.toUpperCase())}\n${value}`);
  580. }
  581. }
  582. }
  583. }
  584. str = arr.join('\n');
  585. } else if (fileFormat === 'md') {
  586. const mkSection = (innerHTML = '') => {
  587. const d = make('details');
  588. const s = make('summary', { innerHTML });
  589. d.append(s);
  590. return d;
  591. };
  592. const toBlock = (s) => `\n\n${codeBlock('txt', s)}\n\n`;
  593. if (Array.isArray(root.storyCards)) {
  594. const section = mkSection(`Story Cards (${root.storyCards.length})`);
  595. for (const card of root.storyCards) {
  596. const s = mkSection(`${card.title} (${card.type})`);
  597. s.innerHTML += `${toBlock(card.value)}${toBlock(card.keys)}${toBlock(card.description)}`;
  598. section.append(s);
  599. }
  600. arr.push(section.outerHTML);
  601. }
  602. if (Array.isArray(root.actionWindow)) {
  603. const section = mkSection(`Actions (${root.actionWindow.length})`);
  604. for (const action of root.actionWindow) {
  605. const createdAt = new Date(action.createdAt);
  606. const s = mkSection(
  607. `${createdAt.toLocaleString(navigator.language)} ${action.type.toUpperCase()}`
  608. );
  609. s.innerHTML += `\n\n${codeBlock('txt', action.text)}\n\n${codeBlock('txt', `# Discord TimeStamp\n<t:${+createdAt}></t:${+createdAt}>`)}\n\n`;
  610. section.append(s);
  611. }
  612. arr.push(section.outerHTML);
  613. }
  614. str = arr.join('\n');
  615. }
  616. doDownloadProcess({
  617. url:
  618. 'data:text/plain;charset=utf-8,' +
  619. encodeURIComponent(fileFormat === 'json' ? JSON.stringify(obj, null, ' ') : str),
  620. filename: `${root.title}_${root.shortId}.${p[1]}.${fileFormat}`
  621. });
  622. };
  623. /**
  624. * @param {HTMLElement} parent
  625. * @param {string} type
  626. */
  627. const inject = (parent, type = 'play', fileFormat = 'json') => {
  628. if (!parent) {
  629. return;
  630. }
  631. if (qs(`.mujs-btn[data-file-format="${fileFormat}"]`)) {
  632. return;
  633. }
  634. const parts = /\/(adventure|scenario)\/([\w-]+)\/.+(\/)?/.exec(location.pathname);
  635. const rootType = parts && parts[1];
  636. const cl = {
  637. play: 'mujs-btn is_Button _bg-0hover-513675900 _btc-0hover-1394778429 _brc-0hover-1394778429 _bbc-0hover-1394778429 _blc-0hover-1394778429 _bxsh-0hover-448821143 _bg-0active-744986709 _btc-0active-1163467620 _brc-0active-1163467620 _bbc-0active-1163467620 _blc-0active-1163467620 _bxsh-0active-680131952 _bg-0focus-455866976 _btc-0focus-1452587353 _brc-0focus-1452587353 _bbc-0focus-1452587353 _blc-0focus-1452587353 _bxsh-0focus-391012219 _dsp-flex _fb-auto _bxs-border-box _pos-relative _mih-0px _miw-0px _fs-1 _cur-pointer _ox-hidden _oy-hidden _jc-center _ai-center _h-606181790 _btlr-1307609905 _btrr-1307609905 _bbrr-1307609905 _bblr-1307609905 _pr-1481558338 _pl-1481558338 _fd-row _bg-1633501478 _btc-2122800589 _brc-2122800589 _bbc-2122800589 _blc-2122800589 _btw-1px _brw-1px _bbw-1px _blw-1px _gap-1481558369 _outlineColor-43811550 _fg-1 _bbs-solid _bts-solid _bls-solid _brs-solid _bxsh-1445571361',
  638. preview:
  639. 'mujs-btn is_Row _dsp-flex _fb-auto _bxs-border-box _pos-relative _mih-0px _miw-0px _fs-1 _fd-row _ai-center _gap-1481558369 _w-5037 _pt-1481558338 _pb-1481558338 _fg-1'
  640. };
  641. const btn = make('div', cl[type], {
  642. id: 'game-blur-button',
  643. dataset: {
  644. fileFormat
  645. }
  646. });
  647. const txt = make(
  648. 'span',
  649. 'is_ButtonText font_body _ff-299667014 _dsp-inline _bxs-border-box _ww-break-word _whiteSpace-pre-wrap _mt-0px _mr-0px _mb-0px _ml-0px _col-675002279 _fos-229441189 _lh-222976573 _tt-uppercase _mah-606181790 _pe-none _zi-1',
  650. {
  651. textContent: `Export ${rootType} (${fileFormat.toUpperCase()})`
  652. }
  653. );
  654. const ico = make(
  655. 'p',
  656. 'is_Paragraph font_icons _dsp-inline _bxs-border-box _ww-break-word _mt-0px _mr-0px _mb-0px _ml-0px _col-675002279 _ff-299667014 _fow-233016109 _ls-167744028 _fos-229441158 _lh-222976511 _ussel-auto _whiteSpace-1357640891 _pe-none _pt-1316335105 _pb-1316335105',
  657. {
  658. textContent: 'w_export'
  659. }
  660. );
  661. let span;
  662. ico.importantforaccessibility = 'no';
  663. ico['aria-hidden'] = true;
  664. btn.append(ico, txt);
  665. ael(btn, 'click', async (evt) => {
  666. evt.preventDefault();
  667. await startDownload(fileFormat).catch(err);
  668. });
  669. if (type === 'play') {
  670. span = make('span', 't_sub_theme t_coreA1 _dsp_contents is_Theme', {
  671. style: 'color: var(--color);'
  672. });
  673. } else if (type === 'preview') {
  674. span = make(
  675. 'div',
  676. 'is_Row _dsp-flex _fb-auto _bxs-border-box _pos-relative _miw-0px _fs-0 _fd-row _pe-auto _jc-441309761 _ai-center _gap-1481558307 _btw-1px _btc-43811426 _mt--1px _mih-606181883 _bts-solid'
  677. );
  678. btn.style = 'cursor: pointer;';
  679. }
  680. span.append(btn);
  681. parent.appendChild(span);
  682. };
  683. /**
  684. * @template { Function } F
  685. * @param { (this: F, doc: Document) => * } onDomReady
  686. */
  687. const loadDOM = (onDomReady) => {
  688. if (isFN(onDomReady)) {
  689. if (document.readyState === 'interactive' || document.readyState === 'complete') {
  690. onDomReady(document);
  691. } else {
  692. document.addEventListener('DOMContentLoaded', (evt) => onDomReady(evt.target), {
  693. once: true
  694. });
  695. }
  696. }
  697. };
  698. loadDOM((doc) => {
  699. try {
  700. if (window.location === null) {
  701. throw new Error('"window.location" is null, reload the webpage or use a different one', {
  702. cause: 'loadDOM'
  703. });
  704. }
  705. if (doc === null) {
  706. throw new Error('"doc" is null, reload the webpage or use a different one', {
  707. cause: 'loadDOM'
  708. });
  709. }
  710. const fileFormats = ['json', 'txt', 'md'];
  711. const ignoreTags = new Set(['br', 'head', 'link', 'meta', 'script', 'style']);
  712. observe(doc, (mutations) => {
  713. for (const mutation of mutations) {
  714. for (const node of mutation.addedNodes) {
  715. if (node.nodeType !== 1) {
  716. continue;
  717. }
  718. if (ignoreTags.has(node.localName)) {
  719. continue;
  720. }
  721. if (node.parentElement === null) {
  722. continue;
  723. }
  724. if (!(node instanceof HTMLElement)) {
  725. continue;
  726. }
  727. if (qs('div._pt-1481558307._btrr-1881205710', node)) {
  728. for (const f of fileFormats)
  729. inject(qs('div._pt-1481558307._btrr-1881205710', node), 'play', f);
  730. }
  731. if (qs('div.is_Column._pt-1481558400[role="list"]', node)) {
  732. for (const f of fileFormats)
  733. inject(qs('div.is_Column._pt-1481558400[role="list"]', node), 'preview', f);
  734. }
  735. }
  736. }
  737. });
  738. Command.register('Export Instructions (JSON)', async () => {
  739. try {
  740. const o = await fromGraphQL('aiVersions');
  741. if (!o.data.aiVisibleVersions)
  742. throw new Error('failed to load', {
  743. cause: 'Export Instructions (JSON) - aiVersions'
  744. });
  745. doDownloadProcess({
  746. url: 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(o, null, ' ')),
  747. filename: `Instructions-${Date.now()}.json`
  748. });
  749. } catch (e) {
  750. err(e);
  751. }
  752. });
  753. Command.register('Export Text Instructions (TXT)', async () => {
  754. try {
  755. const o = await fromGraphQL('aiVersions');
  756. const root = o.data.aiVisibleVersions;
  757. if (!root)
  758. throw new Error('failed to load', {
  759. cause: 'Export Text Instructions (TXT) - aiVersions'
  760. });
  761. const arr = [];
  762. const $line = (v, end = '\n') => {
  763. arr.push(`${v}${end}`);
  764. };
  765. for (const v of root.visibleTextVersions) {
  766. $line(add(v.aiDetails.title));
  767. $line(v.instructions, '\n\n');
  768. }
  769. doDownloadProcess({
  770. url: 'data:text/plain;charset=utf-8,' + encodeURIComponent(arr.join('\n')),
  771. filename: `Text_Versions-${Date.now()}.txt`
  772. });
  773. } catch (e) {
  774. err(e);
  775. }
  776. });
  777. for (const f of fileFormats) {
  778. Command.register(`Export in (${f.toUpperCase()})`, async () => {
  779. await startDownload(f).catch(err);
  780. });
  781. }
  782. } catch (ex) {
  783. err(ex);
  784. }
  785. });
  786. })();