├── config.json ├── tsconfig.json ├── package.json ├── README.md ├── LICENSE ├── index.ts └── script.py /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "interaction": { 3 | "type": "script", 4 | "path": "libfrida-gadget.script.so" 5 | } 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "es2020" 6 | ], 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "allowJs": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "strict": true 13 | } 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "main": "index.ts", 4 | "version": "1.0.0", 5 | "private": true, 6 | "scripts": { 7 | "build": "frida-compile -o _.js -w index.ts", 8 | "attach": "run() { frida -U \"$1\" -l _.js --runtime=v8; }; run", 9 | "spawn": "run() { frida -U -f \"$1\" -l _.js --no-pause --runtime=v8; }; run", 10 | "app0-spawn": "npm run spawn com.example.application0", 11 | "app1": "npm run \"Application1 Name\"", 12 | "app1-spawn": "npm run spawn com.example.application1" 13 | }, 14 | "devDependencies": { 15 | "@types/frida-gum": "^17.1.1", 16 | "@types/node": "^16.11.25", 17 | "frida-compile": "^10.2.5", 18 | "frida-il2cpp-bridge": "^0.6.7" 19 | }, 20 | "dependencies": { 21 | "frida-fs": "^4.0.0", 22 | "fs": "^0.0.1-security" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MD-Replay-Editor 2 | replay saving and viewing for master duel via a gui using frida 3 | 4 | ## Usage 5 | Open the program while Master Duel is open, and click browse to open a replay folder. 6 | You will be presented with 3 modes of operation: 7 | - **Off**: normal behaviour, operates as if you don't have the program running 8 | - **Autosave**: after pressing play on a replay, you will be prompted to save the file to the current directory. 9 | - **Load From File**: after clicking on a file in the list window, a directory will be shown. When you hit play on any replay, the game will load that replay file. 10 | 11 | **BE SURE TO PRESS THE CONFIRM BUTTON AFTER CHANGING MODES!** 12 | 13 | ## Installation 14 | Download the exe file from releases, and simply run the file. 15 | 16 | Alternatively you can run the .py source file. You will need to make sure you have dependencies installed, and that the \_.js file is in the same folder as the python script. 17 | 18 | ## Dependencies 19 | 20 | ### Python 21 | frida, frida-tools, PySimpleGUI 22 | 23 | ### Node 24 | frida-compile, frida-il2cpp-bridge 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 crazydoomy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import "frida-il2cpp-bridge"; 2 | 3 | Il2Cpp.perform(() => { 4 | const il2cpplib = Il2Cpp.Domain.assemblies["Assembly-CSharp"].image; 5 | const mscorlib = Il2Cpp.Domain.assemblies["mscorlib"].image; 6 | const SystemBytes = mscorlib.classes["System.Byte"]; 7 | const deserializeAsync = il2cpplib.classes["YgomSystem.Network.FormatYgom"].methods.DeserializeAsync; 8 | const getpubliclevel = il2cpplib.classes["YgomGame.Duel.Util"].methods.GetPublicLevel; 9 | console.log("attached") 10 | deserializeAsync.implementation = function (ba : Il2Cpp.Array, onfinish : Il2Cpp.Object) : void { 11 | var array = []; 12 | for (let i = 0; i < ba.length; i++){ 13 | array.push(ba.get(i)); 14 | } 15 | //convert to buffer -> b64 16 | var u8array = new Uint8Array(array); 17 | var hexencoded = Buffer.from(u8array).toString('hex'); 18 | if (hexencoded.includes("7265706c61796d")) { //replaym in b64 19 | //send to python program and wait for response 20 | send(hexencoded); 21 | recv(function (obj) { 22 | hexencoded = obj.replay; 23 | }).wait(); 24 | //replace with new 25 | var b64decoded = Buffer.from(hexencoded, 'hex'); 26 | var newarray = []; 27 | for (let i = 0; i() 39 | a.fields["value__"].value = 2 40 | return a 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as sg 2 | import os.path 3 | import frida 4 | import tkinter as tk 5 | from tkinter import filedialog 6 | import time 7 | #ui layout copied from pysimplegui examples 8 | root = tk.Tk() 9 | root.withdraw() 10 | 11 | 12 | custom_toggle = False 13 | custom_replay = "" 14 | save_toggle = False 15 | def on_message(message, data): 16 | if custom_toggle: 17 | with open(custom_replay, "r") as f: 18 | script.post({'replay': f.read()}) 19 | elif save_toggle: 20 | files = [('Master Duel Replay', '*.replay')] 21 | filename = time.strftime("%m-%d-%Y %I-%M-%S %p", time.localtime()) 22 | root.attributes('-topmost', True) 23 | file = filedialog.asksaveasfile(parent=root, filetypes=files, defaultextension=files, initialfile=filename) 24 | if file: 25 | file.write(message['payload']) 26 | file.close() 27 | script.post({'replay': message['payload']}) 28 | else: 29 | script.post({'replay': message['payload']}) 30 | 31 | def inject(): 32 | global script 33 | try: 34 | session = frida.attach("masterduel.exe") 35 | except: 36 | sg.popup_error(f'Game not running!') 37 | exit() 38 | with open("_.js") as f: 39 | script = session.create_script(f.read()) 40 | script.on("message", on_message) 41 | script.load() 42 | 43 | inject() 44 | # First the window layout in 2 columns 45 | 46 | file_list_column = [ 47 | [ 48 | sg.Text("Replay Folder"), 49 | sg.In(size=(25, 1), enable_events=True, key="-FOLDER-"), 50 | sg.FolderBrowse(), 51 | sg.Button('Refresh') 52 | ], 53 | [ 54 | sg.Listbox( 55 | values=[], enable_events=True, size=(40, 20), key="-FILES-" 56 | ) 57 | ], 58 | ] 59 | # For now will only show the name of the file that was chosen 60 | setting_column = [ 61 | [sg.Text("Settings")], 62 | [sg.Text(size=(40, 1), key="-SELECTED-")], 63 | [sg.Radio('Off', 'SETTINGS', key="-OFF-", default=True)], 64 | [sg.Radio('Load Selected File', 'SETTINGS', key="-ENABLE-", default=False)], 65 | [sg.Radio('Autosave', 'SETTINGS', key="-AS-", default=False)], 66 | [sg.Button('Confirm')] 67 | ] 68 | # ----- Full layout ----- 69 | layout = [ 70 | [ 71 | sg.Column(file_list_column), 72 | sg.VSeperator(), 73 | sg.Column(setting_column), 74 | ] 75 | ] 76 | window = sg.Window("Replay Manager", layout) 77 | while True: 78 | event, values = window.read() 79 | custom_toggle = values['-ENABLE-'] 80 | save_toggle = values['-AS-'] 81 | if event == "Exit" or event == sg.WIN_CLOSED: 82 | break 83 | # Folder name was filled in, make a list of files in the folder 84 | if event == "-FOLDER-" or "Refresh": 85 | folder = values["-FOLDER-"] 86 | savepath = folder 87 | try: 88 | # Get list of files in folder 89 | file_list = os.listdir(folder) 90 | except: 91 | file_list = [] 92 | 93 | fnames = [ 94 | f 95 | for f in file_list 96 | if os.path.isfile(os.path.join(folder, f)) 97 | and f.lower().endswith(".replay") 98 | ] 99 | window["-FILES-"].update(fnames) 100 | if event == "-FILES-": # A file was chosen from the listbox 101 | print() 102 | try: 103 | filename = os.path.join( 104 | values["-FOLDER-"], values["-FILES-"][0] 105 | ) 106 | custom_replay = filename 107 | window["-SELECTED-"].update(filename) 108 | except: 109 | print("aa!") 110 | pass 111 | 112 | window.close() --------------------------------------------------------------------------------