m0ram1-maajan-analyzer For Majsoul 开发日志#1

asakurasayori 发布于 2025-06-26 325 次阅读


前言

某一天和朋友打友人场的时候,突然意识到自己可以搭一个自动分析平台来监测玩家水平变化,于是便产生了当前项目的Idea。 本项目会基于已有的分析工具和API协议文档,仅做一个搭积木的工作,所有主要贡献均来自前辈们的付出,感谢!本开发日志会持续更新,直到开发完成。 日志废话会比较多,但希望能给予自己和大家便利,在最后会总结一篇简短的教程之类的文章。

本文写于2025.6.25

初步探究

以前我使用akochan-reviewer进行牌谱分析,效率较低,导出步骤也较为繁琐。先是使用majsoul-plus客户端,安装导出Mod,然后再运行akochan-reviewer。然后我想是不是可以进行自动化,然后部署到服务器上,定期抓取对战数据然后自动分析,最后通过某种手段(比如说Web侧或者DiscordBot)推送给玩家。接着,考虑到AI开销较大,于是不考虑进行统一计算,到时候分发Docker镜像好了。那这样就敲定下来了。

我没有去调查有没有先辈已经做了这份工作,我只是想积累一下项目经验,以及确实对这方面感兴趣。

然后我初步参考了以下网站: 第一个是现成的SDK,尝试了example.py,能够成功抓取数据,了解了雀魂是基于websocket连接通信的,只要知道了协议就能获取所有数据。我使用example.py获取到了不含对战数据的包体,但是可以获取到UUID,于是我接下来会去调查原因。

随后观察到抓取牌谱时,有时会丢失最新的数据。当我网页侧手动登出再运行脚本的时候就能抓取到最新数据,初步胡乱的推测有两个可能性:

  1. API不是实时更新,不能获取到最新数据。对自己的反驳:那前端是通过什么获取的
  2. 直接顶号会导致不能获取最新的数据。 总之这一部分有待考证。

第二个是肥Pig崔的B站专栏,提供了从浏览器Console获取牌谱的脚本,以及整合了相关的许多资料,颇有帮助,感谢!

第三个是协议介绍站,这个网站更加直接,介绍了所有的协议,最重要当然是了解了抓取下来的牌谱数据里面内容的含义。

第四个是伟大的mjai-viewer,发现他是akochan-viewer的后继,当然有akochan的模型,也有最新的Mortal模型。然后了解到它只支持tenhou.net/6格式的对局数据(以前使用的Mod原来也是把雀魂的数据parse成天和数据),所以这一部分我觉得是一个难点。

然后我想到去翻以前导出Mod的源代码,然后魔改了导出,看到raw_data之后发现和我是用上文第一个项目导出的内容是一样的,于是要掰出maj转tenhou的部分,然后解决前文提到的包体不含对战数据的原因,流程就能连起来了。

观察到fetchGameRecordList内的元素并不含具体对战数据,仅有一些预览,而Console导出脚本用的是fetchGameRecord,含有对战数据,似乎是提供UUID就行了。接下来便需要测试是否在模拟连接,不进入具体牌谱的情况下是否能执行该操作。进一步观察,第一个mahjong_soul_api的example中就包含以上两个方法!(其实是最开始眼瞎了,没有看到参数还有UUID)但根据UUID导出似乎使用的是GameDetailRecords,不知道效果如何,接下来进行测试能不能运行,答案是否定的。于是我重新写了一遍fetchGameRecord.py,发现正常运行了,是一个解码后的msg,但是里面的result属性仍然是被加密的,具体来说: { "passed": 28364, "type": 1, "result": "Cg8ubHEuUmVjb3JkQmFCZWkSBAgBQAA=" }, 当type为1,「result」は、牌山、配牌、自摸、打牌、鳴き、和了、流局、などなどをあらわしているにゃ。(引用自wikiwiki站) 然后我需要去具体学习一下result的解析方法。 在2020年以后,数据格式有所不同,这也是原先的mahjong_soul_api不能运行的原因,以下引用wikiwiki站的原文:

牌譜の形式」に書いたように、牌譜にはバージョン差異があるにゃ! 説明するにゃ!
{
  省略
  "data": {
    "name": ".lq.GameDetailRecords",
    "data": {
      "records": [
        省略
      ]
      "version": 210715,
      "actions": [
        省略
      ]
    }
  },
  "data_url": ""
}
キー 説明 参照
data 2020年以降の対局については、ここに牌譜が記録されているにゃ!2019年以前の対局については、本来ここは空っぽにゃ!でも、「牌譜をファイルに保存するにゃ」のページで紹介した方法でダウンロードした2019年以前の牌譜ファイルの場合は、ここに牌譜データを埋め込んでいるにゃ! ResGameRecord
name 牌譜のデータ形式の名前にゃ!「".lq.GameDetailRecords"」で固定にゃ! Wrapper
data 「".lq.GameDetailRecords"」のデータがここに記されているにゃ! Wrapper
records 2021年7月28日アップデート以前の対局については、ここに牌譜が記録されているにゃ!2021年7月28日アップデート以降の対局については、ここは空っぽにゃ! GameDetailRecords
version 2021年7月28日アップデート以前の対局については、ここは空っぽにゃ!2021年7月28日アップデート以降の対局については、「210715」で固定にゃ! GameDetailRecords
actions 2021年7月28日アップデート以前の対局については、ここは空っぽにゃ!2021年7月28日アップデート以降の対局については、ここに牌譜が記録されているにゃ! GameDetailRecords
data_url 2019年以前の対局については、牌譜データの場所を示すURLが書かれているにゃ!2020年以降の対局については、ここは空っぽにゃ! ResGameRecord

以及:

Field Type Label Description
records bytes repeated (下記参照にゃ)
records フィールドの各要素は Wrapper 型の Protocol Buffers メッセージとしてデコードできるにゃ.デコードした結果の Wrapper 型の name フィールドには ".lq.RecordNewRound", ".lq.RecordDealTile", ".lq.RecordDiscardTile", ".lq.RecordChiPengGang", ".lq.RecordAnGangAddGang", ".lq.RecordHule", ".lq.RecordNoTile", ".lq.RecordLiuJu" のいずれかの文字列が入っているにゃ.この文字列は Wrapper 型の data フィールドに入っているバイト列をデコードするための型を表しているにゃ.つまり,以下のどれかの型の Protocol Buffers メッセージとしてデコードできるにゃ.各型の意味は以下の通りにゃ. records フィールドの要素を順番に解析することで牌譜を完全に再現できるにゃ.
RecordNewRound
対局開始にゃ. records フィールドの最初の要素はこれにゃ.
RecordDealTile
自摸にゃ.
RecordDiscardTile
打牌にゃ.
RecordChiPengGang
チー・ポン・大明槓にゃ.
RecordAnGangAddGang
暗槓・加槓にゃ.
RecordHule
和了にゃ. records フィールドの最後にしか現れないにゃ.
RecordNoTile
荒牌平局にゃ. records フィールドの最後にしか現れないにゃ.
RecordLiuJu
途中流局にゃ. records フィールドの最後にしか現れないにゃ.

だから,现在需要将原先的旧代码进行重构,虽然想参考他人仓库,但无一例外全部基于app.NetAgent.sendReq2Lobby。

写在本篇最后

今天必然是弄不完了,现在是2025.6.26凌晨2:02,经过艰苦的奋斗,发现wikiwiki中的gameDetailRecords在mahjong_soul_api库中体现为GameAction而非wikiwiki中所说的records,而我上文其实已经提过,但是我忘记了。先这样吧。

此作者没有提供个人介绍。
最后更新于 2025-06-26