-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
shinkuan
committed
Jan 25, 2024
1 parent
a2bb07e
commit 6cf5dc4
Showing
18 changed files
with
55,732 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,4 +21,6 @@ | |
/players/bot | ||
/players/docker | ||
/players/_docker | ||
/players/bot.zip | ||
/players/bot.zip | ||
/common/proxinject | ||
/log |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"majsoul_account_ids":[24201683]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
from rich.console import Console | ||
from rich.logging import RichHandler | ||
from collections import defaultdict | ||
from dataclasses import dataclass, asdict, field | ||
from os.path import exists | ||
from os import environ | ||
from json import load, dump | ||
from logging import getLogger | ||
from pathlib import Path | ||
|
||
pRoot = Path(".") | ||
|
||
pathConf = pRoot / "mhmp.json" | ||
pathResVer = pRoot / "resver.json" | ||
|
||
|
||
@dataclass | ||
class ResVer: | ||
version: str = None | ||
emotes: dict[str, list] = None | ||
|
||
@classmethod | ||
def fromdict(cls, data: dict): | ||
# purge | ||
if "max_charid" in data: | ||
data.pop("max_charid") | ||
if "emos" in data: | ||
data["emotes"] = data.pop("emos") | ||
return cls(**data) | ||
|
||
|
||
@dataclass | ||
class Conf: | ||
@dataclass | ||
class Base: | ||
log_level: str = "info" | ||
pure_python_protobuf: bool = False | ||
|
||
@dataclass | ||
class Hook: | ||
enable_skins: bool = True | ||
enable_aider: bool = False | ||
enable_chest: bool = False | ||
random_star_char: bool = False | ||
no_cheering_emotes: bool = False | ||
|
||
mhm: Base = None | ||
hook: Hook = None | ||
dump: dict = None | ||
mitmdump: dict = None | ||
proxinject: dict = None | ||
|
||
@classmethod | ||
def default(cls): | ||
return cls( | ||
mhm=cls.Base(), | ||
hook=cls.Hook(), | ||
dump={"with_dumper": False, "with_termlog": True}, | ||
mitmdump={"http2": False, "mode": ["[email protected]:7070"]}, | ||
proxinject={"name": "jantama_mahjongsoul", "set-proxy": "127.0.0.1:7070"}, | ||
) | ||
|
||
@classmethod | ||
def fromdict(cls, data: dict): | ||
# purge | ||
if "server" in data: | ||
data.pop("server") | ||
if "plugin" in data: | ||
data["hook"] = data.pop("plugin") | ||
# to dataclass | ||
for key, struct in [("mhm", cls.Base), ("hook", cls.Hook)]: | ||
if key in data: | ||
data[key] = struct(**data[key]) | ||
return cls(**data) | ||
|
||
|
||
if exists(pathConf): | ||
conf = Conf.fromdict(load(open(pathConf, "r"))) | ||
else: | ||
conf = Conf.default() | ||
|
||
if exists(pathResVer): | ||
resver = ResVer.fromdict(load(open(pathResVer, "r"))) | ||
else: | ||
resver = ResVer() | ||
|
||
|
||
def fetch_resver(): | ||
"""Fetch the latest character id and emojis""" | ||
import requests | ||
import random | ||
import re | ||
|
||
rand_a: int = random.randint(0, int(1e9)) | ||
rand_b: int = random.randint(0, int(1e9)) | ||
|
||
ver_url = f"https://game.maj-soul.com/1/version.json?randv={rand_a}{rand_b}" | ||
response = requests.get(ver_url, proxies={"https": None}) | ||
response.raise_for_status() | ||
version: str = response.json().get("version") | ||
|
||
if resver.version == version: | ||
return | ||
|
||
res_url = f"https://game.maj-soul.com/1/resversion{version}.json" | ||
response = requests.get(res_url, proxies={"https": None}) | ||
response.raise_for_status() | ||
res_data: dict = response.json() | ||
|
||
emotes: defaultdict[str, list[int]] = defaultdict(list) | ||
pattern = rf"en\/extendRes\/emo\/e(\d+)\/(\d+)\.png" | ||
|
||
for text in res_data.get("res"): | ||
matches = re.search(pattern, text) | ||
|
||
if matches: | ||
charid = matches.group(1) | ||
emo = int(matches.group(2)) | ||
|
||
if emo == 13: | ||
continue | ||
emotes[charid].append(emo) | ||
for value in emotes.values(): | ||
value.sort() | ||
|
||
resver.version = version | ||
resver.emotes = {key: value[9:] for key, value in sorted(emotes.items())} | ||
|
||
with open(pathResVer, "w") as f: | ||
dump(asdict(resver), f) | ||
|
||
|
||
def no_cheering_emotes(): | ||
exclude = set(range(13, 19)) | ||
for emo in resver.emotes.values(): | ||
emo[:] = sorted(set(emo) - exclude) | ||
|
||
|
||
def init(): | ||
with console.status("[magenta]Fetch the latest server version") as status: | ||
fetch_resver() | ||
if conf.hook.no_cheering_emotes: | ||
no_cheering_emotes() | ||
if conf.mhm.pure_python_protobuf: | ||
environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" | ||
|
||
with open(pathConf, "w") as f: | ||
dump(asdict(conf), f, indent=2) | ||
|
||
|
||
# console | ||
console = Console() | ||
|
||
|
||
# logger | ||
logger = getLogger(__name__) | ||
logger.propagate = False | ||
logger.setLevel(conf.mhm.log_level.upper()) | ||
logger.addHandler(RichHandler(markup=True, rich_tracebacks=True)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .common import main | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from mitmproxy import http | ||
|
||
|
||
from . import logger | ||
from .hook import hooks | ||
from .proto import MsgManager | ||
|
||
|
||
def log(mger: MsgManager): | ||
msg = mger.m | ||
logger.info(f"[i][gold1]& {mger.tag} {msg.type.name} {msg.method} {msg.id}") | ||
logger.debug(f"[cyan3]# {msg.amended} {msg.data}") | ||
|
||
|
||
class WebSocketAddon: | ||
def __init__(self): | ||
self.manager = MsgManager() | ||
|
||
def websocket_start(self, flow: http.HTTPFlow): | ||
logger.info(" ".join(["[i][green]Connected", flow.id[:13]])) | ||
|
||
def websocket_end(self, flow: http.HTTPFlow): | ||
logger.info(" ".join(["[i][blue]Disconnected", flow.id[:13]])) | ||
|
||
def websocket_message(self, flow: http.HTTPFlow): | ||
# make type checker happy | ||
assert flow.websocket is not None | ||
|
||
try: | ||
self.manager.parse(flow) | ||
except: | ||
logger.warning(" ".join(["[i][red]Unsupported Message @", flow.id[:13]])) | ||
logger.debug(__import__("traceback").format_exc()) | ||
|
||
return | ||
|
||
if self.manager.member: | ||
for hook in hooks: | ||
try: | ||
hook.hook(self.manager) | ||
except: | ||
logger.warning(" ".join(["[i][red]Error", self.manager.m.method])) | ||
logger.debug(__import__("traceback").format_exc()) | ||
|
||
if self.manager.m.amended: | ||
self.manager.apply() | ||
|
||
log(self.manager) | ||
|
||
|
||
addons = [WebSocketAddon()] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import asyncio | ||
|
||
from . import pRoot, logger, conf, resver, init | ||
|
||
|
||
PROXINJECTOR = pRoot / "common/proxinject/proxinjector-cli" | ||
|
||
|
||
def _cmd(dict): | ||
return [obj for key, value in dict.items() for obj in (f"--{key}", value)] | ||
|
||
|
||
async def start_proxy(): | ||
from mitmproxy.tools.dump import DumpMaster | ||
from mitmproxy.options import Options | ||
from .addons import addons | ||
|
||
master = DumpMaster(Options(**conf.mitmdump), **conf.dump) | ||
master.addons.add(*addons) | ||
await master.run() | ||
return master | ||
|
||
|
||
async def start_inject(): | ||
cmd = [PROXINJECTOR, *_cmd(conf.proxinject)] | ||
|
||
while True: | ||
process = await asyncio.subprocess.create_subprocess_exec( | ||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE | ||
) | ||
|
||
stdout, stderr = await process.communicate() | ||
|
||
await asyncio.sleep(0.8) | ||
|
||
|
||
def main(): | ||
async def start(): | ||
logger.info(f"[i]log level: {conf.mhm.log_level}") | ||
logger.info(f"[i]pure python protobuf: {conf.mhm.pure_python_protobuf}") | ||
|
||
logger.info(f"[i]version: {resver.version}") | ||
logger.info(f"[i]characters: {len(resver.emotes)}") | ||
|
||
tasks = set() | ||
|
||
if conf.mitmdump: | ||
tasks.add(start_proxy()) | ||
logger.info(f"[i]mitmdump launched @ {len(conf.mitmdump.get('mode'))} mode") | ||
|
||
# if conf.proxinject: | ||
# tasks.add(start_inject()) | ||
# logger.info(f"[i]proxinject launched @ {conf.proxinject.get('set-proxy')}") | ||
|
||
await asyncio.gather(*tasks) | ||
|
||
init() | ||
|
||
try: | ||
asyncio.run(start()) | ||
except KeyboardInterrupt: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
from mhm import conf | ||
from mhm.proto import MsgManager, MsgType | ||
|
||
|
||
class Hook: | ||
def __init__(self) -> None: | ||
self.mapHook = {} | ||
|
||
def hook(self, mger: MsgManager): | ||
mKey = (mger.m.type, mger.m.method) | ||
if mKey in self.mapHook: | ||
self.mapHook[mKey](mger) | ||
|
||
def bind(self, mType: MsgType, mMethod: str): | ||
def decorator(func): | ||
mKey = (mType, mMethod) | ||
self.mapHook[mKey] = func | ||
return func | ||
|
||
return decorator | ||
|
||
|
||
hooks: list[Hook] = [] | ||
|
||
if conf.hook.enable_aider: | ||
from .aider import DerHook | ||
|
||
hooks.append(DerHook()) | ||
|
||
if conf.hook.enable_chest: | ||
from .chest import OstHook | ||
|
||
hooks.append(OstHook()) | ||
|
||
if conf.hook.enable_skins: | ||
from .skins import KinHook | ||
|
||
hooks.append(KinHook()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import os | ||
import requests | ||
|
||
from urllib3 import disable_warnings | ||
from urllib3.exceptions import InsecureRequestWarning | ||
from socket import socket, AF_INET, SOCK_STREAM | ||
|
||
from mhm import pRoot | ||
from mhm.proto import MsgManager | ||
from mhm.hook import Hook | ||
|
||
|
||
class DerHook(Hook): | ||
def __init__(self) -> None: | ||
self.pool = dict() | ||
|
||
disable_warnings(InsecureRequestWarning) | ||
|
||
self.path = pRoot / "common/endless/mahjong-helper" | ||
|
||
def hook(self, mger: MsgManager): | ||
if mger.m.isReq(): | ||
return | ||
|
||
if mger.member not in self.pool: | ||
self.pool[mger.member] = Aider(self.path) | ||
|
||
if mger.m.method == ".lq.ActionPrototype": | ||
if mger.data["name"] == "ActionNewRound": | ||
mger.data["data"]["md5"] = mger.data["data"]["sha256"][:32] | ||
send_msg = mger.data["data"] | ||
elif mger.m.method == ".lq.FastTest.syncGame": | ||
for action in mger.data["game_restore"]["actions"]: | ||
if action["name"] == "ActionNewRound": | ||
action["data"]["md5"] = action["data"]["sha256"][:32] | ||
send_msg = {"sync_game_actions": mger.data["game_restore"]["actions"]} | ||
else: | ||
send_msg = mger.data | ||
|
||
requests.post(self.pool[mger.member].api, json=send_msg, verify=0) | ||
|
||
|
||
class Aider: | ||
PORT = 43410 | ||
|
||
def __init__(self, path: str) -> None: | ||
with socket(AF_INET, SOCK_STREAM) as s: | ||
s.settimeout(0.2) | ||
if s.connect_ex(("127.0.0.1", Aider.PORT)) != 0: | ||
cmd = f'start cmd /c "title Console · 🀄 && {path} -majsoul -p {Aider.PORT}"' | ||
os.system(cmd) | ||
|
||
self.api = f"https://127.0.0.1:{Aider.PORT}" | ||
|
||
Aider.PORT += 1 |
Oops, something went wrong.