diff --git a/README.md b/README.md index e5bd4b1..b8f6763 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Yu-Gi-Oh! Master Duel Translation Script * i18n,支持简体中文和繁体中文。 * 高兼容性,点开即用,支持各种游戏内语言,最低可支持win7。 * 支持内存检测和图像指纹两种识别模式。 -* 一键导出Master Duel游戏卡组,兼容YGOpro格式。同时提供在线转换YGOpro格式导入。 +* 支持ydk格式卡组导入。 * 可一键直达网页卡查和官方数据库,MDT也有 [Secret Pack查询工具](https://ygo.xn--uesr8qr0rdwk.cn/)。 * 支持全屏置顶、无边框、半透明。 * 支持对重要UR,主流断点进行警示。 @@ -27,7 +27,7 @@ Yu-Gi-Oh! Master Duel Translation Script 如果是Windows7系统请下载后缀`_win7`的版本,如果想使用CLI请下载`_CLI`的版本。 -中国大陆用户可在 [蓝奏云下载](https://wwi.lanzouj.com/b0176jyjc) 密码:5j6f +中国大陆用户可在 [蓝奏云](https://wwi.lanzouj.com/b0176jyjc) 密码:5j6f 或者 [Microsoft OneDrive](https://1drv.ms/u/s!Apo8OlF1smGK6nS7sXukI9Bt9xOd?e=bbzDea) 分流下载。 你可以通过 [YGO.御坂美琴.CN](https://ygo.xn--uesr8qr0rdwk.cn/) 访问MDT网页工具。 @@ -354,6 +354,15 @@ CLI版本在MDT v0.2.3版本进行拆分,拆分后对CLI版本只做基础可 ## Changelog +*v0.2.17* +* 图像模式更新6月10日新卡hash。by wtof1996 +* 修复识别线程失效问题。by chunibyo +* 将效果框置为只读。by funnyvalentine2363 +* 去除了失效的卡组导出功能。(可能需要新的实现方式) + +
+ 展开过往版本 + *v0.2.16* * 对游戏steam版本V1.1.1进行支持。 @@ -363,9 +372,6 @@ CLI版本在MDT v0.2.3版本进行拆分,拆分后对CLI版本只做基础可 *v0.2.14 beta* * 支持4月新卡图像识别。感谢@wtof1996 的贡献。 -
- 展开过往版本 - *v0.2.13* * 自定义BGM支持,在选中一张卡牌时,自动播放BGM或召唤词。样例为青眼亚白龙。可以在设置中开启。 * 分词处理。 diff --git a/mdt_control.py b/mdt_control.py index 25ec24a..f6a68a7 100644 --- a/mdt_control.py +++ b/mdt_control.py @@ -2,7 +2,7 @@ import win32gui import pyautogui import pyperclip -from mdt import get_current_cid, status_change +from mdt import get_current_cid import mdt_deck_reader from mdt_cv import get_reset_button_postion, get_scale, get_search_button_postion diff --git a/mdt_gui.py b/mdt_gui.py index 3face54..9283c25 100644 --- a/mdt_gui.py +++ b/mdt_gui.py @@ -262,7 +262,7 @@ def main(): background_color="#3F3F3F", text_color="white", write_only=True, - disabled = True, + disabled=True, auto_refresh=True, rstrip=True, no_scrollbar=no_scrollbar, @@ -405,7 +405,7 @@ def main(): ], ] window = sg.Window( - "MDT v0.2.16 GPLv3", + "MDT v0.2.17 GPLv3", card_frame, default_element_size=(12, 1), font=("Microsoft YaHei", font_size), @@ -437,6 +437,7 @@ def main(): window["-desc-"].Widget.configure(wrap="char") if cid != cid_temp and cid: cid_temp = cid + print(cid) try: card_t = cards_db[str(cid)] window["-cn_name-"].update(card_t["cn_name"]) @@ -715,17 +716,17 @@ def main(): ] ] ), - sg.Column( - [ - [ - sg.Button( - _("导出卡组"), - button_color=("white", "#238636"), - border_width=1, - ) - ] - ] - ), + # sg.Column( + # [ + # [ + # sg.Button( + # _("导出卡组"), + # button_color=("white", "#238636"), + # border_width=1, + # ) + # ] + # ] + # ), sg.Column( [ [ diff --git a/win7/mdt_control.py b/win7/mdt_control.py new file mode 100644 index 0000000..a4d4c3b --- /dev/null +++ b/win7/mdt_control.py @@ -0,0 +1,98 @@ +import time +import win32gui +import pyautogui +import pyperclip +from mdt import get_current_cid +import mdt_deck_reader + +from mdt_cv import get_reset_button_postion, get_scale, get_search_button_postion + +# pyautogui 强制关闭 +pyautogui.FAILSAFE = True + +# 1920x1080 分辨率下的偏移量,可以通过分辨率换算进行变换 +blank_offset = (0, -150) +clear_offset = (335, 0) +card_offset = (0, 170) +card_width_offset = 89 +card_height_offset = 144 + + +def _add(a: tuple, b: tuple, scale=1.0): + return a[0] + b[0] * scale, a[1] + b[1] * scale + + +def ydk_converter(ydk_deck: list, locale: str, window, callback=None): + """ + Convert YDK deck list to MDT deck. + """ + # TODO: background click + # TODO: 清空当前牌组 + # TODO: 等待搜索时间可调 + if ydk_deck is None or ydk_deck == []: + return + + try: + hWnd = win32gui.FindWindow(None, "masterduel") + win32gui.SetForegroundWindow(hWnd) + box = win32gui.ClientToScreen(hWnd, (0, 0)) + + scale = get_scale() + # 先横后竖 + search = get_search_button_postion() + search = _add(search, box) + reset = get_reset_button_postion() + reset = _add(reset, box) + blank = _add(search, blank_offset, scale) + clear = _add(search, clear_offset, scale) + card = _add(search, card_offset, scale) + target_card_position = None + + pyautogui.click(reset, interval=0.5) + for index, tup in enumerate(ydk_deck): + element, cid = tup + if index == 0 or element != ydk_deck[index - 1][0]: + pyautogui.click(clear, interval=0.1) + pyautogui.click(blank, interval=0.1) + pyautogui.click(search) + # 粘贴卡片名 + pyperclip.copy(element) + pyperclip.paste() + pyautogui.hotkey("ctrl", "v") + pyautogui.press("enter") + # 等待搜索完成 + time.sleep(1.2) + # 处理搜索得到多卡片的情况 + target_card_position = travel_through_deck( + card, card_width_offset * scale, card_height_offset * scale, int(cid) + ) + if target_card_position is None: + continue + + print(f"{element}\n") + pyautogui.rightClick(target_card_position) + + # 对卡组进行校验 + result = mdt_deck_reader.check_deck([int(i[1]) for i in ydk_deck], locale) + if len(result["error1"]) != 0 or len(result["error2"]) != 0: + window.write_event_value("DECK_CHECK_ERROR", result) + else: + window.write_event_value("DECK_CHECK_OK", result) + except Exception as e: + print(e) + finally: + callback() + + +def travel_through_deck(start, width_step, height_step, target_cid=-1): + # 竖 + for i in range(5): + # 横 + for j in range(6): + click_position = start[0] + width_step * j, start[1] + height_step * i + pyautogui.click(click_position) + cid = get_current_cid() + if cid == target_cid: + return click_position + + return None diff --git a/win7/mdt_deck_reader.py b/win7/mdt_deck_reader.py new file mode 100644 index 0000000..69bfb77 --- /dev/null +++ b/win7/mdt_deck_reader.py @@ -0,0 +1,255 @@ +import json + +import i18n +import pymem + +_ = i18n.t + + +def pointer_to_address(process, base, offsets): + offset_final = offsets.pop() + address_next = process.read_longlong(base) + for offset in offsets: + address_next = process.read_longlong(address_next + offset) + address_final = address_next + offset_final + return address_final + + +def read_memory_int(process, address): + value = process.read_int(address) + return value + + +def read_memory_bytes(process, address, count): + inc = 0x18 + value = process.read_bytes(address, count * inc) + return value + + +def deck_bytes_to_list(bytes: bytes, count: int): + inc = 0x18 + card_list = [] + for i in range(count): + card_list.append( + int.from_bytes(bytes[i * inc : i * inc + 2], byteorder="little") + ) + return card_list + + +def get_process(process_name): + process = pymem.Pymem(process_name) + return process + + +def get_base_address(process, module): + base_addr = pymem.process.module_from_name( + process.process_handle, module + ).lpBaseOfDll + return base_addr + + +def get_database(path): + with open(path, "rb") as f: + cards_db = json.load(f) + return cards_db + + +def get_deck_dict(): + main_name = "masterduel.exe" + module_name = "GameAssembly.dll" + ma_count_static = 0x01E99C18 + ma_count_offsets = [0xB8, 0x00, 0xF8, 0x1C0, 0x90, 0x18] + ex_count_static = 0x01E99C18 + ex_count_offsets = [0xB8, 0x00, 0xF8, 0x1C0, 0x98, 0x18] + ma_cards_static = 0x01E99C18 + ma_cards_offsets = [0xB8, 0x00, 0xF8, 0x1C0, 0x90, 0x10, 0x20] + ex_cards_static = 0x01E99C18 + ex_cards_offsets = [0xB8, 0x00, 0xF8, 0x1C0, 0x98, 0x10, 0x20] + deck_dict = {"error": _("无法读取卡组信息")} + try: + pm = get_process(main_name) + base_address = get_base_address(pm, module_name) + ma_count_addr = base_address + ma_count_static + ex_count_addr = base_address + ex_count_static + ma_cards_addr = base_address + ma_cards_static + ex_cards_addr = base_address + ex_cards_static + ma_count = read_memory_int( + pm, pointer_to_address(pm, ma_count_addr, ma_count_offsets) + ) + ex_count = read_memory_int( + pm, pointer_to_address(pm, ex_count_addr, ex_count_offsets) + ) + ma_cid_list = deck_bytes_to_list( + read_memory_bytes( + pm, pointer_to_address(pm, ma_cards_addr, ma_cards_offsets), ma_count + ), + ma_count, + ) + ex_cid_list = deck_bytes_to_list( + read_memory_bytes( + pm, pointer_to_address(pm, ex_cards_addr, ex_cards_offsets), ex_count + ), + ex_count, + ) + deck_dict = { + "ma_count": ma_count, + "ex_count": ex_count, + "ma_cid_list": ma_cid_list, + "ex_cid_list": ex_cid_list, + } + return deck_dict + except Exception: # as e: + # print(e) + return deck_dict + + +def get_deck_string(locale: str): + db_name = "./locales/" + locale + "/cards.json" + deck_string = "" + try: + cards_db = get_database(db_name) + deck = get_deck_dict() + except Exception: # Exception as e: + # print(e) + deck_string += _("无法读取卡组信息") + return deck_string + if "error" not in deck: + deck_string += f"----------------主卡组: {deck['ma_count']}----------------\n" + c = 0 + for cid in deck["ma_cid_list"]: + c += 1 + card_string = "" + try: + card_info = cards_db[str(cid)] + except Exception: + card_string += "查无此卡" + + try: + card_string += f"{card_info['cn_name']} " + card_string += f"{card_info['jp_name']} " + card_string += f"{card_info['en_name']}" + except Exception: + card_string += " " + "该卡信息有缺失" + deck_string += f"{c:<2} {card_string}\n" + + deck_string += f"----------------额外卡组: {deck['ex_count']}----------------\n" + c = 0 + for cid in deck["ex_cid_list"]: + c += 1 + card_string = "" + try: + card_info = cards_db[str(cid)] + except Exception: + card_string += "查无此卡" + + try: + card_string += f"{card_info['cn_name']} " + card_string += f"{card_info['jp_name']} " + card_string += f"{card_info['en_name']}" + except Exception: + card_string += " " + "该卡信息有缺失" + deck_string += f"{c:<2} {card_string}\n" + + return deck_string + + +def ydk_converter(ydk_deck: str, game_client_locale: str = "en"): + if ydk_deck is None: + print(_("格式有误")) + return None + + # 读取数据库,预载卡片信息 + try: + db_name = "./locales/zh-CN/cards.json" + # cid -> id, en, jp + cards_db = get_database(db_name) + # id -> en, jp, cid + cards_db_cache = { + card_info["id"]: { + "jp_name": card_info["jp_name"], + "en_name": card_info["en_name"], + "cid": cid, + } + for (cid, card_info) in cards_db.items() + } + except Exception: + print(_("无法读取卡组信息")) + return None + + result = [] + for line in ydk_deck.split("\n"): + cards_id = line.strip() + if cards_id.startswith("#") or cards_id == "": + # 跳过注释和空行 + continue + elif cards_id.startswith("!"): + # side deck + break + elif not cards_id.isdigit(): + # 用户粘贴的未知字符 + print(_("格式有误")) + return None + + try: + # 获取卡片信息 + card_info = cards_db_cache[int(cards_id)] + except Exception as e: + print(e) + print(_("查无此卡")) + return None + + try: + # 获取卡片名称 + result.append( + (f"{card_info[f'{game_client_locale}_name']}", card_info["cid"]) + ) + except Exception as e: + print(e) + print(_("格式有误")) + return None + + return result + + +def _check_two_array_not_same(deck1: list, deck2: list): + """ + 给两个拥有重复元素的列表,返回各自中独立存在的元素 + 例:[1, 2, 2, 4] [2, 3, 4, 4] -> [1, 2] [3, 4] + 复杂度:O(n) + """ + l, r = 0, 0 + error1 = [] + error2 = [] + while l < len(deck1) or r < len(deck2): + if l < len(deck1) and r < len(deck2) and deck1[l] == deck2[r]: + l += 1 + r += 1 + elif l < len(deck1) and (r == len(deck2) - 1 or deck1[l] < deck2[r]): + error1.append(deck1[l]) + l += 1 + elif r < len(deck2): + error2.append(deck2[r]) + r += 1 + return error1, error2 + + +def check_deck(ydk_deck: list, locale): + _dict = get_deck_dict() + if "error" not in _dict: + deck1 = sorted(list(map(int, _dict["ma_cid_list"] + _dict["ex_cid_list"]))) + deck2 = sorted(ydk_deck) + error1, error2 = _check_two_array_not_same(deck1, deck2) + + db_name = "./locales/" + locale + "/cards.json" + cards_db = get_database(db_name) + error1 = [cards_db[str(cid)]["cn_name"] for cid in error1] + error2 = [cards_db[str(cid)]["cn_name"] for cid in error2] + print(error1, "imported wrong in your deck.") + print(error2, "in ydk deck haven't been imported") + else: + print("卡组读取错误") + return {"error1": error1, "error2": error2} + + +if __name__ == "__main__": + print(get_deck_dict())