├── requirements.txt ├── umaModelReplace ├── __init__.py ├── assets_path.py └── main.py ├── README.md ├── LICENSE ├── replaceCustomT2D.py ├── .gitignore └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | UnityPy~=1.8.15 2 | Pillow -------------------------------------------------------------------------------- /umaModelReplace/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # umamusume-model-replace 2 | - replace umamusume models 3 | 4 | 5 | 6 | # 使用方法 7 | 8 | - 安装`Python3.8及以上版本` 9 | - 安装依赖: `pip install -r requirements.txt` 10 | 11 | ``` 12 | UnityPy~=1.8.15 13 | ``` 14 | 15 | - 运行`main.py` 16 | 17 | ```shell 18 | python main.py 19 | ``` 20 | 21 | - 根据提示操作即可。 22 | 23 | ``` 24 | [1] replace head model 25 | [2] replace body model 26 | [3] replace tail model (deprecated) 27 | [4] replace head and body model 28 | [5] replace body materials 29 | [6] replace gacha character 30 | [7] replace skill character 31 | [8] replace g1 victory character action (Experimental) 32 | [9] unlock live dress 33 | [10] disable live blur 34 | [98] restore your changes 35 | [99] exit 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 chinosk 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 | -------------------------------------------------------------------------------- /replaceCustomT2D.py: -------------------------------------------------------------------------------- 1 | import umaModelReplace 2 | import shutil 3 | 4 | uma = umaModelReplace.UmaReplace() 5 | 6 | 7 | def getAndReplaceTexture2D(bundle_hash, src_names): 8 | is_not_exist, msg = uma.get_texture_in_bundle(bundle_hash, src_names) 9 | if not is_not_exist: 10 | print(f"解包资源已存在: {msg}") 11 | do_replace = input("输入 \"Y\" 覆盖已解包资源, 输入其它内容跳过导出: ") 12 | if do_replace in ["Y", "y"]: 13 | _, msg = uma.get_texture_in_bundle(bundle_hash, src_names, True) 14 | 15 | print(f"已尝试导出资源, 请查看目录: {msg}") 16 | do_fin = input("请进行文件修改/替换, 修改完成后请输入 \"Y\" 打包并替换游戏文件。\n" 17 | "若您不想立刻修改, 可以输入其它任意内容退出, 您可以在下次替换时选择\"跳过导出\"\n" 18 | "请输入: ") 19 | if do_fin.strip() in ["Y", "y"]: 20 | uma.file_backup(bundle_hash) 21 | edited_path = uma.replace_texture2d(bundle_hash) 22 | shutil.copyfile(edited_path, uma.get_bundle_path(bundle_hash)) 23 | print("贴图已修改") 24 | 25 | 26 | # getAndReplaceTexture2D("EUI2AY3HRHIRXFCU5ZUTQRQKS4IJGBF5", ["tex_env_cutin1019_40_00_base01"]) # 数码固有"尊" 27 | # getAndReplaceTexture2D("L6XWAMB2FBPJK32AEWJUMUDB47BJQROC", ["tex_chr_prop1259_00_diff"]) # 数码固有 玩偶 28 | getAndReplaceTexture2D("KM6Z67WZ5C6XUQZBLXJ237TBVVVAGFCS", ["tex_chr_prop1003_06_diff"]) # 杂志封面 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | /.idea 131 | /.vs 132 | /umaModelReplace/backup 133 | /umaModelReplace/edited 134 | /editTexture 135 | -------------------------------------------------------------------------------- /umaModelReplace/assets_path.py: -------------------------------------------------------------------------------- 1 | def get_body_mtl_names(_id): 2 | return [ 3 | f"tex_bdy{_id}_shad_c", 4 | f"tex_bdy{_id}_base", 5 | f"tex_bdy{_id}_ctrl", 6 | f"tex_bdy{_id}_diff" 7 | ] 8 | 9 | 10 | def get_body_mtl_path(_id): 11 | return f"sourceresources/3d/chara/body/bdy{_id}/materials/mtl_bdy{_id}" 12 | 13 | 14 | def get_body_path(_id): 15 | return [ 16 | f"3d/chara/body/bdy{_id}/pfb_bdy{_id}", 17 | get_body_mtl_path(_id) 18 | ] 19 | 20 | 21 | def get_head_path(_id): 22 | return [ 23 | f"3d/chara/head/chr{_id}/pfb_chr{_id}", 24 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_cheek", 25 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_eye", 26 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_face", 27 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_hair", 28 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_mayu", 29 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_tear", 30 | f"sourceresources/3d/chara/head/chr{_id}/facial/ast_chr{_id}_ear_target" 31 | ] 32 | 33 | 34 | def get_tail1_path(_id): 35 | return [ 36 | f"3d/chara/tail/tail0001_00/textures/tex_tail0001_00_{_id[:4]}_diff", 37 | f"3d/chara/tail/tail0001_00/textures/tex_tail0001_00_{_id[:4]}_diff_wet", 38 | f"3d/chara/tail/tail0001_00/textures/tex_tail0001_00_{_id[:4]}_shad_c", 39 | f"3d/chara/tail/tail0001_00/textures/tex_tail0001_00_{_id[:4]}_shad_c_wet", 40 | ] 41 | 42 | 43 | def get_tail2_path(_id): 44 | return [ 45 | f"3d/chara/tail/tail0002_00/textures/tex_tail0002_00_{_id[:4]}_diff", 46 | f"3d/chara/tail/tail0002_00/textures/tex_tail0002_00_{_id[:4]}_diff_wet", 47 | f"3d/chara/tail/tail0002_00/textures/tex_tail0002_00_{_id[:4]}_shad_c", 48 | f"3d/chara/tail/tail0002_00/textures/tex_tail0002_00_{_id[:4]}_shad_c_wet" 49 | ] 50 | 51 | 52 | def get_gac_chr_start_path(type): 53 | return f"cutt/cutin/skill/gac_chr_start_{type}/gac_chr_start_{type}" 54 | 55 | 56 | def get_cutin_skill_path(_id): 57 | return f"cutt/cutin/skill/crd{_id}_001/crd{_id}_001" 58 | 59 | 60 | def get_race_result_path(_id): 61 | return get_chr_race_result_path(_id) + get_crd_race_result_path(_id) 62 | 63 | 64 | def get_chr_race_result_path(_id): 65 | return [ 66 | f"cutt/cutin/raceresult/res_chr{_id[:4]}_001/res_chr{_id[:4]}_001", 67 | f"3d/motion/raceresult/body/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001", 68 | f"3d/motion/raceresult/camera/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001_cam", 69 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001_ear", 70 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001_ear_driven", 71 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001_face", 72 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_chr{_id[:4]}_001_face_driven" 73 | ] 74 | 75 | 76 | def get_crd_race_result_path(_id): 77 | return [ 78 | f"cutt/cutin/raceresult/res_crd{_id}_001/res_crd{_id}_001", 79 | f"3d/motion/raceresult/body/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001", 80 | f"3d/motion/raceresult/camera/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001_cam", 81 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001_ear", 82 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001_ear_driven", 83 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001_face", 84 | f"3d/motion/raceresult/facial/chara/chr{_id[:4]}_00/anm_res_crd{_id}_001_face_driven", 85 | f"sound/v/snd_voi_race_{_id}.acb", 86 | f"sound/v/snd_voi_race_{_id}.awb" 87 | ] 88 | 89 | 90 | def get_head_mtl_path(_id): 91 | return [ 92 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_face", 93 | f"sourceresources/3d/chara/head/chr{_id}/materials/mtl_chr{_id}_hair" 94 | ] 95 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import umaModelReplace 2 | 3 | uma = umaModelReplace.UmaReplace() 4 | 5 | 6 | def replace_char_body_texture(char_id: str): 7 | is_not_exist, msg = uma.save_char_body_texture(char_id, False) 8 | if not is_not_exist: 9 | print(f"解包资源已存在: {msg}") 10 | do_replace = input("输入 \"Y\" 覆盖已解包资源, 输入其它内容跳过导出: ") 11 | if do_replace in ["Y", "y"]: 12 | _, msg = uma.save_char_body_texture(char_id, True) 13 | 14 | print(f"已尝试导出资源, 请查看目录: {msg}") 15 | do_fin = input("请进行文件修改/替换, 修改完成后请输入 \"Y\" 打包并替换游戏文件。\n" 16 | "若您不想立刻修改, 可以输入其它任意内容退出, 您可以在下次替换时选择\"跳过导出\"\n" 17 | "请输入: ") 18 | if do_fin.strip() in ["Y", "y"]: 19 | uma.replace_char_body_texture(char_id) 20 | print("贴图已修改") 21 | 22 | def replace_char_head_texture(char_id: str): 23 | for n, i in enumerate(uma.save_char_head_texture(char_id, False)): 24 | is_not_exist, msg = i 25 | 26 | if not is_not_exist: 27 | print(f"解包资源已存在: {msg}") 28 | do_replace = input("输入 \"Y\" 覆盖已解包资源, 输入其它内容跳过导出: ") 29 | if do_replace in ["Y", "y"]: 30 | _, msg = uma.save_char_head_texture(char_id, True, n)[0] 31 | 32 | print(f"已尝试导出资源, 请查看目录: {msg}") 33 | 34 | do_fin = input("请进行文件修改/替换, 修改完成后请输入 \"Y\" 打包并替换游戏文件。\n" 35 | "若您不想立刻修改, 可以输入其它任意内容退出, 您可以在下次替换时选择\"跳过导出\"\n" 36 | "请输入: ") 37 | if do_fin.strip() in ["Y", "y"]: 38 | uma.replace_char_head_texture(char_id) 39 | print("贴图已修改") 40 | 41 | 42 | if __name__ == "__main__": 43 | while True: 44 | do_type = input("[1] 更换头部模型\n" 45 | "[2] 更换身体模型\n" 46 | "[3] 更换尾巴模型(不建议)\n" 47 | "[4] 更换头部与身体模型\n" 48 | "[5] 修改角色身体贴图\n" 49 | "[6] 更换抽卡开门人物\n" 50 | "[7] 更换技能动画\n" 51 | "[8] 更换G1胜利动作(实验性)\n" 52 | "[9] Live服装解锁\n" 53 | "[10] 清除Live所有模糊效果\n" 54 | "[11] 修改角色头部贴图\n" 55 | "[98] 复原所有修改\n" 56 | "[99] 退出\n" 57 | "请选择您的操作: ") 58 | 59 | if do_type == "1": 60 | print("请输入7位数ID, 例: 1046_01") 61 | uma.replace_head(input("替换ID: "), input("目标ID: ")) 62 | print("替换完成") 63 | 64 | if do_type == "2": 65 | print("请输入7位数ID, 例: 1046_01") 66 | uma.replace_body(input("替换ID: "), input("目标ID: ")) 67 | print("替换完成") 68 | 69 | if do_type == "3": 70 | checkDo = input("注意: 目前无法跨模型更换尾巴, 更换目标不能和原马娘同时出场。\n" 71 | "若您仍要更改, 请输入y继续: ") 72 | if checkDo not in ["y", "Y"]: 73 | continue 74 | print("请输入4位数ID, 例: 1046") 75 | uma.replace_tail(input("替换ID: "), input("目标ID: ")) 76 | print("替换完成") 77 | 78 | if do_type == "4": 79 | print("请输入7位数ID, 例: 1046_01") 80 | inId1 = input("替换ID: ") 81 | inId2 = input("目标ID: ") 82 | uma.replace_head(inId1, inId2) 83 | uma.replace_body(inId1, inId2) 84 | print("替换完成") 85 | 86 | if do_type == "5": 87 | print("请输入7位数ID, 例: 1046_01") 88 | replace_char_body_texture(input("角色7位ID: ")) 89 | 90 | if do_type == "6": 91 | print("请输入普通开门动画的人物服装6位数ID, 例: 100101、100130") 92 | uma.edit_gac_chr_start(input("服装6位数ID: "), '001') 93 | print("请输入理事长开门动画的人物服装的6位数ID, 例: 100101、100130") 94 | uma.edit_gac_chr_start(input("服装6位数ID: "), '002') 95 | print("替换完成") 96 | 97 | if do_type == "7": 98 | print("请输入人物技能6位数ID, 例: 100101、100102") 99 | uma.edit_cutin_skill(input("替换ID: "), input("目标ID: ")) 100 | 101 | if do_type == "8": 102 | checkDo = input("注意: 目前部分胜利动作替换后会出现破音、黑屏等问题。\n" 103 | "若您仍要更改, 请输入y继续: ") 104 | if checkDo not in ["y", "Y"]: 105 | continue 106 | print("请输入胜利动作6位数ID, 例: 100101、100102") 107 | uma.replace_race_result(input("替换ID: "), input("目标ID: ")) 108 | print("替换完成") 109 | 110 | if do_type == "9": 111 | uma.unlock_live_dress() 112 | print("解锁完成") 113 | 114 | if do_type == "10": 115 | edit_live_id = input("Live id (通常为4位, 留空则全部修改): ").strip() 116 | uma.clear_live_blur(edit_live_id) 117 | # print("此功能搭配TLG插件的Live自由镜头功能,使用效果更佳\n" 118 | # "This function is paired with the TLG plug-in's Live free camera for better use\n" 119 | # "Repo: https://github.com/MinamiChiwa/Trainers-Legend-G") 120 | 121 | if do_type == "11": 122 | print("请输入7位数ID, 例: 1046_01") 123 | replace_char_head_texture(input("角色7位ID: ")) 124 | 125 | if do_type == "98": 126 | uma.file_restore() 127 | print("已还原修改") 128 | 129 | if do_type == "99": 130 | break 131 | 132 | input("Press enter to continue...\n") 133 | -------------------------------------------------------------------------------- /umaModelReplace/main.py: -------------------------------------------------------------------------------- 1 | import UnityPy 2 | import sqlite3 3 | import os 4 | import shutil 5 | import typing as t 6 | from PIL import Image 7 | from . import assets_path 8 | 9 | spath = os.path.split(__file__)[0] 10 | BACKUP_PATH = f"{spath}/backup" 11 | EDITED_PATH = f"{spath}/edited" 12 | 13 | 14 | class UmaFileNotFoundError(FileNotFoundError): 15 | pass 16 | 17 | 18 | class UmaReplace: 19 | def __init__(self): 20 | self.init_folders() 21 | profile_path = os.environ.get("UserProfile") 22 | self.base_path = f"{profile_path}/AppData/LocalLow/Cygames/umamusume" 23 | self.conn = sqlite3.connect(f"{self.base_path}/meta") 24 | self.master_conn = sqlite3.connect(f"{self.base_path}/master/master.mdb") 25 | 26 | @staticmethod 27 | def init_folders(): 28 | if not os.path.isdir(BACKUP_PATH): 29 | os.makedirs(BACKUP_PATH) 30 | if not os.path.isdir(EDITED_PATH): 31 | os.makedirs(EDITED_PATH) 32 | 33 | def get_bundle_path(self, bundle_hash: str): 34 | return f"{self.base_path}/dat/{bundle_hash[:2]}/{bundle_hash}" 35 | 36 | def file_backup(self, bundle_hash: str): 37 | if not os.path.isfile(f"{BACKUP_PATH}/{bundle_hash}"): 38 | shutil.copyfile(f"{self.get_bundle_path(bundle_hash)}", f"{BACKUP_PATH}/{bundle_hash}") 39 | 40 | def file_restore(self, hashs: t.Optional[t.List[str]] = None): 41 | """ 42 | 恢复备份 43 | :param hashs: bundle hash 列表, 若为 None, 则恢复备份文件夹内所有文件 44 | """ 45 | if not hashs: 46 | hashs = os.listdir(BACKUP_PATH) 47 | if not isinstance(hashs, list): 48 | raise TypeError(f"hashs must be a list, not {type(hashs)}") 49 | 50 | for i in hashs: 51 | fpath = f"{BACKUP_PATH}/{i}" 52 | if os.path.isfile(fpath): 53 | shutil.copyfile(fpath, self.get_bundle_path(i)) 54 | print(f"restore {i}") 55 | 56 | @staticmethod 57 | def replace_file_path(fname: str, id1: str, id2: str, save_name: t.Optional[str] = None) -> str: 58 | env = UnityPy.load(fname) 59 | 60 | data = None 61 | 62 | for obj in env.objects: 63 | if obj.type.name not in ["Avatar"]: 64 | data = obj.read() 65 | if hasattr(data, "name"): 66 | if id1 in data.name: 67 | # print(obj.type.name, data.name) 68 | if obj.type.name == "MonoBehaviour": 69 | raw = bytes(data.raw_data) 70 | raw = raw.replace(id1.encode("utf8"), id2.encode("utf8")) 71 | data.set_raw_data(raw) 72 | data.save(raw_data=raw) 73 | else: 74 | raw = bytes(data.get_raw_data()) 75 | raw = raw.replace(id1.encode("utf8"), id2.encode("utf8")) 76 | data.set_raw_data(raw) 77 | data.save() 78 | 79 | # if obj.type.name == "Texture2D": 80 | # mono_tree = obj.read_typetree() 81 | # if "m_Name" in mono_tree: 82 | # mono_tree["m_Name"] = mono_tree["m_Name"].replace(id1, id2) 83 | # obj.save_typetree(mono_tree) 84 | 85 | # mono_tree = obj.read_typetree() 86 | # if "m_Name" in mono_tree: 87 | # mono_tree["m_Name"] = mono_tree["m_Name"].replace(id1, id2) 88 | # obj.save_typetree(mono_tree) 89 | 90 | if save_name is None: 91 | save_name = f"{EDITED_PATH}/{os.path.split(fname)[-1]}" 92 | if data is None: 93 | with open(fname, "rb") as f: 94 | data = f.read() 95 | data = data.replace(id1.encode("utf8"), id2.encode("utf8")) 96 | with open(save_name, "wb") as f: 97 | f.write(data) 98 | else: 99 | with open(save_name, "wb") as f: 100 | f.write(env.file.save()) 101 | return save_name 102 | 103 | def replace_texture2d(self, bundle_name: str): 104 | edited_path = f"./editTexture/{bundle_name}" 105 | if not os.path.isdir(edited_path): 106 | raise UmaFileNotFoundError(f"path: {edited_path} not found. Please extract first.") 107 | 108 | file_names = os.listdir(edited_path) 109 | 110 | env = UnityPy.load(self.get_bundle_path(bundle_name)) 111 | for obj in env.objects: 112 | if obj.type.name == "Texture2D": 113 | data = obj.read() 114 | if hasattr(data, "name"): 115 | if f"{data.name}.png" in file_names: 116 | img_data = data.read() 117 | img_data.image = Image.open(f"{edited_path}/{data.name}.png") 118 | data.save() 119 | 120 | save_name = f"{EDITED_PATH}/{os.path.split(bundle_name)[-1]}" 121 | with open(save_name, "wb") as f: 122 | f.write(env.file.save()) 123 | return save_name 124 | 125 | def get_texture_in_bundle(self, bundle_name: str, src_names: t.Optional[t.List[str]], force_replace=False): 126 | base_path = f"./editTexture/{bundle_name}" 127 | if not os.path.isdir(base_path): 128 | os.makedirs(base_path) 129 | 130 | if not force_replace: 131 | if len(os.listdir(base_path)) > 0: 132 | return False, base_path 133 | 134 | env = UnityPy.load(self.get_bundle_path(bundle_name)) 135 | for obj in env.objects: 136 | if obj.type.name == "Texture2D": 137 | data = obj.read() 138 | if hasattr(data, "name"): 139 | if src_names is None or (data.name in src_names): 140 | img_data = data.read() 141 | image: Image = img_data.image 142 | image.save(f"{base_path}/{data.name}.png") 143 | print(f"save {data.name} into {f'{base_path}/{data.name}.png'}") 144 | return True, base_path 145 | 146 | def get_bundle_hash(self, path: str, query_orig_id: t.Optional[str]) -> str: 147 | cursor = self.conn.cursor() 148 | query = cursor.execute("SELECT h FROM a WHERE n=?", [path]).fetchone() 149 | if query is None: 150 | if (query_orig_id is not None) and ("_" in query_orig_id): 151 | query_id, query_sub_id = query_orig_id.split("_") 152 | 153 | if query is None: 154 | new_path = path.replace(query_orig_id, f"{query_id}_%") 155 | query = cursor.execute("SELECT h, n FROM a WHERE n LIKE ?", [new_path]).fetchone() 156 | if query is not None: 157 | print(f"{path} not found, but found {query[1]}") 158 | 159 | if query is None: 160 | raise UmaFileNotFoundError(f"{path} not found!") 161 | 162 | cursor.close() 163 | return query[0] 164 | 165 | def save_char_body_texture(self, char_id: str, force_replace=False): 166 | mtl_bdy_path = assets_path.get_body_mtl_path(char_id) 167 | bundle_hash = self.get_bundle_hash(mtl_bdy_path, None) 168 | return self.get_texture_in_bundle(bundle_hash, assets_path.get_body_mtl_names(char_id), force_replace) 169 | 170 | def save_char_head_texture(self, char_id: str, force_replace=False, on_index=-1): 171 | ret = [] 172 | for n, i in enumerate(assets_path.get_head_mtl_path(char_id)): 173 | if on_index != -1: 174 | if n != on_index: 175 | continue 176 | bundle_hash = self.get_bundle_hash(i, None) 177 | ret.append(self.get_texture_in_bundle(bundle_hash, None, force_replace)) 178 | return ret 179 | 180 | def replace_char_body_texture(self, char_id: str): 181 | mtl_bdy_path = assets_path.get_body_mtl_path(char_id) 182 | bundle_hash = self.get_bundle_hash(mtl_bdy_path, None) 183 | self.file_backup(bundle_hash) 184 | edited_path = self.replace_texture2d(bundle_hash) 185 | # print("save", edited_path) 186 | shutil.copyfile(edited_path, self.get_bundle_path(bundle_hash)) 187 | 188 | def replace_char_head_texture(self, char_id: str): 189 | for mtl_bdy_path in assets_path.get_head_mtl_path(char_id): 190 | bundle_hash = self.get_bundle_hash(mtl_bdy_path, None) 191 | self.file_backup(bundle_hash) 192 | edited_path = self.replace_texture2d(bundle_hash) 193 | # print("save", edited_path) 194 | shutil.copyfile(edited_path, self.get_bundle_path(bundle_hash)) 195 | 196 | def replace_file_ids(self, orig_path: str, new_path: str, id_orig: str, id_new: str): 197 | orig_hash = self.get_bundle_hash(orig_path, id_orig) 198 | new_hash = self.get_bundle_hash(new_path, id_new) 199 | self.file_backup(orig_hash) 200 | edt_bundle_file_path = self.replace_file_path(self.get_bundle_path(new_hash), id_new, id_orig, 201 | f"{EDITED_PATH}/{orig_hash}") 202 | shutil.copyfile(edt_bundle_file_path, self.get_bundle_path(orig_hash)) 203 | 204 | def replace_body(self, id_orig: str, id_new: str): 205 | """ 206 | 替换身体 207 | :param id_orig: 原id, 例: 1046_01 208 | :param id_new: 新id 209 | """ 210 | orig_paths = assets_path.get_body_path(id_orig) 211 | new_paths = assets_path.get_body_path(id_new) 212 | for i in range(len(orig_paths)): 213 | try: 214 | self.replace_file_ids(orig_paths[i], new_paths[i], id_orig, id_new) 215 | except UmaFileNotFoundError as e: 216 | print(e) 217 | 218 | def replace_head(self, id_orig: str, id_new: str): 219 | """ 220 | 替换头部 221 | :param id_orig: 原id, 例: 1046_01 222 | :param id_new: 新id 223 | """ 224 | orig_paths = assets_path.get_head_path(id_orig) 225 | new_paths = assets_path.get_head_path(id_new) 226 | for i in range(len(orig_paths)): 227 | try: 228 | self.replace_file_ids(orig_paths[i], new_paths[i], id_orig, id_new) 229 | except UmaFileNotFoundError as e: 230 | print(e) 231 | 232 | def replace_tail(self, id_orig: str, id_new: str): # 目前无法跨模型更换尾巴, 更换目标不能和原马娘同时出场。 233 | """ 234 | 替换尾巴 235 | :param id_orig: 原id, 例: 1046 236 | :param id_new: 新id 237 | """ 238 | 239 | def check_vaild_path(paths: list): 240 | try: 241 | self.get_bundle_hash(paths[0], None) 242 | except UmaFileNotFoundError: 243 | return False 244 | return True 245 | 246 | orig_paths1 = assets_path.get_tail1_path(id_orig) 247 | orig_paths2 = assets_path.get_tail2_path(id_orig) 248 | 249 | new_paths1 = assets_path.get_tail1_path(id_new) 250 | new_paths2 = assets_path.get_tail2_path(id_new) 251 | 252 | orig_paths = None 253 | new_paths = None 254 | use_id1 = -1 255 | use_id2 = -1 256 | if check_vaild_path(orig_paths1): 257 | orig_paths = orig_paths1 258 | use_id1 = 1 259 | if check_vaild_path(orig_paths2): 260 | orig_paths = orig_paths2 261 | use_id1 = 2 262 | if check_vaild_path(new_paths1): 263 | new_paths = new_paths1 264 | use_id2 = 1 265 | if check_vaild_path(new_paths2): 266 | use_id2 = 2 267 | new_paths = new_paths2 268 | 269 | if (orig_paths is None) or (new_paths is None): 270 | print("tail not found") 271 | return 272 | 273 | if use_id1 != use_id2: 274 | print(f"{id_orig} 模型编号: {use_id1}, {id_new} 模型编号: {use_id2}, 目前无法跨模型修改尾巴。") 275 | return 276 | print("注意, 更换尾巴后, 更换目标不能和原马娘同时出场。") 277 | for i in range(len(orig_paths)): 278 | try: 279 | self.replace_file_ids(orig_paths[i], new_paths[i], id_orig, id_new) 280 | except UmaFileNotFoundError as e: 281 | print(e) 282 | 283 | def edit_gac_chr_start(self, dress_id: str, type: str): 284 | """ 285 | 替换开门人物 286 | :param dress_id: 目标开门id, 例: 100101 287 | :param type: 001骏川手纲,002秋川弥生 288 | """ 289 | 290 | def edit_chr(orig_hash: str, dress_id: str): 291 | env = UnityPy.load(self.get_bundle_path(orig_hash)) 292 | for obj in env.objects: 293 | if obj.type.name == "MonoBehaviour": 294 | if obj.serialized_type.nodes: 295 | tree = obj.read_typetree() 296 | if "runtime_gac_chr_start_00" in tree["m_Name"]: 297 | tree["_characterList"][0]["_characterKeys"]["_selectCharaId"] = int(dress_id[:-2]) 298 | tree["_characterList"][0]["_characterKeys"]["_selectClothId"] = int(dress_id) 299 | obj.save_typetree(tree) 300 | with open(f"{EDITED_PATH}/{orig_hash}", "wb") as f: 301 | f.write(env.file.save()) 302 | 303 | path = assets_path.get_gac_chr_start_path(type) 304 | orig_hash = self.get_bundle_hash(path, None) 305 | self.file_backup(orig_hash) 306 | edit_chr(orig_hash, dress_id) 307 | shutil.copyfile(f"{EDITED_PATH}/{orig_hash}", self.get_bundle_path(orig_hash)) 308 | 309 | def edit_cutin_skill(self, id_orig: str, id_target: str): 310 | """ 311 | 替换技能 312 | :param id_orig: 原id, 例: 100101 313 | :param id_target: 新id 314 | """ 315 | target_path = assets_path.get_cutin_skill_path(id_target) 316 | target_hash = self.get_bundle_hash(target_path, None) 317 | target = UnityPy.load(self.get_bundle_path(target_hash)) 318 | 319 | target_tree = None 320 | target_clothe_id = None 321 | target_cy_spring_name_list = None 322 | 323 | for obj in target.objects: 324 | if obj.type.name == "MonoBehaviour": 325 | if obj.serialized_type.nodes: 326 | tree = obj.read_typetree() 327 | if "runtime_crd1" in tree["m_Name"]: 328 | target_tree = tree 329 | for character in tree["_characterList"]: 330 | target_clothe_id = str(character["_characterKeys"]["_selectClothId"]) 331 | 332 | if target_tree is None: 333 | print("目标无法解析") 334 | return 335 | 336 | for character in target_tree["_characterList"]: 337 | for targetList in character["_characterKeys"]["thisList"]: 338 | if len(targetList["_enableCySpringList"]) > 0: 339 | target_cy_spring_name_list = targetList["_targetCySpringNameList"] 340 | 341 | orig_path = assets_path.get_cutin_skill_path(id_orig) 342 | orig_hash = self.get_bundle_hash(orig_path, None) 343 | self.file_backup(orig_hash) 344 | env = UnityPy.load(self.get_bundle_path(orig_hash)) 345 | 346 | for obj in env.objects: 347 | if obj.type.name == "MonoBehaviour": 348 | if obj.serialized_type.nodes: 349 | tree = obj.read_typetree() 350 | if "runtime_crd1" in tree["m_Name"]: 351 | for character in tree["_characterList"]: 352 | character["_characterKeys"]["_selectCharaId"] = int(target_clothe_id[:-2]) 353 | character["_characterKeys"]["_selectClothId"] = int(target_clothe_id) 354 | character["_characterKeys"]["_selectHeadId"] = 0 355 | for outputList in character["_characterKeys"]["thisList"]: 356 | if len(outputList["_enableCySpringList"]) > 0: 357 | outputList["_enableCySpringList"] = [1] * len(target_cy_spring_name_list) 358 | outputList["_targetCySpringNameList"] = target_cy_spring_name_list 359 | obj.save_typetree(tree) 360 | 361 | with open(f"{EDITED_PATH}/{orig_hash}", "wb") as f: 362 | f.write(env.file.save()) 363 | shutil.copyfile(f"{EDITED_PATH}/{orig_hash}", self.get_bundle_path(orig_hash)) 364 | print("替换完成") 365 | 366 | def replace_race_result(self, id_orig: str, id_new: str): 367 | """ 368 | 替换G1胜利动作 369 | :param id_orig: 原id, 例: 100101 370 | :param id_new: 新id 371 | """ 372 | orig_paths = assets_path.get_crd_race_result_path(id_orig) 373 | new_paths = assets_path.get_crd_race_result_path(id_new) 374 | for i in range(len(orig_paths)): 375 | try: 376 | self.replace_file_ids(orig_paths[i], new_paths[i], id_orig, id_new) 377 | except UmaFileNotFoundError as e: 378 | print(e) 379 | 380 | def unlock_live_dress(self): 381 | 382 | def dict_factory(cursor, row): 383 | d = {} 384 | for idx, col in enumerate(cursor.description): 385 | d[col[0]] = row[idx] 386 | return d 387 | 388 | def get_all_dress_in_table(): 389 | self.master_conn.row_factory = dict_factory 390 | cursor = self.master_conn.cursor() 391 | cursor.execute("SELECT * FROM dress_data") 392 | # fetchall as result 393 | query = cursor.fetchall() 394 | # close connection 395 | cursor.close() 396 | return query 397 | 398 | def get_unique_in_table(): 399 | self.conn.row_factory = dict_factory 400 | cursor = self.conn.cursor() 401 | cursor.execute("SELECT n FROM a WHERE n like '%pfb_chr1____90'") 402 | # fetchall as result 403 | names = cursor.fetchall() 404 | # close connection 405 | cursor.close() 406 | list = [] 407 | for name in names: 408 | list.append(name["n"][-7:-3]) 409 | return list 410 | 411 | def create_data(dress, unique): 412 | dress['id'] = dress['id'] + 89 413 | dress['body_type_sub'] = 90 414 | if str(dress['id'])[:-2] in set(unique): 415 | dress['head_sub_id'] = 90 416 | else: 417 | dress['head_sub_id'] = 0 418 | self.master_conn.row_factory = dict_factory 419 | cursor = self.master_conn.cursor() 420 | cursor.execute("INSERT INTO dress_data VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", 421 | [dress['id'], dress['condition_type'], dress['have_mini'], dress['general_purpose'], 422 | dress['costume_type'], dress['chara_id'], dress['use_gender'], dress['body_shape'], 423 | dress['body_type'], dress['body_type_sub'], dress['body_setting'], dress['use_race'], 424 | dress['use_live'], dress['use_live_theater'], dress['use_home'], dress['use_dress_change'], 425 | dress['is_wet'], dress['is_dirt'], dress['head_sub_id'], dress['use_season'], 426 | dress['dress_color_main'], dress['dress_color_sub'], dress['color_num'], 427 | dress['disp_order'], 428 | dress['tail_model_id'], dress['tail_model_sub_id'], dress['mini_mayu_shader_type'], 429 | dress['start_time'], dress['end_time']]) 430 | self.master_conn.commit() 431 | cursor.close() 432 | 433 | def unlock_data(): 434 | self.master_conn.row_factory = dict_factory 435 | cursor = self.master_conn.cursor() 436 | cursor.execute("UPDATE dress_data SET use_live = 1, use_live_theater = 1") 437 | self.master_conn.commit() 438 | cursor.close() 439 | 440 | dresses = get_all_dress_in_table() 441 | unique = get_unique_in_table() 442 | for dress in dresses: 443 | if 100000 < dress['id'] < 200000 and str(dress['id']).endswith('01'): 444 | create_data(dress, unique) 445 | unlock_data() 446 | 447 | def clear_live_blur(self, edit_id: str): 448 | cursor = self.conn.cursor() 449 | query = cursor.execute("SELECT h, n FROM a WHERE n LIKE 'cutt/cutt_son%/son%_camera'").fetchall() 450 | bundle_names = [i[0] for i in query] 451 | path_names = [i[1] for i in query] 452 | cursor.close() 453 | target_path = f"cutt/cutt_son{edit_id}/son{edit_id}_camera" if edit_id != "" else None 454 | tLen = len(bundle_names) 455 | 456 | for n, bn in enumerate(bundle_names): 457 | path_name = path_names[n] 458 | if target_path is not None: 459 | if path_name != target_path: 460 | continue 461 | print(f"Editing: {path_name} ({n + 1}/{tLen})") 462 | try: 463 | bundle_path = self.get_bundle_path(bn) 464 | if not os.path.isfile(bundle_path): 465 | print(f"File not found: {bundle_path}") 466 | continue 467 | env = UnityPy.load(bundle_path) 468 | for obj in env.objects: 469 | if obj.type.name == "MonoBehaviour": 470 | if not obj.serialized_type.nodes: 471 | continue 472 | tree = obj.read_typetree() 473 | 474 | tree['postEffectDOFKeys']['thisList'] = [tree['postEffectDOFKeys']['thisList'][0]] 475 | dof_set_data = { 476 | "frame": 0, 477 | "attribute": 327680, 478 | "interpolateType": 0, 479 | "curve": { 480 | "m_Curve": [], 481 | "m_PreInfinity": 2, 482 | "m_PostInfinity": 2, 483 | "m_RotationOrder": 4 484 | }, 485 | "easingType": 0, 486 | "forcalSize": 30.0, 487 | "blurSpread": 20.0, 488 | "charactor": 1, 489 | "dofBlurType": 3, 490 | "dofQuality": 1, 491 | "dofForegroundSize": 0.0, 492 | "dofFgBlurSpread": 1.0, 493 | "dofFocalPoint": 1.0, 494 | "dofSmoothness": 1.0, 495 | "BallBlurPowerFactor": 0.0, 496 | "BallBlurBrightnessThreshhold": 0.0, 497 | "BallBlurBrightnessIntensity": 1.0, 498 | "BallBlurSpread": 0.0 499 | } 500 | for k in dof_set_data: 501 | tree['postEffectDOFKeys']['thisList'][0][k] = dof_set_data[k] 502 | 503 | tree['postEffectBloomDiffusionKeys']['thisList'] = [] 504 | tree['radialBlurKeys']['thisList'] = [] 505 | 506 | obj.save_typetree(tree) 507 | 508 | self.file_backup(bn) 509 | with open(bundle_path, 'wb') as f: 510 | f.write(env.file.save()) 511 | 512 | except Exception as e: 513 | print(f"Exception occurred when editing file: {bn}\n{e}") 514 | 515 | print("done.") 516 | 517 | # a = UmaReplace() 518 | # a.file_backup("6NX7AYDRVFFGWKVGA4TDKUX2N63TRWRT") 519 | # a.replace_file_path("5IU2HDJHXDO3ISZSXXOQWXF7VEOG5OCX", "1046", "") 520 | # a.replace_body("1046_02", "1098_00") 521 | 522 | # a.replace_head("1046_02", "1098_00") 523 | # a.replace_tail("1046", "1037") 524 | # a.file_restore() 525 | --------------------------------------------------------------------------------