MWICore

toolkit, for MilkyWayIdle.一些工具函数,和一些注入对象,市场数据API等。

  1. // ==UserScript==
  2. // @name MWICore
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.3
  5. // @description toolkit, for MilkyWayIdle.一些工具函数,和一些注入对象,市场数据API等。
  6. // @author IOMisaka
  7. // @match https://www.milkywayidle.com/*
  8. // @match https://test.milkywayidle.com/*
  9. // @icon https://www.milkywayidle.com/favicon.svg
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. let injectSpace = "mwi";//use window.mwi to access the injected object
  17. if (window[injectSpace]) return;//已经注入
  18. let io = {//供外部调用的接口
  19. version: "0.1.2",//版本号,未改动原有接口只更新最后一个版本号,更改了接口会更改次版本号,主版本暂时不更新,等稳定之后再考虑主版本号更新
  20. MWICoreInitialized: false,//是否初始化完成,完成会还会通过window发送一个自定义事件 MWICoreInitialized
  21.  
  22. /*一些可以直接用的游戏数据,欢迎大家一起来整理
  23. game.state.levelExperienceTable //经验表
  24. game.state.skillingActionTypeBuffsDict },
  25. game.state.characterActions //[0]是当前正在执行的动作,其余是队列中的动作
  26. */
  27. game: null,//注入游戏对象,可以直接访问游戏中的大量数据和方法以及消息事件等
  28. lang: null,//语言翻译, 例如中文物品lang.zh.translation.itemNames['/items/coin']
  29. buffCalculator: null,//注入buff计算对象buffCalculator.mergeBuffs()合并buffs,计算加成效果等
  30. alchemyCalculator: null,//注入炼金计算对象
  31.  
  32.  
  33. /* marketJson兼容接口 */
  34. get marketJson() {
  35. return this.MWICoreInitialized && new Proxy(this.coreMarket, {
  36. get(coreMarket, prop) {
  37. if (prop === "market") {
  38. return new Proxy(coreMarket, {
  39. get(coreMarket, itemHridOrName) {
  40. return coreMarket.getItemPrice(itemHridOrName);
  41. }
  42. });
  43. }
  44. return null;
  45. }
  46.  
  47. });
  48. },
  49. coreMarket: null,//coreMarket.marketData 格式{"/items/apple_yogurt:0":{ask,bid,time}}
  50. itemNameToHridDict: null,//物品名称反查表
  51. ensureItemHrid: function (itemHridOrName) {
  52. let itemHrid = this.itemNameToHridDict[itemHridOrName];
  53. if (itemHrid) return itemHrid;
  54. if (itemHridOrName?.startsWith("/items/") && this?.game?.state?.itemDetailDict) return itemHridOrName;
  55. return null;
  56. },//各种名字转itemHrid,找不到返回原itemHrid或者null
  57. hookCallback: hookCallback,//hook回调,用于hook游戏事件等 例如聊天消息mwi.hookCallback(mwi.game, "handleMessageChatMessageReceived", (_,obj)=>{console.log(obj)})
  58. fetchWithTimeout: fetchWithTimeout,//带超时的fetch
  59. };
  60. window[injectSpace] = io;
  61.  
  62. async function patchScript(node) {
  63. try {
  64. const scriptUrl = node.src;
  65. node.remove();
  66. const response = await fetch(scriptUrl);
  67. if (!response.ok) throw new Error(`Failed to fetch script: ${response.status}`);
  68.  
  69. let sourceCode = await response.text();
  70.  
  71. // Define injection points as configurable patterns
  72. const injectionPoints = [
  73. {
  74. pattern: "Ca.a.use",
  75. replacement: `window.${injectSpace}.lang=Oa;Ca.a.use`,
  76. description: "注入语言翻译对象"
  77. },
  78. {
  79. pattern: "class lp extends s.a.Component{constructor(e){var t;super(e),t=this,",
  80. replacement: `class lp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`,
  81. description: "注入游戏对象"
  82.  
  83. },
  84. {
  85. pattern: "var Q=W;",
  86. replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`,
  87. description: "注入buff计算对象"
  88. },
  89. {
  90. pattern: "class Dn",
  91. replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`,
  92. description: "注入炼金计算对象"
  93. },
  94. {
  95. pattern: "var z=q;",
  96. replacement: `window.${injectSpace}.actionManager=q;var z=q;`,
  97. description: "注入动作管理对象"
  98. }
  99. ];
  100.  
  101. injectionPoints.forEach(({ pattern, replacement,description }) => {
  102. if (sourceCode.includes(pattern)) {
  103. sourceCode = sourceCode.replace(pattern, replacement);
  104. console.info(`MWICore injecting: ${description}`);
  105. }else{
  106. console.warn(`MWICore injecting failed: ${description}`);
  107. }
  108. });
  109.  
  110. const newNode = document.createElement('script');
  111. newNode.textContent = sourceCode;
  112. document.body.appendChild(newNode);
  113. console.info('MWICore patched successfully.')
  114. } catch (error) {
  115. console.error('MWICore patching failed:', error);
  116. }
  117. }
  118. new MutationObserver((mutationsList, obs) => {
  119. mutationsList.forEach((mutationRecord) => {
  120. for (const node of mutationRecord.addedNodes) {
  121. if (node.src) {
  122. if (node.src.search(/.*main\..*\.chunk.js/)===0) {
  123. obs.disconnect();
  124. patchScript(node);
  125. }
  126. }
  127. }
  128. });
  129. }).observe(document, { childList: true, subtree: true });
  130.  
  131. /**
  132. * Hook回调函数并添加后处理
  133. * @param {Object} targetObj 目标对象
  134. * @param {string} callbackProp 回调属性名
  135. * @param {Function} handler 后处理函数
  136. */
  137. function hookCallback(targetObj, callbackProp, handler) {
  138. const originalCallback = targetObj[callbackProp];
  139.  
  140. if (!originalCallback) {
  141. throw new Error(`Callback ${callbackProp} does not exist`);
  142. }
  143.  
  144. targetObj[callbackProp] = function (...args) {
  145. const result = originalCallback.apply(this, args);
  146.  
  147. // 异步处理
  148. if (result && typeof result.then === 'function') {
  149. return result.then(res => {
  150. handler(res, ...args);
  151. return res;
  152. });
  153. }
  154.  
  155. // 同步处理
  156. handler(result, ...args);
  157. return result;
  158. };
  159.  
  160. // 返回取消Hook的方法
  161. return () => {
  162. targetObj[callbackProp] = originalCallback;
  163. };
  164. }
  165. /**
  166. * 带超时功能的fetch封装
  167. * @param {string} url - 请求URL
  168. * @param {object} options - fetch选项
  169. * @param {number} timeout - 超时时间(毫秒),默认10秒
  170. * @returns {Promise} - 返回fetch的Promise
  171. */
  172. function fetchWithTimeout(url, options = {}, timeout = 10000) {
  173. // 创建AbortController实例
  174. const controller = new AbortController();
  175. const { signal } = controller;
  176.  
  177. // 设置超时计时器
  178. const timeoutId = setTimeout(() => {
  179. controller.abort(new Error(`请求超时: ${timeout}ms`));
  180. }, timeout);
  181.  
  182. // 合并选项,添加signal
  183. const fetchOptions = {
  184. ...options,
  185. signal
  186. };
  187.  
  188. // 发起fetch请求
  189. return fetch(url, fetchOptions)
  190. .then(response => {
  191. // 清除超时计时器
  192. clearTimeout(timeoutId);
  193.  
  194. if (!response.ok) {
  195. throw new Error(`HTTP错误! 状态码: ${response.status}`);
  196. }
  197. return response;
  198. })
  199. .catch(error => {
  200. // 清除超时计时器
  201. clearTimeout(timeoutId);
  202.  
  203. // 如果是中止错误,重新抛出超时错误
  204. if (error.name === 'AbortError') {
  205. throw new Error(`请求超时: ${timeout}ms`);
  206. }
  207. throw error;
  208. });
  209. }
  210.  
  211. /*实时市场模块*/
  212. const HOST = "https://mooket.qi-e.top";
  213. const MWIAPI_URL = "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json";
  214.  
  215. class Price {
  216. bid = -1;
  217. ask = -1;
  218. time = -1;
  219. constructor(bid, ask, time) {
  220. this.bid = bid;
  221. this.ask = ask;
  222. this.time = time;
  223. }
  224. }
  225. class CoreMarket {
  226. marketData = {};//市场数据,带强化等级,存储格式{"/items/apple_yogurt:0":{ask,bid,time}}
  227. fetchTimeDict = {};//记录上次API请求时间,防止频繁请求
  228. ttl = 300;//缓存时间,单位秒
  229.  
  230. constructor() {
  231. //core data
  232. let marketDataStr = localStorage.getItem("MWICore_marketData") || "{}";
  233. this.marketData = JSON.parse(marketDataStr);
  234.  
  235. //mwiapi data
  236. let mwiapiJsonStr = localStorage.getItem("MWIAPI_JSON") || localStorage.getItem("MWITools_marketAPI_json");
  237. let mwiapiObj = null;
  238. if (mwiapiJsonStr) {
  239. mwiapiObj = JSON.parse(mwiapiJsonStr);
  240. this.mergeMWIData(mwiapiObj);
  241. }
  242. if (!mwiapiObj || Date.now() / 1000 - mwiapiObj.time > 1800) {//超过半小时才更新,因为mwiapi每小时更新一次,频繁请求github会报错
  243. fetch(MWIAPI_URL).then(res => {
  244. res.text().then(mwiapiJsonStr => {
  245. mwiapiObj = JSON.parse(mwiapiJsonStr);
  246. this.mergeMWIData(mwiapiObj);
  247. //更新本地缓存数据
  248. localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
  249. console.info("MWIAPI_JSON updated:", new Date(mwiapiObj.time * 1000).toLocaleString());
  250. })
  251. });
  252. }
  253.  
  254. //市场数据更新
  255. hookCallback(io.game, "handleMessageMarketItemOrderBooksUpdated", (res, obj) => {
  256. //更新本地
  257. let timestamp = parseInt(Date.now() / 1000);
  258. let itemHrid = obj.marketItemOrderBooks.itemHrid;
  259. obj.marketItemOrderBooks?.orderBooks?.forEach((item, enhancementLevel) => {
  260. let bid = item.bids?.length > 0 ? item.bids[0].price : -1;
  261. let ask = item.asks?.length > 0 ? item.asks[0].price : -1;
  262. this.updateItem(itemHrid + ":" + enhancementLevel, new Price(bid, ask, timestamp));
  263. });
  264. //上报数据
  265. obj.time = timestamp;
  266. fetchWithTimeout(`${HOST}/market/upload/order`, {
  267. method: "POST",
  268. headers: {
  269. "Content-Type": "application/json"
  270. },
  271. body: JSON.stringify(obj)
  272. });
  273. })
  274. setInterval(() => { this.save(); }, 1000 * 600);//十分钟保存一次
  275. }
  276.  
  277. /**
  278. * 合并MWIAPI数据,只包含0级物品
  279. *
  280. * @param obj 包含市场数据的对象
  281. */
  282. mergeMWIData(obj) {
  283. Object.entries(obj.market).forEach(([itemName, price]) => {
  284. let itemHrid = io.ensureItemHrid(itemName);
  285. if (itemHrid) this.updateItem(itemHrid + ":" + 0, new Price(price.bid, price.ask, obj.time), false);//本地更新
  286. });
  287. this.save();
  288. }
  289. mergeCoreDataBeforeSave() {
  290. let obj = JSON.parse(localStorage.getItem("MWICore_marketData") || "{}");
  291. Object.entries(obj).forEach(([itemHridLevel, priceObj]) => {
  292. this.updateItem(itemHridLevel, priceObj, false);//本地更新
  293. });
  294. //不保存,只合并
  295. }
  296. save() {//保存到localStorage
  297. this.mergeCoreDataBeforeSave();//从其他角色合并保存的数据
  298. localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
  299. }
  300.  
  301. /**
  302. * 部分特殊物品的价格
  303. * 例如金币固定1,牛铃固定为牛铃袋/10的价格
  304. * @param {string} itemHrid - 物品hrid
  305. * @returns {Price|null} - 返回对应商品的价格对象,如果没有则null
  306. */
  307. getSpecialPrice(itemHrid) {
  308. switch (itemHrid) {
  309. case "/items/coin":
  310. return new Price(1, 1, Date.now() / 1000);
  311. case "/items/cowbell": {
  312. let cowbells = this.getItemPrice("/items/bag_of_10_cowbells");
  313. return cowbells && { bid: cowbells.bid / 10, ask: cowbells.ask / 10, time: cowbells.time };
  314. }
  315. default:
  316. return null;
  317. }
  318. }
  319. /**
  320. * 获取商品的价格
  321. *
  322. * @param {string} itemHridOrName 商品HRID或名称
  323. * @param {number} [enhancementLevel=0] 装备强化等级,普通商品默认为0
  324. * @returns {number|null} 返回商品的价格,如果商品不存在或无法获取价格则返回null
  325. */
  326. getItemPrice(itemHridOrName, enhancementLevel = 0) {
  327. let itemHrid = io.ensureItemHrid(itemHridOrName);
  328. if (!itemHrid) return null;
  329. let specialPrice = this.getSpecialPrice(itemHrid);
  330. if (specialPrice) return specialPrice;
  331.  
  332. let priceObj = this.marketData[itemHrid + ":" + enhancementLevel];
  333. if (Date.now() / 1000 - this.fetchTimeDict[itemHrid + ":" + enhancementLevel] < this.ttl) return priceObj;//1分钟内直接返回本地数据,防止频繁请求服务器
  334. if (this.fetchCount > 10) return priceObj;//过于频繁请求服务器
  335. this.fetchCount++;
  336. setTimeout(() => { this.fetchCount--;this.getItemPriceAsync(itemHrid, enhancementLevel); }, this.fetchCount*200);//后台获取最新数据,防止阻塞
  337. return priceObj;
  338. }
  339. fetchCount = 0;//防止频繁请求服务器,后台获取最新数据
  340.  
  341. /**
  342. * 异步获取物品价格
  343. *
  344. * @param {string} itemHridOrName 物品HRID或名称
  345. * @param {number} [enhancementLevel=0] 增强等级,默认为0
  346. * @returns {Promise<Object|null>} 返回物品价格对象或null
  347. */
  348. async getItemPriceAsync(itemHridOrName, enhancementLevel = 0) {
  349. let itemHrid = io.ensureItemHrid(itemHridOrName);
  350. if (!itemHrid) return null;
  351. let specialPrice = this.getSpecialPrice(itemHrid);
  352. if (specialPrice) return specialPrice;
  353. let itemHridLevel = itemHrid + ":" + enhancementLevel;
  354. if (Date.now() / 1000 - this.fetchTimeDict[itemHridLevel] < this.ttl) return this.marketData[itemHridLevel];//1分钟内请求直接返回本地数据,防止频繁请求服务器
  355.  
  356. // 构造请求参数
  357. const params = new URLSearchParams();
  358. params.append("name", itemHrid);
  359. params.append("level", enhancementLevel);
  360.  
  361. let res = null;
  362. try {
  363. this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//记录请求时间
  364. res = await fetchWithTimeout(`${HOST}/market/item/price?${params}`);
  365. } catch (e) {
  366. return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
  367. } finally {
  368. }
  369. if (res.status != 200) {
  370. return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
  371. }
  372. let resObj = await res.json();
  373. let priceObj = new Price(resObj.bid, resObj.ask, Date.now() / 1000);
  374. if (resObj.ttl) this.ttl = resObj.ttl;//更新ttl
  375. this.updateItem(itemHridLevel, priceObj);
  376. return priceObj;
  377. }
  378. updateItem(itemHridLevel, priceObj, isFetch = true) {
  379. let localItem = this.marketData[itemHridLevel];
  380. if (isFetch) this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//fetch时间戳
  381. if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据
  382. this.marketData[itemHridLevel] = priceObj;
  383. }
  384. }
  385. save() {
  386. localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
  387. }
  388. }
  389. function init() {
  390. io.itemNameToHridDict = {};
  391. Object.entries(io.lang.en.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
  392. Object.entries(io.lang.zh.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
  393. io.coreMarket = new CoreMarket();
  394. io.MWICoreInitialized = true;
  395. window.dispatchEvent(new CustomEvent("MWICoreInitialized"))
  396. console.info("MWICoreInitialized event dispatched. window.mwi.MWICoreInitialized=true");
  397. }
  398. new Promise(resolve => {
  399. const interval = setInterval(() => {
  400. if (io.game && io.lang) {//等待必须组件加载完毕后再初始化
  401. clearInterval(interval);
  402. resolve();
  403. }
  404. }, 100);
  405. }).then(() => {
  406. init();
  407. });
  408.  
  409. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址