├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── changelog.md ├── pyproject.toml ├── setup.py └── src └── BCGM_Python ├── __init__.py ├── __main__.py ├── encrypt_decrypt ├── __init__.py ├── decrypt_pack.py └── encrypt_pack.py ├── feature_handler.py ├── file_mods ├── __init__.py ├── enemy_mod.py ├── stage_mod.py └── unit_mod.py ├── files └── version.txt └── helper.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: fieryhenry 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.egg* 3 | *__pychache__* 4 | *.pyc* 5 | /game_files 6 | /decrypted_lists 7 | /encrypted_files 8 | *.c 9 | *.so 10 | /.vscode 11 | .venv 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [fieryhenry] 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/BCGM_Python/files * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Battle Cats Game Modding Tool 2 | 3 | A python game modding tool for the mobile game The Battle Cats that can decrypt, 4 | encrypt, and modify some game files. 5 | 6 | Note: I am no longer actively working on this tool so there may be bugs. 7 | 8 | If you know how to program and want to do more powerful things with the game 9 | then you should use [TBCML](https://github.com/fieryhenry/tbcml) instead. 10 | 11 | Join the [discord server](https://discord.gg/DvmMgvn5ZB) if you want to suggest 12 | new features, report bugs or get help on how to use the modder. (Discord is the 13 | same one as for save editing as I haven't made a modding specific one yet) 14 | 15 | PyPi: 16 | 17 | If you want to support me then consider gifting me some ko-fi here: 18 | 19 | 20 | ## Thanks to 21 | 22 | - EasyMoneko for the original keys for decrypting/encrypting: 23 | 24 | 25 | - Battle Cats Ultimate for what some of the numbers mean in various csvs. 26 | 27 | 28 | - This resource for unit csvs: 29 | 30 | - Vi on discord for enemy csvs 31 | 32 | ## How to use 33 | 34 | 1. Install python (If you haven't already) 35 | 36 | 1. Enter the command: `py -m pip install -U battle-cats-game-modder` into cmd or 37 | another terminal to install the editor. If that doesn't work then use 38 | `python` instead of `py` in the command 39 | 40 | 1. Download the apk you want to edit from somewhere like 41 | [uptodown](https://the-battle-cats.en.uptodown.com/android/download) or 42 | [apkmirror](https://www.apkmirror.com/apk/ponos/the-battle-cats) 43 | 44 | 1. Unpack the apk file for the game using 45 | [Apktool](https://ibotpeaches.github.io/Apktool/) or 46 | [APKToolGui](https://github.com/AndnixSH/APKToolGUI). 47 | 48 | 1. You can then find the .pack and .list files in the assets folder of the 49 | extracted APK. 50 | 51 | 1. Get the .pack and .list files that contain the files you want to edit: 52 | 53 | - Most stats are in DataLocal 54 | 55 | - Most text is in resLocal 56 | 57 | - Sprites are in various Server files 58 | 59 | 1. Then enter the command: `py -m BCGM_Python` to run the tool. If that doesn't 60 | work then use `python` instead of `py` in the command 61 | 62 | 1. Select option to decrypt .pack files 63 | 64 | 1. Select .pack files that you want, they will be in the `assets` folder in the 65 | apk for local files, or `/data/data/jp.co.ponos.battlecatsen/files` on your 66 | device (if rooted) for downloaded server files 67 | 68 | 1. Also decrypt the DownloadLocal pack as you will need it for later 69 | 70 | 1. Once completed the files will be in a `game_files` folder in the folder you 71 | ran the command from 72 | 73 | 1. You can manually edit the data, or use the option in the tool that you want 74 | 75 | 1. Once edited, you should place any modified files in the DownloadLocal pack 76 | folder instead of the original pack folder. This is because the game does not 77 | check if DownloadLocal has been modified, but it does check if the original 78 | pack has been modified. The game also prioritises DownloadLocal over the 79 | original pack, so if you have a file in both, the game will use the one in 80 | DownloadLocal. 81 | 82 | 1. Open the tool again and select the `encrypt` option 83 | 84 | 1. Select the DownloadLocal folder 85 | 86 | 1. Once complete the encrypted .pack and .list files will be in 87 | an`encrypted_files` folder in the folder you ran the command from 88 | 89 | 1. If you are asked if you want to patch the libnative file, say no as this 90 | feature is broken and you do not need to do it if you placed your 91 | files in DownloadLocal 92 | 93 | 1. Then you need to place the encrypted .pack and .list files back into the 94 | assets folder of the apk 95 | 96 | 1. You then need to pack the apk using apktool or apktoolgui 97 | 98 | 1. You then need to sign the apk using apktool or apktoolgui 99 | 100 | 1. You then need to install the apk, you may have to uninstall the game first 101 | before installing the modified apk for the first time 102 | 103 | 1. Open the game and see if it works 104 | 105 | ## Install from source 106 | 107 | 1. Install python 108 | 109 | 1. Install git 110 | 111 | 1. Enter the command: `git clone https://github.com/fieryhenry/BCGM-Python.git` 112 | 113 | 1. Enter the command: `cd BCGM-Python` 114 | 115 | 1. Enter the command: `py -m pip install -e .` to install the tool. If that 116 | doesn't work then use `python` instead of `py` in the command 117 | 118 | 1. Enter the command: `py -m BCGM_Python` to run the tool. If that doesn't work 119 | then use `python` instead of `py` in the command 120 | 121 | 1. If you want to update the tool then enter the command: `git pull` in the 122 | `BCGM-Python` folder 123 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.6] - 2024-03-20 4 | 5 | #### Added 6 | 7 | - Ways to manually select files/folders without tkinter if it's not installed 8 | 9 | #### Fixed 10 | 11 | - Not removing padding from the end of the file after decrypting 12 | 13 | - Modifying files in order to add padding before encryption, this is now done in 14 | memory. This should fix any issues with incorrect file permissions (e.g 15 | desktop.ini) 16 | 17 | - Display error message if no files are in the folder that you are trying to 18 | encrypt 19 | 20 | #### Removed 21 | 22 | - Broken patch libnative option 23 | 24 | ## [1.0.5] - 2023-07-08 25 | 26 | #### Fixed 27 | 28 | - Changed colored to version 1.4.4 because newer versions of colored don't work with the tool 29 | 30 | ## [1.0.4] - 2022-05-24 31 | 32 | #### Fixed 33 | 34 | - Some adb issues with shell=True thanks to [!j0](https://github.com/j0912345) 35 | 36 | ## [1.0.3] - 2022-05-20 37 | 38 | #### Changed 39 | 40 | - Removed wxPython, and for stage mod the tool now uses tk, the reason for why i used it was because in tkinter the file filters automatically have a `*` before them. 41 | 42 | #### Fixed 43 | 44 | - Stage mod crashing 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | with open("src/BCGM_Python/files/version.txt", "r", encoding="utf-8") as fh: 7 | version = fh.read() 8 | 9 | setuptools.setup( 10 | name="battle-cats-game-modder", 11 | version=version, 12 | author="fieryhenry", 13 | description="A battle cats tool for modifying, encrypting, and decrypting game files", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/fieryhenry/BCGM-Python", 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ], 22 | package_dir={"" : "src"}, 23 | packages=setuptools.find_packages(where="src"), 24 | python_requires=">=3.6", 25 | install_requires=[ 26 | "colored==1.4.4", "tk", "alive-progress", "pycryptodomex", "requests" 27 | ], 28 | include_package_data=True 29 | ) -------------------------------------------------------------------------------- /src/BCGM_Python/__init__.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import feature_handler, helper, file_mods, encrypt_decrypt 2 | 3 | root = None 4 | tk = None 5 | fd = None 6 | 7 | try: 8 | import tkinter as tk 9 | from tkinter import filedialog as fd 10 | 11 | root = tk.Tk() 12 | root.withdraw() 13 | except Exception: 14 | print( 15 | "Failed to initialize tkinter. File / folder selection will not be gui based." 16 | ) 17 | -------------------------------------------------------------------------------- /src/BCGM_Python/__main__.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import feature_handler, helper 2 | 3 | 4 | def main(): 5 | helper.check_update() 6 | while True: 7 | feature_handler.menu() 8 | print() 9 | 10 | 11 | try: 12 | main() 13 | except KeyboardInterrupt: 14 | exit() 15 | -------------------------------------------------------------------------------- /src/BCGM_Python/encrypt_decrypt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieryhenry/BCGM-Python/e401cfbd1a57417afc3bdc37de86fb60752e415c/src/BCGM_Python/encrypt_decrypt/__init__.py -------------------------------------------------------------------------------- /src/BCGM_Python/encrypt_decrypt/decrypt_pack.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import helper 2 | import os 3 | from Cryptodome.Cipher import AES 4 | from alive_progress import alive_bar 5 | 6 | output_path = "game_files" 7 | lists_paths = "decrypted_lists" 8 | 9 | 10 | def unpack_list(ls_file): 11 | data = helper.open_file_b(ls_file) 12 | key = helper.md5_str("pack") 13 | cipher = AES.new(key, AES.MODE_ECB) 14 | decrypted_data = cipher.decrypt(data) 15 | decrypted_data = helper.remove_pkcs7_padding(data=decrypted_data) 16 | return decrypted_data 17 | 18 | 19 | def decrypt_pack(chunk_data, jp, pk_name): 20 | aes_mode = AES.MODE_CBC 21 | if jp: 22 | key = bytes.fromhex("d754868de89d717fa9e7b06da45ae9e3") 23 | iv = bytes.fromhex("40b2131a9f388ad4e5002a98118f6128") 24 | else: 25 | key = bytes.fromhex("0ad39e4aeaf55aa717feb1825edef521") 26 | iv = bytes.fromhex("d1d7e708091941d90cdf8aa5f30bb0c2") 27 | 28 | if "server" in pk_name.lower(): 29 | key = helper.md5_str("battlecats") 30 | iv = None 31 | aes_mode = AES.MODE_ECB 32 | if iv: 33 | cipher = AES.new(key, aes_mode, iv) 34 | else: 35 | cipher = AES.new(key, aes_mode) 36 | decrypted_data = cipher.decrypt(chunk_data) 37 | decrypted_data = helper.remove_pkcs7_padding(data=decrypted_data) 38 | return decrypted_data 39 | 40 | 41 | def unpack_pack(pk_file_path, ls_data, jp, base_path): 42 | list_data = ls_data.decode("utf-8") 43 | split_data = helper.parse_csv_file(None, list_data.split("\n"), 3) 44 | 45 | pack_data = helper.open_file_b(pk_file_path) 46 | 47 | with alive_bar(len(split_data)) as bar: 48 | for i in range(len(split_data)): 49 | file = split_data[i] 50 | 51 | name = file[0] 52 | start_offset = int(file[1]) 53 | length = int(file[2]) 54 | 55 | pk_chunk = pack_data[start_offset : start_offset + length] 56 | base_name = os.path.basename(pk_file_path) 57 | if "imagedatalocal" in base_name.lower(): 58 | pk_chunk_decrypted = pk_chunk 59 | else: 60 | pk_chunk_decrypted = decrypt_pack(pk_chunk, jp, base_name) 61 | helper.write_file_b(os.path.join(base_path, name), pk_chunk_decrypted) 62 | bar() 63 | 64 | 65 | def decrypt(): 66 | jp = helper.coloured_text( 67 | "Are you using game version &jp& 10.8 and up? (y/n):", is_input=True 68 | ) 69 | jp = helper.validate_bool(jp) 70 | 71 | pack_paths = helper.select_files( 72 | "Select .pack files", [(".pack files", "*.pack")], False 73 | ) 74 | if not pack_paths: 75 | helper.coloured_text("Please select .pack files", base=helper.red) 76 | return 77 | helper.check_and_create_dir(output_path) 78 | 79 | file_groups = find_lists(pack_paths) 80 | 81 | for i in range(len(file_groups)): 82 | file_group = file_groups[i] 83 | 84 | ls_base_name = os.path.basename(file_group["list"]) 85 | pk_base_name = os.path.basename(file_group["pack"]) 86 | 87 | name = pk_base_name.rstrip(".pack") 88 | path = os.path.join(output_path, name) 89 | 90 | helper.check_and_create_dir(path) 91 | helper.check_and_create_dir(lists_paths) 92 | 93 | ls_data = unpack_list(file_group["list"]) 94 | helper.write_file_b(os.path.join(lists_paths, ls_base_name), ls_data) 95 | 96 | helper.coloured_text( 97 | f"\n&{i+1}&\t\t{name}\t{i+1} / {len(file_groups)}", 98 | base=helper.green, 99 | new=helper.white, 100 | ) 101 | unpack_pack(file_group["pack"], ls_data, jp, path) 102 | helper.coloured_text( 103 | f"\nSuccessfully decrypted all .pack files to: &{output_path}&", 104 | base=helper.green, 105 | new=helper.white, 106 | ) 107 | 108 | 109 | def find_lists(pack_paths): 110 | files = [] 111 | for pack_path in pack_paths: 112 | directory = os.path.dirname(pack_path) 113 | ls_path = os.path.join(directory, pack_path.rstrip(".pack") + ".list") 114 | group = {"pack": pack_path, "list": ls_path} 115 | files.append(group) 116 | return files 117 | -------------------------------------------------------------------------------- /src/BCGM_Python/encrypt_decrypt/encrypt_pack.py: -------------------------------------------------------------------------------- 1 | from alive_progress import alive_bar 2 | from BCGM_Python import helper 3 | import os 4 | import glob 5 | from Cryptodome.Cipher import AES 6 | 7 | initial_dir = "game_files" 8 | output_path = "encrypted_files" 9 | 10 | 11 | def create_list(game_files_dir, pkname): 12 | # return open("decrypted_lists/DataLocal.list", "r").read() 13 | list_of_files = glob.glob(game_files_dir + "/*") 14 | files_with_size = [ 15 | (file_path, os.stat(file_path).st_size) 16 | for file_path in list_of_files 17 | if os.path.isfile(file_path) 18 | ] 19 | files = files_with_size 20 | list_file = f"{len(files_with_size)}\n" 21 | address = 0 22 | for i in range(len(files_with_size)): 23 | file = files_with_size[i] 24 | if "imagedatalocal" not in pkname.lower(): 25 | data = helper.open_file_b(file[0]) 26 | data = helper.add_pkcs7_padding(data) 27 | file = (file[0], len(data)) 28 | files[i] = file 29 | 30 | list_file += f"{os.path.basename(file[0])},{address},{file[1]}\n" 31 | address += file[1] 32 | if address == 0: 33 | helper.coloured_text( 34 | "No files found in the directory, please check the directory and try again", 35 | base=helper.red, 36 | ) 37 | return None 38 | return list_file 39 | 40 | 41 | def create_pack(game_files_dir, ls_data, jp, pk_name): 42 | split_data = helper.parse_csv_file(None, ls_data.split("\n"), 3) 43 | pack_data = [0] * (int(split_data[-1][1]) + int(split_data[-1][2])) 44 | with alive_bar(len(split_data)) as bar: 45 | for i in range(len(split_data)): 46 | file = split_data[i] 47 | 48 | name = file[0] 49 | start_offset = int(file[1]) 50 | 51 | file_data = helper.open_file_b(os.path.join(game_files_dir, name)) 52 | if "imagedatalocal" in pk_name.lower(): 53 | encrypted_data = file_data 54 | else: 55 | encrypted_data = encrypt_file(file_data, jp, pk_name) 56 | encrypted_data = list(encrypted_data) 57 | pack_data = helper.insert_list(pack_data, encrypted_data, start_offset) 58 | bar() 59 | return pack_data 60 | 61 | 62 | def encrypt_file(file_data, jp, pk_name): 63 | file_data = helper.add_pkcs7_padding(file_data) 64 | aes_mode = AES.MODE_CBC 65 | if jp: 66 | key = bytes.fromhex("d754868de89d717fa9e7b06da45ae9e3") 67 | iv = bytes.fromhex("40b2131a9f388ad4e5002a98118f6128") 68 | else: 69 | key = bytes.fromhex("0ad39e4aeaf55aa717feb1825edef521") 70 | iv = bytes.fromhex("d1d7e708091941d90cdf8aa5f30bb0c2") 71 | 72 | if "server" in pk_name.lower(): 73 | key = helper.md5_str("battlecats") 74 | iv = None 75 | aes_mode = AES.MODE_ECB 76 | if iv: 77 | cipher = AES.new(key, aes_mode, iv) 78 | else: 79 | cipher = AES.new(key, aes_mode) 80 | encrypted_data = cipher.encrypt(file_data) 81 | return encrypted_data 82 | 83 | 84 | def encrypt_list(list_data): 85 | list_data = helper.add_pkcs7_padding(list_data) 86 | key = helper.md5_str("pack") 87 | cipher = AES.new(key, AES.MODE_ECB) 88 | encrypted_data = cipher.encrypt(list_data) 89 | return encrypted_data 90 | 91 | 92 | def encrypt(): 93 | jp = helper.coloured_text( 94 | "Are you using game version &jp& 10.8 and up? (y/n):", is_input=True 95 | ) 96 | jp = helper.validate_bool(jp) 97 | 98 | name = input( 99 | "Enter the name of the file to be outputed e.g DataLocal, ImageDataLocal etc:" 100 | ) 101 | 102 | game_files_dir = helper.select_dir("Select a folder of game files", initial_dir) 103 | if not game_files_dir: 104 | helper.coloured_text("Please select a folder of game files", base=helper.red) 105 | return 106 | 107 | helper.check_and_create_dir(output_path) 108 | list_data = create_list(game_files_dir, name) 109 | if list_data is None: 110 | return 111 | 112 | encrypted_data_list = encrypt_list(list_data.encode("utf-8")) 113 | ls_output = os.path.join(output_path, name + ".list") 114 | helper.write_file_b(ls_output, encrypted_data_list) 115 | 116 | pack_data = create_pack(game_files_dir, list_data, jp, name) 117 | pk_output = os.path.join(output_path, name + ".pack") 118 | helper.write_file_b(pk_output, bytes(pack_data)) 119 | print(f"Successfully created .pack and .list files to: {output_path}") 120 | -------------------------------------------------------------------------------- /src/BCGM_Python/feature_handler.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import helper 2 | from BCGM_Python.encrypt_decrypt import decrypt_pack, encrypt_pack 3 | from BCGM_Python.file_mods import unit_mod, enemy_mod, stage_mod 4 | 5 | features = { 6 | "Pack File Encrypting / Decrypting": { 7 | "Decrypt .pack files": decrypt_pack.decrypt, 8 | "Encrypt game files to .pack file": encrypt_pack.encrypt, 9 | }, 10 | "Game File Modding": { 11 | "Edit unit*.csv files (cat stats)": unit_mod.edit_unit, 12 | "Edit t_unit.csv file (enemy stats)": enemy_mod.edit_enemy, 13 | "Edit stage*.csv files (stage data)": stage_mod.edit_stage, 14 | }, 15 | } 16 | 17 | 18 | def display_features(): 19 | helper.create_list(list(features)) 20 | 21 | 22 | def search_dict(dictionary, item, results=[]): 23 | for k, v in dictionary.items(): 24 | if type(v) == dict: 25 | search_dict(v, item, results) 26 | else: 27 | if item.lower() in k.lower().replace(" ", ""): 28 | results.append({"Name": k, "Function": v}) 29 | return results 30 | 31 | 32 | def show_options(user_input, feature_dict): 33 | result_input = helper.validate_int(user_input) 34 | to_search = user_input 35 | 36 | if result_input != None: 37 | if result_input > len(list(feature_dict)): 38 | print(f"Please enter a number between 1 and {len(list(feature_dict))}") 39 | return 40 | name = list(feature_dict)[result_input - 1] 41 | result_data = feature_dict[name] 42 | results = [] 43 | if type(result_data) != dict: 44 | return result_data() 45 | for result in result_data: 46 | results.append({"Name": result, "Function": feature_dict[name][result]}) 47 | else: 48 | to_search = to_search.replace(" ", "") 49 | results = search_dict(feature_dict, to_search, []) 50 | if len(results) == 1: 51 | return results[0]["Function"]() 52 | else: 53 | options = [] 54 | for i in range(len(results)): 55 | options.append(results[i]["Name"]) 56 | if not options: 57 | print(f"Error a feature with name: {user_input} doesn't exist") 58 | return 59 | helper.create_list(options) 60 | user_input = input("Enter an option:\n") 61 | index = helper.validate_int(user_input) 62 | if index != None: 63 | if index > len(results): 64 | print(f"Please enter a number between 1 and {len(results)}") 65 | return 66 | if type(results[index - 1]["Function"]) == dict: 67 | return show_options(user_input, feature_dict[name]) 68 | return results[index - 1]["Function"]() 69 | 70 | 71 | def menu(): 72 | display_features() 73 | user_input = input( 74 | "What do you want to do (some options contain other features within them)\nYou can enter a number to run a feature or a word to search for that feature (e.g entering decrypt will run the Decrypt .pack files feature, and entering csv will show you all the features that edit csv files)\nYou can press enter to see all of the features:\n" 75 | ) 76 | show_options(user_input, features) 77 | -------------------------------------------------------------------------------- /src/BCGM_Python/file_mods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieryhenry/BCGM-Python/e401cfbd1a57417afc3bdc37de86fb60752e415c/src/BCGM_Python/file_mods/__init__.py -------------------------------------------------------------------------------- /src/BCGM_Python/file_mods/enemy_mod.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import helper 2 | from BCGM_Python.file_mods import unit_mod 3 | 4 | values = [ 5 | "HP", 6 | "Knockback amount", 7 | "Movement Speed", 8 | "Attack Power", 9 | "Time between attacks", 10 | "Attack range", 11 | "Money Drop", 12 | "?", 13 | "Width", 14 | "?", 15 | "Red flag", 16 | "Area attack flag", 17 | "Foreswing", 18 | "Floating flag", 19 | "Black flag", 20 | "Metal Flag", 21 | "Traitless Flag", 22 | "Angel Flag", 23 | "Alien Flag", 24 | "Zombie Flag", 25 | "Knock back chance", 26 | "Freeze chance", 27 | "Freeze duration", 28 | "Slow Chance", 29 | "Slow Duration", 30 | "Crit Chance", 31 | "Base Destroyer Flag", 32 | "Wave Chance", 33 | "Wave Level", 34 | "Weaken Chance", 35 | "Weaken Duration", 36 | "Weaken %", 37 | "Strengthen Activation health", 38 | "Strengthen Multiplier", 39 | "Survive Lethal Chance", 40 | "Long distance start", 41 | "Long distance range", 42 | "Wave Immunity", 43 | "?", 44 | "Knockback immunity", 45 | "Freeze immunity", 46 | "Slow immunity", 47 | "Weaken immunity", 48 | "Burrow count", 49 | "Burrow distance", 50 | "Revive count", 51 | "Revive time", 52 | "Percentage of health after revive", 53 | "Witch Flag", 54 | "Is enemy base flag", 55 | "loop?", 56 | "?", 57 | "Self-Destruct Flag (2=self-destruct)", 58 | "?", 59 | "Soul when dead", 60 | "Second attack damage", 61 | "Third attack damage", 62 | "Second attack start frame", 63 | "Third attack start frame", 64 | "Use ability on first hit flag", 65 | "Second attack flag", 66 | "Third attack flag", 67 | "?", 68 | "Some flag for gudetama", 69 | "Barrier HP", 70 | "Warp Chance", 71 | "Warp Duration", 72 | "Warp min range", 73 | "Warp max range", 74 | "Starred Alien Flag (2-4 = god)", 75 | "Some flag for doge sun and the winds", 76 | "Eva angel flag", 77 | "Relic flag", 78 | "Curse Chance", 79 | "Cuse Duration", 80 | "Savage Blow Chance", 81 | "Savage Blow Multiplier", 82 | "Invincibility Chance", 83 | "Invincibility Duration", 84 | "Toxic Chance", 85 | "Toxic Percentage health", 86 | "Surge Chance", 87 | "Surge Min range", 88 | "Surge max range", 89 | "Surge Level", 90 | "Some flag for doge sun, wind enemies and doron", 91 | "Mini wave toggle", 92 | "Shield HP", 93 | "Percentage HP healed when knockbacked", 94 | "Surge Chance when killed", 95 | "Surge min range when killed", 96 | "Surge max range when killed", 97 | "Surge Level when killed", 98 | "Aku flag", 99 | "Baron Trait Flag", 100 | ] 101 | 102 | 103 | def edit_enemy(): 104 | csv_path = helper.select_files( 105 | "Select t_unit.csv file", 106 | [("Enemy stats", "t_unit.csv")], 107 | default=unit_mod.get_initial_dir(), 108 | ) 109 | if not csv_path: 110 | print("Please select an enemy data file") 111 | return 112 | csv_file_data = helper.parse_csv_file( 113 | csv_path, min_length=3, black_list=["//", "\n"] 114 | ) 115 | 116 | ids = helper.get_range_input( 117 | helper.coloured_text( 118 | "Enter enemy ids (Look up enemy release order battle cats to find ids)(You can enter &all& to get all, a range e.g &1&-&50&, or ids separate by spaces e.g &5 4 7&):", 119 | is_input=True, 120 | ), 121 | len(csv_file_data), 122 | ) 123 | 124 | for id in ids: 125 | enemy_data = csv_file_data[id] 126 | 127 | helper.coloured_text(f"Enemy unit &{id}& is selected") 128 | enemy_data = helper.edit_array_user( 129 | values, enemy_data, "Stats", "value", extra_values=enemy_data 130 | ) 131 | csv_file_data[id] = enemy_data 132 | 133 | helper.write_csv_file(csv_path, csv_file_data) 134 | print("Successfully edited csv file") 135 | -------------------------------------------------------------------------------- /src/BCGM_Python/file_mods/stage_mod.py: -------------------------------------------------------------------------------- 1 | import os 2 | from BCGM_Python import helper 3 | from BCGM_Python.file_mods import unit_mod 4 | 5 | stage_infos = [ 6 | "Stage Width", 7 | "Base health", 8 | "Minimum spawn frame", 9 | "Maximum spawn frame", 10 | "Background type", 11 | "Maximum enemies", 12 | ] 13 | 14 | enemy_infos = [ 15 | "Enemy ID", 16 | "Amount to spawn in total", 17 | "First spawn frame", 18 | "Time between spawns in frames min", 19 | "Time between spawns in frames max", 20 | "Spawn when base health has reached %", 21 | "Front z-layer", 22 | "Back z-layer", 23 | "Boss flag", 24 | "Strength multiplier", 25 | ] 26 | 27 | 28 | def edit_stage(): 29 | df_path = os.path.abspath(unit_mod.get_initial_dir()) 30 | csv_path = helper.select_files( 31 | "Select *stage.csv files", [("stage data files", "stage*.csv")], default=df_path 32 | ) 33 | if not csv_path: 34 | print("Please select a stage data file") 35 | return 36 | 37 | csv_file_data = helper.parse_csv_file( 38 | csv_path, min_length=3, black_list=["//", "\n"] 39 | ) 40 | 41 | stage_id = None 42 | 43 | if len(csv_file_data[0]) < 9: 44 | stage_id = helper.ls_to_str(csv_file_data[0]) 45 | csv_file_data = csv_file_data[1:] 46 | 47 | csv_file_data[-1] = csv_file_data[-1][:-1] 48 | 49 | stage_data = helper.int_ls_to_str_ls(csv_file_data[0]) 50 | enemy_slot_data = [] 51 | 52 | for i in range(1, len(csv_file_data)): 53 | enemy_data = helper.int_ls_to_str_ls(csv_file_data[i]) 54 | enemy_slot_data.append(enemy_data) 55 | 56 | options = ["Edit basic stage data", "Edit enemy slots"] 57 | if stage_id != None: 58 | options.append("Edit stage id") 59 | helper.create_list(options) 60 | option = helper.validate_int(input("What do you want to do?:")) 61 | if option == None: 62 | print("Please enter a valid number") 63 | return 64 | if option == 1: 65 | stage_data = helper.edit_array_user( 66 | stage_infos, 67 | stage_data, 68 | "Basic Stage Stats", 69 | "value", 70 | extra_values=stage_data, 71 | all_at_once=False, 72 | ) 73 | elif option == 2: 74 | enemy_ids = [] 75 | for i in range(len(enemy_slot_data)): 76 | slot = enemy_slot_data[i] 77 | if slot[0] != 0: 78 | enemy_ids.append(f"Enemy id {slot[0]}") 79 | 80 | helper.coloured_text( 81 | "Enter an enemy slot id (to add new slots just enter a number larger than the highest displayed slot id):" 82 | ) 83 | ids = helper.selection_list(enemy_ids, "edit", all_at_once=False) 84 | for id in ids: 85 | id = helper.validate_int(id) 86 | if id == None: 87 | print("Please enter a valid number") 88 | return 89 | if id > len(enemy_slot_data): 90 | enemy_slot_data.append([0, 0, 0, 0, 0, 0, 0, 9, 0, 100]) 91 | id = len(enemy_slot_data) 92 | id -= 1 93 | enemy_infos_trimmed = enemy_infos 94 | if len(enemy_slot_data[id]) < len(enemy_infos): 95 | enemy_infos_trimmed = enemy_infos[:-1] 96 | helper.coloured_text(f"Slot: &{id+1}& is currently selected") 97 | enemy_slot_data[id] = helper.edit_array_user( 98 | enemy_infos_trimmed, 99 | enemy_slot_data[id], 100 | "Enemy Slots", 101 | "value", 102 | extra_values=enemy_slot_data[id], 103 | ) 104 | elif option == 3 and stage_id != None: 105 | stage_id = helper.coloured_text( 106 | f"Current stage id: &{stage_id}&\nWhat do you want to set the stage id to?:", 107 | is_input=True, 108 | ) 109 | 110 | new_csv_data = [] 111 | 112 | if stage_id != None: 113 | new_csv_data.append(helper.str_to_ls(stage_id)) 114 | new_csv_data.append(stage_data) 115 | new_csv_data += enemy_slot_data 116 | 117 | helper.write_csv_file(csv_path, new_csv_data) 118 | print("Successfully modified csv file") 119 | -------------------------------------------------------------------------------- /src/BCGM_Python/file_mods/unit_mod.py: -------------------------------------------------------------------------------- 1 | from BCGM_Python import helper 2 | from BCGM_Python.encrypt_decrypt import decrypt_pack 3 | import os 4 | 5 | 6 | def get_initial_dir(): 7 | test_path = os.path.join(decrypt_pack.output_path, "DataLocal") 8 | if os.path.exists(test_path): 9 | return test_path 10 | else: 11 | return decrypt_pack.output_path 12 | 13 | 14 | values = [ 15 | "HP", 16 | "Knockback amount", 17 | "Movement Speed", 18 | "Attack Power", 19 | "Time between attacks", 20 | "Attack Range", 21 | "Base cost", 22 | "Recharge time", 23 | "Hit box position", 24 | "Hit box size", 25 | "Red effective flag", 26 | "Always zero", 27 | "Area attack flag", 28 | "Attack animation", 29 | "Min z layer", 30 | "Max z layer", 31 | "Floating effective flag", 32 | "Black effective flag", 33 | "Metal effective flag", 34 | "White effective flag", 35 | "Angel effective flag", 36 | "Alien effective flag", 37 | "Zombie effective flag", 38 | "Strong against flag", 39 | "Knockback chance", 40 | "Freeze chance", 41 | "Freeze duration", 42 | "Slow chance", 43 | "Slow duration", 44 | "Resistant flag", 45 | "Triple damage flag", 46 | "Critical chance", 47 | "Attack only flag", 48 | "Extra money from enemies flag", 49 | "Base destroyer flag", 50 | "Wave chance", 51 | "Wave attack level", 52 | "Weaken chance", 53 | "Weaken duration", 54 | "Weaken to (decrease attack to percentage left)", 55 | "HP remain strength", 56 | "Boost strength multiplier", 57 | "Survive chance", 58 | "If unit is metal flag", 59 | "Long range start", 60 | "Long range append", 61 | "Immune to wave flag", 62 | "Block wave flag", 63 | "Resist knockbacks flag", 64 | "Resist freeze flag", 65 | "Resist slow flag", 66 | "Resist weaken flag", 67 | "Zombie killer flag", 68 | "Witch killer flag", 69 | "Witch effective flag", 70 | "Not effected by boss wave flag", 71 | "Frames before automatically dying -1 to never die automatically", 72 | "Always -1", 73 | "Death after attack flag", 74 | "Second attack power", 75 | "Third attack power", 76 | "Second attack animation", 77 | "Third attack animation", 78 | "Use ability on first hit flag", 79 | "Second attack flag", 80 | "Third attack flag", 81 | "Spawn animation, -1, 0", 82 | "Soul animation -1, 0, 1, 2, 3, 5, 6, 7", 83 | "Unike spawn animation", 84 | "Gudetama soul animation", 85 | "Barrier break chance", 86 | "Warp Chance", 87 | "Warp Duration", 88 | "Min warp distance", 89 | "Max warp Distance", 90 | "Warp blocker flag", 91 | "Eva Angel Effective", 92 | "Eva angel killer flag", 93 | "Relic effective flag", 94 | "Immune to curse flag", 95 | "Insanely tough flag", 96 | "Insane damage flag", 97 | "Savage blow chance", 98 | "Savage blow level", 99 | "Dodge attack chance", 100 | "Dodge attack duration", 101 | "Surge attack chance", 102 | "Surge attack min range", 103 | "Surge attack max range", 104 | "Surge attack level", 105 | "Toxic immunity flag", 106 | "Surge immunity flag", 107 | "Curse chance", 108 | "Curse duration", 109 | "Unkown", 110 | "Aku shield break chance", 111 | "Aku effective flag", 112 | "Colossus Slayer", 113 | ] 114 | 115 | 116 | def edit_unit(): 117 | csv_path = helper.select_files( 118 | "Select a unit*.csv file", 119 | [("Unit csv files", "unit*.csv")], 120 | default=get_initial_dir(), 121 | ) 122 | if not csv_path: 123 | print("Please select a unit data file") 124 | return 125 | csv_file_data = helper.parse_csv_file( 126 | csv_path, min_length=3, black_list=["//", "\n"] 127 | ) 128 | 129 | forms = ["First Form", "Second Form", "Third Form"] 130 | 131 | choices = helper.selection_list(forms, "edit") 132 | for choice in choices: 133 | 134 | form_id = helper.validate_int(choice) 135 | 136 | if not form_id or form_id > len(forms): 137 | print("Please enter a valid number") 138 | continue 139 | form_id -= 1 140 | form_data = csv_file_data[form_id] 141 | form_data = extend_length(form_data, values) 142 | 143 | helper.coloured_text(f"The cat's &{forms[form_id]}& is selected") 144 | form_data = helper.edit_array_user( 145 | values, form_data, "Stats", "value", extra_values=form_data 146 | ) 147 | 148 | csv_file_data[form_id] = form_data 149 | 150 | helper.write_csv_file(csv_path, csv_file_data) 151 | print("Successfully edited csv file") 152 | 153 | 154 | def extend_length(short_ls, long_ls): 155 | extra = len(long_ls) - len(short_ls) 156 | if extra > 0: 157 | short_ls += [0] * extra 158 | # values that are required so that the unit works properly 159 | short_ls[55] = -1 160 | short_ls[57] = -1 161 | short_ls[63] = 1 162 | short_ls[66] = -1 163 | return short_ls 164 | -------------------------------------------------------------------------------- /src/BCGM_Python/files/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.6 -------------------------------------------------------------------------------- /src/BCGM_Python/helper.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import hashlib 3 | import math 4 | import requests 5 | import os 6 | from colored import fg 7 | 8 | import BCGM_Python 9 | 10 | 11 | def did_tk_fail(): 12 | return BCGM_Python.root is None or BCGM_Python.tk is None or BCGM_Python.fd is None 13 | 14 | 15 | green = "#008000" 16 | dark_yellow = "#d7c32a" 17 | red = "#ff0000" 18 | black = "#000000" 19 | white = "#ffffff" 20 | cyan = "#00ffff" 21 | 22 | 23 | def ls_to_str(list): 24 | val = "" 25 | for item in list: 26 | val += item 27 | return val 28 | 29 | 30 | def str_to_ls(str): 31 | ls = [] 32 | for char in str: 33 | ls.append(char) 34 | return ls 35 | 36 | 37 | def create_list(list, index=True, extra_values=None, offset=None, color=True): 38 | output = "" 39 | for i in range(len(list)): 40 | if index: 41 | output += f"{i+1}. &{list[i]}&" 42 | else: 43 | output += str(list[i]) 44 | if extra_values: 45 | if offset != None: 46 | output += f" &:& {extra_values[i]+offset}" 47 | else: 48 | output += f" &:& {extra_values[i]}" 49 | output += "\n" 50 | output = output.removesuffix("\n") 51 | if not color: 52 | output = output.replace("&", "") 53 | coloured_text(output) 54 | 55 | 56 | def coloured_text(text, base="#ffffff", new=dark_yellow, chr="&", is_input=False): 57 | color_new = fg(new) 58 | color_base = fg(base) 59 | color_reset = fg("#ffffff") 60 | 61 | text_split = text.split(chr) 62 | for i in range(len(text_split)): 63 | if i % 2: 64 | print(f"{color_new}{text_split[i]}{color_base}", end="") 65 | else: 66 | print(f"{color_base}{text_split[i]}{color_base}", end="") 67 | print(color_reset, end="") 68 | if is_input: 69 | return input() 70 | else: 71 | print() 72 | 73 | 74 | def validate_bool(string, true="y"): 75 | string = string.strip(" ") 76 | 77 | if string.lower() == true: 78 | return True 79 | else: 80 | return False 81 | 82 | 83 | def get_real_path(path): 84 | base_path = os.path.dirname(os.path.realpath(__file__)) 85 | path = os.path.join(base_path, path) 86 | return path 87 | 88 | 89 | def open_file_s(path): 90 | return open(path, "r", encoding="utf-8").read() 91 | 92 | 93 | def get_files_path(path): 94 | base_path = get_real_path("files/") 95 | path = os.path.join(base_path, path) 96 | return path 97 | 98 | 99 | def get_version(): 100 | path = get_files_path("version.txt") 101 | version = open_file_s(path) 102 | return version 103 | 104 | 105 | def get_latest_version(): 106 | package_name = "battle-cats-game-modder" 107 | r = requests.get(f"https://pypi.org/pypi/{package_name}/json") 108 | if not r.ok: 109 | coloured_text( 110 | "An error has occurred while checking for a new version", base=red 111 | ) 112 | return 113 | return r.json()["info"]["version"] 114 | 115 | 116 | def check_update(): 117 | installed_version = get_version() 118 | coloured_text( 119 | f"\nYou currently have version &{installed_version}& installed", new=green 120 | ) 121 | latest_version = get_latest_version() 122 | if not latest_version: 123 | return 124 | coloured_text(f"The latest version available is &{latest_version}&\n", new=green) 125 | if installed_version < latest_version: 126 | coloured_text( 127 | f"&A new version is available!&\n&Please run &py -m pip install -U battle-cats-game-modder& to install it&", 128 | base=cyan, 129 | new=green, 130 | ) 131 | coloured_text( 132 | f"&See the changelog here: &https://github.com/fieryhenry/BCGM-Python/blob/master/changelog.md\n", 133 | base=cyan, 134 | new=green, 135 | ) 136 | 137 | 138 | def select_files(title, file_types, single=True, default=""): 139 | if single: 140 | if did_tk_fail(): 141 | path = input(f"Enter the path to {title}: ") 142 | if not os.path.exists(path): 143 | print(f"The path {path} does not exist") 144 | return 145 | else: 146 | path = BCGM_Python.fd.askopenfilename( 147 | title=title, filetypes=file_types, initialdir=default 148 | ) 149 | else: 150 | if did_tk_fail(): 151 | paths = [] 152 | print(f"Enter paths:") 153 | while True: 154 | path = input( 155 | f"Enter a path to {title} (Leave blank to finish inputing): " 156 | ) 157 | if path == "": 158 | break 159 | if not os.path.exists(path): 160 | print(f"The path {path} does not exist") 161 | continue 162 | paths.append(path) 163 | path = paths 164 | else: 165 | path = list( 166 | BCGM_Python.fd.askopenfilenames( 167 | title=title, filetypes=file_types, initialdir=default 168 | ) 169 | ) 170 | return path 171 | 172 | 173 | def get_range_input(input, length=None, min=0): 174 | ids = [] 175 | if length != None and input.lower() == "all": 176 | return range(min, length) 177 | if "-" in input: 178 | content = input.split("-") 179 | first = validate_int(content[0]) 180 | second = validate_int(content[1]) 181 | if first == None or second == None: 182 | print( 183 | f"Please enter 2 valid numbers when making a range : {first} | {second}" 184 | ) 185 | return [] 186 | ids = range(first, second + 1) 187 | else: 188 | content = input.split(" ") 189 | for id in content: 190 | item_id = validate_int(id) 191 | if item_id == None: 192 | print(f"Please enter a valid number : {id}") 193 | continue 194 | ids.append(item_id) 195 | return ids 196 | 197 | 198 | def selection_list( 199 | names, 200 | mode="Edit", 201 | index_flag=True, 202 | include_at_once=False, 203 | extra_values=None, 204 | all_at_once=True, 205 | ): 206 | create_list(names, index_flag, extra_values) 207 | 208 | total = len(names) + 1 209 | if all_at_once: 210 | ids = coloured_text( 211 | f"{total}. &All at once&\nWhat do you want to {mode} (You can enter multiple values separated by spaces to edit multiple at once):", 212 | is_input=True, 213 | ).split(" ") 214 | else: 215 | ids = coloured_text( 216 | f"What do you want to {mode} (You can enter multiple values separated by spaces to edit multiple at once):", 217 | is_input=True, 218 | ).split(" ") 219 | individual = True 220 | if str(total) in ids and all_at_once: 221 | ids = range(1, total) 222 | ids = [format(x, "02d") for x in ids] 223 | individual = False 224 | if include_at_once: 225 | return {"ids": ids, "individual": individual} 226 | return ids 227 | 228 | 229 | def int_ls_to_str_ls(ls): 230 | new_ls = [] 231 | for item in ls: 232 | try: 233 | new_ls.append(int(item)) 234 | except: 235 | continue 236 | return new_ls 237 | 238 | 239 | def edit_array_user( 240 | names, 241 | data, 242 | name, 243 | type_name="level", 244 | range=False, 245 | length=None, 246 | item_name=None, 247 | offset=0, 248 | extra_values=None, 249 | all_at_once=True, 250 | ): 251 | individual = True 252 | if range: 253 | ids = get_range_input( 254 | coloured_text( 255 | f"Enter {name} ids(You can enter &all& to get all, a range e.g &1&-&50&, or ids separate by spaces e.g &5 4 7&):", 256 | is_input=True, 257 | ), 258 | length, 259 | ) 260 | if len(ids) > 1: 261 | individual = coloured_text( 262 | f"Do you want to set the {name} for each {item_name} individually(&1&), or all at once(&2&):", 263 | is_input=True, 264 | ) 265 | else: 266 | ids = selection_list(names, "edit", True, True, extra_values, all_at_once) 267 | individual = ids["individual"] 268 | ids = ids["ids"] 269 | first = True 270 | val = None 271 | for id in ids: 272 | id = validate_int(id) 273 | if id == None: 274 | print("Please enter a number") 275 | continue 276 | id -= 1 277 | if not individual and first: 278 | val = validate_int( 279 | coloured_text( 280 | f"What {type_name} do you want to set your &{name}& to?:", 281 | is_input=True, 282 | ) 283 | ) 284 | if val == None: 285 | print("Please enter a valid number") 286 | continue 287 | first = False 288 | if individual: 289 | val = validate_int( 290 | coloured_text( 291 | f"What &{type_name}& do you want to set your &{names[id]}& to?:", 292 | is_input=True, 293 | ) 294 | ) 295 | if val == None: 296 | print("Please enter a valid number") 297 | continue 298 | data[id] = val - offset 299 | return data 300 | 301 | 302 | def select_dir(title, initial_dir): 303 | if did_tk_fail(): 304 | path = input(f"Enter the path to {title}: ") 305 | if not os.path.exists(path): 306 | print(f"The path {path} does not exist") 307 | return 308 | else: 309 | path = BCGM_Python.fd.askdirectory(title=title, initialdir=initial_dir) 310 | return path 311 | 312 | 313 | def write_csv_file(path, data): 314 | final = "" 315 | for row in data: 316 | for item in row: 317 | final += f"{item}," 318 | final += "\n" 319 | write_file_b(path, final.encode("utf-8")) 320 | 321 | 322 | def find_architecture(so_path): 323 | data = open_file_b(so_path) 324 | 325 | machine = data[18] 326 | if machine == 3: 327 | architecture = "x86" 328 | elif machine == 62: 329 | architecture = "x86_64" 330 | elif machine == 40: 331 | architecture = "armeabi-v7a" 332 | elif machine == 183: 333 | architecture = "arm64-v8a" 334 | 335 | return architecture 336 | 337 | 338 | def find_app_path(game_version): 339 | package_name = f"jp.co.ponos.battlecats{game_version}" 340 | output = subprocess.run( 341 | f"adb shell ls /data/app/ | grep {package_name}", 342 | capture_output=True, 343 | shell=True, 344 | ) 345 | app_path_name = ( 346 | output.stdout.decode("utf-8").split("\n")[0].strip("\n").strip("\r").strip() 347 | ) 348 | return app_path_name 349 | 350 | 351 | def adb_push_lib(game_version, local_path): 352 | print("Pushing libnative-lib.so to the game") 353 | if game_version == "jp": 354 | game_version = "" 355 | 356 | app_path_name = find_app_path(game_version) 357 | 358 | architecture = find_architecture(local_path) 359 | path = f"/data/app/{app_path_name}/lib/{architecture}/libnative-lib.so" 360 | 361 | subprocess.run(f'adb push "{local_path}" "{path}"', shell=True) 362 | 363 | 364 | def parse_csv_file(path, lines=None, min_length=0, black_list=None): 365 | if not lines: 366 | lines = open(path, "r", encoding="utf-8").readlines() 367 | data = [] 368 | for line in lines: 369 | line_data = line.split(",") 370 | if len(line_data) < min_length: 371 | continue 372 | if black_list: 373 | line_data = filter_list(line_data, black_list) 374 | 375 | data.append(line_data) 376 | return data 377 | 378 | 379 | def filter_list(data: list, black_list: list): 380 | trimmed_data = data 381 | for i in range(len(data)): 382 | item = data[i] 383 | for banned in black_list: 384 | if banned in item: 385 | index = item.index(banned) 386 | item = item[:index] 387 | trimmed_data[i] = item 388 | return trimmed_data 389 | 390 | 391 | def md5_str(string, length=8): 392 | return ( 393 | bytearray(hashlib.md5(string.encode("utf-8")).digest()[:length]) 394 | .hex() 395 | .encode("utf-8") 396 | ) 397 | 398 | 399 | def check_and_create_dir(path): 400 | if not os.path.exists(path): 401 | os.mkdir(path) 402 | 403 | 404 | def open_file_b(path): 405 | f = open(path, "rb").read() 406 | return f 407 | 408 | 409 | def add_pkcs7_padding(data, block_size=16): 410 | padding = block_size - len(data) % block_size 411 | data += bytes([padding] * padding) 412 | return data 413 | 414 | 415 | def remove_pkcs7_padding(data): 416 | if not data: 417 | return data 418 | padding = data[-1] 419 | return data[:-padding] 420 | 421 | 422 | def insert_list(main, list, index): 423 | for i in range(len(list)): 424 | main[index + i] = list[i] 425 | return main 426 | 427 | 428 | def write_file_b(path, data): 429 | open(path, "wb").write(data) 430 | 431 | 432 | def validate_int(string): 433 | string = string.strip(" ") 434 | if string.isdigit(): 435 | return int(string) 436 | else: 437 | return None 438 | --------------------------------------------------------------------------------