// ==UserScript==
// @name 雀魂体验卡
// @name:en MahjongSoul VIP
// @namespace https://msvip.example.com
// @license GPL-3.0
// @version 0.0.1
// @author Tooomm
// @match https://mahjongsoul.game.yo-star.com/*
// @match https://majsoul.union-game.com/*
// @match https://game.mahjongsoul.com/*
// @match https://game.maj-soul.com/1/*
// @description 解锁角色与装扮
// @description:en Unlock Characters and Costumes
// ==/UserScript==
/* jshint esversion: 11 */
(function () {
"use strict";
function newStore(name) {
function getKey(id) {
return id !== undefined ? `${name}.${id}` : name;
}
return {
set(value, id) {
const key = getKey(id);
if (value === null || typeof value !== "object") {
localStorage.setItem(key, value);
} else {
localStorage.setItem(key, JSON.stringify(value));
}
},
get(id) {
const item = localStorage.getItem(getKey(id));
try {
return JSON.parse(item);
} catch {
const number = parseInt(item);
return isNaN(number) ? item : number;
}
}
};
}
const store = {
skin: newStore("vip.skin"),
title: newStore("vip.title"),
loading: newStore("vip.loading"),
account: newStore("vip.account"),
mainCharacter: newStore("vip.char.main"),
characterSort: newStore("vip.char.sort"),
hiddenCharacters: newStore("vip.char.hidden"),
use: newStore("vip.views.use"),
views: newStore("vip.views")
};
const resource = {
items() {
return cfg.item_definition.item.rows_
.filter(i => i.category === 5 || i.category === 8)
.map(i => ({ stack: 1, item_id: i.id }));
},
use() {
return store.use.get() || 0;
},
values() {
return store.views.get(this.use())?.values || [];
},
views() {
return this.values().map(view => ({
type: 0,
slot: view.slot,
item_id_list: [],
item_id: view.type
? view.item_id_list[Math.floor(Math.random() * view.item_id_list.length)]
: view.item_id
}));
},
titles() {
return Object.keys(cfg.item_definition.title.map_);
},
initSkin(charid) {
return cfg.item_definition.character.map_[charid].init_skin;
},
avatarId() {
const charid = this.mainCharId();
return store.skin.get(charid) || this.initSkin(charid);
},
avatarFrame() {
for (const view of this.values())
if (view.slot === 5) {
return view.item_id;
}
return 0;
},
character(charid) {
return {
rewarded_level: [1, 2, 3, 4, 5],
is_upgraded: true,
extra_emoji: [],
charid: charid,
level: 5,
views: [],
skin: store.skin.get(charid) || this.initSkin(charid),
exp: 1
};
},
mainCharId() {
return store.mainCharacter.get() || 200007;
},
mainCharacter() {
return this.character(this.mainCharId());
},
commonViews() {
return {
use: this.use(),
views: [...Array(10).keys()].map(i => store.views.get(i) || {})
};
},
characterInfo() {
return {
send_gift_count: 0,
send_gift_limit: 2,
skins: Object.keys(cfg.item_definition.skin.map_),
finished_endings: Object.keys(cfg.spot.rewards.map_),
rewarded_endings: Object.keys(cfg.spot.rewards.map_),
main_character_id: this.mainCharId(),
character_sort: store.characterSort.get() || [],
hidden_characters: store.hiddenCharacters.get() || [],
characters: Object.keys(cfg.item_definition.character.map_).map(id => this.character(id))
};
}
};
function override(players) {
for (const player of players || []) {
if (player.account_id === store.account.get()) {
const updates = {
views: () => resource.views(),
avatar_id: () => resource.avatarId(),
character: () => resource.mainCharacter(),
avatar_frame: () => resource.avatarFrame(),
title: () => store.title.get() || 0,
loading_image: () => store.loading.get() || []
};
for (const [key, update] of Object.entries(updates)) {
if (key in player) {
player[key] = update();
}
}
} else {
if (player.character)
Object.assign(player.character, { level: 5, exp: 1, is_upgraded: true });
}
}
}
function hookReq2Lobby(fn) {
return function (service, name, req, callback) {
// console.log(service, name, req);
switch (name) {
// RESPONSE
case "login":
case "emailLogin":
case "oauth2Login":
return fn.call(this, service, name, req, (_null, res) => {
store.account.set(res.account_id);
override([res.account]);
callback(_null, res);
});
case "fetchInfo":
return fn.call(this, service, name, req, (_null, res) => {
res.character_info = resource.characterInfo();
res.all_common_views = resource.commonViews();
res.title_list.title_list = resource.titles();
res.bag_info.bag.items.unshift(...resource.items());
callback(_null, res);
});
case "joinRoom":
case "fetchRoom":
case "createRoom":
return fn.call(this, service, name, req, (_null, res) => {
override(res.room?.persons);
callback(_null, res);
});
case "fetchGameRecord":
return fn.call(this, service, name, req, (_null, res) => {
override(res.head?.accounts);
callback(_null, res);
});
case "fetchAccountInfo":
return fn.call(this, service, name, req, (_null, res) => {
override([res.account]);
callback(_null, res);
});
// REQUEST
case "useTitle":
store.title.set(req.title);
return callback(null, {});
case "setLoadingImage":
store.loading.set(req.images);
return callback(null, {});
case "changeMainCharacter":
store.mainCharacter.set(req.character_id);
return callback(null, {});
case "changeCharacterSkin":
store.skin.set(req.skin, req.character_id);
return callback(null, {});
case "updateCharacterSort":
store.characterSort.set(req.sort);
return callback(null, {});
case "setHiddenCharacter":
store.hiddenCharacters.set(req.chara_list);
return callback(null, { hidden_characters: req.chara_list });
case "useCommonView":
store.use.set(req.index);
return callback(null, {});
case "saveCommonViews":
store.views.set({ values: req.views, index: req.save_index }, req.save_index);
return callback(null, {});
case "addFinishedEnding":
return callback(null, {});
default:
return fn.call(this, service, name, req, callback);
}
};
}
function hookReq2MJ(fn) {
return function (service, name, req, callback) {
// console.log(service, name, req);
switch (name) {
// RESPONSE
case "authGame":
return fn.call(this, service, name, req, (_null, res) => {
override(res?.players);
callback(_null, res);
});
default:
return fn.call(this, service, name, req, callback);
}
};
}
function hookAddL(fn) {
return function (name, handler) {
// console.log(name);
const { method: callback } = handler;
switch (name) {
case "NotifyRoomPlayerUpdate":
return fn.call(
this,
name,
Object.assign(handler, {
method(data) {
override(data?.player_list);
callback(data);
}
})
);
case "NotifyGameFinishRewardV2":
return fn.call(
this,
name,
Object.assign(handler, {
method(data) {
Object.assign(data.main_character, {
exp: 1,
add: 0,
level: 5
});
callback(data);
}
})
);
default:
return fn.call(this, name, handler);
}
};
}
function inGame() {
try {
return app != null && app.NetAgent != null;
} catch {
return false;
}
}
(function main() {
console.log("Loading...");
if (!inGame()) {
return setTimeout(main, 1500);
}
console.log("Game loaded !");
const { sendReq2Lobby, sendReq2MJ } = app.NetAgent;
app.NetAgent.sendReq2MJ = hookReq2MJ(sendReq2MJ);
app.NetAgent.sendReq2Lobby = hookReq2Lobby(sendReq2Lobby);
const { AddListener2Lobby, AddListener2MJ } = app.NetAgent;
app.NetAgent.AddListener2Lobby = hookAddL(AddListener2Lobby);
console.log("Hook loaded !");
})();
})();