// ==UserScript==
// @name MWICore
// @namespace http://tampermonkey.net/
// @version 0.2.1
// @description mwicore已弃用,不再更新,仅保留,请安装mooket
// @author IOMisaka
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @icon https://www.milkywayidle.com/favicon.svg
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
let injectSpace = "mwi";//use window.mwi to access the injected object
if (window[injectSpace]) return;//已经注入
let io = {//供外部调用的接口
version: "0.1.2",//版本号,未改动原有接口只更新最后一个版本号,更改了接口会更改次版本号,主版本暂时不更新,等稳定之后再考虑主版本号更新
MWICoreInitialized: false,//是否初始化完成,完成会还会通过window发送一个自定义事件 MWICoreInitialized
/*一些可以直接用的游戏数据,欢迎大家一起来整理
game.state.levelExperienceTable //经验表
game.state.skillingActionTypeBuffsDict },
game.state.characterActions //[0]是当前正在执行的动作,其余是队列中的动作
*/
game: null,//注入游戏对象,可以直接访问游戏中的大量数据和方法以及消息事件等
lang: null,//语言翻译, 例如中文物品lang.zh.translation.itemNames['/items/coin']
buffCalculator: null,//注入buff计算对象buffCalculator.mergeBuffs()合并buffs,计算加成效果等
alchemyCalculator: null,//注入炼金计算对象
/* marketJson兼容接口 */
get marketJson() {
return this.MWICoreInitialized && new Proxy(this.coreMarket, {
get(coreMarket, prop) {
if (prop === "market") {
return new Proxy(coreMarket, {
get(coreMarket, itemHridOrName) {
return coreMarket.getItemPrice(itemHridOrName);
}
});
}
return null;
}
});
},
coreMarket: null,//coreMarket.marketData 格式{"/items/apple_yogurt:0":{ask,bid,time}}
itemNameToHridDict: null,//物品名称反查表
ensureItemHrid: function (itemHridOrName) {
let itemHrid = this.itemNameToHridDict[itemHridOrName];
if (itemHrid) return itemHrid;
if (itemHridOrName?.startsWith("/items/") && this?.game?.state?.itemDetailDict) return itemHridOrName;
return null;
},//各种名字转itemHrid,找不到返回原itemHrid或者null
hookCallback: hookCallback,//hook回调,用于hook游戏事件等 例如聊天消息mwi.hookCallback(mwi.game, "handleMessageChatMessageReceived", (_,obj)=>{console.log(obj)})
fetchWithTimeout: fetchWithTimeout,//带超时的fetch
};
window[injectSpace] = io;
async function patchScript(node) {
try {
const scriptUrl = node.src;
node.remove();
const response = await fetch(scriptUrl);
if (!response.ok) throw new Error(`Failed to fetch script: ${response.status}`);
let sourceCode = await response.text();
// Define injection points as configurable patterns
const injectionPoints = [
{
pattern: "Ca.a.use",
replacement: `window.${injectSpace}.lang=Oa;Ca.a.use`,
description: "注入语言翻译对象"
},
{
pattern: "class lp extends s.a.Component{constructor(e){var t;super(e),t=this,",
replacement: `class lp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`,
description: "注入游戏对象"
},
{
pattern: "var Q=W;",
replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`,
description: "注入buff计算对象"
},
{
pattern: "class Dn",
replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`,
description: "注入炼金计算对象"
},
{
pattern: "var z=q;",
replacement: `window.${injectSpace}.actionManager=q;var z=q;`,
description: "注入动作管理对象"
}
];
injectionPoints.forEach(({ pattern, replacement,description }) => {
if (sourceCode.includes(pattern)) {
sourceCode = sourceCode.replace(pattern, replacement);
console.info(`MWICore injecting: ${description}`);
}else{
console.warn(`MWICore injecting failed: ${description}`);
}
});
const newNode = document.createElement('script');
newNode.textContent = sourceCode;
document.body.appendChild(newNode);
console.info('MWICore patched successfully.')
} catch (error) {
console.error('MWICore patching failed:', error);
}
}
new MutationObserver((mutationsList, obs) => {
mutationsList.forEach((mutationRecord) => {
for (const node of mutationRecord.addedNodes) {
if (node.src) {
if (node.src.search(/.*main\..*\.chunk.js/)===0) {
obs.disconnect();
patchScript(node);
}
}
}
});
}).observe(document, { childList: true, subtree: true });
/**
* Hook回调函数并添加后处理
* @param {Object} targetObj 目标对象
* @param {string} callbackProp 回调属性名
* @param {Function} handler 后处理函数
*/
function hookCallback(targetObj, callbackProp, handler) {
const originalCallback = targetObj[callbackProp];
if (!originalCallback) {
throw new Error(`Callback ${callbackProp} does not exist`);
}
targetObj[callbackProp] = function (...args) {
const result = originalCallback.apply(this, args);
// 异步处理
if (result && typeof result.then === 'function') {
return result.then(res => {
handler(res, ...args);
return res;
});
}
// 同步处理
handler(result, ...args);
return result;
};
// 返回取消Hook的方法
return () => {
targetObj[callbackProp] = originalCallback;
};
}
/**
* 带超时功能的fetch封装
* @param {string} url - 请求URL
* @param {object} options - fetch选项
* @param {number} timeout - 超时时间(毫秒),默认10秒
* @returns {Promise} - 返回fetch的Promise
*/
function fetchWithTimeout(url, options = {}, timeout = 10000) {
// 创建AbortController实例
const controller = new AbortController();
const { signal } = controller;
// 设置超时计时器
const timeoutId = setTimeout(() => {
controller.abort(new Error(`请求超时: ${timeout}ms`));
}, timeout);
// 合并选项,添加signal
const fetchOptions = {
...options,
signal
};
// 发起fetch请求
return fetch(url, fetchOptions)
.then(response => {
// 清除超时计时器
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return response;
})
.catch(error => {
// 清除超时计时器
clearTimeout(timeoutId);
// 如果是中止错误,重新抛出超时错误
if (error.name === 'AbortError') {
throw new Error(`请求超时: ${timeout}ms`);
}
throw error;
});
}
/*实时市场模块*/
const HOST = "https://mooket.qi-e.top";
const MWIAPI_URL = "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json";
class Price {
bid = -1;
ask = -1;
time = -1;
constructor(bid, ask, time) {
this.bid = bid;
this.ask = ask;
this.time = time;
}
}
class CoreMarket {
marketData = {};//市场数据,带强化等级,存储格式{"/items/apple_yogurt:0":{ask,bid,time}}
fetchTimeDict = {};//记录上次API请求时间,防止频繁请求
ttl = 300;//缓存时间,单位秒
constructor() {
//core data
let marketDataStr = localStorage.getItem("MWICore_marketData") || "{}";
this.marketData = JSON.parse(marketDataStr);
//mwiapi data
let mwiapiJsonStr = localStorage.getItem("MWIAPI_JSON") || localStorage.getItem("MWITools_marketAPI_json");
let mwiapiObj = null;
if (mwiapiJsonStr) {
mwiapiObj = JSON.parse(mwiapiJsonStr);
this.mergeMWIData(mwiapiObj);
}
if (!mwiapiObj || Date.now() / 1000 - mwiapiObj.time > 1800) {//超过半小时才更新,因为mwiapi每小时更新一次,频繁请求github会报错
fetch(MWIAPI_URL).then(res => {
res.text().then(mwiapiJsonStr => {
mwiapiObj = JSON.parse(mwiapiJsonStr);
this.mergeMWIData(mwiapiObj);
//更新本地缓存数据
localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
console.info("MWIAPI_JSON updated:", new Date(mwiapiObj.time * 1000).toLocaleString());
})
});
}
//市场数据更新
hookCallback(io.game, "handleMessageMarketItemOrderBooksUpdated", (res, obj) => {
//更新本地
let timestamp = parseInt(Date.now() / 1000);
let itemHrid = obj.marketItemOrderBooks.itemHrid;
obj.marketItemOrderBooks?.orderBooks?.forEach((item, enhancementLevel) => {
let bid = item.bids?.length > 0 ? item.bids[0].price : -1;
let ask = item.asks?.length > 0 ? item.asks[0].price : -1;
this.updateItem(itemHrid + ":" + enhancementLevel, new Price(bid, ask, timestamp));
});
//上报数据
obj.time = timestamp;
fetchWithTimeout(`${HOST}/market/upload/order`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(obj)
});
})
setInterval(() => { this.save(); }, 1000 * 600);//十分钟保存一次
}
/**
* 合并MWIAPI数据,只包含0级物品
*
* @param obj 包含市场数据的对象
*/
mergeMWIData(obj) {
Object.entries(obj.market).forEach(([itemName, price]) => {
let itemHrid = io.ensureItemHrid(itemName);
if (itemHrid) this.updateItem(itemHrid + ":" + 0, new Price(price.bid, price.ask, obj.time), false);//本地更新
});
this.save();
}
mergeCoreDataBeforeSave() {
let obj = JSON.parse(localStorage.getItem("MWICore_marketData") || "{}");
Object.entries(obj).forEach(([itemHridLevel, priceObj]) => {
this.updateItem(itemHridLevel, priceObj, false);//本地更新
});
//不保存,只合并
}
save() {//保存到localStorage
this.mergeCoreDataBeforeSave();//从其他角色合并保存的数据
localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
}
/**
* 部分特殊物品的价格
* 例如金币固定1,牛铃固定为牛铃袋/10的价格
* @param {string} itemHrid - 物品hrid
* @returns {Price|null} - 返回对应商品的价格对象,如果没有则null
*/
getSpecialPrice(itemHrid) {
switch (itemHrid) {
case "/items/coin":
return new Price(1, 1, Date.now() / 1000);
case "/items/cowbell": {
let cowbells = this.getItemPrice("/items/bag_of_10_cowbells");
return cowbells && { bid: cowbells.bid / 10, ask: cowbells.ask / 10, time: cowbells.time };
}
default:
return null;
}
}
/**
* 获取商品的价格
*
* @param {string} itemHridOrName 商品HRID或名称
* @param {number} [enhancementLevel=0] 装备强化等级,普通商品默认为0
* @returns {number|null} 返回商品的价格,如果商品不存在或无法获取价格则返回null
*/
getItemPrice(itemHridOrName, enhancementLevel = 0) {
let itemHrid = io.ensureItemHrid(itemHridOrName);
if (!itemHrid) return null;
let specialPrice = this.getSpecialPrice(itemHrid);
if (specialPrice) return specialPrice;
let priceObj = this.marketData[itemHrid + ":" + enhancementLevel];
if (Date.now() / 1000 - this.fetchTimeDict[itemHrid + ":" + enhancementLevel] < this.ttl) return priceObj;//1分钟内直接返回本地数据,防止频繁请求服务器
if (this.fetchCount > 10) return priceObj;//过于频繁请求服务器
this.fetchCount++;
setTimeout(() => { this.fetchCount--;this.getItemPriceAsync(itemHrid, enhancementLevel); }, this.fetchCount*200);//后台获取最新数据,防止阻塞
return priceObj;
}
fetchCount = 0;//防止频繁请求服务器,后台获取最新数据
/**
* 异步获取物品价格
*
* @param {string} itemHridOrName 物品HRID或名称
* @param {number} [enhancementLevel=0] 增强等级,默认为0
* @returns {Promise<Object|null>} 返回物品价格对象或null
*/
async getItemPriceAsync(itemHridOrName, enhancementLevel = 0) {
let itemHrid = io.ensureItemHrid(itemHridOrName);
if (!itemHrid) return null;
let specialPrice = this.getSpecialPrice(itemHrid);
if (specialPrice) return specialPrice;
let itemHridLevel = itemHrid + ":" + enhancementLevel;
if (Date.now() / 1000 - this.fetchTimeDict[itemHridLevel] < this.ttl) return this.marketData[itemHridLevel];//1分钟内请求直接返回本地数据,防止频繁请求服务器
// 构造请求参数
const params = new URLSearchParams();
params.append("name", itemHrid);
params.append("level", enhancementLevel);
let res = null;
try {
this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//记录请求时间
res = await fetchWithTimeout(`${HOST}/market/item/price?${params}`);
} catch (e) {
return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
} finally {
}
if (res.status != 200) {
return this.marketData[itemHridLevel];//获取失败,直接返回本地数据
}
let resObj = await res.json();
let priceObj = new Price(resObj.bid, resObj.ask, Date.now() / 1000);
if (resObj.ttl) this.ttl = resObj.ttl;//更新ttl
this.updateItem(itemHridLevel, priceObj);
return priceObj;
}
updateItem(itemHridLevel, priceObj, isFetch = true) {
let localItem = this.marketData[itemHridLevel];
if (isFetch) this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//fetch时间戳
if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据
this.marketData[itemHridLevel] = priceObj;
}
}
save() {
localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
}
}
function init() {
io.itemNameToHridDict = {};
Object.entries(io.lang.en.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
Object.entries(io.lang.zh.translation.itemNames).forEach(([k, v]) => { io.itemNameToHridDict[v] = k });
io.coreMarket = new CoreMarket();
io.MWICoreInitialized = true;
window.dispatchEvent(new CustomEvent("MWICoreInitialized"))
console.info("MWICoreInitialized event dispatched. window.mwi.MWICoreInitialized=true");
}
new Promise(resolve => {
const interval = setInterval(() => {
if (io.game && io.lang) {//等待必须组件加载完毕后再初始化
clearInterval(interval);
resolve();
}
}, 100);
}).then(() => {
init();
});
})();