MahjongSoul VIP

Unlock Characters and Costumes

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