Skip to content

Commit

Permalink
Merge pull request #15 from shinkuan/dev
Browse files Browse the repository at this point in the history
v1.2.0
  • Loading branch information
shinkuan authored Jan 27, 2024
2 parents 3513bce + 7a26083 commit c53bd7f
Show file tree
Hide file tree
Showing 22 changed files with 55,754 additions and 20 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
/players/bot
/players/docker
/players/_docker
/players/bot.zip
/players/bot.zip
/common/proxinject
/log
/account
6 changes: 5 additions & 1 deletion action.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def __init__(self, rpc_server: ServerProxy):
def page_clicker(self, coord: tuple[float, float]):
self.rpc_server.page_clicker(coord)

def do_autohu(self):
self.rpc_server.do_autohu()

def decide_random_time(self):
if self.isNewRound:
return random.uniform(2.3, 2.5)
Expand Down Expand Up @@ -254,6 +257,7 @@ def click_dahai(self, mjai_msg: dict | None, tehai: list[str], tsumohai: str | N
if dahai == temp_tehai[i]:
pai_coord = self.get_pai_coord(i, temp_tehai)
self.page_clicker(pai_coord)
self.do_autohu()
self.isNewRound = False
return
if tsumohai != '?':
Expand All @@ -277,7 +281,7 @@ def mjai2action(self, mjai_msg: dict | None, tehai: list[str], tsumohai: str | N
self.click_dahai(mjai_msg, tehai, tsumohai)
return
if mjai_msg['type'] in ['none', 'chi', 'pon', 'daiminkan', 'ankan', 'kakan', 'hora', 'reach', 'ryukyoku']:
time.sleep(1.5)
time.sleep(2)
self.click_chiponkan(mjai_msg, tehai, tsumohai)
# kan can have multiple candidates too! ex: tehai=1111m 1111p 111s 11z, tsumohai=1s

2 changes: 2 additions & 0 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def refresh_log(self) -> None:
self.akagi_pai.label = "None"
self.pai_unicode_art.update(TILE_2_UNICODE_ART_RICH["?"])
# Action
logger.info(f"Current tehai: {self.app.bridge[self.flow_id].my_tehais}")
logger.info(f"Current tsumohai: {self.app.bridge[self.flow_id].my_tsumohai}")
if not self.syncing and ENABLE_PLAYWRIGHT and AUTOPLAY:
logger.log("CLICK", self.app.mjai_msg_dict[self.flow_id][-1])
self.app.set_timer(0.05, self.autoplay)
Expand Down
Binary file added common/endless/mahjong-helper.exe
Binary file not shown.
1 change: 1 addition & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"majsoul_account_ids":[24201683]}
2 changes: 1 addition & 1 deletion liqi.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def parse(self, flow_msg, injected=False) -> dict:
return result

def parse_syncGame(self, syncGame):
assert syncGame['method'] == '.lq.FastTest.syncGame'
assert syncGame['method'] == '.lq.FastTest.syncGame' or syncGame['method'] == '.lq.FastTest.enterGame'
msgs = []
if 'gameRestore' in syncGame['data']:
for action in syncGame['data']['gameRestore']['actions']:
Expand Down
4 changes: 2 additions & 2 deletions majsoul2mjai.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self) -> None:

def input(self, mjai_client: list[MjaiPlayerClient], parse_msg: dict) -> dict | None:
# TODO SyncGame
if parse_msg['method'] == '.lq.FastTest.syncGame':
if parse_msg['method'] == '.lq.FastTest.syncGame' or parse_msg['method'] == '.lq.FastTest.enterGame':
self.syncing = True
syncGame_msgs = LiqiProto().parse_syncGame(parse_msg)
reacts = []
Expand Down Expand Up @@ -293,6 +293,7 @@ def input(self, mjai_client: list[MjaiPlayerClient], parse_msg: dict) -> dict |
for pai in consumed:
self.my_tehais.remove(pai)
self.my_tehais.append("?")
self.my_tehais.remove("?")
self.my_tehais = sorted(self.my_tehais, key=cmp_to_key(compare_pai))
case OperationAnGangAddGang.AddGang:
pai = MS_TILE_2_MJAI_TILE[parse_msg['data']['data']['tiles']]
Expand All @@ -314,7 +315,6 @@ def input(self, mjai_client: list[MjaiPlayerClient], parse_msg: dict) -> dict |
else:
self.my_tehais.append("?")
self.my_tehais.remove(pai)
self.my_tehais.append("?")
self.my_tehais = sorted(self.my_tehais, key=cmp_to_key(compare_pai))
# hora
if parse_msg['data']['name'] == 'ActionHule':
Expand Down
159 changes: 159 additions & 0 deletions mhm/__init__.py
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))
4 changes: 4 additions & 0 deletions mhm/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .common import main

if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions mhm/addons.py
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()]
62 changes: 62 additions & 0 deletions mhm/common.py
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
Loading

0 comments on commit c53bd7f

Please sign in to comment.