├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── changelog.md ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.py ├── src └── BCSFE_Python │ ├── __init__.py │ ├── __main__.py │ ├── adb_handler.py │ ├── config_manager.py │ ├── csv_handler.py │ ├── edits │ ├── __init__.py │ ├── basic │ │ ├── __init__.py │ │ ├── basic_items.py │ │ ├── catfruit.py │ │ ├── catseyes.py │ │ ├── ototo_base_mats.py │ │ ├── talent_orbs.py │ │ └── talent_orbs_new.py │ ├── cats │ │ ├── __init__.py │ │ ├── cat_helper.py │ │ ├── cat_id_selector.py │ │ ├── chara_drop.py │ │ ├── clear_cat_guide.py │ │ ├── evolve_cats.py │ │ ├── get_remove_cats.py │ │ ├── talents.py │ │ ├── upgrade_blue.py │ │ └── upgrade_cats.py │ ├── gamototo │ │ ├── __init__.py │ │ ├── fix_gamatoto.py │ │ ├── gamatoto_xp.py │ │ ├── helpers.py │ │ └── ototo_cat_cannon.py │ ├── levels │ │ ├── __init__.py │ │ ├── aku.py │ │ ├── allow_filibuster_clearing.py │ │ ├── behemoth_culling.py │ │ ├── clear_tutorial.py │ │ ├── enigma_stages.py │ │ ├── event_stages.py │ │ ├── gauntlet.py │ │ ├── itf_timed_scores.py │ │ ├── legend_quest.py │ │ ├── main_story.py │ │ ├── outbreaks.py │ │ ├── story_level_id_selector.py │ │ ├── towers.py │ │ ├── treasures.py │ │ ├── uncanny.py │ │ ├── unlock_aku_realm.py │ │ └── zerolegends.py │ ├── other │ │ ├── __init__.py │ │ ├── cat_shrine.py │ │ ├── claim_user_rank_rewards.py │ │ ├── create_new_account.py │ │ ├── fix_elsewhere.py │ │ ├── fix_time_issues.py │ │ ├── get_gold_pass.py │ │ ├── meow_medals.py │ │ ├── missions.py │ │ ├── play_time.py │ │ ├── scheme_item.py │ │ ├── trade_progress.py │ │ ├── unlock_enemy_guide.py │ │ └── unlock_equip_menu.py │ └── save_management │ │ ├── __init__.py │ │ ├── convert.py │ │ ├── load.py │ │ ├── other.py │ │ ├── save.py │ │ └── server_upload.py │ ├── feature_handler.py │ ├── files │ ├── config_path.txt │ ├── enigma_names_en.txt │ ├── enigma_names_jp.txt │ ├── locales │ │ ├── en │ │ │ ├── config.properties │ │ │ ├── item.properties │ │ │ ├── main.properties │ │ │ └── user_input.properties │ │ └── th │ │ │ └── main.properties │ ├── order.json │ └── version.txt │ ├── game_data_getter.py │ ├── helper.py │ ├── item.py │ ├── locale_handler.py │ ├── managed_item.py │ ├── parse_save.py │ ├── patcher.py │ ├── py.typed │ ├── root_handler.py │ ├── serialise_save.py │ ├── server_handler.py │ ├── updater.py │ ├── user_info.py │ └── user_input_handler.py └── tests ├── __init__.py ├── test_edits ├── __init__.py ├── test_basic │ ├── __init__.py │ ├── test_basic.py │ └── test_talent_orbs.py └── test_cats │ ├── __init__.py │ └── test_cat_id_selector.py ├── test_helper.py ├── test_item.py └── test_parse.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 | *SAVE_DATA* 2 | /dist 3 | __pychache__ 4 | *.pyc 5 | /tests/saves/* 6 | .* 7 | *.egg* 8 | *BACKUP_META_DATA* 9 | tests.txt 10 | src/BCSFE_Python/files/item_tracker.json 11 | src/BCSFE_Python/files/game_data 12 | pyrightconfig.json -------------------------------------------------------------------------------- /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/BCSFE_Python/files * 2 | recursive-exclude src/BCSFE_Python/files/game_data * -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | addopts = ["--cov=BCSFE_Python"] 7 | testpaths = ["tests"] 8 | 9 | [tool.mypi] 10 | mypi_path = "src" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colored==1.4.3 2 | python_dateutil==2.8.2 3 | PyYAML==6.0 4 | requests==2.32.2 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | tox==3.24.3 3 | pytest==6.2.5 4 | pytest-cov==2.12.1 5 | mypy==0.910 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup for installing the package.""" 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | with open("src/BCSFE_Python/files/version.txt", "r", encoding="utf-8") as fh: 9 | version = fh.read() 10 | 11 | setuptools.setup( 12 | name="battle-cats-save-editor", 13 | version=version, 14 | author="fieryhenry", 15 | description="A battle cats save file editor", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/fieryhenry/BCSFE-Python", 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | package_dir={"": "src"}, 25 | packages=setuptools.find_packages(where="src"), 26 | python_requires=">=3.9", 27 | install_requires=[ 28 | "colored==1.4.4", 29 | "tk", 30 | "python-dateutil", 31 | "requests", 32 | "pyyaml", 33 | ], 34 | include_package_data=True, 35 | extras_require={ 36 | "testing": [ 37 | "pytest", 38 | "pytest-cov", 39 | ], 40 | }, 41 | package_data={"BCSFE_Python": ["py.typed"]}, 42 | flake8={"max-line-length": 160}, 43 | ) 44 | -------------------------------------------------------------------------------- /src/BCSFE_Python/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | adb_handler, 3 | feature_handler, 4 | helper, 5 | item, 6 | server_handler, 7 | user_input_handler, 8 | edits, 9 | updater, 10 | patcher, 11 | managed_item, 12 | serialise_save, 13 | csv_handler, 14 | parse_save, 15 | config_manager, 16 | root_handler, 17 | user_info, 18 | ) 19 | -------------------------------------------------------------------------------- /src/BCSFE_Python/csv_handler.py: -------------------------------------------------------------------------------- 1 | """Handler for parsing CSV files.""" 2 | 3 | 4 | from typing import Any 5 | 6 | 7 | def remove_pkcs7_padding(data: bytes) -> bytes: 8 | """Remove pkcs7 padding from data.""" 9 | 10 | if len(data) % 16 != 0: 11 | raise Exception("Invalid data length") 12 | 13 | padding_length = data[-1] 14 | if padding_length > 16: 15 | raise Exception("Invalid padding length") 16 | if data[-padding_length:] != bytes([padding_length] * padding_length): 17 | raise Exception("Invalid padding") 18 | 19 | return data[:-padding_length] 20 | 21 | 22 | def remove_comments(data: str) -> str: 23 | """Remove in-line comments from data.""" 24 | 25 | data_ls = data.split("\n") 26 | data_ls = [line.split("//")[0] for line in data_ls] 27 | data_ls = [line.strip() for line in data_ls] 28 | data_ls = [line for line in data_ls if line != ""] 29 | return "\n".join(data_ls) 30 | 31 | 32 | def parse_csv(data: str, delimeter: str = ",") -> list[list[str]]: 33 | """Parse CSV data.""" 34 | 35 | data = remove_comments(data) 36 | data_ls = data.split("\n") 37 | data_ls_ls = [line.split(delimeter) for line in data_ls] 38 | data_ls_ls = remove_empty_items(data_ls_ls) 39 | data_ls_ls = [line for line in data_ls_ls if line != []] 40 | return data_ls_ls 41 | 42 | 43 | def remove_empty_items(data: list[list[Any]]) -> list[list[Any]]: 44 | """Remove empty items from a list of lists.""" 45 | 46 | data_ls: list[list[Any]] = [] 47 | for line in data: 48 | line_ls: list[Any] = [] 49 | for item in line: 50 | if item != "": 51 | line_ls.append(item) 52 | data_ls.append(line_ls) 53 | return data_ls 54 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/__init__.py: -------------------------------------------------------------------------------- 1 | from . import basic, gamototo, levels, other, cats, save_management -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/basic/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | basic_items, 3 | talent_orbs, 4 | catfruit, 5 | catseyes, 6 | talent_orbs_new, 7 | ototo_base_mats, 8 | ) 9 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/basic/catfruit.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ... import item, csv_handler, game_data_getter, helper 4 | 5 | 6 | def get_fruit_names(is_jp: bool) -> list[str]: 7 | """Get the catfruit fruit names""" 8 | 9 | file_data = game_data_getter.get_file_latest("resLocal", "GatyaitemName.csv", is_jp) 10 | if file_data is None: 11 | helper.error_text("Failed to get catfruit names") 12 | return [] 13 | item_names = csv_handler.parse_csv( 14 | file_data.decode("utf-8"), 15 | delimeter=helper.get_text_splitter(is_jp), 16 | ) 17 | file_data = game_data_getter.get_file_latest("DataLocal", "Matatabi.tsv", is_jp) 18 | if file_data is None: 19 | helper.error_text("Failed to get matatabi data") 20 | return [] 21 | fruit_ids = helper.parse_int_list_list( 22 | csv_handler.parse_csv( 23 | file_data.decode("utf-8"), 24 | delimeter="\t", 25 | ) 26 | )[1:] 27 | fruit_names: list[str] = [] 28 | for fruit in fruit_ids: 29 | fruit_names.append(item_names[int(fruit[0])][0]) 30 | return fruit_names 31 | 32 | 33 | def edit_catfruit(save_stats: dict[str, Any]) -> dict[str, Any]: 34 | """Handler for editing catruit""" 35 | 36 | max_cf = 128 37 | if save_stats["game_version"]["Value"] >= 110400: 38 | max_cf = None 39 | 40 | catfruit = item.IntItemGroup.from_lists( 41 | names=get_fruit_names(helper.check_data_is_jp(save_stats)), 42 | values=save_stats["cat_fruit"], 43 | maxes=max_cf, 44 | group_name="Catfruit", 45 | ) 46 | catfruit.edit() 47 | save_stats["cat_fruit"] = catfruit.get_values() 48 | return save_stats 49 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/basic/catseyes.py: -------------------------------------------------------------------------------- 1 | """Module for editing catseyes""" 2 | from typing import Any 3 | 4 | from ... import csv_handler, game_data_getter, helper, item 5 | 6 | 7 | def get_catseye_ids(is_jp: bool) -> list[int]: 8 | """Get the catseye ids""" 9 | 10 | file_data = game_data_getter.get_file_latest("DataLocal", "Gatyaitembuy.csv", is_jp) 11 | if file_data is None: 12 | helper.error_text("Failed to get catseye ids") 13 | return [] 14 | items = helper.parse_int_list_list( 15 | csv_handler.parse_csv( 16 | file_data.decode("utf-8"), 17 | ",", 18 | )[1:] 19 | ) 20 | catseye_ids: dict[int, int] = {} 21 | for item_id, item_data in enumerate(items): 22 | category = item_data[6] 23 | if category == 5: 24 | index = item_data[7] 25 | catseye_ids[index] = item_id 26 | ids = sorted(catseye_ids.items(), key=lambda x: x[0]) 27 | return [id[1] for id in ids] 28 | 29 | 30 | def get_catseye_names(is_jp: bool) -> list[str]: 31 | """Get the catseye names""" 32 | 33 | file_data = game_data_getter.get_file_latest("resLocal", "GatyaitemName.csv", is_jp) 34 | if file_data is None: 35 | helper.error_text("Failed to get catseye names") 36 | return [] 37 | item_names = csv_handler.parse_csv( 38 | file_data.decode("utf-8"), 39 | helper.get_text_splitter(is_jp), 40 | ) 41 | catseye_names: list[str] = [] 42 | for catseye_id in get_catseye_ids(is_jp): 43 | try: 44 | catseye_names.append(item_names[catseye_id][0]) 45 | except IndexError: 46 | helper.error_text(f"Failed to get catseye name for {catseye_id}") 47 | return catseye_names 48 | 49 | 50 | def edit_catseyes(save_stats: dict[str, Any]) -> dict[str, Any]: 51 | """Handler for editing catseyes""" 52 | 53 | catseyes = item.IntItemGroup.from_lists( 54 | names=get_catseye_names(helper.check_data_is_jp(save_stats)), 55 | values=save_stats["catseyes"], 56 | maxes=9999, 57 | group_name="Catseyes", 58 | ) 59 | catseyes.edit() 60 | save_stats["catseyes"] = catseyes.get_values() 61 | return save_stats 62 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/basic/ototo_base_mats.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ... import item, csv_handler, game_data_getter, helper 4 | 5 | 6 | def get_base_mats_names(is_jp: bool) -> list[str]: 7 | """Get the base material names""" 8 | 9 | file_data = game_data_getter.get_file_latest("resLocal", "GatyaitemName.csv", is_jp) 10 | if file_data is None: 11 | helper.error_text("Failed to get base material names") 12 | return [] 13 | item_names = csv_handler.parse_csv( 14 | file_data.decode("utf-8"), 15 | delimeter=helper.get_text_splitter(is_jp), 16 | ) 17 | file_data = game_data_getter.get_file_latest("DataLocal", "Gatyaitembuy.csv", is_jp) 18 | if file_data is None: 19 | helper.error_text("Failed to get gatya item buy data") 20 | return [] 21 | all_items = helper.parse_int_list_list( 22 | csv_handler.parse_csv( 23 | file_data.decode("utf-8"), 24 | ) 25 | )[1:] 26 | base_mat_indexes: dict[int, str] = {} 27 | for item_id, item in enumerate(all_items): 28 | if item[6] == 7: 29 | index = int(item[7]) 30 | base_mat_indexes[index] = item_names[item_id][0] 31 | 32 | base_mats_names: list[str] = [] 33 | for index in sorted(base_mat_indexes): 34 | base_mats_names.append(base_mat_indexes[index]) 35 | 36 | return base_mats_names 37 | 38 | 39 | def edit_base_mats(save_stats: dict[str, Any]) -> dict[str, Any]: 40 | """Handler for editing base materials""" 41 | 42 | base_mats = item.IntItemGroup.from_lists( 43 | names=get_base_mats_names(helper.check_data_is_jp(save_stats)), 44 | values=save_stats["base_materials"], 45 | maxes=9999, 46 | group_name="Base Materials", 47 | ) 48 | base_mats.edit() 49 | save_stats["base_materials"] = base_mats.get_values() 50 | return save_stats 51 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/basic/talent_orbs.py: -------------------------------------------------------------------------------- 1 | """Handler for editing talent orbs""" 2 | 3 | from typing import Any 4 | 5 | from ... import helper, user_input_handler 6 | 7 | 8 | def edit_all_orbs(save_stats: dict[str, Any], orb_list: list[str]) -> dict[str, Any]: 9 | """Handler for editing all talent orbs""" 10 | 11 | val = user_input_handler.colored_input( 12 | "What do you want to set the value of all talent orbs to?:" 13 | ) 14 | val = helper.check_int_max(val) 15 | if val is None: 16 | print("Error please enter a number") 17 | return save_stats 18 | 19 | for orb in orb_list: 20 | try: 21 | orb_id = orb_list.index(orb) 22 | except ValueError: 23 | continue 24 | save_stats["talent_orbs"][orb_id] = val 25 | 26 | helper.colored_text(f"Set all talent orbs to &{val}&") 27 | return save_stats 28 | 29 | 30 | def edit_talent_orbs(save_stats: dict[str, Any]) -> dict[str, Any]: 31 | """Handler for editing talent orbs""" 32 | 33 | orb_list = get_talent_orbs_types() 34 | 35 | talent_orbs = save_stats["talent_orbs"] 36 | print("You have:") 37 | for orb in talent_orbs: 38 | amount = talent_orbs[orb] 39 | text = "orbs" if amount != 1 else "orb" 40 | try: 41 | helper.colored_text(f"&{amount}& {orb_list[orb]} {text}") 42 | except IndexError: 43 | helper.colored_text(f"&{amount}& Unknown {orb} {text}") 44 | 45 | orbs_str = user_input_handler.colored_input( 46 | "Enter the name of the orb that you want. You can enter multiple orb names separated by &spaces& to edit multiple at once or you can enter &all& to select all talent orbs to edit (e.g &angel a massive red d strong black b resistant&):" 47 | ).split(" ") 48 | if orbs_str[0] == "all": 49 | return edit_all_orbs(save_stats, orb_list) 50 | length = len(orbs_str) // 3 51 | orbs_to_set: list[int] = [] 52 | 53 | for i in range(length): 54 | orb_name = " ".join(orbs_str[i * 3 : i * 3 + 3]).lower() 55 | orb_name = orb_name.replace("angle", "angel").title() 56 | try: 57 | orbs_to_set.append(orb_list.index(orb_name)) 58 | except ValueError: 59 | helper.colored_text( 60 | f"Error orb &{orb_name}& does not exist or is not recognized" 61 | ) 62 | 63 | for orb_id in orbs_to_set: 64 | name = orb_list[orb_id] 65 | val = helper.check_int_max( 66 | user_input_handler.colored_input( 67 | f"What do you want to set the value of &{name}& to?:" 68 | ) 69 | ) 70 | if val is None: 71 | print("Error please enter a number") 72 | continue 73 | talent_orbs[orb_id] = val 74 | save_stats["talent_orbs"] = talent_orbs 75 | 76 | return save_stats 77 | 78 | 79 | ATTRIBUTES = [ 80 | "Red", 81 | "Floating", 82 | "Black", 83 | "Metal", 84 | "Angel", 85 | "Alien", 86 | "Zombie", 87 | ] 88 | EFFECTS = [ 89 | "Attack", 90 | "Defense", 91 | "Strong", 92 | "Massive", 93 | "Resistant", 94 | ] 95 | GRADES = [ 96 | "D", 97 | "C", 98 | "B", 99 | "A", 100 | "S", 101 | ] 102 | 103 | 104 | def create_orb_list( 105 | attributes: list[str], effects: list[str], grades: list[str], incl_metal: bool 106 | ) -> list[str]: 107 | """Create a list of all possible talent orbs""" 108 | 109 | orb_list: list[str] = [] 110 | for attribute in attributes: 111 | effects_trim = effects 112 | 113 | if attribute == "Metal" and incl_metal: 114 | effects_trim = [effects[1]] 115 | if attribute == "Metal" and not incl_metal: 116 | effects_trim = [] 117 | 118 | for effect in effects_trim: 119 | for grade in grades: 120 | orb_list.append(f"{attribute} {grade} {effect}") 121 | 122 | return orb_list 123 | 124 | 125 | def create_aku_orbs(effects: list[str], grades: list[str]) -> list[str]: 126 | """Create a list of all possible aku orbs""" 127 | 128 | orb_list: list[str] = [] 129 | for effect in effects: 130 | for grade in grades: 131 | orb_list.append(f"Aku {grade} {effect}") 132 | 133 | return orb_list 134 | 135 | 136 | def get_talent_orbs_types() -> list[str]: 137 | """Get a list of all possible talent orbs""" 138 | 139 | orb_list = create_orb_list(ATTRIBUTES, EFFECTS[0:2], GRADES, True) 140 | orb_list += create_orb_list(ATTRIBUTES, EFFECTS[2:], GRADES, False) 141 | orb_list += create_aku_orbs(EFFECTS, GRADES) 142 | print(orb_list) 143 | return orb_list 144 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | chara_drop, 3 | clear_cat_guide, 4 | upgrade_cats, 5 | evolve_cats, 6 | get_remove_cats, 7 | talents, 8 | upgrade_blue, 9 | cat_id_selector, 10 | cat_helper, 11 | ) 12 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/chara_drop.py: -------------------------------------------------------------------------------- 1 | """Handler for editing character drops""" 2 | 3 | from typing import Any 4 | 5 | from ... import helper, user_input_handler, csv_handler, game_data_getter 6 | from . import cat_id_selector 7 | 8 | 9 | def set_t_ids(save_stats: dict[str, Any]) -> dict[str, Any]: 10 | """handler for editing treasure ids""" 11 | 12 | unit_drops_stats = save_stats["unit_drops"] 13 | data = get_data(helper.check_data_is_jp(save_stats)) 14 | 15 | usr_t_ids = user_input_handler.get_range( 16 | user_input_handler.colored_input( 17 | "Enter treasures ids (Look up item drop cats 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&):" 18 | ), 19 | all_ids=data["t_ids"], 20 | ) 21 | 22 | unit_drops_stats = set_t_ids_val(unit_drops_stats, data, usr_t_ids) 23 | 24 | save_stats["unit_drops"] = unit_drops_stats 25 | return save_stats 26 | 27 | 28 | def set_c_ids(save_stats: dict[str, Any]) -> dict[str, Any]: 29 | """handler for editing cat ids""" 30 | 31 | unit_drops_stats = save_stats["unit_drops"] 32 | data = get_data(helper.check_data_is_jp(save_stats)) 33 | 34 | ids = cat_id_selector.select_cats(save_stats) 35 | 36 | usr_c_ids = helper.check_cat_ids(ids, save_stats) 37 | unit_drops_stats = set_c_ids_val(unit_drops_stats, data, usr_c_ids) 38 | 39 | save_stats["unit_drops"] = unit_drops_stats 40 | return save_stats 41 | 42 | 43 | def get_character_drops(save_stats: dict[str, Any]) -> dict[str, Any]: 44 | """handler for getting character drops""" 45 | 46 | flag_t_ids = ( 47 | user_input_handler.colored_input( 48 | "Do you want to select treasure ids &(1)&, or cat ids? &(2)&:" 49 | ) 50 | == "1" 51 | ) 52 | 53 | if flag_t_ids: 54 | save_stats = set_t_ids(save_stats) 55 | else: 56 | save_stats = set_c_ids(save_stats) 57 | print("Successfully set unit drops") 58 | 59 | return save_stats 60 | 61 | 62 | def get_data(is_jp: bool) -> dict[str, Any]: 63 | """gets all of the cat ids and treasure ids that can be dropped""" 64 | 65 | file_data = game_data_getter.get_file_latest("DataLocal", "drop_chara.csv", is_jp) 66 | if file_data is None: 67 | helper.error_text("Failed to get drop_chara.csv") 68 | return {"t_ids": [], "c_ids": [], "indexes": []} 69 | character_data = helper.parse_int_list_list( 70 | csv_handler.parse_csv(file_data.decode("utf-8"))[1:] 71 | ) 72 | 73 | treasure_ids = helper.copy_first_n(character_data, 0) 74 | indexes = helper.copy_first_n(character_data, 1) 75 | cat_ids = helper.copy_first_n(character_data, 2) 76 | 77 | return {"t_ids": treasure_ids, "indexes": indexes, "c_ids": cat_ids} 78 | 79 | 80 | def set_t_ids_val( 81 | unit_drops_stats: list[int], data: dict[str, Any], user_t_ids: list[int] 82 | ) -> list[int]: 83 | """sets the treasure ids of the unit drops""" 84 | 85 | for t_id in user_t_ids: 86 | if t_id in data["t_ids"]: 87 | index = data["t_ids"].index(t_id) 88 | save_index = data["indexes"][index] 89 | unit_drops_stats[save_index] = 1 90 | return unit_drops_stats 91 | 92 | 93 | def set_c_ids_val( 94 | unit_drops_stats: list[int], data: dict[str, Any], user_t_ids: list[int] 95 | ) -> list[int]: 96 | """sets the cat ids of the unit drops""" 97 | 98 | for c_id in user_t_ids: 99 | if c_id in data["c_ids"]: 100 | index = data["c_ids"].index(c_id) 101 | save_index = data["indexes"][index] 102 | unit_drops_stats[save_index] = 1 103 | return unit_drops_stats 104 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/clear_cat_guide.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing the cat guide""" 2 | 3 | from typing import Any 4 | 5 | from ... import helper 6 | from . import cat_id_selector 7 | 8 | 9 | def collect_cat_guide(save_stats: dict[str, Any]) -> dict[str, Any]: 10 | """Collect cat guide for cats""" 11 | 12 | ids = cat_id_selector.select_cats(save_stats) 13 | 14 | save_stats = cat_guide_ids(save_stats, ids, 1, "collected") 15 | return save_stats 16 | 17 | 18 | def remove_cat_guide(save_stats: dict[str, Any]) -> dict[str, Any]: 19 | """Remove cat guide for cats""" 20 | 21 | ids = cat_id_selector.select_cats(save_stats) 22 | 23 | save_stats = cat_guide_ids(save_stats, ids, 0, "removed") 24 | return save_stats 25 | 26 | 27 | def cat_guide_ids( 28 | save_stats: dict[str, Any], ids: list[int], val: int, string: str 29 | ) -> dict[str, Any]: 30 | """Clear cat guide for a set of cat ids""" 31 | ids = helper.check_cat_ids(ids, save_stats) 32 | cat_guide_collected = save_stats["cat_guide_collected"] 33 | for cat_id in ids: 34 | cat_guide_collected[cat_id] = val 35 | 36 | save_stats["cat_guide_collected"] = cat_guide_collected 37 | print(f"Successfully {string} cat guide") 38 | return save_stats 39 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/evolve_cats.py: -------------------------------------------------------------------------------- 1 | """Handler for evolving cats""" 2 | from typing import Any 3 | 4 | from ... import helper, csv_handler, game_data_getter 5 | from . import cat_id_selector 6 | 7 | 8 | def get_evolve(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Handler for evolving cats""" 10 | 11 | cat_ids = cat_id_selector.select_cats(save_stats) 12 | return evolve_handler_ids( 13 | save_stats=save_stats, 14 | val=2, 15 | string="set", 16 | ids=cat_ids, 17 | forced=False, 18 | ) 19 | 20 | 21 | def get_evolve_forced(save_stats: dict[str, Any]) -> dict[str, Any]: 22 | """Handler for evolving cats without the form check""" 23 | 24 | cat_ids = cat_id_selector.select_cats(save_stats) 25 | return evolve_handler_ids( 26 | save_stats=save_stats, 27 | val=2, 28 | string="set", 29 | ids=cat_ids, 30 | forced=True, 31 | ) 32 | 33 | 34 | def remove_evolve(save_stats: dict[str, Any]) -> dict[str, Any]: 35 | """Handler for de-evolving cats""" 36 | 37 | cat_ids = cat_id_selector.select_cats(save_stats) 38 | return evolve_handler_ids( 39 | save_stats=save_stats, 40 | val=0, 41 | string="removed", 42 | ids=cat_ids, 43 | forced=True, 44 | ) 45 | 46 | 47 | def evolve_handler( 48 | save_stats: dict[str, Any], val: int, string: str, forced: bool 49 | ) -> dict[str, Any]: 50 | """Evolve specific cats""" 51 | 52 | ids = cat_id_selector.select_cats(save_stats) 53 | return evolve_handler_ids(save_stats, val, string, ids, forced) 54 | 55 | 56 | def get_evolve_data(is_jp: bool) -> list[int]: 57 | """Get max form of cats""" 58 | 59 | file_data = game_data_getter.get_file_latest( 60 | "DataLocal", "nyankoPictureBookData.csv", is_jp 61 | ) 62 | if file_data is None: 63 | helper.error_text("Failed to get evolve data") 64 | return [] 65 | data = helper.parse_int_list_list(csv_handler.parse_csv(file_data.decode("utf-8"))) 66 | forms = helper.copy_first_n(data, 2) 67 | forms = helper.offset_list(forms, -1) 68 | return forms 69 | 70 | 71 | def evolve_handler_ids( 72 | save_stats: dict[str, Any], val: int, string: str, ids: list[int], forced: bool 73 | ) -> dict[str, Any]: 74 | """Evolve specific cats by ids""" 75 | ids = helper.check_cat_ids(ids, save_stats) 76 | evolves = save_stats["unlocked_forms"] 77 | if not forced: 78 | form_data = get_evolve_data(helper.check_data_is_jp(save_stats)) 79 | length = min([len(ids), len(form_data)]) 80 | for i in range(length): 81 | try: 82 | evolves[ids[i]] = form_data[ids[i]] 83 | except IndexError: 84 | pass 85 | else: 86 | for cat_id in ids: 87 | evolves[cat_id] = val 88 | for cat_id, (unlocked_flag, current_flag) in enumerate( 89 | zip(evolves, save_stats["current_forms"]) 90 | ): 91 | save_stats["current_forms"][cat_id] = max(unlocked_flag, current_flag) 92 | 93 | flags_evolved = [0 if form == 1 else form for form in evolves] 94 | save_stats["unlocked_forms"] = flags_evolved 95 | 96 | print(f"Successfully {string} true forms of cats") 97 | return save_stats 98 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/get_remove_cats.py: -------------------------------------------------------------------------------- 1 | """Handler to add and remove cats""" 2 | from typing import Any 3 | 4 | from ... import helper 5 | from . import cat_id_selector 6 | 7 | 8 | def get_cat(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Handler to get cats""" 10 | 11 | cat_ids = cat_id_selector.select_cats(save_stats, False) 12 | 13 | save_stats = get_cat_ids( 14 | save_stats=save_stats, 15 | val=1, 16 | string="gave", 17 | ids=cat_ids, 18 | ) 19 | return save_stats 20 | 21 | 22 | def remove_cats(save_stats: dict[str, Any]) -> dict[str, Any]: 23 | """Handler to remove cats""" 24 | 25 | cat_ids = cat_id_selector.select_cats(save_stats, False) 26 | 27 | save_stats = get_cat_ids( 28 | save_stats=save_stats, 29 | val=0, 30 | string="removed", 31 | ids=cat_ids, 32 | ) 33 | return save_stats 34 | 35 | 36 | def get_cat_ids( 37 | save_stats: dict[str, Any], val: int, string: str, ids: list[int] 38 | ) -> dict[str, Any]: 39 | """Get specific cats by ids""" 40 | 41 | ids = helper.check_cat_ids(ids, save_stats) 42 | 43 | cats = save_stats["cats"] 44 | seen_cats = save_stats["gatya_seen_cats"] 45 | 46 | for cat_id in ids: 47 | cats[cat_id] = val 48 | seen_cats[cat_id] = val 49 | 50 | save_stats["cats"] = cats 51 | save_stats["gatya_seen_cats"] = seen_cats 52 | save_stats["menu_unlocks"][2] = 1 53 | print(f"Successfully {string} cats") 54 | return save_stats 55 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/talents.py: -------------------------------------------------------------------------------- 1 | """Handler to edit cat talents""" 2 | from typing import Any, Optional 3 | 4 | from ... import helper, item, csv_handler, game_data_getter, user_input_handler 5 | from . import cat_id_selector 6 | 7 | 8 | def get_talent_data(save_stats: dict[str, Any]) -> Optional[dict[Any, Any]]: 9 | """Get talent data for all cats""" 10 | 11 | file_data = game_data_getter.get_file_latest( 12 | "DataLocal", "SkillAcquisition.csv", helper.check_data_is_jp(save_stats) 13 | ) 14 | if file_data is None: 15 | helper.error_text("Failed to get talent data") 16 | return None 17 | talent_data_raw = helper.parse_int_list_list( 18 | csv_handler.parse_csv( 19 | file_data.decode("utf-8"), 20 | ) 21 | ) 22 | file_data = game_data_getter.get_file_latest( 23 | "resLocal", "SkillDescriptions.csv", helper.check_data_is_jp(save_stats) 24 | ) 25 | if file_data is None: 26 | helper.error_text("Failed to get talent names") 27 | return None 28 | talent_names = csv_handler.parse_csv( 29 | file_data.decode("utf-8"), 30 | helper.get_text_splitter(helper.check_data_is_jp(save_stats)), 31 | ) 32 | columns = helper.int_to_str_ls(talent_data_raw[0]) 33 | new_talent_data: dict[Any, Any] = {} 34 | for j in range(1, len(talent_data_raw)): 35 | data = talent_data_raw[j] 36 | cat_id: int = int(data[0]) 37 | new_talent_data[cat_id] = {} 38 | 39 | for data_i, column in zip(data, columns): 40 | new_talent_data = replace_name( 41 | cat_id=cat_id, 42 | column=column, 43 | data=data_i, 44 | talent_names=talent_names, 45 | new_data=new_talent_data, 46 | ) 47 | return new_talent_data 48 | 49 | 50 | def replace_name( 51 | cat_id: int, 52 | column: str, 53 | data: int, 54 | talent_names: list[list[str]], 55 | new_data: dict[Any, Any], 56 | ) -> dict[str, Any]: 57 | """Replace the text ids with the corresponding names""" 58 | 59 | new_data[cat_id][column] = data 60 | if ( 61 | "textID" in column or "tFxtID_F" in column 62 | ): # ponos made a typo, should be textID_F 63 | new_data[cat_id][column] = talent_names[data][1] 64 | stop_at = "
" 65 | if stop_at in new_data[cat_id][column]: 66 | index = new_data[cat_id][column].index(stop_at) 67 | new_data[cat_id][column] = new_data[cat_id][column][:index] 68 | return new_data 69 | 70 | 71 | def find_order( 72 | cat_talents: list[dict[str, Any]], cat_talent_data: dict[str, Any] 73 | ) -> list[str]: 74 | """Find what talent slot each letter corresponds to""" 75 | 76 | letters = ["A", "B", "C", "D", "E", "F", "G", "H"] 77 | letter_order: list[str] = [] 78 | 79 | for talent in cat_talents: 80 | talent_id = talent["id"] 81 | for letter in letters: 82 | key = f"abilityID_{letter}" 83 | if key not in cat_talent_data: 84 | continue 85 | ability_id = int(cat_talent_data[key]) 86 | if ability_id == talent_id: 87 | letter_order.append(letter) 88 | break 89 | return letter_order 90 | 91 | 92 | def get_cat_talents( 93 | cat_talents: list[dict[str, Any]], cat_talent_data: dict[str, Any] 94 | ) -> dict[Any, Any]: 95 | """Get the name and max value of each talent for a specific cat""" 96 | 97 | data: dict[Any, Any] = {} 98 | letter_order = find_order(cat_talents, cat_talent_data) 99 | for i, letter in enumerate(letter_order): 100 | cat_data = {} 101 | if letter == "F": 102 | text_id_str = "tFxtID_F" # ponos made a typo, should be textID_F 103 | else: 104 | text_id_str = f"textID_{letter}" 105 | cat_data["name"] = cat_talent_data[text_id_str].strip("\n") 106 | cat_data["max"] = int(cat_talent_data[f"MAXLv_{letter}"]) 107 | if cat_data["max"] == 0: 108 | cat_data["max"] = 1 109 | data[i] = cat_data 110 | return data 111 | 112 | 113 | def get_talent_levels( 114 | talent_data: dict[int, Any], talents: dict[int, Any], cat_id: int 115 | ) -> list[int]: 116 | """Get the level of each talent for a specific cat""" 117 | 118 | cat_talent_data = talent_data[cat_id] 119 | cat_talents = talents[cat_id] 120 | cat_talent_data_formatted = get_cat_talents(cat_talents, cat_talent_data) 121 | cat_talents_levels: list[int] = [] 122 | for talent_formatted in cat_talent_data_formatted.values(): 123 | max_val = talent_formatted["max"] 124 | cat_talents_levels.append(max_val) 125 | return cat_talents_levels 126 | 127 | 128 | def max_all_talents(save_stats: dict[str, Any]): 129 | """Max all talents for all cats""" 130 | max_all = ( 131 | user_input_handler.colored_input( 132 | "Do you want to max talents or reset talents? (&m&/&r&):" 133 | ) 134 | == "m" 135 | ) 136 | if not max_all: 137 | return remove_all_talents(save_stats) 138 | talents = save_stats["talents"] 139 | 140 | ids = cat_id_selector.select_cats(save_stats) 141 | 142 | talent_data = get_talent_data(save_stats) 143 | if talent_data is None: 144 | return save_stats 145 | cat_talents_levels: list[int] = [] 146 | for cat_id in ids: 147 | if cat_id not in talents or cat_id not in talent_data: 148 | continue 149 | cat_talents = talents[cat_id] 150 | cat_talents_levels = get_talent_levels(talent_data, talents, cat_id) 151 | for i, cat_talent_level in enumerate(cat_talents_levels): 152 | cat_talents[i]["level"] = cat_talent_level 153 | save_stats["talents"] = talents 154 | 155 | print("Successfully set talents") 156 | return save_stats 157 | 158 | 159 | def remove_all_talents(save_stats: dict[str, Any]) -> dict[str, Any]: 160 | """ 161 | Remove all talents for all cats 162 | 163 | Args: 164 | save_stats (dict[str, Any]): The save stats 165 | 166 | Returns: 167 | dict[str, Any]: The save stats 168 | """ 169 | talents = save_stats["talents"] 170 | 171 | ids = cat_id_selector.select_cats(save_stats) 172 | 173 | talent_data = get_talent_data(save_stats) 174 | if talent_data is None: 175 | return save_stats 176 | cat_talents_levels: list[int] = [] 177 | for cat_id in ids: 178 | if cat_id not in talents or cat_id not in talent_data: 179 | continue 180 | cat_talents = talents[cat_id] 181 | cat_talents_levels = get_talent_levels(talent_data, talents, cat_id) 182 | for i in range(len(cat_talents_levels)): 183 | cat_talents[i]["level"] = 0 184 | save_stats["talents"] = talents 185 | 186 | print("Successfully removed talents") 187 | return save_stats 188 | 189 | 190 | def edit_talents_individual(save_stats: dict[str, Any]) -> dict[str, Any]: 191 | """Handler for editing talents""" 192 | 193 | talents = save_stats["talents"] 194 | ids = cat_id_selector.select_cats(save_stats) 195 | 196 | talent_data = get_talent_data(save_stats) 197 | if talent_data is None: 198 | return save_stats 199 | for cat_id in ids: 200 | cat_talents_levels: list[int] = [] 201 | if cat_id not in talents or cat_id not in talent_data: 202 | # don't spam the user with messages if they selected alot of ids at once 203 | if len(ids) < 20: 204 | helper.colored_text( 205 | f"Error cat &{cat_id}& does not have any talents", 206 | helper.RED, 207 | helper.WHITE, 208 | ) 209 | continue 210 | cat_talent_data = talent_data[cat_id] 211 | cat_talents = talents[cat_id] 212 | cat_talent_data_formatted = get_cat_talents(cat_talents, cat_talent_data) 213 | names: list[str] = [] 214 | maxes: list[int] = [] 215 | for talent_index, cat_talent_formatted in cat_talent_data_formatted.items(): 216 | names.append(cat_talent_formatted["name"]) 217 | cat_talents_levels.append(cat_talents[talent_index]["level"]) 218 | maxes.append(cat_talent_formatted["max"]) 219 | helper.colored_text(f"Cat &{cat_id}& is selected:") 220 | cat_talents_levels_g = item.IntItemGroup.from_lists( 221 | names=names, 222 | values=cat_talents_levels, 223 | maxes=maxes, 224 | group_name="Talents", 225 | ) 226 | cat_talents_levels_g.edit() 227 | cat_talents_levels = cat_talents_levels_g.get_values() 228 | for i, cat_talent_level in enumerate(cat_talents_levels): 229 | cat_talents[i]["level"] = cat_talent_level 230 | 231 | talents[cat_id] = cat_talents 232 | 233 | save_stats["talents"] = talents 234 | 235 | print("Successfully set talents") 236 | return save_stats 237 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/upgrade_blue.py: -------------------------------------------------------------------------------- 1 | """Handler for upgrading the blue upgrades""" 2 | from typing import Any 3 | 4 | from ... import helper, user_input_handler 5 | from . import upgrade_cats 6 | 7 | TYPES = [ 8 | "Power", 9 | "Range", 10 | "Charge", 11 | "Efficiency", 12 | "Wallet", 13 | "Health", 14 | "Research", 15 | "Accounting", 16 | "Study", 17 | "Energy", 18 | ] 19 | 20 | 21 | def upgrade_blue_ids(save_stats: dict[str, Any], ids: list[int]) -> dict[str, Any]: 22 | """Upgrade blue upgrades for a set of ids""" 23 | 24 | save_stats["blue_upgrades"] = upgrade_cats.upgrade_handler( 25 | data=save_stats["blue_upgrades"], 26 | ids=ids, 27 | item_name="upgrade", 28 | save_stats=save_stats, 29 | ) 30 | save_stats = upgrade_cats.set_user_popups(save_stats) 31 | print("Successfully set special skills") 32 | return save_stats 33 | 34 | 35 | def upgrade_blue(save_stats: dict[str, Any]) -> dict[str, Any]: 36 | """Handler for editing blue upgrades""" 37 | 38 | levels = save_stats["blue_upgrades"] 39 | levels_removed = { 40 | "Base": [levels["Base"][0]] + levels["Base"][2:], 41 | "Plus": [levels["Plus"][0]] + levels["Plus"][2:], 42 | } 43 | 44 | levels_removed_formated: list[str] = [] 45 | for base, plus in zip(levels_removed["Base"], levels_removed["Plus"]): 46 | levels_removed_formated.append(f"{base + 1}+{plus}") 47 | 48 | print("What do you want to upgrade:") 49 | helper.colored_list(TYPES, extra_data=levels_removed_formated) 50 | 51 | total = len(TYPES) + 1 52 | ids = user_input_handler.colored_input( 53 | f"{total}. &All at once&\nEnter a number from 1 to {total} (You can enter multiple values separated by spaces to edit multiple at once):" 54 | ).split(" ") 55 | ids = user_input_handler.create_all_list_not_inc(ids, 11) 56 | ids = helper.parse_int_list(ids, -1) 57 | new_ids: list[int] = [] 58 | for blue_id in ids: 59 | if blue_id > 0: 60 | blue_id += 1 61 | new_ids.append(blue_id) 62 | ids = new_ids 63 | save_stats = upgrade_blue_ids(save_stats, ids) 64 | return save_stats 65 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/cats/upgrade_cats.py: -------------------------------------------------------------------------------- 1 | """Handler for cat upgrades""" 2 | from typing import Any, Union 3 | 4 | from ... import helper, user_input_handler 5 | from . import cat_id_selector, cat_helper 6 | 7 | 8 | def set_level_caps(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """ 10 | Set the level caps for the cats 11 | 12 | Args: 13 | save_stats (dict[str, Any]): The save stats 14 | 15 | Returns: 16 | dict[str, Any]: The save stats 17 | """ 18 | 19 | unit_max_data = cat_helper.get_unit_max_levels(helper.is_jp(save_stats)) 20 | rarities = cat_helper.get_rarities(helper.is_jp(save_stats)) 21 | for cat_id in range(len(save_stats["cats"])): 22 | base_level = save_stats["cat_upgrades"]["Base"][cat_id] 23 | if unit_max_data is not None: 24 | max_base_level = cat_helper.get_unit_max_level(unit_max_data, cat_id)[0] 25 | else: 26 | max_base_level = 50000 27 | try: 28 | rarity = rarities[cat_id] 29 | except IndexError: 30 | rarity = 0 31 | max_base_level_ur = cat_helper.get_max_level(save_stats, rarity, cat_id) 32 | level_cap = cat_helper.get_level_cap_increase_amount( 33 | min(base_level, max_base_level, max_base_level_ur) 34 | ) 35 | save_stats["catseye_cat_data"][cat_id] = level_cap 36 | save_stats["catseye_related_data"]["Base"][cat_id] = level_cap + 10 37 | return save_stats 38 | 39 | 40 | def set_user_popups(save_stats: dict[str, Any]) -> dict[str, Any]: 41 | """Set user popups, stops the user rank popups from spamming up the screen""" 42 | 43 | save_stats["user_rank_popups"]["Value"] = 0x7FFFFFFF 44 | return save_stats 45 | 46 | 47 | def get_plus_base(usr_input: str) -> tuple[Union[int, None], Union[int, None]]: 48 | """Get the base and plus level of an input""" 49 | 50 | split = usr_input.split("+") 51 | base = None 52 | plus = None 53 | if split[0]: 54 | base = helper.check_int_max(split[0]) 55 | if len(split) == 2 and split[1]: 56 | plus = helper.check_int_max(split[1]) 57 | if len(split) == 1: 58 | plus = 0 59 | return base, plus 60 | 61 | 62 | def upgrade_cats(save_stats: dict[str, Any]) -> dict[str, Any]: 63 | """Upgrade specific cats""" 64 | 65 | ids = cat_id_selector.select_cats(save_stats) 66 | 67 | return upgrade_cats_ids(save_stats, ids) 68 | 69 | 70 | def upgrade_handler( 71 | data: dict[str, Any], ids: list[int], item_name: str, save_stats: dict[str, Any] 72 | ) -> dict[str, Any]: 73 | """Handler for cat upgrades""" 74 | 75 | ids = helper.check_cat_ids(ids, save_stats) 76 | 77 | base = data["Base"] 78 | plus = data["Plus"] 79 | individual = True 80 | if len(ids) > 1: 81 | individual = user_input_handler.ask_if_individual( 82 | f"upgrades for each {item_name}" 83 | ) 84 | first = True 85 | base_lvl = None 86 | plus_lvl = None 87 | for cat_id in ids: 88 | if not individual and first: 89 | levels = get_plus_base( 90 | user_input_handler.colored_input( 91 | 'Enter the base level followed by a "&+&" then the plus level, e.g 5&+&12. If you want to ignore the base level do &+&12, if you want to ignore the plus level do 5&+&:\n' 92 | ) 93 | ) 94 | base_lvl = levels[0] 95 | plus_lvl = levels[1] 96 | first = False 97 | elif individual: 98 | helper.colored_text( 99 | f"The current upgrade level of id &{cat_id}& is &{base[cat_id]+1}&+&{plus[cat_id]}&" 100 | ) 101 | levels = get_plus_base( 102 | user_input_handler.colored_input( 103 | f'Enter the base level for {item_name}: &{cat_id}& followed by a "&+&" then the plus level, e.g 5&+&12. If you want to ignore the base level do &+&12, if you want to ignore the plus level do 5&+&:\n' 104 | ) 105 | ) 106 | base_lvl = levels[0] 107 | plus_lvl = levels[1] 108 | if base_lvl is not None: 109 | if base_lvl > 0: 110 | base_lvl = helper.clamp(base_lvl, 0, 50000) 111 | base[cat_id] = base_lvl - 1 112 | if plus_lvl is not None: 113 | plus_lvl = helper.clamp(plus_lvl, 0, 50000) 114 | plus[cat_id] = plus_lvl 115 | data["Base"] = base 116 | data["Plus"] = plus 117 | 118 | return data 119 | 120 | 121 | def upgrade_cats_ids(save_stats: dict[str, Any], ids: list[int]) -> dict[str, Any]: 122 | """Upgrade cats by ids""" 123 | 124 | save_stats["cat_upgrades"] = upgrade_handler( 125 | data=save_stats["cat_upgrades"], 126 | ids=ids, 127 | item_name="cat", 128 | save_stats=save_stats, 129 | ) 130 | save_stats = set_user_popups(save_stats) 131 | # save_stats = set_level_caps(save_stats) 132 | print("Successfully set cat levels") 133 | return save_stats 134 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/gamototo/__init__.py: -------------------------------------------------------------------------------- 1 | from . import fix_gamatoto, gamatoto_xp, helpers, ototo_cat_cannon -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/gamototo/fix_gamatoto.py: -------------------------------------------------------------------------------- 1 | """Fix gamatoto from crashing the game""" 2 | from typing import Any 3 | 4 | 5 | def fix_gamatoto(save_stats: dict[str, Any]) -> dict[str, Any]: 6 | """Fix gamatoto from crashing the game""" 7 | 8 | save_stats["gamatoto_skin"]["Value"] = 2 9 | print("Successfully fixed gamatoto from crashing the game") 10 | return save_stats 11 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/gamototo/gamatoto_xp.py: -------------------------------------------------------------------------------- 1 | """Handler for editing gamatoto xp""" 2 | from typing import Any, Optional 3 | 4 | from ... import helper, user_input_handler, item, game_data_getter 5 | 6 | 7 | def get_boundaries(is_jp: bool) -> Optional[list[int]]: 8 | """Get the xp requirements for each level""" 9 | 10 | file_data = game_data_getter.get_file_latest( 11 | "DataLocal", "GamatotoExpedition.csv", is_jp 12 | ) 13 | if file_data is None: 14 | helper.error_text("Failed to get gamatoto xp requirements") 15 | return None 16 | boundaries = file_data.decode("utf-8").splitlines() 17 | previous = 0 18 | xp_requirements: list[int] = [] 19 | previous = 0 20 | for line in boundaries: 21 | requirement = int(line.split(",")[0]) 22 | if previous >= requirement: 23 | break 24 | xp_requirements.append(requirement) 25 | previous = requirement 26 | return xp_requirements 27 | 28 | 29 | def get_level_from_xp(gamatoto_xp: int, is_jp: bool) -> Optional[dict[str, Any]]: 30 | """Get the level from the xp amount""" 31 | 32 | xp_requirements = get_boundaries(is_jp) 33 | if xp_requirements is None: 34 | return None 35 | level = 1 36 | for requirement in xp_requirements: 37 | if gamatoto_xp >= requirement: 38 | level += 1 39 | return { 40 | "level": level, 41 | "max_level": len(xp_requirements), 42 | "max_xp": xp_requirements[-2], 43 | } 44 | 45 | 46 | def get_xp_from_level(level: int, is_jp: bool) -> Optional[int]: 47 | """Get the xp amount from the level""" 48 | 49 | xp_requirements = get_boundaries(is_jp) 50 | if xp_requirements is None: 51 | return None 52 | if level <= 1: 53 | gamatoto_xp = 0 54 | else: 55 | gamatoto_xp = xp_requirements[level - 2] 56 | return gamatoto_xp 57 | 58 | 59 | def edit_gamatoto_xp(save_stats: dict[str, Any]) -> dict[str, Any]: 60 | """Handler for gamatoto xp""" 61 | 62 | gamatoto_xp = save_stats["gamatoto_xp"] 63 | 64 | data = get_level_from_xp(gamatoto_xp["Value"], helper.check_data_is_jp(save_stats)) 65 | if data is None: 66 | return save_stats 67 | level = data["level"] 68 | 69 | helper.colored_text(f"Gamatoto xp: &{gamatoto_xp['Value']}&\nLevel: &{level}&") 70 | raw = ( 71 | user_input_handler.colored_input( 72 | "Do you want to edit raw xp(&1&) or the level(&2&)?:" 73 | ) 74 | == "1" 75 | ) 76 | 77 | if raw: 78 | gam_xp = item.IntItem( 79 | name="Gamatoto XP", 80 | value=item.Int(gamatoto_xp["Value"]), 81 | max_value=None, 82 | ) 83 | gam_xp.edit() 84 | gamatoto_xp["Value"] = gam_xp.get_value() 85 | else: 86 | gam_level = item.IntItem( 87 | name="Gamatoto Level", 88 | value=item.Int(level), 89 | max_value=data["max_level"], 90 | ) 91 | gam_level.edit() 92 | gamatoto_xp["Value"] = get_xp_from_level( 93 | gam_level.get_value(), helper.check_data_is_jp(save_stats) 94 | ) 95 | 96 | save_stats["gamatoto_xp"] = gamatoto_xp 97 | return save_stats 98 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/gamototo/helpers.py: -------------------------------------------------------------------------------- 1 | """Handler for editing gamatoto helpers""" 2 | from typing import Any, Optional 3 | 4 | from ... import item, game_data_getter, helper 5 | 6 | 7 | def get_gamatoto_helpers(is_jp: bool) -> Optional[dict[str, Any]]: 8 | """Get the rarities of all gamatoto helpers""" 9 | 10 | if is_jp: 11 | country_code = "ja" 12 | else: 13 | country_code = "en" 14 | 15 | file_data = game_data_getter.get_file_latest( 16 | "resLocal", f"GamatotoExpedition_Members_name_{country_code}.csv", is_jp 17 | ) 18 | if file_data is None: 19 | helper.error_text("Failed to get gamatoto helper data") 20 | return None 21 | data = file_data.decode("utf-8").splitlines()[1:] 22 | helpers: dict[str, Any] = {} 23 | for line in data: 24 | line_data = line.split(helper.get_text_splitter(is_jp)) 25 | if len(line_data) < 5: 26 | break 27 | 28 | helper_id = line_data[0] 29 | rarity = int(line_data[1]) 30 | type_str = line_data[4] 31 | helpers[helper_id] = {"Rarity_id": rarity, "Rarity_name": type_str} 32 | return helpers 33 | 34 | 35 | def generate_helpers(user_input: list[int], helper_data: dict[str, Any]) -> list[int]: 36 | """Generate unique helpers from amounts of each""" 37 | 38 | final_helpers: list[int] = [] 39 | values = list(helper_data.values()) 40 | for i, usr_input in enumerate(user_input): 41 | for j, value in enumerate(values): 42 | if value["Rarity_id"] == i: 43 | final_helpers += list(range(j + 1, j + 1 + usr_input)) 44 | break 45 | return final_helpers 46 | 47 | 48 | def get_helper_rarities(helper_data: dict[str, Any]) -> list[str]: 49 | """Get the rarities of all gamatoto helpers""" 50 | 51 | rarities: list[str] = [] 52 | for helpers in helper_data.values(): 53 | if helpers["Rarity_name"] not in rarities: 54 | rarities.append(helpers["Rarity_name"]) 55 | return rarities 56 | 57 | 58 | def get_helpers(helpers: list[int], helper_data: dict[str, Any]) -> dict[str, Any]: 59 | """Get the amount of each type of helper""" 60 | 61 | current_helpers: dict[int, Any] = {} 62 | 63 | rarities = get_helper_rarities(helper_data) 64 | helper_count: dict[str, int] = {} 65 | for rarity in rarities: 66 | helper_count[rarity] = 0 67 | 68 | for helper_id in helpers: 69 | if helper_id == 0xFFFFFFFF: 70 | break 71 | current_helpers[helper_id] = helper_data[str(helper_id)] 72 | helper_count[current_helpers[helper_id]["Rarity_name"]] += 1 73 | return helper_count 74 | 75 | 76 | def add_empty_helper_slots(helpers: list[int], final_helpers: list[int]): 77 | """Add empty helper slots to the end of the list""" 78 | 79 | empty_slots = len(helpers) - len(final_helpers) 80 | if empty_slots > 0: 81 | final_helpers += [0xFFFFFFFF] * empty_slots 82 | return final_helpers 83 | 84 | 85 | def edit_helpers(save_stats: dict[str, Any]) -> dict[str, Any]: 86 | """Handler for gamatoto helpers""" 87 | 88 | helpers = save_stats["helpers"] 89 | helper_data = get_gamatoto_helpers(helper.check_data_is_jp(save_stats)) 90 | if helper_data is None: 91 | return save_stats 92 | 93 | helper_count = get_helpers(helpers, helper_data) 94 | 95 | helpers_counts_input = item.IntItemGroup.from_lists( 96 | names=list(helper_count.keys()), 97 | values=list(helper_count.values()), 98 | group_name="Gamatoto Helpers", 99 | maxes=10, 100 | ) 101 | helpers_counts_input.edit() 102 | final_helpers = generate_helpers(helpers_counts_input.get_values(), helper_data) 103 | helpers = add_empty_helper_slots(helpers, final_helpers) 104 | save_stats["helpers"] = helpers 105 | return save_stats 106 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/gamototo/ototo_cat_cannon.py: -------------------------------------------------------------------------------- 1 | """Handler for editing the ototo cat cannon""" 2 | from typing import Any, Optional 3 | 4 | from ... import user_input_handler, game_data_getter, csv_handler, helper 5 | 6 | 7 | def get_canon_types(is_jp: bool) -> Optional[list[str]]: 8 | """Get the cannon types""" 9 | 10 | file_data = game_data_getter.get_file_latest( 11 | "resLocal", "CastleRecipeDescriptions.csv", is_jp 12 | ) 13 | if file_data is None: 14 | helper.error_text("Could not find CastleRecipeDescriptions.csv") 15 | return None 16 | data = csv_handler.parse_csv( 17 | file_data.decode("utf-8"), 18 | delimeter=helper.get_text_splitter(is_jp), 19 | ) 20 | types: list[str] = [] 21 | for cannon in data: 22 | types.append(cannon[1]) 23 | return types 24 | 25 | 26 | def get_cannon_maxes(is_jp: bool) -> Optional[dict[int, dict[int, int]]]: 27 | """Get the cannon maxes""" 28 | file_data = game_data_getter.get_file_latest( 29 | "DataLocal", "CastleRecipeUnlock.csv", is_jp 30 | ) 31 | if file_data is None: 32 | helper.error_text("Could not find CastleRecipeUnlock.csv") 33 | return None 34 | data = helper.parse_int_list_list(csv_handler.parse_csv(file_data.decode("utf-8"))) 35 | maxes: dict[int, dict[int, int]] = {} 36 | for cannon in data: 37 | cannon_id = cannon[0] 38 | part = cannon[1] 39 | max_val = cannon[-1] 40 | if cannon_id not in maxes: 41 | maxes[cannon_id] = {} 42 | if part not in maxes[cannon_id]: 43 | maxes[cannon_id][part] = max_val 44 | elif max_val > maxes[cannon_id][part]: 45 | maxes[cannon_id][part] = max_val 46 | return maxes 47 | 48 | 49 | def get_part_id_from_str(part: str) -> int: 50 | """Get the part id from the string""" 51 | if part == "effect": 52 | return 0 53 | if part == "foundation": 54 | return 1 55 | if part == "style": 56 | return 2 57 | return 0 58 | 59 | 60 | def get_max( 61 | part: str, cannon_id: int, cannon_maxes: dict[int, dict[int, int]] 62 | ) -> Optional[int]: 63 | """Get the max value for the part""" 64 | part_id = get_part_id_from_str(part) 65 | if cannon_id not in cannon_maxes: 66 | return None 67 | if part_id not in cannon_maxes[cannon_id]: 68 | return None 69 | return cannon_maxes[cannon_id][part_id] 70 | 71 | 72 | def edit_cat_cannon(save_stats: dict[str, Any]) -> dict[str, Any]: 73 | """Handler for ototo cat cannon upgrades""" 74 | 75 | cannons: dict[int, dict[str, Any]] = save_stats["ototo_cannon"] 76 | 77 | cannon_types = get_canon_types(helper.check_data_is_jp(save_stats)) 78 | if cannon_types is None: 79 | return save_stats 80 | 81 | cannon_maxes = get_cannon_maxes(helper.check_data_is_jp(save_stats)) 82 | if cannon_maxes is None: 83 | return save_stats 84 | 85 | extra_data: list[str] = [] 86 | for i in range(len(cannon_types)): 87 | levels = cannons[i]["levels"] 88 | if i == 0: 89 | extra_data.append(f"Level: &{levels['effect']+1}&") 90 | continue 91 | string = "" 92 | for level_str, level in levels.items(): 93 | part_id = get_part_id_from_str(level_str) 94 | if part_id == 0: 95 | level += 1 96 | string += f"{level_str.title()}: &{level}&, " 97 | string = string[:-2] 98 | string += f" (Development: &{cannons[i]['unlock_flag']}&)" 99 | extra_data.append(string) 100 | 101 | cannon_ids = user_input_handler.select_not_inc(cannon_types, extra_data=extra_data) 102 | if len(cannon_ids) > 1: 103 | individual = user_input_handler.ask_if_individual("Cat Cannons") 104 | else: 105 | individual = True 106 | 107 | if individual: 108 | for cannon_id in cannon_ids: 109 | helper.colored_text( 110 | f"Editing &{cannon_types[cannon_id]}&", helper.WHITE, helper.GREEN 111 | ) 112 | cannon = cannons[cannon_id] 113 | if cannon_id == 0: 114 | max = get_max("effect", cannon_id, cannon_maxes) 115 | if max is None: 116 | continue 117 | level = user_input_handler.get_int( 118 | f"Enter the level to upgrade the base to (Max &{max}&):", 119 | ) 120 | level -= 1 121 | level = helper.config_clamp(level, 0, max) 122 | cannon["levels"]["effect"] = level 123 | continue 124 | develop_stage = ( 125 | user_input_handler.colored_input( 126 | "Do you want to set the stage of development (&1&) or the upgrade level? (&2&):", 127 | ) 128 | == "1" 129 | ) 130 | if develop_stage: 131 | unlock_flag = user_input_handler.get_int( 132 | "Enter the stage of development (1=effect, 2=foundation, 3=style):", 133 | ) 134 | unlock_flag = helper.config_clamp(unlock_flag, 0, 3) 135 | cannon["unlock_flag"] = unlock_flag 136 | if unlock_flag != 3: 137 | for level_str in cannon["levels"]: 138 | cannon["levels"][level_str] = 0 139 | else: 140 | cannon["upgrade_flag"] = 3 141 | for level_str in cannon["levels"]: 142 | max = get_max(level_str, cannon_id, cannon_maxes) 143 | if max is None: 144 | continue 145 | part_id = get_part_id_from_str(level_str) 146 | level = user_input_handler.get_int( 147 | f"Enter the level to upgrade &{level_str}& to (Max &{max}&):" 148 | ) 149 | if part_id == 0: 150 | level -= 1 151 | level = helper.config_clamp(level, 0, max) 152 | cannon["levels"][level_str] = level 153 | else: 154 | develop_stage = ( 155 | user_input_handler.colored_input( 156 | "Do you want to set the stage of development (&1&) or the upgrade level? (&2&):", 157 | ) 158 | == "1" 159 | ) 160 | if develop_stage: 161 | unlock_value = user_input_handler.get_int( 162 | "Enter the stage of development (1=effect, 2=foundation, 3=style):", 163 | ) 164 | unlock_value = helper.config_clamp(unlock_value, 0, 3) 165 | for cannon_id in cannon_ids: 166 | cannons[cannon_id]["unlock_flag"] = unlock_value 167 | if unlock_value != 3: 168 | for level_str in cannons[cannon_id]["levels"]: 169 | cannons[cannon_id]["levels"][level_str] = 0 170 | else: 171 | max_max = 0 172 | for cannon_id in cannon_ids: 173 | for part_id in cannon_maxes[cannon_id]: 174 | if cannon_maxes[cannon_id][part_id] > max_max: 175 | max_max = cannon_maxes[cannon_id][part_id] 176 | 177 | level = user_input_handler.get_int( 178 | f"Enter the level to upgrade everything to (Max &{max_max}&):", 179 | ) 180 | for cannon_id in cannon_ids: 181 | cannon = cannons[cannon_id] 182 | cannon["upgrade_flag"] = 3 183 | for level_str in cannon["levels"]: 184 | max = get_max(level_str, cannon_id, cannon_maxes) 185 | if max is None: 186 | continue 187 | part_id = get_part_id_from_str(level_str) 188 | level_ = level 189 | if part_id == 0: 190 | level_ -= 1 191 | if cannon_id == 0: 192 | part_id = 0 193 | level_ = helper.config_clamp( 194 | level_, 0, cannon_maxes[cannon_id][part_id] 195 | ) 196 | cannon["levels"][level_str] = level_ 197 | 198 | return save_stats 199 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | aku, 3 | clear_tutorial, 4 | enigma_stages, 5 | event_stages, 6 | gauntlet, 7 | itf_timed_scores, 8 | main_story, 9 | outbreaks, 10 | story_level_id_selector, 11 | towers, 12 | treasures, 13 | uncanny, 14 | allow_filibuster_clearing, 15 | unlock_aku_realm, 16 | behemoth_culling, 17 | legend_quest, 18 | zerolegends, 19 | ) 20 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/aku.py: -------------------------------------------------------------------------------- 1 | """Handler for editing the aku realm""" 2 | from typing import Any 3 | from . import story_level_id_selector, unlock_aku_realm 4 | from ... import helper 5 | 6 | 7 | def edit_aku(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Clear whole chapters""" 9 | 10 | save_stats = unlock_aku_realm.unlock_aku_realm(save_stats) 11 | 12 | aku = save_stats["aku"]["Value"] 13 | 14 | progress = story_level_id_selector.select_level_progress(None, total=49) 15 | progress = helper.clamp(progress, 0, 49) 16 | if progress == 0: 17 | aku["clear_progress"][0][0] = 0 18 | aku["clear_amount"][0][0] = [0] * len(aku["clear_amount"][0][0]) 19 | 20 | else: 21 | stage_index = progress - 1 22 | aku["clear_progress"][0][0] = min(progress, 48) 23 | aku["clear_amount"][0][0][stage_index] = 1 24 | for i in range(stage_index): 25 | aku["clear_amount"][0][0][i] = 1 26 | for i in range(stage_index + 1, 49): 27 | aku["clear_amount"][0][0][i] = 0 28 | 29 | save_stats["aku"]["Value"] = aku 30 | helper.colored_text("Successfully set aku stages") 31 | return save_stats 32 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/allow_filibuster_clearing.py: -------------------------------------------------------------------------------- 1 | """Handler for allowing the filibuster stage to reappear in the game.""" 2 | import random 3 | from typing import Any 4 | from ... import helper 5 | 6 | 7 | def allow_filibuster_clearing(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Allow filibuster clearing in the game.""" 9 | 10 | save_stats["filibuster_stage_enabled"]["Value"] = 1 11 | save_stats["filibuster_stage_id"]["Value"] = random.randint(0, 47) 12 | 13 | helper.colored_text("Filibuster stage has successfully been re-enabled.") 14 | 15 | return save_stats 16 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/behemoth_culling.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing behemoth culling stages""" 2 | from typing import Any 3 | 4 | from . import event_stages 5 | from ... import user_input_handler 6 | 7 | 8 | def edit_behemoth_culling(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Handler for clearing behemoth culling stages""" 10 | 11 | stage_data = save_stats["behemoth_culling"] 12 | lengths = stage_data["Lengths"] 13 | 14 | ids = [] 15 | ids = user_input_handler.get_range( 16 | user_input_handler.colored_input( 17 | "Enter behemoth culling ids (e.g &0& = &Hidden Forest of Gapra&, &1& = &Ashvini Desert&) (You can enter &all& to get all, a range e.g 1-49, or ids separate by spaces e.g &5 4 7&):" 18 | ), 19 | lengths["total"], 20 | ) 21 | save_stats["behemoth_culling"] = event_stages.stage_handler(stage_data, ids, 0) 22 | return save_stats 23 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/clear_tutorial.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing the tutorial""" 2 | from typing import Any 3 | 4 | 5 | def clear_tutorial(save_stats: dict[str, Any]) -> dict[str, Any]: 6 | """Handler for clearing the tutorial""" 7 | 8 | save_stats["tutorial_cleared"]["Value"] = 1 9 | if save_stats["story_chapters"]["Chapter Progress"][0] == 0: 10 | save_stats["story_chapters"]["Chapter Progress"][0] = 1 11 | save_stats["story_chapters"]["Times Cleared"][0][0] = 1 12 | print("Successfully cleared the tutorial") 13 | 14 | return save_stats 15 | 16 | 17 | def is_tutorial_cleared(save_stats: dict[str, Any]) -> bool: 18 | """Check if the tutorial is cleared""" 19 | 20 | return save_stats["tutorial_cleared"]["Value"] == 1 21 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/enigma_stages.py: -------------------------------------------------------------------------------- 1 | """Handler for editing enigma stages""" 2 | import time 3 | from typing import Any 4 | 5 | from ... import helper, user_input_handler 6 | 7 | 8 | def edit_enigma_stages(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """ 10 | Edit enigma stages 11 | 12 | Args: 13 | save_stats (dict[str, Any]): Save stats 14 | 15 | Returns: 16 | dict[str, Any]: save stats 17 | """ 18 | enigma_stages = save_stats["enigma_data"] 19 | 20 | if helper.check_data_is_jp(save_stats): 21 | file_name = "enigma_names_jp.txt" 22 | else: 23 | file_name = "enigma_names_en.txt" 24 | enigma_names = helper.read_file_string(helper.get_file(file_name)).splitlines() 25 | ids = user_input_handler.select_not_inc(enigma_names, "select") 26 | level = 3 27 | 28 | base_level = 25000 29 | for enigma_id in ids: 30 | abs_id = enigma_id + base_level 31 | data: dict[str, int] = {} 32 | data["level"] = level 33 | data["stage_id"] = abs_id 34 | data["decoding_status"] = 2 35 | data["start_time"] = int(time.time()) 36 | enigma_stages["stages"].append(data) 37 | 38 | save_stats["enigma_data"] = enigma_stages 39 | 40 | print("Successfully edited enigma stages") 41 | return save_stats 42 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/event_stages.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing event stages""" 2 | from typing import Any 3 | 4 | from ... import user_input_handler, helper 5 | from ...edits.other import meow_medals 6 | 7 | 8 | def set_stage_data( 9 | stage_data_edit: dict[str, Any], 10 | stage_id: int, 11 | stars: int, 12 | lengths: dict[str, int], 13 | unlock_next: bool, 14 | ) -> dict[str, Any]: 15 | """Set the stage data for a stage""" 16 | 17 | if stage_id >= len(stage_data_edit["Value"]["clear_progress"]): 18 | return stage_data_edit 19 | stage_data_edit = set_clear_progress(stage_data_edit, stage_id, stars, lengths) 20 | if unlock_next and stage_id + 1 < len(stage_data_edit["Value"]["clear_progress"]): 21 | stage_data_edit = set_unlock_next(stage_data_edit, stage_id, stars, lengths) 22 | stage_data_edit = set_clear_amount(stage_data_edit, stage_id, stars, lengths) 23 | return stage_data_edit 24 | 25 | 26 | def set_clear_progress( 27 | stage_data: dict[str, Any], stage_id: int, stars: int, lengths: dict[str, int] 28 | ) -> dict[str, Any]: 29 | """Set the clear progress for a stage""" 30 | 31 | stage_data["Value"]["clear_progress"][stage_id] = ([lengths["stages"]] * stars) + ( 32 | [0] * (lengths["stars"] - stars) 33 | ) 34 | return stage_data 35 | 36 | 37 | def set_unlock_next( 38 | stage_data: dict[str, Any], stage_id: int, stars: int, lengths: dict[str, int] 39 | ) -> dict[str, Any]: 40 | """Set the unlock next for a stage""" 41 | 42 | stage_data["Value"]["unlock_next"][stage_id + 1] = ( 43 | [lengths["stars"] - 1] * stars 44 | ) + ([0] * (lengths["stars"] - stars)) 45 | return stage_data 46 | 47 | 48 | def set_clear_amount( 49 | stage_data: dict[str, Any], stage_id: int, stars: int, lengths: dict[str, int] 50 | ) -> dict[str, Any]: 51 | """Set the clear amount for a stage""" 52 | 53 | stage_data["Value"]["clear_amount"][stage_id] = ( 54 | [[1] * lengths["stages"]] * stars 55 | ) + ([[0] * lengths["stages"]] * (lengths["stars"] - stars)) 56 | return stage_data 57 | 58 | 59 | def set_medals( 60 | stage_stats: dict[str, Any], 61 | medal_stats: dict[str, Any], 62 | valid_range: tuple[int, int], 63 | offset: int, 64 | is_jp: bool, 65 | ) -> tuple[dict[str, Any], dict[str, Any]]: 66 | """Set the medals for completed stages""" 67 | 68 | medal_data = meow_medals.get_medal_data(is_jp) 69 | if medal_data is None: 70 | return stage_stats, medal_stats 71 | 72 | unlock_next = stage_stats["Value"]["unlock_next"] 73 | 74 | for medal in medal_data.stages: 75 | if not medal.maps: 76 | continue 77 | completed = True 78 | for map_id in medal.maps: 79 | star = medal.star 80 | if map_id < 0: 81 | continue 82 | if map_id < valid_range[0] or map_id > valid_range[1]: 83 | completed = False 84 | break 85 | map_id += offset 86 | next_chapter = unlock_next[map_id + 1] 87 | if star is None: 88 | star = 0 89 | if next_chapter[star] == 0: 90 | completed = False 91 | break 92 | if completed: 93 | if medal.medal_id not in medal_stats["medal_data_1"]: 94 | medal_stats["medal_data_1"].append(medal.medal_id) 95 | medal_stats["medal_data_2"][medal.medal_id] = 1 96 | return stage_stats, medal_stats 97 | 98 | 99 | def stage_handler( 100 | stage_data: dict[str, Any], ids: list[int], offset: int, unlock_next: bool = True 101 | ) -> dict[str, Any]: 102 | """Clear stages from a set of ids""" 103 | 104 | lengths = stage_data["Lengths"] 105 | 106 | individual = True 107 | if len(ids) > 1: 108 | individual = user_input_handler.ask_if_individual( 109 | "stars / crowns for each stage" 110 | ) 111 | first = True 112 | stars = 0 113 | stage_data_edit = stage_data 114 | for stage_id in ids: 115 | if not individual and first: 116 | stars = helper.check_int( 117 | user_input_handler.colored_input( 118 | f"Enter the number of stars/crowns (max &{lengths['stars']}&):" 119 | ) 120 | ) 121 | if stars is None: 122 | print("Please enter a valid number") 123 | break 124 | stars = helper.clamp(stars, 0, lengths["stars"]) 125 | first = False 126 | elif individual: 127 | stars = helper.check_int( 128 | user_input_handler.colored_input( 129 | f"Enter the number of stars/crowns for subchapter &{stage_id}& (max &{lengths['stars']}&):" 130 | ) 131 | ) 132 | if stars is None: 133 | print("Please enter a valid number") 134 | break 135 | stars = helper.clamp(stars, 0, lengths["stars"]) 136 | stage_id += offset 137 | stage_data_edit = stage_data 138 | stage_data_edit = set_stage_data( 139 | stage_data_edit, stage_id, stars, lengths, unlock_next 140 | ) 141 | 142 | print("Successfully set subchapters") 143 | 144 | return stage_data_edit 145 | 146 | 147 | def stories_of_legend(save_stats: dict[str, Any]) -> dict[str, Any]: 148 | """Handler for clearing stories of legend""" 149 | 150 | stage_data = save_stats["event_stages"] 151 | 152 | ids = user_input_handler.get_range( 153 | user_input_handler.colored_input( 154 | "Enter subchapter ids (e.g &1& = legend begins, &2& = passion land)(You can enter &all& to get all, a range e.g &1&-&49&, or ids separate by spaces e.g &5 4 7&):" 155 | ), 156 | 50, 157 | ) 158 | offset = -1 159 | save_stats["event_stages"] = stage_handler(stage_data, ids, offset) 160 | save_stats["event_stages"], save_stats["medals"] = set_medals( 161 | save_stats["event_stages"], 162 | save_stats["medals"], 163 | (0, 50), 164 | 0, 165 | helper.check_data_is_jp(save_stats), 166 | ) 167 | return save_stats 168 | 169 | 170 | def event_stages(save_stats: dict[str, Any]) -> dict[str, Any]: 171 | """Handler for clearing event stages""" 172 | 173 | stage_data = save_stats["event_stages"] 174 | lengths = stage_data["Lengths"] 175 | 176 | ids = user_input_handler.get_range( 177 | user_input_handler.colored_input( 178 | "Enter subchapter ids (Look up &Event 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&):" 179 | ), 180 | lengths["total"] - 400, 181 | ) 182 | offset = 400 183 | save_stats["event_stages"] = stage_handler(stage_data, ids, offset) 184 | save_stats["event_stages"], save_stats["medals"] = set_medals( 185 | save_stats["event_stages"], 186 | save_stats["medals"], 187 | (0, len(save_stats["event_stages"]["Value"]["unlock_next"])), 188 | -600, 189 | helper.check_data_is_jp(save_stats), 190 | ) 191 | return save_stats 192 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/gauntlet.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing gauntlets""" 2 | from typing import Any 3 | 4 | from . import event_stages 5 | from ... import user_input_handler, helper 6 | from ..other import meow_medals 7 | 8 | 9 | def edit_gauntlet(save_stats: dict[str, Any]) -> dict[str, Any]: 10 | """Handler for clearing gauntlets""" 11 | 12 | stage_data = save_stats["gauntlets"] 13 | lengths = stage_data["Lengths"] 14 | 15 | ids = [] 16 | ids = user_input_handler.get_range( 17 | user_input_handler.colored_input( 18 | "Enter gauntlet ids (Look up &Event Release Order battle cats& and scroll past the &events& to find &gauntlet& ids) (You can enter &all& to get all, a range e.g 1-49, or ids separate by spaces e.g &5 4 7&):" 19 | ), 20 | lengths["total"], 21 | ) 22 | save_stats["gauntlets"] = event_stages.stage_handler(stage_data, ids, 0) 23 | base_addr = meow_medals.BaseMapIds.GAUNTLETS.value 24 | save_stats["gauntlets"], save_stats["medals"] = event_stages.set_medals( 25 | save_stats["gauntlets"], 26 | save_stats["medals"], 27 | (base_addr, base_addr + len(save_stats["gauntlets"]["Value"]["unlock_next"])), 28 | -base_addr, 29 | helper.check_data_is_jp(save_stats), 30 | ) 31 | return save_stats 32 | 33 | 34 | def edit_collab_gauntlet(save_stats: dict[str, Any]) -> dict[str, Any]: 35 | """Handler for clearing collab gauntlets""" 36 | 37 | stage_data = save_stats["collab_gauntlets"] 38 | lengths = stage_data["Lengths"] 39 | 40 | ids = [] 41 | ids = user_input_handler.get_range( 42 | user_input_handler.colored_input( 43 | "Enter collab gauntlet ids (Look up &Event Release Order battle cats& and scroll past the &events& and past &gauntlet& to find &Collaboration Gauntlet& ids) (You can enter &all& to get all, a range e.g 1-49, or ids separate by spaces e.g &5 4 7&):" 44 | ), 45 | lengths["total"], 46 | ) 47 | save_stats["collab_gauntlets"] = event_stages.stage_handler(stage_data, ids, 0) 48 | return save_stats 49 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/itf_timed_scores.py: -------------------------------------------------------------------------------- 1 | """Handler for setting into the future timed scores""" 2 | from typing import Any, Union 3 | 4 | from ... import item 5 | from . import main_story 6 | 7 | 8 | def set_scores( 9 | scores: list[list[int]], usr_scores: list[Union[int, None]] 10 | ) -> list[list[int]]: 11 | """Set the scores for a stage""" 12 | for i, usr_score in enumerate(usr_scores): 13 | if usr_score is None: 14 | continue 15 | scores[i] = ([usr_score] * 48) + ([0] * 3) 16 | return scores 17 | 18 | 19 | def timed_scores(save_stats: dict[str, Any]) -> dict[str, Any]: 20 | """Handler for setting into the future timed scores""" 21 | 22 | scores = save_stats["itf_timed_scores"] 23 | print("Enter the scores for the following chapters:") 24 | usr_scores = item.IntItemGroup.from_lists( 25 | names=main_story.CHAPTERS[3:6], 26 | values=None, 27 | maxes=9999, 28 | group_name="Into The Future Timed Scores", 29 | ) 30 | usr_scores.edit() 31 | save_stats["itf_timed_scores"] = set_scores(scores, usr_scores.get_values_none()) 32 | 33 | print("Successfully set timed scores") 34 | return save_stats 35 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/legend_quest.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing the legend quest""" 2 | from typing import Any 3 | from . import story_level_id_selector 4 | from ... import helper 5 | 6 | 7 | def edit_legend_quest(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Handler for clearing the legend quest""" 9 | stage_data = save_stats["legend_quest"] 10 | lengths = stage_data["Lengths"] 11 | total = lengths["stages"] 12 | progress = story_level_id_selector.select_level_progress(None, total=total, examples=["LEVEL 1", "LEVEL 2"]) 13 | 14 | if progress == 0: 15 | stage_data["Value"]["clear_progress"][0][0] = 0 16 | stage_data["Value"]["clear_amount"][0][0] = [0] * len(stage_data["Value"]["clear_amount"][0][0]) 17 | else: 18 | stage_id = progress - 1 19 | stage_data["Value"]["clear_progress"][0][0] = min(progress, total) 20 | stage_data["Value"]["clear_amount"][0][0][stage_id] = 1 21 | stage_data["Value"]["tries"][0][0][stage_id] = 1 22 | for i in range(stage_id): 23 | stage_data["Value"]["clear_amount"][0][0][i] = 1 24 | stage_data["Value"]["tries"][0][0][i] = 1 25 | for i in range(stage_id + 1, total): 26 | stage_data["Value"]["clear_amount"][0][0][i] = 0 27 | stage_data["Value"]["tries"][0][0][i] = 0 28 | 29 | if stage_data["Value"]["clear_progress"][0][0] == total: 30 | stage_data["Value"]["unlock_next"][0][1] = lengths["stars"] - 1 31 | 32 | save_stats["legend_quest"] = stage_data 33 | helper.colored_text("Successfully set legend quest stages") 34 | return save_stats -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/main_story.py: -------------------------------------------------------------------------------- 1 | """Handler for clearing main story chapters""" 2 | from typing import Any 3 | 4 | 5 | from ... import helper 6 | from . import story_level_id_selector 7 | 8 | CHAPTERS = [ 9 | "Empire of Cats 1", 10 | "Empire of Cats 2", 11 | "Empire of Cats 3", 12 | "Into the Future 1", 13 | "Into the Future 2", 14 | "Into the Future 3", 15 | "Cats of the Cosmos 1", 16 | "Cats of the Cosmos 2", 17 | "Cats of the Cosmos 3", 18 | ] 19 | 20 | 21 | def clear_specific_level_ids( 22 | save_stats: dict[str, Any], chapter_id: int, progress: int 23 | ) -> dict[str, Any]: 24 | """Clear specific levels in a chapter""" 25 | story_chapters = save_stats["story_chapters"] 26 | progress = helper.clamp(progress, 0, 48) 27 | if progress == 0: 28 | story_chapters["Chapter Progress"][chapter_id] = 0 29 | story_chapters["Times Cleared"][chapter_id] = [0] * 51 30 | else: 31 | stage_index = progress - 1 32 | story_chapters["Chapter Progress"][chapter_id] = progress 33 | # set all levels before the one being cleared to 1 34 | story_chapters["Times Cleared"][chapter_id][stage_index] = 1 35 | for i in range(stage_index): 36 | story_chapters["Times Cleared"][chapter_id][i] = 1 37 | # set all levels after the one being cleared to 0 38 | for i in range(stage_index + 1, get_total_stages(save_stats, chapter_id) + 3): 39 | story_chapters["Times Cleared"][chapter_id][i] = 0 40 | 41 | save_stats["story_chapters"] = story_chapters 42 | return save_stats 43 | 44 | 45 | def has_cleared_chapter(save_stats: dict[str, Any], chapter_id: int) -> bool: 46 | """ 47 | Check if a chapter has been cleared 48 | 49 | Args: 50 | save_stats (dict[str, Any]): Save stats 51 | chapter_id (int): Chapter ID 52 | 53 | Returns: 54 | bool: True if cleared, False if not 55 | """ 56 | chapter_id = format_story_id(chapter_id) 57 | 58 | return save_stats["story_chapters"]["Chapter Progress"][chapter_id] >= 48 59 | 60 | 61 | def format_story_ids(ids: list[int]) -> list[int]: 62 | """For some reason there is a gap after EoC 3. This adds that""" 63 | 64 | formatted_ids: list[int] = [] 65 | for story_id in ids: 66 | formatted_ids.append(format_story_id(story_id)) 67 | return formatted_ids 68 | 69 | 70 | def format_story_id(chapter_id: int) -> int: 71 | """For some reason there is a gap after EoC 3. This adds that""" 72 | 73 | if chapter_id > 2: 74 | chapter_id += 1 75 | return chapter_id 76 | 77 | 78 | def clear_levels( 79 | story_chapters: dict[str, Any], 80 | treasures: list[list[int]], 81 | ids: list[int], 82 | val: int, 83 | chapter_progress: int, 84 | clear: bool, 85 | ) -> tuple[dict[str, Any], list[list[int]]]: 86 | """Clear levels in a chapter""" 87 | 88 | for chapter_id in ids: 89 | story_chapters["Chapter Progress"][chapter_id] = chapter_progress 90 | story_chapters["Times Cleared"][chapter_id] = ( 91 | ([val] * chapter_progress) + ([0] * (48 - chapter_progress)) + ([0] * 3) 92 | ) 93 | if not clear: 94 | treasures[chapter_id] = [0] * 49 95 | return story_chapters, treasures 96 | 97 | 98 | def get_total_stages(save_stats: dict[str, Any], chapter_id: int) -> int: 99 | """Get the total number of stages in a chapter""" 100 | 101 | return len(save_stats["story_chapters"]["Times Cleared"][chapter_id]) - 3 102 | 103 | 104 | def clear_each(save_stats: dict[str, Any]): 105 | """Clear stages for each chapter""" 106 | 107 | chapter_ids = story_level_id_selector.select_specific_chapters() 108 | 109 | for chapter_id in chapter_ids: 110 | helper.colored_text(f"Chapter: &{chapter_id+1}& : &{CHAPTERS[chapter_id]}&") 111 | formatted_id = format_story_id(chapter_id) 112 | progress = story_level_id_selector.select_level_progress( 113 | chapter_id, get_total_stages(save_stats, formatted_id) 114 | ) 115 | save_stats = clear_specific_level_ids(save_stats, formatted_id, progress) 116 | helper.colored_text("Successfully set main story chapters") 117 | return save_stats 118 | 119 | 120 | def clear_all(save_stats: dict[str, Any]) -> dict[str, Any]: 121 | """Clear whole chapters""" 122 | 123 | chapter_ids = story_level_id_selector.select_specific_chapters() 124 | text = "" 125 | for chapter_id in chapter_ids: 126 | text += f"Chapter: &{chapter_id+1}& : &{CHAPTERS[chapter_id]}&\n" 127 | helper.colored_text(text.strip("\n")) 128 | progress = story_level_id_selector.select_level_progress( 129 | None, get_total_stages(save_stats, 0) 130 | ) 131 | for chapter_id in chapter_ids: 132 | chapter_id = format_story_id(chapter_id) 133 | save_stats = clear_specific_level_ids(save_stats, chapter_id, progress) 134 | helper.colored_text("Successfully set main story chapters") 135 | return save_stats 136 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/outbreaks.py: -------------------------------------------------------------------------------- 1 | """Handler for editting outbreaks""" 2 | from typing import Any, Optional 3 | 4 | from ... import user_input_handler, helper 5 | from . import main_story 6 | 7 | 8 | def get_available_chapters(outbreaks: dict[int, Any]) -> list[str]: 9 | """Get available chapters""" 10 | 11 | available_chapters: list[str] = [] 12 | for chapter_index in outbreaks: 13 | if chapter_index > 2: 14 | chapter_index -= 1 15 | if chapter_index > 7: 16 | continue 17 | available_chapters.append(main_story.CHAPTERS[chapter_index]) 18 | return available_chapters 19 | 20 | 21 | def set_outbreak( 22 | chapter_data: dict[int, int], val_to_set: int, total: Optional[int] = None 23 | ) -> dict[int, int]: 24 | """Set a chapter of an outbreak""" 25 | if total is None: 26 | total = len(chapter_data) 27 | 28 | for level_id in range(total): 29 | chapter_data[level_id] = val_to_set 30 | return chapter_data 31 | 32 | 33 | def set_outbreaks( 34 | outbreaks: dict[int, Any], 35 | current_outbreaks: dict[int, Any], 36 | ids: list[int], 37 | clear: bool = True, 38 | ) -> tuple[dict[int, Any], dict[int, Any]]: 39 | """Set outbreaks""" 40 | for chapter_id in ids: 41 | outbreaks[chapter_id] = set_outbreak( 42 | outbreaks[chapter_id], 1 if clear else 0, 48 43 | ) 44 | if chapter_id in current_outbreaks: 45 | if clear: 46 | current_outbreaks[chapter_id] = {} 47 | return outbreaks, current_outbreaks 48 | 49 | 50 | def edit_outbreaks(save_stats: dict[str, Any]) -> dict[str, Any]: 51 | """Handler for editting outbreaks""" 52 | 53 | outbreaks = save_stats["outbreaks"] 54 | current_outbreaks = save_stats["current_outbreaks"] 55 | 56 | clear = ( 57 | user_input_handler.colored_input( 58 | "Do you want to clear or un-clear outbreaks? (&c&/&u&): " 59 | ) 60 | == "c" 61 | ) 62 | 63 | available_chapters = get_available_chapters(outbreaks) 64 | 65 | print("What chapter do you want to edit:") 66 | ids = user_input_handler.select_not_inc( 67 | options=available_chapters, 68 | mode="clear the outbreaks for?", 69 | ) 70 | ids = helper.check_clamp(ids, len(available_chapters) + 1, 0, 0) 71 | ids = main_story.format_story_ids(ids) 72 | outbreaks, current_outbreaks = set_outbreaks( 73 | outbreaks, current_outbreaks, ids, clear 74 | ) 75 | save_stats["outbreaks"] = outbreaks 76 | save_stats["current_outbreaks"] = current_outbreaks 77 | print("Successfully set outbreaks") 78 | return save_stats 79 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/story_level_id_selector.py: -------------------------------------------------------------------------------- 1 | """Handler for selecting story levels""" 2 | from typing import Optional 3 | 4 | from ... import user_input_handler, helper 5 | from . import main_story 6 | 7 | 8 | def select_specific_chapters() -> list[int]: 9 | """Select specific levels""" 10 | 11 | print("What chapters do you want to select?") 12 | ids = user_input_handler.select_not_inc(main_story.CHAPTERS, "clear") 13 | return ids 14 | 15 | 16 | def get_option(): 17 | """Get option""" 18 | 19 | options = [ 20 | "Select specific levels with stage ids", 21 | "Select all levels up to a certain stage", 22 | "Select all levels", 23 | ] 24 | return user_input_handler.select_single(options) 25 | 26 | 27 | def select_levels( 28 | chapter_id: Optional[int], forced_option: Optional[int] = None, total: int = 48 29 | ) -> list[int]: 30 | """Select levels""" 31 | 32 | if forced_option is None: 33 | choice = get_option() 34 | else: 35 | choice = forced_option 36 | if choice == 1: 37 | return select_specific_levels(chapter_id, total) 38 | if choice == 2: 39 | return select_levels_up_to(chapter_id, total) 40 | if choice == 3: 41 | return select_all(total) 42 | return [] 43 | 44 | 45 | def select_specific_levels(chapter_id: Optional[int], total: int) -> list[int]: 46 | """Select specific levels""" 47 | 48 | print("What levels do you want to select?") 49 | if chapter_id is not None: 50 | helper.colored_text( 51 | f"Chapter: &{chapter_id+1}& : &{main_story.CHAPTERS[chapter_id]}&" 52 | ) 53 | ids = user_input_handler.get_range_ids( 54 | "Level ids (e.g &1&=korea, &2&=mongolia)", total 55 | ) 56 | ids = helper.check_clamp(ids, total, 1, -1) 57 | return ids 58 | 59 | 60 | def select_levels_up_to(chapter_id: Optional[int], total: int) -> list[int]: 61 | """Select levels up to a certain level""" 62 | 63 | print("What levels do you want to select?") 64 | if chapter_id is not None: 65 | helper.colored_text( 66 | f"Chapter: &{chapter_id+1}& : &{main_story.CHAPTERS[chapter_id]}&" 67 | ) 68 | stage_id = user_input_handler.get_int( 69 | f"Enter the stage id that you want to clear/unclear up to (and including) (e.g &1&=korea cleared, &2&=korea &and& mongolia cleared, &{total}&=all)?:" 70 | ) 71 | stage_id = helper.clamp(stage_id, 1, total) 72 | return list(range(0, stage_id)) 73 | 74 | 75 | def select_all(total: int) -> list[int]: 76 | """Select all levels""" 77 | 78 | return list(range(0, total)) 79 | 80 | 81 | def select_level_progress( 82 | chapter_id: Optional[int], total: int, examples: Optional[list[str]] = None 83 | ) -> int: 84 | """Select level progress""" 85 | 86 | if examples is None: 87 | examples = [ 88 | "korea", 89 | "mongolia", 90 | ] 91 | 92 | print("What level do you want to clear up to and including?") 93 | if chapter_id is not None: 94 | helper.colored_text( 95 | f"Chapter: &{chapter_id+1}& : &{main_story.CHAPTERS[chapter_id]}&" 96 | ) 97 | progress = user_input_handler.get_int( 98 | f"Enter the stage id that you want to clear/unclear (e.g &1&={examples[0]} cleared, &2&={examples[0]} &and& {examples[1]} cleared, &{total}&=all, &0&=unclear all)?:" 99 | ) 100 | progress = helper.clamp(progress, 0, total) 101 | return progress 102 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/towers.py: -------------------------------------------------------------------------------- 1 | """Handler for editing tower stages""" 2 | from typing import Any 3 | 4 | from . import event_stages 5 | from ... import user_input_handler 6 | 7 | def edit_tower(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Handler for editing tower stages""" 9 | 10 | stage_data = save_stats["tower"]["progress"] 11 | stage_data = { 12 | "Value": stage_data, 13 | "Lengths": {"stars": stage_data["stars"], "stages": stage_data["stages"]}, 14 | } 15 | 16 | ids = [] 17 | ids = user_input_handler.get_range( 18 | user_input_handler.colored_input( 19 | "Enter tower ids (Look up &Event Release Order battle cats& and scroll past the &events& and &gauntlets& to find &tower& ids) (You can enter &all& to get all, a range e.g &1&-&49&, or ids separate by spaces e.g &5 4 7&):" 20 | ), 21 | stage_data["Value"]["total"], 22 | ) 23 | save_stats["tower"]["progress"] = event_stages.stage_handler( 24 | stage_data, ids, 0, False 25 | )["Value"] 26 | save_stats["tower"]["progress"]["total"] = stage_data["Value"]["total"] 27 | save_stats["tower"]["progress"]["stars"] = stage_data["Lengths"]["stars"] 28 | save_stats["tower"]["progress"]["stages"] = stage_data["Lengths"]["stages"] 29 | 30 | return save_stats 31 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/uncanny.py: -------------------------------------------------------------------------------- 1 | """Handler for editting uncanny legends""" 2 | from typing import Any 3 | 4 | from . import event_stages 5 | from ... import user_input_handler 6 | 7 | def edit_uncanny(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Handler for editting uncanny legends""" 9 | stage_data = save_stats["uncanny"] 10 | lengths = stage_data["Lengths"] 11 | 12 | ids = [] 13 | ids = user_input_handler.get_range( 14 | user_input_handler.colored_input( 15 | "Enter stage ids (e.g &1& = a new legend, &2& = here be dragons)(You can enter &all& to get all, a range e.g &1&-&49&, or ids separate by spaces e.g &5 4 7&):" 16 | ), 17 | lengths["total"], 18 | ) 19 | save_stats["uncanny"] = event_stages.stage_handler(stage_data, ids, -1) 20 | 21 | return save_stats 22 | 23 | def is_ancient_curse_clear(save_stats: dict[str, Any]) -> bool: 24 | """ 25 | Check if the ancient curse is cleared 26 | 27 | Args: 28 | save_stats (dict[str, Any]): The save stats 29 | 30 | Returns: 31 | bool: If the ancient curse is cleared 32 | """ 33 | return save_stats["uncanny"]["Value"]["clear_progress"][0][0] >= 1 34 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/unlock_aku_realm.py: -------------------------------------------------------------------------------- 1 | """Handler for unlocking the aku realm""" 2 | from typing import Any 3 | from ... import helper 4 | from . import event_stages 5 | 6 | 7 | def unlock_aku_realm(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """ 9 | Unlock the aku realm 10 | 11 | Args: 12 | save_stats (dict[str, Any]): The save stats to edit 13 | 14 | Returns: 15 | dict[str, Any]: The edited save stats 16 | """ 17 | stage_ids = [255, 256, 257, 258, 265, 266, 268] 18 | offset = 400 19 | for stage_id in stage_ids: 20 | save_stats["event_stages"] = event_stages.set_stage_data( 21 | save_stats["event_stages"], 22 | stage_id + offset, 23 | 1, 24 | save_stats["event_stages"]["Lengths"], 25 | True, 26 | ) 27 | helper.colored_text("&The Aku realm has successfully been unlocked.") 28 | return save_stats 29 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/levels/zerolegends.py: -------------------------------------------------------------------------------- 1 | """Handler for editting zero legends""" 2 | from typing import Any 3 | 4 | from . import event_stages 5 | from ... import user_input_handler 6 | 7 | def count_chapters(save_stats) -> int: 8 | data1 = save_stats.get("zero_legends", {}) 9 | count = len(data1) 10 | return count 11 | 12 | def count_stages(data) -> int: 13 | data1 = data.get("stages", {}) 14 | count = len(data1) 15 | return count 16 | 17 | def set_zl(stage_data, ids, lengths): 18 | for stage_id in ids: 19 | chapter_index = int(stage_id - 1) 20 | chapter_stages_count = count_stages(stage_data[chapter_index]["stars"][0]) 21 | stage_data[chapter_index]["stars"][0]["stages_cleared"] = chapter_stages_count #stage count 22 | stage_data[chapter_index]["stars"][0]["unlock_next"] = 3 #idk what this means, but when i cleared stages myself, value was 3. 23 | for i in range(0, (chapter_stages_count - 1)): 24 | stage_data[chapter_index]["stars"][0]["stages"][i] = 1 #how many you cleared this stage 25 | i += 1 26 | return stage_data 27 | 28 | def edit_zl(save_stats: dict[str, Any]) -> dict[str, Any]: 29 | """Handler for editting zero legends""" 30 | stage_data = save_stats["zero_legends"] 31 | lengths = count_chapters(save_stats) 32 | ids = [] 33 | ids = user_input_handler.get_range( 34 | user_input_handler.colored_input( 35 | "Enter stage ids (e.g &1& = Zero Field, &2& = The Edge of Spacetime)(You can enter &all& to get all, a range e.g &1&-&8&, or ids separate by spaces e.g &5 4 7&):" 36 | ), 37 | lengths, 38 | ) 39 | save_stats["zero_legends"] = set_zl(stage_data, ids, lengths) 40 | print("Successfully set Zero Legend Chapters.") 41 | return save_stats 42 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | fix_elsewhere, 3 | meow_medals, 4 | missions, 5 | play_time, 6 | trade_progress, 7 | unlock_enemy_guide, 8 | create_new_account, 9 | unlock_equip_menu, 10 | get_gold_pass, 11 | claim_user_rank_rewards, 12 | cat_shrine, 13 | fix_time_issues, 14 | scheme_item, 15 | ) 16 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/cat_shrine.py: -------------------------------------------------------------------------------- 1 | """Handler for editing cata shrine xp and level""" 2 | from typing import Any, Optional 3 | 4 | from ... import game_data_getter, helper, item, user_input_handler 5 | 6 | 7 | def get_boundaries(is_jp: bool) -> Optional[list[int]]: 8 | """ 9 | Returns the xp requirements for each level 10 | 11 | Args: 12 | is_jp (bool): If the save file is japanese 13 | 14 | Returns: 15 | list[int]: The xp requirements for each level 16 | """ 17 | file_data = game_data_getter.get_file_latest("resLocal", "jinja_level.csv", is_jp) 18 | if file_data is None: 19 | helper.error_text("Failed to get jinja level data") 20 | return None 21 | boundaries = file_data.decode("utf-8").splitlines() 22 | xp_requirements: list[int] = [] 23 | counter = 0 24 | for line in boundaries: 25 | requirement = int(line.split(helper.get_text_splitter(is_jp))[0]) 26 | counter += requirement 27 | xp_requirements.append(counter) 28 | return xp_requirements 29 | 30 | 31 | def get_level_from_xp(shrine_xp: int, is_jp: bool) -> Optional[dict[str, Any]]: 32 | """ 33 | Returns the level, max level and max xp from the given xp 34 | 35 | Args: 36 | shrine_xp (int): The xp of the shrine 37 | is_jp (bool): If the save file is japanese 38 | 39 | Returns: 40 | dict[str, Any]: The level, max level, and max xp 41 | """ 42 | xp_requirements = get_boundaries(is_jp) 43 | if xp_requirements is None: 44 | return None 45 | level = 1 46 | for requirement in xp_requirements: 47 | if shrine_xp >= requirement: 48 | level += 1 49 | if level > len(xp_requirements): 50 | level = len(xp_requirements) 51 | return { 52 | "level": level, 53 | "max_level": len(xp_requirements), 54 | "max_xp": xp_requirements[-2], 55 | } 56 | 57 | 58 | def get_xp_from_level(level: int, is_jp: bool) -> Optional[int]: 59 | """ 60 | Returns the xp required to reach the given level 61 | 62 | Returns: 63 | _type_: int 64 | """ 65 | xp_requirements = get_boundaries(is_jp) 66 | if xp_requirements is None: 67 | return None 68 | if level <= 1: 69 | shrine_xp = 0 70 | else: 71 | shrine_xp = xp_requirements[level - 2] 72 | return shrine_xp 73 | 74 | 75 | def edit_shrine_xp(save_stats: dict[str, Any]) -> dict[str, Any]: 76 | """ 77 | Edit the shrine xp of the save file 78 | 79 | Args: 80 | save_stats (dict[str, Any]): The save file stats 81 | 82 | Returns: 83 | dict[str, Any]: The edited save file stats 84 | """ 85 | 86 | shrine_xp = save_stats["cat_shrine"]["xp_offering"] 87 | 88 | data = get_level_from_xp(shrine_xp, helper.check_data_is_jp(save_stats)) 89 | if data is None: 90 | return save_stats 91 | level = data["level"] 92 | 93 | helper.colored_text(f"Shrine XP: &{shrine_xp}&\nLevel: &{level}&") 94 | raw = ( 95 | user_input_handler.colored_input( 96 | "Do you want to edit raw xp(&1&) or the level(&2&)?:" 97 | ) 98 | == "1" 99 | ) 100 | 101 | if raw: 102 | cat_shrine_xp = item.IntItem( 103 | name="Shrine XP", 104 | value=item.Int(shrine_xp), 105 | max_value=None, 106 | ) 107 | cat_shrine_xp.edit() 108 | shrine_xp = int(cat_shrine_xp.get_value()) 109 | else: 110 | shrine_level = item.IntItem( 111 | name="Shrine Level", 112 | value=item.Int(level), 113 | max_value=data["max_level"], 114 | ) 115 | shrine_level.edit() 116 | shrine_xp = get_xp_from_level( 117 | int(shrine_level.get_value()), helper.check_data_is_jp(save_stats) 118 | ) 119 | if shrine_xp is None: 120 | return save_stats 121 | shrine_data = get_level_from_xp(shrine_xp, helper.check_data_is_jp(save_stats)) 122 | if shrine_data is None: 123 | return save_stats 124 | shrine_level = shrine_data["level"] 125 | if shrine_level > data["max_level"]: 126 | shrine_level = data["max_level"] 127 | save_stats["shrine_dialogs"]["Value"] = shrine_level - 1 # Level up dialog 128 | save_stats["shrine_gone"] = 0 129 | save_stats["cat_shrine"]["stamp_1"] = 0 130 | save_stats["cat_shrine"]["stamp_2"] = 0 131 | 132 | save_stats["cat_shrine"]["xp_offering"] = shrine_xp 133 | return save_stats 134 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/claim_user_rank_rewards.py: -------------------------------------------------------------------------------- 1 | """Handler for claiming all user rank rewards""" 2 | 3 | from typing import Any 4 | 5 | from ... import helper, user_input_handler 6 | 7 | 8 | def claim(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Claim all user rank rewards""" 10 | 11 | save_stats["user_rank_rewards"] = [1] * len(save_stats["user_rank_rewards"]) 12 | 13 | helper.colored_text("Claimed all user rank rewards", helper.GREEN) 14 | 15 | return save_stats 16 | 17 | 18 | def edit_rewards(save_stats: dict[str, Any]) -> dict[str, Any]: 19 | """Edit all user rank rewards""" 20 | 21 | option = user_input_handler.select_single( 22 | [ 23 | "Claim all user rank rewards", 24 | "Clear all user rank rewards", 25 | "Back", 26 | ], 27 | ) 28 | if option == 1: 29 | save_stats = claim(save_stats) 30 | elif option == 2: 31 | save_stats = clear(save_stats) 32 | return save_stats 33 | 34 | 35 | def clear(save_stats: dict[str, Any]) -> dict[str, Any]: 36 | """Clear all user rank rewards""" 37 | 38 | save_stats["user_rank_rewards"] = [0] * len(save_stats["user_rank_rewards"]) 39 | 40 | helper.colored_text("Unclaimed all user rank rewards", helper.GREEN) 41 | 42 | return save_stats 43 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/create_new_account.py: -------------------------------------------------------------------------------- 1 | """Handler for creating a new account""" 2 | 3 | from typing import Any 4 | 5 | from . import fix_elsewhere 6 | from ... import helper, server_handler 7 | 8 | 9 | def create_new_account(save_stats: dict[str, Any]): 10 | """Create a new account""" 11 | 12 | helper.colored_text("Creating a new inquiry code and token...", helper.GREEN) 13 | 14 | save_stats["inquiry_code"] = server_handler.get_inquiry_code() 15 | save_stats["token"] = "0" * 40 16 | save_stats = fix_elsewhere.fix_elsewhere(save_stats, force_mi=True) 17 | 18 | return save_stats 19 | 20 | 21 | def create_new_account_no_input(save_stats: dict[str, Any]) -> dict[str, Any]: 22 | """ 23 | Create a new account without asking for input 24 | 25 | Args: 26 | save_stats (dict[str, Any]): The save stats 27 | 28 | Returns: 29 | dict[str, Any]: The save stats 30 | """ 31 | helper.colored_text("Creating a new inquiry code and token...", helper.GREEN) 32 | 33 | save_stats["inquiry_code"] = server_handler.get_inquiry_code() 34 | save_stats["token"] = "0" * 40 35 | save_stats = fix_elsewhere.fix_elsewhere(save_stats, force_mi=True, text=False) 36 | 37 | return save_stats 38 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/fix_elsewhere.py: -------------------------------------------------------------------------------- 1 | """Fix the elsewhere issue and unban an account""" 2 | import json 3 | import os 4 | from typing import Any 5 | 6 | 7 | from ... import helper, adb_handler, server_handler, user_info 8 | 9 | 10 | def edit_cache(password: str, token: str, save_stats: dict[str, Any]) -> bool: 11 | """Edit the cache file in /data/data/jp.co.ponos.battlecats/files/cache/ to add the token and password""" 12 | 13 | data = {"password": password, "token": token} 14 | data_s = json.dumps(data) 15 | data_s = data_s.replace(" ", "") 16 | inquiry_code = save_stats["inquiry_code"] 17 | local_path = os.path.abspath(inquiry_code + ".json") 18 | 19 | helper.write_file_string(local_path, data_s) 20 | game_v = save_stats["version"] 21 | if game_v == "jp": 22 | game_v = "" 23 | try: 24 | success = adb_handler.run_adb_command( 25 | f'shell mv "{local_path}" "/data/data/jp.co.ponos.battlecats{game_v}/cache/{inquiry_code}.json"' 26 | ) 27 | except adb_handler.ADBException: 28 | success = False 29 | os.remove(local_path) 30 | return success 31 | 32 | 33 | def fix_elsewhere( 34 | save_stats: dict[str, Any], force_mi: bool = False, text: bool = True 35 | ) -> dict[str, Any]: 36 | """Handler for fixing the elsewhere issue and unban an account""" 37 | 38 | helper.colored_text("Getting account password...", helper.GREEN) 39 | original_iq = save_stats["inquiry_code"] 40 | data = server_handler.check_gen_token(save_stats) 41 | token = data["token"] 42 | inquiry_code = data["inquiry_code"] 43 | if token is None: 44 | helper.colored_text("Failed to get auth token", helper.RED) 45 | return save_stats 46 | if original_iq != inquiry_code or force_mi: 47 | info = user_info.UserInfo(inquiry_code) 48 | info.clear_managed_items() 49 | server_handler.update_managed_items( 50 | save_stats["inquiry_code"], token, save_stats 51 | ) 52 | if text: 53 | helper.colored_text( 54 | "Done!\nYou may get a ban message when pressing play. If you do, just press play again and it should go away\nPress enter to continue...(You still need to save your changes)", 55 | helper.DARK_YELLOW, 56 | ) 57 | input() 58 | return save_stats 59 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/fix_time_issues.py: -------------------------------------------------------------------------------- 1 | """Handler for fixing time issues""" 2 | from typing import Any 3 | from ... import helper 4 | 5 | 6 | def fix_time_issues(save_stats: dict[str, Any]) -> dict[str, Any]: 7 | """ 8 | Fix time issues 9 | 10 | Args: 11 | save_stats (dict[str, Any]): Save stats 12 | 13 | Returns: 14 | dict[str, Any]: Save stats 15 | """ 16 | save_stats["third_time"] = helper.get_iso_time() 17 | 18 | save_stats["time_stamp"] = helper.get_time() 19 | save_stats["time_stamp_4"] = helper.get_time() 20 | 21 | helper.colored_text( 22 | "Successfully fixed time issues &(Your device time on both devices must be correct for this to work!)&", 23 | helper.GREEN, 24 | helper.RED, 25 | ) 26 | return save_stats 27 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/get_gold_pass.py: -------------------------------------------------------------------------------- 1 | """Handler for getting the gold pass""" 2 | 3 | import datetime 4 | import random 5 | import time 6 | from typing import Any 7 | from ... import helper, user_input_handler 8 | 9 | 10 | def remove_gold_pass_val(save_stats: dict[str, Any]) -> dict[str, Any]: 11 | """ 12 | Remove the gold pass 13 | 14 | Args: 15 | save_stats (dict[str, Any]): The save stats 16 | 17 | Returns: 18 | dict[str, Any]: The save stats 19 | """ 20 | 21 | gold_pass = save_stats["gold_pass"] 22 | 23 | gold_pass["officer_id"]["Value"] = 0xFFFFFFFF 24 | gold_pass["renewal_times"]["Value"] = 0 25 | gold_pass["start_date"] = 0 26 | gold_pass["expiry_date"] = 0 27 | gold_pass["unknown_2"][0] = 0 28 | gold_pass["unknown_2"][1] = 0 29 | 30 | gold_pass["start_date_2"] = 0 31 | gold_pass["expiry_date_2"] = 0 32 | gold_pass["unknown_3"] = 0 33 | gold_pass["flag_2"]["Value"] = 0 34 | gold_pass["expiry_date_3"] = 0 35 | 36 | gold_pass["unknown_4"]["Value"] = 0 37 | gold_pass["unknown_5"]["Value"] = 0 38 | gold_pass["unknown_6"]["Value"] = 0 39 | save_stats["gold_pass"] = gold_pass 40 | save_stats["login_bonuses"][5100] = 0 41 | 42 | return save_stats 43 | 44 | 45 | def get_gold_pass_val( 46 | save_stats: dict[str, Any], total_days: int, officer_id: int 47 | ) -> dict[str, Any]: 48 | """ 49 | Give the gold pass 50 | 51 | Args: 52 | save_stats (dict[str, Any]): The save stats 53 | total_days (int): The total days 54 | officer_id (int): The officer ID 55 | 56 | Returns: 57 | dict[str, Any]: The save stats 58 | """ 59 | 60 | gold_pass = save_stats["gold_pass"] 61 | 62 | start_date = int(time.time()) 63 | expiry_date = start_date + datetime.timedelta(days=total_days).total_seconds() 64 | expiry_date_2 = start_date + datetime.timedelta(days=total_days * 2).total_seconds() 65 | 66 | gold_pass["officer_id"]["Value"] = officer_id 67 | if gold_pass["renewal_times"]["Value"] == 0: 68 | gold_pass["renewal_times"]["Value"] = 1 69 | gold_pass["renewal_times"]["Value"] += 1 70 | gold_pass["start_date"] = start_date 71 | gold_pass["expiry_date"] = expiry_date 72 | gold_pass["unknown_2"][0] = expiry_date 73 | gold_pass["unknown_2"][1] = expiry_date_2 74 | 75 | gold_pass["start_date_2"] = start_date 76 | gold_pass["expiry_date_2"] = expiry_date_2 77 | gold_pass["unknown_3"] = start_date 78 | gold_pass["flag_2"]["Value"] = 2 79 | gold_pass["expiry_date_3"] = expiry_date 80 | 81 | gold_pass["unknown_4"]["Value"] = 0 82 | gold_pass["unknown_5"]["Value"] = 1 83 | gold_pass["unknown_6"]["Value"] = 0 84 | save_stats["gold_pass"] = gold_pass 85 | save_stats["login_bonuses"][5100] = 0 86 | 87 | return save_stats 88 | 89 | 90 | def get_random_officer_id() -> int: 91 | """Get a random officer ID""" 92 | 93 | return random.randint(1, 2**16 - 1) 94 | 95 | 96 | def get_gold_pass(save_stats: dict[str, Any]) -> dict[str, Any]: 97 | """Give the gold pass""" 98 | 99 | officer_id = user_input_handler.colored_input( 100 | "Enter the &officer ID& you want (Press &enter& for a &random id&, and enter &-1& to &remove the gold pass&):" 101 | ) 102 | if officer_id == "": 103 | officer_id = get_random_officer_id() 104 | elif officer_id == "-1": 105 | officer_id = -1 106 | else: 107 | officer_id = helper.check_int_max(officer_id) 108 | 109 | if officer_id is None: 110 | officer_id = 0 111 | 112 | if officer_id == -1: 113 | save_stats = remove_gold_pass_val(save_stats) 114 | helper.colored_text("Successfully removed the gold pass", helper.GREEN) 115 | else: 116 | helper.colored_text(f"Officer ID: &{officer_id}&", helper.GREEN, helper.WHITE) 117 | save_stats = get_gold_pass_val(save_stats, 30, officer_id) 118 | helper.colored_text("Successfully gave the gold pass", helper.GREEN) 119 | 120 | return save_stats 121 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/meow_medals.py: -------------------------------------------------------------------------------- 1 | """Handler for editting meow medals""" 2 | from enum import Enum 3 | import json 4 | from typing import Any, Optional 5 | 6 | from ... import helper, user_input_handler, game_data_getter 7 | 8 | 9 | def get_medal_names(is_jp: bool) -> Optional[list[str]]: 10 | """Get all medal names""" 11 | 12 | file_data = game_data_getter.get_file_latest("resLocal", "medalname.tsv", is_jp) 13 | if file_data is None: 14 | helper.error_text("Failed to get medal names") 15 | return None 16 | medal_names = file_data.decode("utf-8").splitlines() 17 | names: list[str] = [] 18 | for line in medal_names: 19 | line_split = line.split("\t") 20 | name = ( 21 | line_split[0] 22 | .rstrip("\n") 23 | .replace("&", "and") 24 | .replace("★", "") 25 | .lstrip(" ") 26 | ) 27 | names.append(name) 28 | return names 29 | 30 | 31 | def set_medals(medal_stats: dict[str, Any], ids: list[int]) -> dict[str, Any]: 32 | """Set the medal stats of a set of medals""" 33 | 34 | for medal_id in ids: 35 | if medal_id == 0: 36 | continue 37 | medal_id -= 1 38 | if medal_id not in medal_stats["medal_data_1"]: 39 | if medal_id not in medal_stats["medal_data_2"]: 40 | medal_stats["medal_data_1"].append(medal_id) 41 | medal_stats["medal_data_2"][medal_id] = 0 42 | return medal_stats 43 | 44 | 45 | def remove_medals(medal_stats: dict[str, Any], ids: list[int]) -> dict[str, Any]: 46 | """Remove the medal stats of a set of medals""" 47 | 48 | for medal_id in ids: 49 | if medal_id == 0: 50 | continue 51 | medal_id -= 1 52 | if medal_id in medal_stats["medal_data_1"]: 53 | medal_stats["medal_data_1"].remove(medal_id) 54 | if medal_id in medal_stats["medal_data_2"]: 55 | medal_stats["medal_data_2"].pop(medal_id) 56 | return medal_stats 57 | 58 | 59 | class BaseMapIds(Enum): 60 | """Base map IDs""" 61 | 62 | STORY_CHAPTERS = 3000 63 | OUTBREAKS_EOC = 20000 64 | OUTBREAKS_ITF = 21000 65 | OUTBREAKS_COTC = 22000 66 | FILIBUSTER = 23000 67 | LEGEND_STAGES = 0 68 | EVENT_STAGES = 1000 69 | TOWER_STAGES = 7000 70 | LEGEND_QUEST = 16000 71 | IDI_RE = 4026 72 | AKU_REALM = 4042 73 | GAUNTLETS = 24000 74 | 75 | 76 | class ActionTypes(Enum): 77 | """Action types""" 78 | 79 | EARN_CENT = 0 80 | GAMATOTO_EXPLORE = 1 81 | CAT_BASE_WEAPONS = 2 82 | USER_RANK = 3 83 | RECRUIT_GAMATOTO_ASSISTANT = 4 84 | 85 | 86 | class Medal: 87 | """Medal""" 88 | 89 | def __init__(self, medal_id: int, grade: int, line: int): 90 | self.medal_id = medal_id 91 | self.grade = grade 92 | self.line = line 93 | 94 | 95 | class StageMedal(Medal): 96 | """Stage medal""" 97 | 98 | def __init__( 99 | self, 100 | medal_id: int, 101 | grade: int, 102 | line: int, 103 | maps: Optional[list[int]], 104 | condition: Optional[dict[str, Any]] = None, 105 | star: Optional[int] = None, 106 | ): 107 | super().__init__(medal_id, grade, line) 108 | self.maps = maps 109 | self.condition = condition 110 | self.star = star 111 | 112 | 113 | class TreasureMedal(StageMedal): 114 | """Treasure medal""" 115 | 116 | def __init__( 117 | self, 118 | medal_id: int, 119 | grade: int, 120 | line: int, 121 | maps: Optional[list[int]], 122 | treasure: int, 123 | condition: Optional[dict[str, Any]] = None, 124 | ): 125 | super().__init__(medal_id, grade, line, maps, condition) 126 | self.treasure = treasure 127 | 128 | 129 | class ActionMedal(Medal): 130 | """Action medal""" 131 | 132 | def __init__(self, medal_id: int, grade: int, line: int, action: ActionTypes): 133 | super().__init__(medal_id, grade, line) 134 | self.action = action 135 | 136 | 137 | class CharacterMedal(StageMedal): 138 | """Character medal""" 139 | 140 | def __init__( 141 | self, 142 | medal_id: int, 143 | grade: int, 144 | line: int, 145 | maps: Optional[list[int]], 146 | chara: int, 147 | condition: Optional[dict[str, Any]] = None, 148 | ): 149 | super().__init__(medal_id, grade, line, maps, condition) 150 | self.chara = chara 151 | 152 | 153 | class Medals: 154 | """Medals""" 155 | 156 | def __init__( 157 | self, 158 | treasures: list[TreasureMedal], 159 | characters: list[CharacterMedal], 160 | actions: list[ActionMedal], 161 | stages: list[StageMedal], 162 | ): 163 | self.treasures = treasures 164 | self.characters = characters 165 | self.actions = actions 166 | self.stages = stages 167 | 168 | 169 | def get_medal_data(is_jp: bool) -> Optional[Medals]: 170 | """Get the medal data""" 171 | 172 | file_data = game_data_getter.get_file_latest("DataLocal", "medallist.json", is_jp) 173 | if file_data is None: 174 | helper.error_text("Failed to get medal data") 175 | return None 176 | medal_data = json.loads(file_data.decode("utf-8"))["iconID"] 177 | 178 | treasures: list[TreasureMedal] = [] 179 | characters: list[CharacterMedal] = [] 180 | actions: list[ActionMedal] = [] 181 | stages: list[StageMedal] = [] 182 | 183 | for i, medal in enumerate(medal_data): 184 | if "condition" not in medal: 185 | medal["condition"] = None 186 | if "treasure" in medal: 187 | treasures.append( 188 | TreasureMedal( 189 | i, 190 | medal["grade"], 191 | medal["line"], 192 | medal["map"], 193 | medal["treasure"], 194 | medal["condition"], 195 | ) 196 | ) 197 | elif "chara" in medal: 198 | characters.append( 199 | CharacterMedal( 200 | i, 201 | medal["grade"], 202 | medal["line"], 203 | None, 204 | medal["chara"], 205 | medal["condition"], 206 | ) 207 | ) 208 | elif "action" in medal: 209 | actions.append( 210 | ActionMedal( 211 | i, 212 | medal["grade"], 213 | medal["line"], 214 | ActionTypes(medal["action"]), 215 | ) 216 | ) 217 | else: 218 | if "star" not in medal: 219 | medal["star"] = None 220 | if "map" not in medal: 221 | medal["map"] = None 222 | stages.append( 223 | StageMedal( 224 | i, 225 | medal["grade"], 226 | medal["line"], 227 | medal["map"], 228 | medal["condition"], 229 | medal["star"], 230 | ) 231 | ) 232 | 233 | return Medals(treasures, characters, actions, stages) 234 | 235 | 236 | def medals(save_stats: dict[str, Any]) -> dict[str, Any]: 237 | """Handler for editting meow medals""" 238 | 239 | medal_stats = save_stats["medals"] 240 | remove = ( 241 | user_input_handler.colored_input( 242 | "Do you want to add or remove medals? (&a&/&r&):" 243 | ) 244 | == "r" 245 | ) 246 | 247 | names = get_medal_names(helper.check_data_is_jp(save_stats)) 248 | if names is None: 249 | return save_stats 250 | helper.colored_list(names) 251 | 252 | ids = user_input_handler.get_range( 253 | user_input_handler.colored_input( 254 | "Enter medal ids (You can enter all to get &all&, a range e.g &1&-&50&, or ids separate by spaces e.g &5 4 7&):" 255 | ), 256 | len(names) + 1, 257 | ) 258 | if remove: 259 | medal_stats = remove_medals(medal_stats, ids) 260 | else: 261 | medal_stats = set_medals(medal_stats, ids) 262 | save_stats["medals"] = medal_stats 263 | print(f"Successfully {'gave' if not remove else 'removed'} medals") 264 | return save_stats 265 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/missions.py: -------------------------------------------------------------------------------- 1 | """Handler for editing catnip missions""" 2 | from typing import Any, Optional 3 | 4 | from ... import user_input_handler, game_data_getter, csv_handler, helper 5 | 6 | 7 | def get_mission_conditions(is_jp: bool) -> Optional[dict[Any, Any]]: 8 | """Get the mission data and what you need to do to complete it""" 9 | 10 | file_data = game_data_getter.get_file_latest( 11 | "DataLocal", "Mission_Condition.csv", is_jp 12 | ) 13 | if file_data is None: 14 | helper.error_text("Failed to get mission conditions") 15 | return None 16 | mission_condition_data = file_data.decode("utf-8") 17 | mission_conditions_list = helper.parse_int_list_list( 18 | csv_handler.parse_csv(mission_condition_data) 19 | ) 20 | mission_conditions: dict[Any, Any] = {} 21 | for line in mission_conditions_list[1:]: 22 | mission_id = line[0] 23 | mission_conditions[mission_id] = { 24 | "mission_type": line[1], 25 | "conditions_type": line[2], 26 | "progress_count": line[3], 27 | "conditions_value": line[4:], 28 | } 29 | return mission_conditions 30 | 31 | 32 | def get_mission_names(is_jp: bool) -> Optional[dict[int, Any]]: 33 | """Get all mission names""" 34 | 35 | file_data = game_data_getter.get_file_latest("resLocal", "Mission_Name.csv", is_jp) 36 | if file_data is None: 37 | helper.error_text("Failed to get mission names") 38 | return None 39 | mission_name = file_data.decode("utf-8") 40 | mission_name_list = mission_name.split("\n") 41 | mission_names: dict[int, Any] = {} 42 | for mission_name in mission_name_list: 43 | line_data = mission_name.split(helper.get_text_splitter(is_jp)) 44 | if helper.check_int(line_data[0]) is None: 45 | continue 46 | mission_id = int(line_data[0]) 47 | name = line_data[1] 48 | name = name.replace("&", "\\&") 49 | mission_names[mission_id] = name 50 | return mission_names 51 | 52 | 53 | def get_mission_names_from_ids( 54 | ids: list[int], mission_names: dict[int, Any] 55 | ) -> list[str]: 56 | """Get the mission names from the ids""" 57 | 58 | names: list[str] = [] 59 | for mission_id in ids: 60 | if mission_id in mission_names: 61 | names.append(mission_names[mission_id]) 62 | return names 63 | 64 | 65 | def get_mission_ids( 66 | missions: dict[str, Any], conditions: dict[int, Any], names: dict[int, Any] 67 | ) -> tuple[list[int], list[str]]: 68 | """Get the mission ids and names from the conditions""" 69 | 70 | mission_ids_to_use: list[int] = [] 71 | for mission_id in missions["states"]: 72 | if mission_id in conditions: 73 | mission_ids_to_use.append(mission_id) 74 | 75 | names_to_use = get_mission_names_from_ids(mission_ids_to_use, names) 76 | return mission_ids_to_use, names_to_use 77 | 78 | 79 | def set_missions( 80 | missions: dict[str, Any], 81 | ids: list[int], 82 | conditions: dict[Any, Any], 83 | mission_ids_to_use: list[int], 84 | re_claim: bool, 85 | ) -> dict[str, Any]: 86 | """Set the missions""" 87 | 88 | for mission_id in ids: 89 | mission_id = helper.clamp(mission_id, 1, len(mission_ids_to_use)) 90 | mission_id = mission_ids_to_use[mission_id] 91 | if re_claim: 92 | claim = True 93 | elif not re_claim and missions["states"][mission_id] != 4: 94 | claim = True 95 | else: 96 | claim = False 97 | if claim: 98 | missions["states"][mission_id] = 2 99 | missions["requirements"][mission_id] = conditions[mission_id][ 100 | "progress_count" 101 | ] 102 | return missions 103 | 104 | 105 | def edit_missions(save_stats: dict[str, Any]) -> dict[str, Any]: 106 | """Handler for editting catnip missions""" 107 | 108 | missions = save_stats["missions"] 109 | 110 | names = get_mission_names(helper.check_data_is_jp(save_stats)) 111 | conditions = get_mission_conditions(helper.check_data_is_jp(save_stats)) 112 | 113 | if names is None or conditions is None: 114 | return save_stats 115 | 116 | mission_ids_to_use, names_to_use = get_mission_ids(missions, conditions, names) 117 | 118 | ids = user_input_handler.select_not_inc( 119 | options=names_to_use, 120 | mode="complete", 121 | ) 122 | re_claim = ( 123 | user_input_handler.colored_input( 124 | "Do you want to re-complete already claimed missions &(1)& (Allows you to get the rewards again) or only complete non-claimed missions&(2)&:" 125 | ) 126 | == "1" 127 | ) 128 | missions = set_missions(missions, ids, conditions, mission_ids_to_use, re_claim) 129 | save_stats["missions"] = missions 130 | print("Successfully completed missions") 131 | return save_stats 132 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/play_time.py: -------------------------------------------------------------------------------- 1 | """Handler for editting play time""" 2 | from typing import Any 3 | 4 | from ... import helper, user_input_handler 5 | 6 | 7 | def edit_play_time(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Handler for editting play time""" 9 | play_time = save_stats["play_time"] 10 | 11 | hours = play_time["hh"] 12 | minutes = play_time["mm"] 13 | 14 | helper.colored_text( 15 | f"You currently have a play time of: &{hours}& hours and &{minutes}& minutes" 16 | ) 17 | hours = helper.check_int_max( 18 | user_input_handler.colored_input("How many hours do you want to set?:") 19 | ) 20 | minutes = helper.check_int_max( 21 | user_input_handler.colored_input("How many minutes do you want to set?:") 22 | ) 23 | if hours is None or minutes is None or hours < 0 or minutes < 0: 24 | print("Please enter valid numbers") 25 | return save_stats 26 | play_time["hh"] = hours 27 | play_time["mm"] = minutes 28 | 29 | save_stats["play_time"] = play_time 30 | 31 | print("Successfully set play time") 32 | return save_stats 33 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/scheme_item.py: -------------------------------------------------------------------------------- 1 | """Scheme item edit""" 2 | from typing import Any 3 | 4 | from ... import csv_handler, game_data_getter, helper, user_input_handler 5 | 6 | 7 | def get_item_names(is_jp: bool) -> list[str]: 8 | """Get the item names 9 | 10 | Args: 11 | is_jp (bool): If the data is for jp 12 | 13 | Returns: 14 | list[str]: The item names 15 | """ 16 | item_names = game_data_getter.get_file_latest( 17 | "resLocal", "GatyaitemName.csv", is_jp 18 | ) 19 | if item_names is None: 20 | helper.error_text("Failed to get item names") 21 | return [] 22 | 23 | item_names = csv_handler.parse_csv( 24 | item_names.decode("utf-8"), 25 | delimeter=helper.get_text_splitter(is_jp), 26 | ) 27 | names: list[str] = [] 28 | for item in item_names: 29 | names.append(item[0]) 30 | return names 31 | 32 | 33 | def get_scheme_data(is_jp: bool) -> list[list[int]]: 34 | """Get the scheme data 35 | 36 | Args: 37 | is_jp (bool): If the data is for jp 38 | 39 | Returns: 40 | list[list[int]]: The scheme data 41 | """ 42 | scheme_data = game_data_getter.get_file_latest( 43 | "DataLocal", "schemeItemData.tsv", is_jp 44 | ) 45 | if scheme_data is None: 46 | helper.error_text("Failed to get scheme data") 47 | return [] 48 | 49 | scheme_data_data = helper.parse_int_list_list( 50 | csv_handler.parse_csv( 51 | scheme_data.decode("utf-8"), 52 | delimeter="\t", 53 | ) 54 | ) 55 | return scheme_data_data 56 | 57 | 58 | def get_scheme_names(is_jp: bool, scheme_data: list[list[int]]) -> dict[int, str]: 59 | """Get the scheme names""" 60 | 61 | file_data = game_data_getter.get_file_latest("resLocal", "localizable.tsv", is_jp) 62 | if file_data is None: 63 | helper.error_text("Failed to get scheme names") 64 | return {} 65 | 66 | localizable = csv_handler.parse_csv( 67 | file_data.decode("utf-8"), 68 | delimeter="\t", 69 | ) 70 | names: dict[int, str] = {} 71 | for scheme in scheme_data[1:]: 72 | scheme_id = scheme[0] 73 | for name in localizable: 74 | scheme_str = f"scheme_popup_{scheme_id}" 75 | if name[0] == scheme_str: 76 | scheme_name = name[1].replace("", "").replace("", "") 77 | names[scheme_id] = scheme_name 78 | break 79 | return names 80 | 81 | 82 | def get_cat_name(cat_id: int, is_jp: bool, cc: str) -> str: 83 | """Get the cat name""" 84 | 85 | file_data = game_data_getter.get_file_latest( 86 | "resLocal", f"Unit_Explanation{cat_id+1}_{cc}.csv", is_jp 87 | ) 88 | if file_data is None: 89 | helper.error_text("Failed to get cat names") 90 | return "" 91 | 92 | cat_name = csv_handler.parse_csv( 93 | file_data.decode("utf-8"), 94 | delimeter=helper.get_text_splitter(is_jp), 95 | ) 96 | return cat_name[0][0] 97 | 98 | 99 | def edit_scheme_data(save_stats: dict[str, Any]) -> dict[str, Any]: 100 | """Handler for editing scheme data""" 101 | 102 | is_jp = helper.check_data_is_jp(save_stats) 103 | data = get_scheme_data(is_jp) 104 | names = get_scheme_names(is_jp, data) 105 | item_names = get_item_names(is_jp) 106 | 107 | options: list[str] = [] 108 | for scheme in data[1:]: 109 | scheme_id = scheme[0] 110 | is_cat = scheme[2] == 1 111 | item_id = scheme[3] 112 | amount = scheme[4] 113 | try: 114 | scheme_name = names[scheme_id] 115 | except KeyError: 116 | continue 117 | string = "\n\t" 118 | if is_cat: 119 | cat_name = get_cat_name(item_id, is_jp, helper.get_lang(is_jp)) 120 | string += scheme_name.replace("%@", cat_name) 121 | else: 122 | try: 123 | item_name = item_names[item_id] 124 | except IndexError: 125 | continue 126 | string += scheme_name 127 | first_index = string.find("%@") 128 | second_index = string.find("%@", first_index + 1) 129 | string = ( 130 | string[:first_index] 131 | + str(amount) 132 | + " " 133 | + item_name 134 | + string[second_index + 2 :] 135 | ) 136 | 137 | string = string.replace("
", "\n\t") 138 | options.append(string) 139 | 140 | scheme_ids = user_input_handler.select_not_inc(options, "get") 141 | scheme_data = save_stats["item_schemes"] 142 | for scheme_index in scheme_ids: 143 | try: 144 | scheme_id = data[scheme_index + 1][0] 145 | except IndexError: 146 | continue 147 | obtain_ids: list[int] = scheme_data["to_obtain_ids"] 148 | obtain_ids.append(scheme_id) 149 | received_ids: list[int] = scheme_data["received_ids"] 150 | if scheme_id in received_ids: 151 | received_ids.remove(scheme_id) 152 | scheme_data["to_obtain_ids"] = obtain_ids 153 | scheme_data["received_ids"] = received_ids 154 | save_stats["item_schemes"] = scheme_data 155 | return save_stats 156 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/trade_progress.py: -------------------------------------------------------------------------------- 1 | """Handler for editting trade progress to allow for unbannable rare tickets""" 2 | from typing import Any 3 | 4 | from ... import helper, item 5 | 6 | 7 | def set_trade_progress_val(storage: dict[str, Any]) -> tuple[dict[str, Any], bool]: 8 | """Handler for editting trade progress to allow for unbannable rare tickets""" 9 | 10 | space = False 11 | for i in range(len(storage["types"])): 12 | storage_item = storage["types"][i] 13 | if storage_item == 0 or (storage["ids"][i] == 1 and storage_item == 2): 14 | storage["ids"][i] = 1 15 | storage["types"][i] = 2 16 | space = True 17 | break 18 | return storage, space 19 | 20 | 21 | def set_trade_progress(save_stats: dict[str, Any]) -> dict[str, Any]: 22 | """Handler for editting trade progress to allow for unbannable rare tickets""" 23 | 24 | trade_progress = save_stats["trade_progress"] 25 | max_value = helper.clamp(299 - save_stats["rare_tickets"]["Value"], 0, 299) 26 | storage = save_stats["cat_storage"] 27 | tickets = item.IntItem( 28 | name="Rare Tickets", 29 | max_value=max_value, 30 | value=item.Int(save_stats["rare_tickets"]["Value"]), 31 | ) 32 | tickets.edit() 33 | trade_progress["Value"] = tickets.get_value() * 5 34 | 35 | storage, has_space = set_trade_progress_val(storage) 36 | 37 | if not has_space: 38 | helper.colored_text("Your cat storage is full, please free 1 space!") 39 | return save_stats 40 | 41 | save_stats["cat_storage"] = storage 42 | save_stats["trade_progress"] = trade_progress 43 | helper.colored_text( 44 | 'You now need to go into your storage and press &"Use all"& and then press &"Trade for Ticket"&' 45 | ) 46 | 47 | return save_stats 48 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/unlock_enemy_guide.py: -------------------------------------------------------------------------------- 1 | """Handler for unlocking the enemy guide""" 2 | from typing import Any 3 | 4 | from ... import user_input_handler 5 | 6 | 7 | def enemy_guide(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Handler for unlocking the enemy guide""" 9 | 10 | enemy_guide_stats = save_stats["enemy_guide"] 11 | total = len(enemy_guide_stats) 12 | unlock = ( 13 | user_input_handler.colored_input( 14 | "Do you want to remove enemy guide entries &(1)& or unlock them &(2)&:" 15 | ) 16 | == "2" 17 | ) 18 | set_val = 1 19 | if not unlock: 20 | set_val = 0 21 | ids = user_input_handler.get_range( 22 | user_input_handler.colored_input( 23 | "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&):" 24 | ), 25 | total, 26 | ) 27 | 28 | for enemy_id in ids: 29 | if enemy_id >= 2: 30 | enemy_id -= 2 31 | if enemy_id >= len(enemy_guide_stats): 32 | print(f"Invalid enemy id: {enemy_id+2}") 33 | continue 34 | enemy_guide_stats[enemy_id] = set_val 35 | save_stats["enemy_guide"] = enemy_guide_stats 36 | if not unlock: 37 | print("Successfully removed enemy guide entries") 38 | else: 39 | print("Successfully unlocked enemy guide entries") 40 | return save_stats 41 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/other/unlock_equip_menu.py: -------------------------------------------------------------------------------- 1 | """Handler for unlocking the equip menu.""" 2 | from typing import Any 3 | 4 | from ... import helper 5 | 6 | 7 | def unlock_equip(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Unlocks the equip menu.""" 9 | 10 | save_stats["menu_unlocks"][2] = 1 11 | helper.colored_text("Equip menu successfully unlocked", helper.GREEN) 12 | return save_stats 13 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/__init__.py: -------------------------------------------------------------------------------- 1 | from . import save, server_upload, other, load, convert 2 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/convert.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ... import helper, user_input_handler 4 | 5 | 6 | def convert_to_jp(save_stats: dict[str, Any]) -> dict[str, Any]: 7 | save_stats["version"] = "jp" 8 | save_stats["dst"] = False 9 | 10 | helper.colored_text("Save data converted to jp", helper.GREEN) 11 | return save_stats 12 | 13 | 14 | def convert_to_non_jp(save_stats: dict[str, Any], cc: str) -> dict[str, Any]: 15 | save_stats["version"] = cc 16 | save_stats["dst"] = True 17 | 18 | helper.colored_text(f"Save data converted to {cc}", helper.GREEN) 19 | return save_stats 20 | 21 | 22 | def convert(save_stats: dict[str, Any], version: str) -> dict[str, Any]: 23 | if version == "jp": 24 | return convert_to_jp(save_stats) 25 | else: 26 | return convert_to_non_jp(save_stats, version) 27 | 28 | 29 | def convert_save(save_stats: dict[str, Any]) -> dict[str, Any]: 30 | gvs = ["en", "jp", "kr", "tw"] 31 | 32 | helper.colored_text( 33 | "WARNING: This may cause issues, and both apps must be the same version (e.g both 12.1.0)!", 34 | helper.RED, 35 | ) 36 | 37 | if save_stats["version"] in gvs: 38 | gvs.remove(save_stats["version"]) 39 | 40 | gv_index = ( 41 | user_input_handler.select_single( 42 | gvs, title="Select a version to convert the save into:" 43 | ) 44 | - 1 45 | ) 46 | gv = gvs[gv_index] 47 | 48 | return convert(save_stats, gv) 49 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/load.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from ... import user_input_handler, server_handler, helper, adb_handler 3 | from ..levels import clear_tutorial 4 | 5 | 6 | def select(save_stats: dict[str, Any]) -> dict[str, Any]: 7 | helper.check_changes(None) 8 | options = [ 9 | "Download save data from the game using transfer and confirmation codes", 10 | "Select a save file from file", 11 | "Use adb to pull the save from a rooted device", 12 | "Load save data from json", 13 | ] 14 | index = ( 15 | user_input_handler.select_single( 16 | options, title="Select an option to get save data:" 17 | ) 18 | - 1 19 | ) 20 | save_path = handle_index(index) 21 | if not save_path: 22 | return save_stats 23 | helper.set_save_path(save_path) 24 | data = helper.load_save_file(save_path) 25 | save_stats = data["save_stats"] 26 | if save_path.endswith(".json"): 27 | input( 28 | "Your save data seems to be in json format. Please use to import json option if you want to load json data.\nPress enter to continue...:" 29 | ) 30 | if not clear_tutorial.is_tutorial_cleared(save_stats): 31 | save_stats = clear_tutorial.clear_tutorial(save_stats) 32 | return save_stats 33 | 34 | 35 | def handle_index(index: int) -> Optional[str]: 36 | path = None 37 | if index == 0: 38 | print("Enter details for data transfer:") 39 | path = server_handler.download_handler() 40 | elif index == 1: 41 | print("Select save file:") 42 | path = helper.select_file( 43 | "Select a save file:", 44 | helper.get_save_file_filetype(), 45 | initial_file=helper.get_save_path_home(), 46 | ) 47 | elif index == 2: 48 | print("Enter details for save pulling:") 49 | game_versions = adb_handler.find_game_versions() 50 | if not game_versions: 51 | game_version = helper.ask_cc() 52 | else: 53 | index = ( 54 | user_input_handler.select_single( 55 | game_versions, "Select", "Select a game version to pull from:", True 56 | ) 57 | - 1 58 | ) 59 | game_version = game_versions[index] 60 | path = adb_handler.adb_pull_save_data(game_version) 61 | elif index == 3: 62 | print("Select save data json file") 63 | js_path = helper.select_file( 64 | "Select save data json file", 65 | [("Json", "*.json")], 66 | initial_file=helper.get_save_path_home() + ".json", 67 | ) 68 | if js_path: 69 | path = helper.load_json_handler(js_path) 70 | else: 71 | helper.colored_text("Please enter a recognised option", base=helper.RED) 72 | return None 73 | return path 74 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/other.py: -------------------------------------------------------------------------------- 1 | """Handler for miscalanous save management functions""" 2 | 3 | from typing import Any 4 | 5 | from ... import adb_handler, helper 6 | 7 | 8 | def export(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Export the save stats to a json file""" 10 | 11 | helper.export_json(save_stats, helper.get_save_path() + ".json") 12 | 13 | return save_stats 14 | 15 | 16 | def clear_data(save_stats: dict[str, Any]) -> dict[str, Any]: 17 | """Clear data wrapper for the clear_data function""" 18 | 19 | confirm = input("Do want to clear your data (y/n)?:").lower() 20 | if confirm == "y": 21 | adb_handler.adb_clear_save_data(save_stats["version"]) 22 | print("Data cleared") 23 | 24 | return save_stats 25 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/save.py: -------------------------------------------------------------------------------- 1 | """Handler for saving and exiting the editor""" 2 | 3 | from typing import Any 4 | 5 | from ... import helper, serialise_save, patcher, adb_handler, root_handler 6 | 7 | 8 | def save(save_stats: dict[str, Any]) -> dict[str, Any]: 9 | """Serialise the save data and exit""" 10 | 11 | save_data = serialise_save.start_serialize(save_stats) 12 | helper.write_save_data( 13 | save_data, save_stats["version"], helper.get_save_path(), True 14 | ) 15 | 16 | helper.check_managed_items(save_stats, helper.get_save_path()) 17 | 18 | return save_stats 19 | 20 | 21 | def save_save(save_stats: dict[str, Any]) -> dict[str, Any]: 22 | """Serialise the save data""" 23 | 24 | save_data = serialise_save.start_serialize(save_stats) 25 | helper.write_save_data( 26 | save_data, save_stats["version"], helper.get_save_path(), False 27 | ) 28 | 29 | helper.check_managed_items(save_stats, helper.get_save_path()) 30 | 31 | return save_stats 32 | 33 | 34 | def save_and_push(save_stats: dict[str, Any]) -> dict[str, Any]: 35 | """Serialise the save data and and push it to the game""" 36 | 37 | save_data = serialise_save.start_serialize(save_stats) 38 | save_data = patcher.patch_save_data(save_data, save_stats["version"]) 39 | helper.write_file_bytes(helper.get_save_path(), save_data) 40 | 41 | helper.check_managed_items(save_stats, helper.get_save_path()) 42 | 43 | if not helper.is_android(): 44 | adb_handler.adb_push_save_data(save_stats["version"], helper.get_save_path()) 45 | 46 | return save_stats 47 | 48 | 49 | def save_and_push_rerun(save_stats: dict[str, Any]) -> dict[str, Any]: 50 | """Serialise the save data and push it to the game and restart the game""" 51 | 52 | save_data = serialise_save.start_serialize(save_stats) 53 | save_data = patcher.patch_save_data(save_data, save_stats["version"]) 54 | helper.write_file_bytes(helper.get_save_path(), save_data) 55 | 56 | helper.check_managed_items(save_stats, helper.get_save_path()) 57 | 58 | if not helper.is_android(): 59 | adb_handler.adb_push_save_data(save_stats["version"], helper.get_save_path()) 60 | adb_handler.rerun_game(save_stats["version"]) 61 | else: 62 | root_handler.rerun_game(save_stats["version"]) 63 | 64 | return save_stats 65 | -------------------------------------------------------------------------------- /src/BCSFE_Python/edits/save_management/server_upload.py: -------------------------------------------------------------------------------- 1 | """Handler for server save management functions""" 2 | from typing import Any 3 | 4 | from ... import helper, serialise_save, server_handler, user_info 5 | 6 | 7 | def upload_metadata(save_stats: dict[str, Any]) -> dict[str, Any]: 8 | """Upload the metadata to the game server""" 9 | 10 | _, save_stats = server_handler.meta_data_upload_handler( 11 | save_stats, helper.get_save_path() 12 | ) 13 | return save_stats 14 | 15 | 16 | def set_managed_items(save_stats: dict[str, Any]) -> dict[str, Any]: 17 | """Set the managed items for the save stats""" 18 | 19 | data = server_handler.check_gen_token(save_stats) 20 | token = data["token"] 21 | save_stats = data["save_stats"] 22 | if token is None: 23 | helper.colored_text("Error generating token") 24 | return save_stats 25 | server_handler.update_managed_items(save_stats["inquiry_code"], token, save_stats) 26 | return save_stats 27 | 28 | 29 | def handle_upload_error(inquiry_code: str): 30 | """Show an error message""" 31 | info = user_info.UserInfo(inquiry_code) 32 | info.set_auth_token("") 33 | info.set_password("") 34 | helper.colored_text( 35 | "Error uploading save data\nPlease try again. If error persists, please report this in #bug-reports" 36 | ) 37 | 38 | 39 | def save_and_upload(save_stats: dict[str, Any]) -> dict[str, Any]: 40 | """Serialise the save data, and upload it to the game server""" 41 | 42 | save_data = serialise_save.start_serialize(save_stats) 43 | save_data = helper.write_save_data( 44 | save_data, save_stats["version"], helper.get_save_path(), False 45 | ) 46 | upload_data = server_handler.upload_handler(save_stats, helper.get_save_path()) 47 | if upload_data is None: 48 | handle_upload_error(save_stats["inquiry_code"]) 49 | return save_stats 50 | upload_data, save_stats = upload_data 51 | inquiry_code = save_stats["inquiry_code"] 52 | if upload_data is None: 53 | handle_upload_error(inquiry_code) 54 | return save_stats 55 | if "transferCode" not in upload_data: 56 | handle_upload_error(inquiry_code) 57 | return save_stats 58 | else: 59 | helper.colored_text(f"Transfer code : &{upload_data['transferCode']}&") 60 | helper.colored_text(f"Confirmation Code : &{upload_data['pin']}&") 61 | 62 | return save_stats 63 | -------------------------------------------------------------------------------- /src/BCSFE_Python/files/config_path.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieryhenry/BCSFE-Python/648aa49526b1d7422ce4edb68d0373a3da136e31/src/BCSFE_Python/files/config_path.txt -------------------------------------------------------------------------------- /src/BCSFE_Python/files/enigma_names_en.txt: -------------------------------------------------------------------------------- 1 | Aitum Fields 2 | Vitamin Valley 3 | Ticket Hunter 4 | Shack of Spirit 5 | Cave of Spirit 6 | Labyrinth of Spirit 7 | Cymophane Cave 8 | Vitamin Volcano 9 | Ticket Hunter G 10 | Red Cradle 11 | Red Summit 12 | Red Frontier 13 | Sky Cradle 14 | Sky Summit 15 | Sky Frontier 16 | Black Cradle 17 | Black Summit 18 | Black Frontier 19 | Holy Cradle 20 | Holy Summit 21 | Holy Frontier 22 | Cosmic Cradle 23 | Cosmic Summit 24 | Cosmic Frontier 25 | Necro Cradle 26 | Necro Summit 27 | Necro Frontier 28 | Steel Cradle 29 | Steel Summit 30 | Steel Frontier 31 | Rundown Hideout 32 | Stylish Hideout 33 | Perfect Hideout 34 | Red Cradle 35 | Red Summit 36 | Red Frontier 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Rundown Hideout 53 | Rundown Hideout 54 | Rundown Hideout 55 | Stylish Hideout 56 | Stylish Hideout 57 | Stylish Hideout 58 | Perfect Hideout 59 | Perfect Hideout 60 | Perfect Hideout 61 | Hunter's Map I 62 | Hunter's Map II 63 | Hunter's Map III -------------------------------------------------------------------------------- /src/BCSFE_Python/files/enigma_names_jp.txt: -------------------------------------------------------------------------------- 1 | アテイム古戦場 2 | ビタミン渓谷 3 | にゃんチケ★ハンター 4 | 精神と時間の小屋 5 | 精神と時間の洞窟 6 | 精神と時間の迷宮 7 | ねこの目洞窟 8 | ビタミン火山 9 | にゃんチケ★ハンターG 10 | 紅き本能の起源 11 | 紅き本能の頂 12 | 紅き本能の秘境 13 | 浮ける本能の起源 14 | 浮ける本能の頂 15 | 浮ける本能の秘境 16 | 黒き本能の起源 17 | 黒き本能の頂 18 | 黒き本能の秘境 19 | 聖なる本能の起源 20 | 聖なる本能の頂 21 | 聖なる本能の秘境 22 | 蒼き本能の起源 23 | 蒼き本能の頂 24 | 蒼き本能の秘境 25 | 朽ちた本能の起源 26 | 朽ちた本能の頂 27 | 朽ちた本能の秘境 28 | 硬き本能の起源 29 | 硬き本能の頂 30 | 硬き本能の秘境 31 | おんぼろ秘密基地 32 | イケてる秘密基地 33 | かんぺきな秘密基地 34 | 紅き本能の起源 35 | 紅き本能の頂 36 | 紅き本能の秘境 37 | 浮ける本能の起源 38 | 浮ける本能の頂 39 | 浮ける本能の秘境 40 | 黒き本能の起源 41 | 黒き本能の頂 42 | 黒き本能の秘境 43 | 聖なる本能の起源 44 | 聖なる本能の頂 45 | 聖なる本能の秘境 46 | 朽ちた本能の起源 47 | 朽ちた本能の頂 48 | 朽ちた本能の秘境 49 | 蒼き本能の起源 50 | 蒼き本能の頂 51 | 蒼き本能の秘境 52 | おんぼろ秘密基地 53 | おんぼろ秘密基地 54 | おんぼろ秘密基地 55 | イケてる秘密基地 56 | イケてる秘密基地 57 | イケてる秘密基地 58 | かんぺきな秘密基地 59 | かんぺきな秘密基地 60 | かんぺきな秘密基地 61 | 狩人の地図Ⅰ 62 | 狩人の地図Ⅱ 63 | 狩人の地図Ⅲ -------------------------------------------------------------------------------- /src/BCSFE_Python/files/locales/en/config.properties: -------------------------------------------------------------------------------- 1 | current_val=Current value: &%s& 2 | # 1: current value 3 | 4 | enter_default_gv=Enter the default game version. {{current_val}} Leave blank to not specify a default game version: 5 | select_default_save_path=Select the default save file path 6 | fixed_save_path=Fixed save path? (on=not based on current working directory) 7 | select_locale=Select the locale to use. {{current_val}} 8 | flag_set_config=Do you want to enable (&1&) or disable (&0&) %s. {{current_val}}: 9 | # 1: flag name 10 | enter_new_val_config=Enter the new value for %s. {{current_val}}: 11 | # 1: value name 12 | select_config_path=Select the config file path 13 | enabled=Enabled 14 | disabled=Disabled 15 | -------------------------------------------------------------------------------- /src/BCSFE_Python/files/locales/en/item.properties: -------------------------------------------------------------------------------- 1 | ban_warning=WARNING: Editing in %s will most likely lead to a ban! 2 | ban_warning_leave=Do you want to continue? (&y&/&n&): 3 | current_item_value=The current value of %s is &%s& 4 | # 1: item name 5 | # 2: value 6 | max_str=(max &%s&) 7 | # 1: max value 8 | enter_value_text=Enter the value of %s you want to set%s: 9 | # 1: item name 10 | # 2: max value 11 | success_set=Successfully set the value of %s to &%s& 12 | # 1: item name 13 | # 2: value 14 | item_value_changed=The value of &%s& has changed from &%s& to &%s&. 15 | # 1: item name 16 | # 2: old value 17 | # 3: new value -------------------------------------------------------------------------------- /src/BCSFE_Python/files/locales/en/main.properties: -------------------------------------------------------------------------------- 1 | # start up text 2 | welcome_message=Welcome to the &Battle Cats Save File Editor& 3 | author_message=Made by &fieryhenry& 4 | github_message=GitHub: &https://github.com/fieryhenry/BCSFE-Python& 5 | discord_message=Discord: &https://discord.gg/DvmMgvn5ZB& - Please report any bugs to &#bug-reports&, or any suggestions to &#suggestions& 6 | donate_message=Donate: &https://ko-fi.com/fieryhenry& 7 | config_file_message=Config file path: &%s& 8 | # 1: config file path 9 | scam_warning_message=If you are asked to pay for this program, it is a scam. This program is free and always will be. If you have been scammed, please report it to the discord. 10 | 11 | # update info 12 | beta_message=You are using a &beta& release, some things may be broken. Please report any bugs you find to &#bug-reports& on Discord and specify that you are using a beta version 13 | update_check_failed=Failed to check for updates 14 | local_version=Local version: &%s& 15 | # 1: local version 16 | latest_stable_version=Latest stable version: &%s& 17 | # 1: latest stable version 18 | latest_pre_release_version=Latest pre-release version: &%s& 19 | # 1: latest pre-release version 20 | update_available=An update is available! would you like to update? 21 | 22 | # main options 23 | download_save=Download save data from the game using transfer and confirmation codes 24 | select_save_file=Select a save file from file 25 | adb_pull_save=Use adb to pull the save from a rooted android device 26 | load_save_data_json=Load save data from json 27 | android_direct_pull=Use root access to access the save from local storage 28 | select_save_option_title=Select an option to get save data: 29 | 30 | # thanks 31 | thanks_title=Thanks To: 32 | lethal_thanks=&Lethal's editor& for giving me inspiration to start the project and it helped me work out how to patch the save data and edit cf/xp: &https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren& 33 | beeven_cse_thanks=&Beeven& and &csehydrogen's& code, which helped me figure out how to patch save data: &https://github.com/beeven/battlecats& and &https://github.com/csehydrogen/BattleCatsHacker& 34 | support_thanks=Anyone who has supported my work for giving me motivation to keep working on this and similar projects: &https://ko-fi.com/fieryhenry& 35 | discord_thanks=Everyone in the discord for giving me saves, reporting bugs, suggesting new features, and for being an amazing community: &https://discord.gg/DvmMgvn5ZB& 36 | 37 | # save selection 38 | data_transfer_message_enter=Enter details for data transfer: 39 | select_save_file_message=Select a save file: 40 | adb_pull_message_enter=Enter details for adb save pulling: 41 | pull_game_version_select=Select a game version to pull from: 42 | json_save_data_json_message=Select a save json file to load: 43 | 44 | # misc 45 | press_enter=Press enter to continue...: 46 | save_data_saved=Save data saved to &%s& 47 | # 1: save file path 48 | 49 | # errors 50 | error_save_json=Your save data seems to be in json format. Please use to import json option if you want to load json data. 51 | generic_error=An error occurred: &%s& 52 | # 1: error message 53 | -------------------------------------------------------------------------------- /src/BCSFE_Python/files/locales/en/user_input.properties: -------------------------------------------------------------------------------- 1 | enter_item_name_explain=Enter %s %s: 2 | # 1: broad item name (e.g stage) 3 | # 2: explanation 4 | enter_item_name_group_explain=Enter %s for %s &%s& %s: 5 | # 1: broad item name (e.g stage) 6 | # 2: group name 7 | # 3: item name 8 | # 4: explanation 9 | all_text=all 10 | edit_text=edit 11 | enter_range_text=Enter %s ids (You can enter &{{all_text}}& to get all, a range e.g &1&-&50&, or ids separate by spaces e.g &5 4 7&): 12 | # 1: group name 13 | select_list=What do you want to %s? (You can enter multiple values separated by spaces to %s multiple at once): 14 | # 1: action (e.g. select) 15 | # 2: action (e.g. select) 16 | select_option_to=Select an option to %s: 17 | # 1: action (e.g. edit) 18 | ask_individual=Do you want to edit %s individually (&1&), or all at once (&2&)?: 19 | # 1: item name 20 | select_all=&Select all& 21 | select=Select 22 | select_l=select 23 | 24 | invalid_input=Invalid input. 25 | invalid_all={{invalid_input}} You can't use &all& here. 26 | invalid_range_format={{invalid_input}} Please enter a valid range of numbers separated by a dash. 27 | invalid_range={{invalid_input}} Please enter a valid integer between 1 and %s. 28 | # 1: max number 29 | invalid_int={{invalid_input}} Please enter a valid integer. 30 | invalid_yes_no={{invalid_input}} Please enter &yes& or &no&. 31 | 32 | error_option=Please enter a recognised option 33 | error_no_options=No options available to select from -------------------------------------------------------------------------------- /src/BCSFE_Python/files/locales/th/main.properties: -------------------------------------------------------------------------------- 1 | # start up text 2 | welcome_message=ยินดีต้อนรับสู่ &Battle Cats Save File Editor& 3 | author_message=สร้างโดย &fieryhenry& 4 | github_message=GitHub: &https://github.com/fieryhenry/BCSFE-Python& 5 | discord_message=Discord: &https://discord.gg/DvmMgvn5ZB& - หากพบข้อผิดพลาดใดๆให้แจ้งได้ที่ห้อง &#bug-reports& ใน Discord หากมีข้อเสนอแนะสามารถบอกได้ที่ห้อง &#suggestions& 6 | donate_message=Donate: &https://ko-fi.com/fieryhenry& 7 | config_file_message=ที่อยู่ของไฟล์ตั้งค่า: &%s& 8 | scam_warning_message=ถ้าหากคุณซื้อโปรแกรมนี้มาแปลว่าคุณถูกหลอกแล้ว โปรแกรมนี้ฟรี หากมีคนทำเช่นนี้โปรดแจ้งเราที่ Discord 9 | 10 | # update info 11 | beta_message=คุณกำลังใช้เวอร์ชั่น &เบต้า& หากพบข้อผิดพลาดใดๆให้แจ้งได้ที่ห้อง &#bug-reports& ใน Discord พร้อมรายละเอียดและเวอร์ชั่นที่ใช้งาน 12 | update_check_failed=ผิดพลาดในการตรวจสอบอัพเดท 13 | local_version=กำลังใช้งานเวอร์ชั่น: &%s& 14 | latest_stable_version=เวอร์ชั่นล่าสุดในตอนนี้: &%s& 15 | latest_pre_release_version=ตัวเบต้าล่าสุดเวอร์ชั่น: &%s& 16 | update_available=ขณะนี้มีอัพเดทใหม่คุณต้องการอัพเดทไหม? 17 | 18 | # main options 19 | download_save=โหลดเซฟโดยใช้ transfer และ confirmation code 20 | select_save_file=โหลดเซฟจากเครื่อง 21 | adb_pull_save=ใช้ adb ดึงเซฟจาก android ที่ root แล้ว 22 | load_save_data_json=โหลดเซฟจากไฟล์ json 23 | android_direct_pull=ใช้สิทธิ root ดึงเซฟจากเครื่อง 24 | select_save_option_title=Select an option to get save data: 25 | 26 | # thanks 27 | thanks_title=ขอขอบคุณ: 28 | lethal_thanks=&Lethal's editor& ที่ให้แรงบันดาลใจแก่ฉันและทำให้ฉันได้เริ่มโปรเจกต์นี้ขึ้นมา และมันยังช่วยให้เข้าใจการแก้ไขเซฟ อย่าง catfood และ xp: &https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren& 29 | beeven_cse_thanks=โค้ดของ &Beeven& และ &csehydrogen's& ช่วยให้ฉันเข้าใจถึงวิธีการแก้ไขเซฟ: &https://github.com/beeven/battlecats& และ &https://github.com/csehydrogen/BattleCatsHacker& 30 | support_thanks=ขอบคุณถึงทุกคนที่สนับสนุนฉัน มันทำให้ฉันมีกำลังใจในการทำโปรเจกต์นี้ต่อไปและรวมถึงอื่นๆ: &https://ko-fi.com/fieryhenry& 31 | discord_thanks=ขอขอบคุณทุกคนใน Discord ที่ช่วยกันแจ้งปัญหา ข้อเสนอแนะ และช่วยเหลือกันสร้างสังคมที่น่าอยู่: &https://discord.gg/DvmMgvn5ZB& 32 | 33 | # save selection 34 | data_transfer_message_enter=กรอกรายละเอียดเพิ่มเติมจากการ data transfer: 35 | select_save_file_message=เลือกเซฟ: 36 | adb_pull_message_enter=กรอกรายละเอียดเพิ่มเติมจากการดึงเซฟด้วย adb: 37 | pull_game_version_select=เลือกเวอร์ชั่นของเซฟไฟล์: 38 | json_save_data_json_message=เลือกเซฟจากไฟล์ json: 39 | 40 | # misc 41 | select=เลือก 42 | press_enter=กด Enter เพื่อไปต่อ: 43 | 44 | save_data_saved=บันทึกข้อมูลเซฟแล้ว &%s& 45 | 46 | # errors 47 | error_option=กรุณาเลือก ตัวเลือกที่ถูกต้อง 48 | error_save_json=ไฟล์เซฟของคุณดูเหมือนจะเป็น json คุณต้องนำเข้าเซฟแบบ json ก่อน 49 | generic_error=เกิดข้อผิดพลาด: &%s& 50 | -------------------------------------------------------------------------------- /src/BCSFE_Python/files/order.json: -------------------------------------------------------------------------------- 1 | [ 2 | "game_version", 3 | "editor_version", 4 | "version", 5 | "inquiry_code", 6 | "token", 7 | "play_time", 8 | "account_created_time_stamp", 9 | "time", 10 | "cat_food", 11 | "xp", 12 | "normal_tickets", 13 | "rare_tickets", 14 | "platinum_tickets", 15 | "platinum_shards", 16 | "legend_tickets", 17 | "np", 18 | "leadership", 19 | "battle_items", 20 | "base_materials", 21 | "cat_fruit", 22 | "catseyes", 23 | "lucky_tickets_1", 24 | "lucky_tickets_2", 25 | "engineers", 26 | "catamins", 27 | "current_energy", 28 | "gamatoto_xp", 29 | "ototo_cannon", 30 | "talent_orbs", 31 | "cats", 32 | "cat_upgrades", 33 | "blue_upgrades", 34 | "unlocked_forms" 35 | ] -------------------------------------------------------------------------------- /src/BCSFE_Python/files/version.txt: -------------------------------------------------------------------------------- 1 | 2.7.2.6 2 | -------------------------------------------------------------------------------- /src/BCSFE_Python/game_data_getter.py: -------------------------------------------------------------------------------- 1 | """Get game data from the BCData GitHub repository.""" 2 | import os 3 | from typing import Optional 4 | import requests 5 | 6 | from . import helper 7 | 8 | URL = "https://raw.githubusercontent.com/fieryhenry/BCData/master/" 9 | 10 | 11 | def download_file( 12 | game_version: str, 13 | pack_name: str, 14 | file_name: str, 15 | get_data: bool = True, 16 | print_progress: bool = True, 17 | ) -> bytes: 18 | """ 19 | Downloads the file. 20 | 21 | Args: 22 | game_version (str): The game version to download from. 23 | pack_name (str): The pack name to download from. 24 | file_name (str): The file name to download. 25 | get_data (bool, optional): Whether to return the data. Defaults to True. 26 | print_progress (bool, optional): Whether to print the progress. Defaults to True. 27 | 28 | Returns: 29 | bytes: The data of the file. 30 | """ 31 | 32 | path = helper.get_file(os.path.join("game_data", game_version, pack_name)) 33 | file_path = os.path.join(path, file_name) 34 | if os.path.exists(file_path): 35 | if get_data: 36 | return helper.read_file_bytes(file_path) 37 | return b"" 38 | 39 | if print_progress: 40 | helper.colored_text( 41 | f"Downloading game data file &{file_name}& from &{pack_name}& with game version &{game_version}&", 42 | helper.GREEN, 43 | helper.WHITE, 44 | ) 45 | url = URL + game_version + "/" + pack_name + "/" + file_name 46 | response = requests.get(url) 47 | 48 | helper.create_dirs(path) 49 | helper.write_file_bytes(file_path, response.content) 50 | return response.content 51 | 52 | 53 | def get_latest_versions() -> Optional[list[str]]: 54 | """ 55 | Gets the latest versions of the game data. 56 | 57 | Returns: 58 | Optional[list[str]]: The latest versions of the game data. 59 | """ 60 | try: 61 | response = requests.get(URL + "latest.txt") 62 | except requests.exceptions.ConnectionError: 63 | return None 64 | versions = response.text.splitlines() 65 | return versions 66 | 67 | 68 | def get_latest_version(is_jp: bool) -> Optional[str]: 69 | """ 70 | Gets the latest version of the game data. 71 | 72 | Args: 73 | is_jp (bool): Whether to get the japanese version. 74 | 75 | Returns: 76 | str: The latest version of the game data. 77 | """ 78 | versions = get_latest_versions() 79 | if versions is None: 80 | return None 81 | if is_jp: 82 | return versions[1] 83 | else: 84 | return versions[0] 85 | 86 | 87 | def get_file_latest(pack_name: str, file_name: str, is_jp: bool) -> Optional[bytes]: 88 | """ 89 | Gets the latest version of the file. 90 | 91 | Args: 92 | pack_name (str): The pack name to find. 93 | file_name (str): The file name to find. 94 | is_jp (bool): Whether to get the japanese version. 95 | 96 | Returns: 97 | Optional[bytes]: The data of the file. 98 | """ 99 | version = get_latest_version(is_jp) 100 | if version is None: 101 | return None 102 | return download_file(version, pack_name, file_name) 103 | 104 | 105 | def get_file_latest_path(path: str, is_jp: bool) -> Optional[bytes]: 106 | """ 107 | Gets the latest version of the file. 108 | 109 | Args: 110 | path (str): The path to find. 111 | is_jp (bool): Whether to get the japanese version. 112 | 113 | Returns: 114 | Optional[bytes]: The data of the file. 115 | """ 116 | version = get_latest_version(is_jp) 117 | if version is None: 118 | return None 119 | packname, filename = path.split("/") 120 | return download_file(version, packname, filename) 121 | 122 | 123 | def get_path(pack_name: str, file_name: str, is_jp: bool) -> Optional[str]: 124 | """ 125 | Gets the path of the file. 126 | 127 | Args: 128 | pack_name (str): The pack name to find. 129 | file_name (str): The file name to find. 130 | is_jp (bool): Whether to get the japanese version. 131 | 132 | Returns: 133 | Optional[str]: The path of the file. 134 | """ 135 | version = get_latest_version(is_jp) 136 | if version is None: 137 | return None 138 | return os.path.join("game_data", version, pack_name, file_name) 139 | 140 | 141 | def check_remove(new_version: str, is_jp: bool): 142 | """ 143 | Checks if older game data is downloaded, and deletes if out of date. 144 | 145 | Args: 146 | new_version (str): The new version. 147 | is_jp (bool): Whether to get the japanese version. 148 | """ 149 | all_versions = helper.get_dirs(helper.get_file("game_data")) 150 | for version in all_versions: 151 | if is_jp: 152 | if "jp" not in version: 153 | continue 154 | if version != new_version: 155 | helper.delete_dir(helper.get_file(os.path.join("game_data", version))) 156 | else: 157 | if "jp" in version: 158 | continue 159 | if version != new_version: 160 | helper.delete_dir(helper.get_file(os.path.join("game_data", version))) 161 | 162 | 163 | def check_remove_handler(): 164 | """ 165 | Checks if older game data is downloaded, and deletes if out of date. 166 | """ 167 | 168 | versions = get_latest_versions() 169 | if versions is None: 170 | return None 171 | check_remove(versions[0], is_jp=False) 172 | check_remove(versions[1], is_jp=True) 173 | -------------------------------------------------------------------------------- /src/BCSFE_Python/item.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | from . import ( 3 | managed_item, 4 | user_input_handler, 5 | helper, 6 | locale_handler, 7 | config_manager, 8 | user_info, 9 | ) 10 | 11 | 12 | class Bannable: 13 | def __init__( 14 | self, 15 | type: "managed_item.ManagedItemType", 16 | inquiry_code: str, 17 | work_around: str = "", 18 | ): 19 | self.type = type 20 | self.inquiry_code = inquiry_code 21 | self.work_around = work_around 22 | 23 | 24 | class Int: 25 | def __init__(self, value: Optional[int], byte_size: int = 4, signed: bool = True): 26 | self.value = value 27 | self.byte_size = byte_size 28 | self.signed = signed 29 | 30 | def get_max_value(self) -> int: 31 | if self.signed: 32 | return (2 ** (self.byte_size * 8 - 1)) - 1 33 | return (2 ** (self.byte_size * 8)) - 1 34 | 35 | 36 | class IntItem: 37 | def __init__( 38 | self, 39 | name: str, 40 | value: Int, 41 | max_value: Optional[int], 42 | bannable: Optional[Bannable] = None, 43 | offset: int = 0, 44 | ): 45 | self.name = name 46 | self.__value = value 47 | disable_maxes = config_manager.get_config_value_category( 48 | "EDITOR", "DISABLE_MAXES" 49 | ) 50 | self.max_value = max_value 51 | if disable_maxes: 52 | self.max_value = None 53 | self.bannable = bannable 54 | self.offset = offset 55 | self.locale_manager = locale_handler.LocalManager.from_config() 56 | 57 | def get_max_value(self) -> int: 58 | if self.max_value is not None: 59 | return self.max_value 60 | return self.__value.get_max_value() 61 | 62 | def show_ban_warning(self) -> bool: 63 | if self.bannable is None: 64 | return True 65 | helper.colored_text(self.locale_manager.search_key("ban_warning") % self.name) 66 | if self.bannable.work_around: 67 | helper.colored_text(self.bannable.work_around) 68 | return user_input_handler.get_yes_no( 69 | self.locale_manager.search_key("ban_warning_leave") 70 | ) 71 | 72 | def edit(self) -> None: 73 | end = not self.show_ban_warning() 74 | if end: 75 | return 76 | original_value = self.__value.value 77 | helper.colored_text( 78 | self.locale_manager.search_key("current_item_value") 79 | % (self.name, self.get_value_off()) 80 | ) 81 | max_str = "" 82 | if self.max_value is not None: 83 | max_str = " " + self.locale_manager.search_key("max_str") % self.max_value 84 | new_value = user_input_handler.get_int( 85 | self.locale_manager.search_key("enter_value_text") % (self.name, max_str), 86 | ) 87 | new_value -= self.offset 88 | new_value = helper.clamp(new_value, 0, self.get_max_value()) 89 | self.__value.value = new_value 90 | helper.colored_text( 91 | self.locale_manager.search_key("item_value_changed") 92 | % ( 93 | self.name, 94 | 0 if original_value is None else original_value, 95 | self.get_value_off(), 96 | ) 97 | ) 98 | if self.bannable is not None and self.__value.value != original_value: 99 | new_value = self.__value.value 100 | if original_value is None: 101 | original_value = 0 102 | 103 | info = user_info.UserInfo(self.bannable.inquiry_code) 104 | info.update_item(self.bannable.type, self.__value.value - original_value) 105 | 106 | def get_value_off(self) -> int: 107 | if self.__value.value is None: 108 | return 0 109 | return self.__value.value + self.offset 110 | 111 | def get_value(self) -> int: 112 | if self.__value.value is None: 113 | return 0 114 | return self.__value.value 115 | 116 | def get_value_none(self) -> Optional[int]: 117 | return self.__value.value 118 | 119 | def set_value(self, value: int) -> None: 120 | self.__value.value = value 121 | 122 | 123 | class IntItemGroup: 124 | def __init__(self, group_name: str, items: list[IntItem]): 125 | self.items = items 126 | self.locale_manager = locale_handler.LocalManager.from_config() 127 | self.group_name = group_name 128 | 129 | def get_values(self) -> list[int]: 130 | return [item.get_value() for item in self.items] 131 | 132 | def get_values_none(self) -> list[Optional[int]]: 133 | return [item.get_value_none() for item in self.items] 134 | 135 | def get_values_off(self) -> list[int]: 136 | return [item.get_value_off() for item in self.items] 137 | 138 | def all_none(self) -> bool: 139 | return all([item.get_value_none() is None for item in self.items]) 140 | 141 | def get_names(self) -> list[str]: 142 | return [item.name for item in self.items] 143 | 144 | def edit(self) -> None: 145 | if not self.items: 146 | return 147 | ids, individual = user_input_handler.select_options( 148 | self.get_names(), 149 | self.locale_manager.search_key("select_l"), 150 | self.get_values_off() if not self.all_none() else None, 151 | ) 152 | if individual: 153 | for id in ids: 154 | self.items[id].edit() 155 | else: 156 | max_value = self.get_max_max_value() 157 | offset = self.items[ids[0]].offset 158 | max_str = "" 159 | if self.items[ids[0]].max_value is not None: 160 | max_str = " " + self.locale_manager.search_key("max_str") % ( 161 | max_value + offset 162 | ) 163 | new_value = user_input_handler.get_int( 164 | self.locale_manager.search_key("enter_value_text") 165 | % (self.group_name, max_str) 166 | ) 167 | new_value -= offset 168 | entered_value = helper.clamp(new_value, 0, max_value) 169 | for id in ids: 170 | max_value = self.items[id].get_max_value() 171 | value = helper.clamp(new_value, 0, max_value) 172 | self.items[id].set_value(value) 173 | 174 | helper.colored_text( 175 | self.locale_manager.search_key("success_set") 176 | % (self.group_name, entered_value + offset) 177 | ) 178 | 179 | def get_max_max_value(self) -> int: 180 | return max([item.get_max_value() for item in self.items]) 181 | 182 | @staticmethod 183 | def from_lists( 184 | names: list[str], 185 | values: Optional[list[int]], 186 | maxes: Union[list[int], int, None], 187 | group_name: str, 188 | offset: int = 0, 189 | ) -> "IntItemGroup": 190 | items: list[IntItem] = [] 191 | for i in range(len(names)): 192 | max_value = maxes[i] if isinstance(maxes, list) else maxes 193 | try: 194 | value = values[i] if values is not None else None 195 | except IndexError: 196 | value = None 197 | items.append( 198 | IntItem( 199 | names[i], 200 | Int(value), 201 | max_value, 202 | offset=offset, 203 | ) 204 | ) 205 | return IntItemGroup(group_name, items) 206 | 207 | 208 | class StrItem: 209 | def __init__(self, name: str, value: str): 210 | self.name = name 211 | self.value = value 212 | self.locale_manager = locale_handler.LocalManager.from_config() 213 | 214 | def edit(self) -> None: 215 | original_value = self.value 216 | helper.colored_text( 217 | self.locale_manager.search_key("current_item_value") 218 | % (self.name, self.value) 219 | ) 220 | new_value = user_input_handler.colored_input( 221 | self.locale_manager.search_key("enter_value_text") % (self.name, "") 222 | ) 223 | self.value = new_value 224 | helper.colored_text( 225 | self.locale_manager.search_key("item_value_changed") 226 | % (self.name, original_value, self.value) 227 | ) 228 | 229 | def get_value(self) -> str: 230 | return self.value 231 | -------------------------------------------------------------------------------- /src/BCSFE_Python/locale_handler.py: -------------------------------------------------------------------------------- 1 | from . import config_manager, helper 2 | import os 3 | 4 | 5 | class PropertySet: 6 | def __init__(self, locale: str, property: str): 7 | self.locale = locale 8 | self.path = os.path.join( 9 | helper.get_local_files_path(), "locales", locale, property + ".properties" 10 | ) 11 | if not os.path.exists(self.path): 12 | os.makedirs(os.path.dirname(self.path)) 13 | self.properties: dict[str, str] = {} 14 | self.parse() 15 | 16 | def parse(self): 17 | lines = helper.read_file_string(self.path).splitlines() 18 | for line in lines: 19 | if line.startswith("#") or line == "": 20 | continue 21 | parts = line.split("=") 22 | if len(parts) < 2: 23 | continue 24 | key = parts[0] 25 | value = "=".join(parts[1:]) 26 | self.properties[key] = value 27 | 28 | def get_key(self, key: str) -> str: 29 | return self.properties[key].replace("\\n", "\n") 30 | 31 | @staticmethod 32 | def from_config(property: str) -> "PropertySet": 33 | return PropertySet(config_manager.get_config_value("LOCALE"), property) 34 | 35 | 36 | class LocalManager: 37 | def __init__(self, locale: str): 38 | self.locale = locale 39 | self.path = os.path.join(helper.get_local_files_path(), "locales", locale) 40 | if not os.path.exists(self.path): 41 | os.makedirs(self.path) 42 | self.properties: dict[str, PropertySet] = {} 43 | self.is_en = locale == "en" 44 | if not self.is_en: 45 | self.en_path = os.path.join(helper.get_local_files_path(), "locales", "en") 46 | if not os.path.exists(self.en_path): 47 | os.makedirs(self.en_path) 48 | self.en_properties: dict[str, PropertySet] = {} 49 | self.parse() 50 | 51 | def parse(self): 52 | for file in helper.get_files_in_dir(self.path): 53 | file_name = os.path.basename(file) 54 | if file_name.endswith(".properties"): 55 | self.properties[file_name[:-11]] = PropertySet( 56 | self.locale, file_name[:-11] 57 | ) 58 | if not self.is_en: 59 | for file in helper.get_files_in_dir(self.en_path): 60 | file_name = os.path.basename(file) 61 | if file_name.endswith(".properties"): 62 | self.en_properties[file_name[:-11]] = PropertySet( 63 | "en", file_name[:-11] 64 | ) 65 | 66 | def get_key(self, property: str, key: str) -> str: 67 | return self.properties[property].get_key(key) 68 | 69 | def search_key(self, key: str) -> str: 70 | value = None 71 | for prop in self.properties.values(): 72 | if key in prop.properties: 73 | value = prop.get_key(key) 74 | if not self.is_en: 75 | for prop in self.en_properties.values(): 76 | if key in prop.properties: 77 | value = prop.get_key(key) 78 | if value is None: 79 | raise KeyError(f"Key {key} not found") 80 | 81 | while True: 82 | start = value.find("{{") 83 | if start == -1: 84 | break 85 | end = value.find("}}") 86 | if end == -1: 87 | break 88 | value = value.replace( 89 | value[start : end + 2], self.search_key(value[start + 2 : end]) 90 | ) 91 | return value 92 | 93 | @staticmethod 94 | def from_config() -> "LocalManager": 95 | return LocalManager(config_manager.get_config_value("LOCALE")) 96 | 97 | @staticmethod 98 | def get_locales() -> list[str]: 99 | return helper.get_dirs(os.path.join(helper.get_local_files_path(), "locales")) 100 | -------------------------------------------------------------------------------- /src/BCSFE_Python/managed_item.py: -------------------------------------------------------------------------------- 1 | """ManagedItem class for the BCSFE editor.""" 2 | 3 | from enum import Enum 4 | from typing import Any 5 | import uuid 6 | from . import server_handler 7 | 8 | 9 | class DetailType(Enum): 10 | """Enum for the different types of details.""" 11 | 12 | GET = "get" 13 | USE = "use" 14 | 15 | 16 | class ManagedItemType(Enum): 17 | """Enum for the different types of managed items.""" 18 | 19 | CATFOOD = "catfood" 20 | RARE_TICKET = "rareTicket" 21 | PLATINUM_TICKET = "platinumTicket" 22 | LEGEND_TICKET = "legendTicket" 23 | 24 | 25 | class ManagedItem: 26 | """Managed item for backupmetadata""" 27 | 28 | def __init__( 29 | self, amount: int, detail_type: DetailType, managed_item_type: ManagedItemType 30 | ): 31 | self.amount = amount 32 | self.detail_type = detail_type 33 | self.managed_item_type = managed_item_type 34 | self.detail_code = str(uuid.uuid4()) 35 | self.detail_created_at = server_handler.get_current_time() - 109600 36 | 37 | def to_dict(self) -> dict[str, Any]: 38 | """Convert the managed item to a dictionary.""" 39 | 40 | data = { 41 | "amount": self.amount, 42 | "detailCode": self.detail_code, 43 | "detailCreatedAt": self.detail_created_at, 44 | "detailType": self.detail_type.value, 45 | "managedItemType": self.managed_item_type.value, 46 | } 47 | return data 48 | 49 | -------------------------------------------------------------------------------- /src/BCSFE_Python/patcher.py: -------------------------------------------------------------------------------- 1 | """Handler for patching save data""" 2 | 3 | import hashlib 4 | from typing import Union 5 | 6 | 7 | def get_md5_sum(data: bytes) -> str: 8 | """Get MD5 sum of data.""" 9 | 10 | return hashlib.md5(data).hexdigest() 11 | 12 | 13 | def get_save_data_sum(save_data: bytes, game_version: str) -> str: 14 | """Get MD5 sum of save data.""" 15 | 16 | if game_version in ("jp", "ja"): 17 | game_version = "" 18 | 19 | salt = f"battlecats{game_version}".encode("utf-8") 20 | data_to_hash = salt + save_data[:-32] 21 | 22 | return get_md5_sum(data_to_hash) 23 | 24 | 25 | def detect_game_version(save_data: bytes) -> Union[str, None]: 26 | """Detect the game version of the save file""" 27 | 28 | if not save_data: 29 | return None 30 | 31 | game_versions = [ 32 | "jp", 33 | "en", 34 | "kr", 35 | "tw", 36 | ] 37 | try: 38 | curr_hash = save_data[-32:].decode("utf-8") 39 | except UnicodeDecodeError as err: 40 | raise Exception("Invalid save hash") from err 41 | 42 | for game_version in game_versions: 43 | if curr_hash == get_save_data_sum(save_data, game_version): 44 | return game_version 45 | return None 46 | 47 | 48 | def patch_save_data(save_data: bytes, game_version: str) -> bytes: 49 | """Set the md5 sum of the save data""" 50 | 51 | save_hash = get_save_data_sum(save_data, game_version) 52 | save_data = save_data[:-32] + save_hash.encode("utf-8") 53 | return save_data 54 | -------------------------------------------------------------------------------- /src/BCSFE_Python/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieryhenry/BCSFE-Python/648aa49526b1d7422ce4edb68d0373a3da136e31/src/BCSFE_Python/py.typed -------------------------------------------------------------------------------- /src/BCSFE_Python/root_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from typing import Optional 4 | from . import helper 5 | 6 | 7 | def get_data_path() -> str: 8 | """ 9 | Get the data path 10 | 11 | Returns: 12 | str: The data path 13 | """ 14 | return "/data/data/" 15 | 16 | 17 | def get_installed_battlecats_versions() -> Optional[list[str]]: 18 | """ 19 | Get a list of installed battle cats versions 20 | 21 | Returns: 22 | Optional[list[str]]: A list of installed battle cats versions 23 | """ 24 | if not is_ran_as_root(): 25 | return None 26 | path = get_data_path() 27 | if not os.path.exists(path): 28 | return None 29 | versions: list[str] = [] 30 | for folder in os.listdir(path): 31 | if folder == "jp.co.ponos.battlecats": 32 | versions.append("jp") 33 | elif folder.startswith("jp.co.ponos.battlecats"): 34 | versions.append(folder.replace("jp.co.ponos.battlecats", "")) 35 | return versions 36 | 37 | 38 | def pull_save_data(game_version: str) -> Optional[str]: 39 | """ 40 | Pull save data from a game version 41 | 42 | Args: 43 | game_version (str): The game version to pull from 44 | 45 | Returns: 46 | Optional[str]: The path to the pulled save data 47 | """ 48 | if not is_ran_as_root(): 49 | return None 50 | package_name = "jp.co.ponos.battlecats" + game_version.replace("jp", "") 51 | path = get_data_path() + package_name + "/files/SAVE_DATA" 52 | if not os.path.exists(path): 53 | return None 54 | return path 55 | 56 | 57 | def is_ran_as_root() -> bool: 58 | """ 59 | Check if the program is ran as root 60 | 61 | Returns: 62 | bool: If the program is ran as root 63 | """ 64 | if not helper.is_android(): 65 | return False 66 | try: 67 | os.listdir(get_data_path()) 68 | except PermissionError: 69 | helper.colored_text( 70 | "Root access is required to get installed game versions. Try adding sudo before the run command", 71 | base=helper.RED, 72 | ) 73 | return False 74 | return True 75 | 76 | 77 | def rerun_game(version: str) -> None: 78 | """ 79 | Rerun the game on the device without adb 80 | 81 | Args: 82 | version (str): The game version to rerun 83 | """ 84 | if not is_ran_as_root(): 85 | return 86 | package_name = "jp.co.ponos.battlecats" + version.replace("jp", "") 87 | subprocess.run( 88 | f"sudo pkill -f {package_name}", capture_output=True, check=False, shell=True 89 | ) 90 | subprocess.run( 91 | f"sudo monkey -p {package_name} -c android.intent.category.LAUNCHER 1", 92 | capture_output=True, 93 | check=False, 94 | shell=True, 95 | ) 96 | -------------------------------------------------------------------------------- /src/BCSFE_Python/updater.py: -------------------------------------------------------------------------------- 1 | """Update the editor""" 2 | 3 | import subprocess 4 | from typing import Any, Optional 5 | 6 | import requests 7 | 8 | from . import config_manager, helper 9 | 10 | 11 | def update(latest_version: str, command: str = "py") -> bool: 12 | """Update pypi package testing for py and python""" 13 | 14 | helper.colored_text("Updating...", base=helper.GREEN) 15 | try: 16 | full_cmd = f"{command} -m pip install --upgrade battle-cats-save-editor=={latest_version}" 17 | subprocess.run( 18 | full_cmd, 19 | shell=True, 20 | capture_output=True, 21 | check=True, 22 | ) 23 | helper.colored_text("Update successful", base=helper.GREEN) 24 | return True 25 | except subprocess.CalledProcessError: 26 | return False 27 | 28 | 29 | def try_update(latest_version: str): 30 | """ 31 | Try to update the editor 32 | 33 | Args: 34 | latest_version (str): The latest version of the editor 35 | """ 36 | success = update(latest_version, "py") 37 | if success: 38 | return 39 | success = update(latest_version, "python3") 40 | if success: 41 | return 42 | success = update(latest_version, "python") 43 | if success: 44 | return 45 | helper.colored_text( 46 | "Update failed\nYou may need to manually update with py -m pip install -U battle-cats-save-editor", 47 | base=helper.RED, 48 | ) 49 | 50 | 51 | def get_local_version() -> str: 52 | """Returns the local version of the editor""" 53 | 54 | return helper.read_file_string(helper.get_file("version.txt")) 55 | 56 | 57 | def get_version_info() -> Optional[tuple[str, str]]: 58 | """Gets the latest version of the program""" 59 | 60 | package_name = "battle-cats-save-editor" 61 | try: 62 | response = requests.get(f"https://pypi.org/pypi/{package_name}/json") 63 | response.raise_for_status() 64 | data = response.json() 65 | except requests.exceptions.RequestException: 66 | return None 67 | 68 | info = ( 69 | get_pypi_version(data), 70 | get_latest_prerelease_version(data), 71 | ) 72 | return info 73 | 74 | 75 | def get_pypi_version(data: dict[str, Any]) -> str: 76 | """Get latest pypi version of the program""" 77 | return data["info"]["version"] 78 | 79 | 80 | def get_latest_prerelease_version(data: dict[str, Any]) -> str: 81 | """Get latest prerelease version of the program""" 82 | releases = list(data["releases"]) 83 | releases.reverse() 84 | for release in releases: 85 | if "b" in release: 86 | return release 87 | return "" 88 | 89 | 90 | def pypi_is_newer(local_version: str, pypi_version: str, remove_b: bool = True) -> bool: 91 | """Checks if the local version is newer than the pypi version""" 92 | if remove_b: 93 | if "b" in pypi_version: 94 | pypi_version = pypi_version.split("b")[0] 95 | if "b" in local_version: 96 | local_version = local_version.split("b")[0] 97 | 98 | return pypi_version > local_version 99 | 100 | 101 | def check_update(version_info: tuple[str, str]) -> tuple[bool, str]: 102 | """Checks if the editor is updated""" 103 | 104 | local_version = get_local_version() 105 | pypi_version, latest_prerelease_version = version_info 106 | 107 | check_pre = "b" in local_version or config_manager.get_config_value_category( 108 | "START_UP", "UPDATE_TO_BETAS" 109 | ) 110 | if check_pre and pypi_is_newer( 111 | local_version, latest_prerelease_version, remove_b=False 112 | ): 113 | helper.colored_text("Prerelease update available\n", base=helper.GREEN) 114 | return True, latest_prerelease_version 115 | 116 | if pypi_is_newer(local_version, pypi_version): 117 | helper.colored_text("Stable update available\n", base=helper.GREEN) 118 | return True, pypi_version 119 | 120 | helper.colored_text("No update available\n", base=helper.GREEN) 121 | return False, local_version 122 | -------------------------------------------------------------------------------- /src/BCSFE_Python/user_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | from . import config_manager, managed_item, helper 4 | import os 5 | 6 | 7 | class ManagedItems: 8 | def __init__(self, managed_items: dict[managed_item.ManagedItemType, int]): 9 | self.managed_items = managed_items 10 | 11 | def to_dict(self) -> dict[str, int]: 12 | """Convert to dict""" 13 | 14 | return { 15 | item.value: self.managed_items[item] 16 | for item in managed_item.ManagedItemType 17 | } 18 | 19 | @staticmethod 20 | def from_dict(data: dict[str, int]) -> "ManagedItems": 21 | """Convert from dict""" 22 | 23 | managed_items = { 24 | managed_item.ManagedItemType(item): data[item] for item in data 25 | } 26 | return ManagedItems(managed_items) 27 | 28 | 29 | class UserInfo: 30 | def __init__(self, inquiry_code: str): 31 | self.inquiry_code = inquiry_code 32 | self.read_user_info() 33 | 34 | def get_path(self) -> str: 35 | """Get the path to the user info""" 36 | 37 | app_data_folder = config_manager.get_app_data_folder() 38 | path = os.path.join(app_data_folder, "user_info", self.inquiry_code + ".json") 39 | os.makedirs(os.path.dirname(path), exist_ok=True) 40 | return path 41 | 42 | def create_empty_user_info(self): 43 | managed_items = {item: 0 for item in managed_item.ManagedItemType} 44 | managed_items = ManagedItems(managed_items) 45 | data = { 46 | "managedItems": managed_items.to_dict(), 47 | "password": "", 48 | "authToken": "", 49 | } 50 | self.write_user_info(data) 51 | 52 | def read_user_info(self): 53 | if not os.path.exists(self.get_path()): 54 | self.create_empty_user_info() 55 | data = helper.read_file_string(self.get_path()) 56 | try: 57 | data = json.loads(data) 58 | except json.JSONDecodeError: 59 | self.create_empty_user_info() 60 | data = helper.read_file_string(self.get_path()) 61 | data = json.loads(data) 62 | self.managed_items = ManagedItems.from_dict(data["managedItems"]) 63 | self.password = data["password"] 64 | self.auth_token = data["authToken"] 65 | 66 | def write_user_info(self, data: dict[str, Any]): 67 | helper.write_file_string(self.get_path(), json.dumps(data, indent=4)) 68 | 69 | def save(self): 70 | data = { 71 | "managedItems": self.managed_items.to_dict(), 72 | "password": self.password, 73 | "authToken": self.auth_token, 74 | } 75 | self.write_user_info(data) 76 | 77 | def get_managed_items(self) -> ManagedItems: 78 | return self.managed_items 79 | 80 | def get_password(self) -> str: 81 | return self.password 82 | 83 | def get_auth_token(self) -> str: 84 | return self.auth_token 85 | 86 | def set_managed_items(self, managed_items: ManagedItems): 87 | self.managed_items = managed_items 88 | self.save() 89 | 90 | def set_password(self, password: str): 91 | self.password = password 92 | self.save() 93 | 94 | def set_auth_token(self, auth_token: str): 95 | self.auth_token = auth_token 96 | self.save() 97 | 98 | def clear_managed_items(self): 99 | self.managed_items = ManagedItems( 100 | {item: 0 for item in managed_item.ManagedItemType} 101 | ) 102 | self.save() 103 | 104 | def get_managed_items_lst(self) -> list[managed_item.ManagedItem]: 105 | items: list[managed_item.ManagedItem] = [] 106 | for item in self.managed_items.managed_items: 107 | value = self.managed_items.managed_items[item] 108 | if value > 0: 109 | detail_type = managed_item.DetailType.GET 110 | elif value < 0: 111 | detail_type = managed_item.DetailType.USE 112 | value = abs(value) 113 | else: 114 | continue 115 | items.append(managed_item.ManagedItem(value, detail_type, item)) 116 | return items 117 | 118 | def has_managed_items(self) -> bool: 119 | for item in self.managed_items.managed_items: 120 | value = self.managed_items.managed_items[item] 121 | if value != 0: 122 | return True 123 | return False 124 | 125 | def update_item(self, item_type: managed_item.ManagedItemType, amount: int): 126 | self.managed_items.managed_items[item_type] += amount 127 | self.save() 128 | 129 | @staticmethod 130 | def clear_all_items(): 131 | app_data_folder = config_manager.get_app_data_folder() 132 | path = os.path.join(app_data_folder, "user_info") 133 | os.makedirs(path, exist_ok=True) 134 | files = helper.get_files_in_dir(path) 135 | for file in files: 136 | if file.endswith(".json"): 137 | info = UserInfo(file.replace(".json", "")) 138 | info.clear_managed_items() 139 | -------------------------------------------------------------------------------- /src/BCSFE_Python/user_input_handler.py: -------------------------------------------------------------------------------- 1 | """Handler for user input""" 2 | 3 | from typing import Any, Optional, Tuple, Union 4 | 5 | from . import helper, locale_handler 6 | 7 | 8 | def handle_all_at_once( 9 | ids: list[int], 10 | all_at_once: bool, 11 | data: list[int], 12 | names: list[Any], 13 | item_name: str, 14 | group_name: str, 15 | explain_text: str = "", 16 | ) -> list[int]: 17 | """Handle all at once option""" 18 | locale_manager = locale_handler.LocalManager.from_config() 19 | first = True 20 | value = None 21 | for item_id in ids: 22 | if all_at_once and first: 23 | value = helper.check_int( 24 | colored_input( 25 | locale_manager.search_key("enter_item_name_explain") 26 | % (item_name, explain_text) 27 | ) 28 | ) 29 | first = False 30 | elif not all_at_once: 31 | value = helper.check_int( 32 | colored_input( 33 | locale_manager.search_key("enter_item_name_group_explain") 34 | % (item_name, group_name, names[item_id], explain_text) 35 | ) 36 | ) 37 | if value is None: 38 | continue 39 | data[item_id] = value 40 | return data 41 | 42 | 43 | def create_all_list( 44 | ids: list[str], 45 | max_val: int, 46 | ) -> dict[str, Any]: 47 | """Creates a list with an all at once option""" 48 | 49 | all_at_once = False 50 | if f"{max_val}" in ids: 51 | ids_s = list(range(1, max_val)) 52 | ids = [format(x, "02d") for x in ids_s] 53 | all_at_once = True 54 | return {"ids": ids, "at_once": all_at_once} 55 | 56 | 57 | def create_all_list_inc(ids: list[str], max_val: int) -> dict[str, Any]: 58 | """Creates a list with an all at once option and include all""" 59 | 60 | return create_all_list(ids, max_val) 61 | 62 | 63 | def create_all_list_not_inc(ids: list[str], max_val: int) -> list[str]: 64 | """Creates a list with an all at once option and don't include all""" 65 | 66 | return create_all_list(ids, max_val)["ids"] 67 | 68 | 69 | def get_range( 70 | usr_input: str, 71 | length: Union[int, None] = None, 72 | min_val: int = 0, 73 | all_ids: Union[list[int], None] = None, 74 | ) -> list[int]: 75 | """Get a range of numbers from user input""" 76 | locale_manager = locale_handler.LocalManager.from_config() 77 | ids: list[int] = [] 78 | for item in usr_input.split(" "): 79 | if item.lower() == locale_manager.search_key("all_text").lower(): 80 | if length is None and all_ids is None: 81 | helper.colored_text( 82 | locale_manager.search_key("invalid_all"), helper.RED 83 | ) 84 | return [] 85 | if all_ids: 86 | return all_ids 87 | if length is not None: 88 | return list(range(min_val, length)) 89 | if "-" in item: 90 | start_s, end_s = item.split("-") 91 | start = helper.check_int(start_s) 92 | end = helper.check_int(end_s) 93 | if start is None or end is None: 94 | helper.colored_text( 95 | locale_manager.search_key("invalid_range_format"), 96 | helper.RED, 97 | ) 98 | return ids 99 | if start > end: 100 | start, end = end, start 101 | ids.extend(list(range(start, end + 1))) 102 | else: 103 | item_id = helper.check_int(item) 104 | if item_id is None: 105 | helper.colored_text( 106 | locale_manager.search_key("invalid_int"), helper.RED 107 | ) 108 | return ids 109 | ids.append(item_id) 110 | return ids 111 | 112 | 113 | def colored_input( 114 | dialog: str, base: Optional[str] = None, new: Optional[str] = None 115 | ) -> str: 116 | """Format dialog as a colored string""" 117 | if base is None: 118 | base = helper.WHITE 119 | if new is None: 120 | new = helper.DARK_YELLOW 121 | helper.colored_text(dialog, end="", base=base, new=new) 122 | return input() 123 | 124 | 125 | def get_range_ids(group_name: str, length: int) -> list[int]: 126 | """Get a range of ids from user input""" 127 | 128 | locale_manager = locale_handler.LocalManager.from_config() 129 | ids = get_range( 130 | colored_input(locale_manager.search_key("enter_range_text") % (group_name)), 131 | length, 132 | ) 133 | return ids 134 | 135 | 136 | def select_options( 137 | options: list[str], 138 | mode: Optional[str] = None, 139 | extra_data: Union[list[Any], None] = None, 140 | offset: int = 0, 141 | ) -> Tuple[list[int], bool]: 142 | """Select an option or multiple options from a list""" 143 | 144 | if len(options) == 1: 145 | return [0], True 146 | 147 | locale_manager = locale_handler.LocalManager.from_config() 148 | if mode is None: 149 | mode = locale_manager.search_key("edit_text") 150 | 151 | helper.colored_list(options, extra_data=extra_data, offset=offset) 152 | total = len(options) 153 | helper.colored_text(f"{total+1}. {locale_manager.search_key('select_all')}") 154 | ids_s = colored_input( 155 | locale_manager.search_key("select_list") % (mode, mode) 156 | ).split(" ") 157 | individual = True 158 | if str(total + 1) in ids_s: 159 | ids = list(range(1, total + 1)) 160 | individual = False 161 | ids_s = helper.int_to_str_ls(ids) 162 | 163 | ids = helper.parse_int_list(ids_s, -1) 164 | for item_id in ids: 165 | if item_id < 0 or item_id > total - 1: 166 | helper.colored_text( 167 | locale_manager.search_key("invalid_range") % (total + 1), 168 | helper.RED, 169 | ) 170 | return select_options(options, mode, extra_data, offset) 171 | return ids, individual 172 | 173 | 174 | def select_inc( 175 | options: list[str], 176 | mode: Optional[str] = None, 177 | extra_data: Union[list[Any], None] = None, 178 | offset: int = 0, 179 | ) -> Tuple[list[int], bool]: 180 | """Select an option or multiple options from a list and include all""" 181 | 182 | return select_options(options, mode, extra_data, offset) 183 | 184 | 185 | def select_not_inc( 186 | options: list[str], 187 | mode: Optional[str] = None, 188 | extra_data: Union[list[Any], None] = None, 189 | offset: int = 0, 190 | ) -> list[int]: 191 | """Select an option or multiple options from a list and don't include all""" 192 | return select_options(options, mode, extra_data, offset)[0] 193 | 194 | 195 | def select_single( 196 | options: list[str], 197 | mode: Optional[str] = None, 198 | title: str = "", 199 | allow_text: bool = False, 200 | ) -> int: 201 | "Select a single option from a list" 202 | locale_manager = locale_handler.LocalManager.from_config() 203 | if not options: 204 | raise ValueError(locale_manager.search_key("error_no_options")) 205 | if len(options) == 1: 206 | return 1 207 | helper.colored_list(options) 208 | if not title: 209 | title = locale_manager.search_key("select_option_to") % (mode) 210 | val = colored_input(title) 211 | if allow_text: 212 | if val in options: 213 | return options.index(val) + 1 214 | val = helper.check_int(val) 215 | if val is None: 216 | helper.colored_text(locale_manager.search_key("invalid_int"), helper.RED) 217 | return select_single(options, mode, title, allow_text) 218 | if val < 1 or val > len(options): 219 | helper.colored_text( 220 | locale_manager.search_key("invalid_range") % (len(options)), 221 | helper.RED, 222 | ) 223 | return select_single(options, mode, title, allow_text) 224 | return val 225 | 226 | 227 | def get_int(dialog: str, default: Optional[int] = None) -> int: 228 | """Get user input as an integer and keep asking until a valid integer is entered""" 229 | 230 | helper.colored_text(dialog, end="") 231 | locale_manager = locale_handler.LocalManager.from_config() 232 | while True: 233 | try: 234 | val = input() 235 | val = val.strip(" ") 236 | return int(val) 237 | except ValueError: 238 | if default is not None: 239 | return default 240 | helper.colored_text(locale_manager.search_key("invalid_int"), helper.RED) 241 | 242 | 243 | def ask_if_individual(item_name: str) -> bool: 244 | """Ask if the user wants to edit an individual item""" 245 | locale_manager = locale_handler.LocalManager.from_config() 246 | is_individual = ( 247 | colored_input( 248 | locale_manager.search_key("ask_individual") % (item_name), 249 | ) 250 | == "1" 251 | ) 252 | return is_individual 253 | 254 | 255 | def get_yes_no(dialog: str) -> bool: 256 | """Get user input as a yes or no""" 257 | locale_manager = locale_handler.LocalManager.from_config() 258 | while True: 259 | val = colored_input(dialog) 260 | if val: 261 | if val.lower()[0] == "y": 262 | return True 263 | if val.lower()[0] == "n": 264 | return False 265 | helper.colored_text(locale_manager.search_key("invalid_yes_no"), helper.RED) 266 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_item, test_parse, test_edits -------------------------------------------------------------------------------- /tests/test_edits/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_basic, test_cats -------------------------------------------------------------------------------- /tests/test_edits/test_basic/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_talent_orbs, test_basic -------------------------------------------------------------------------------- /tests/test_edits/test_basic/test_basic.py: -------------------------------------------------------------------------------- 1 | """Test basic item editing""" 2 | 3 | from pytest import MonkeyPatch 4 | from BCSFE_Python.edits.basic import basic_items 5 | 6 | 7 | def test_cf_normal(monkeypatch: MonkeyPatch): 8 | """Test that the value is set correctly""" 9 | 10 | inputs = ["y", "10"] 11 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 12 | 13 | save_stats = {"cat_food": {"Value": 50}} 14 | 15 | save_stats = basic_items.edit_cat_food(save_stats) 16 | assert save_stats["cat_food"]["Value"] == 10 17 | 18 | 19 | def test_catfood_leave(monkeypatch: MonkeyPatch): 20 | """Test that the value is left unchanged""" 21 | 22 | inputs = ["n", "10"] 23 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 24 | 25 | save_stats = {"cat_food": {"Value": 50}} 26 | 27 | save_stats = basic_items.edit_cat_food(save_stats) 28 | assert save_stats["cat_food"]["Value"] == 50 29 | 30 | 31 | def test_iq_normal(monkeypatch: MonkeyPatch): 32 | """Test that the value is set correctly""" 33 | 34 | inputs = ["abcdefghi"] 35 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 36 | 37 | save_stats = {"inquiry_code": "123456789"} 38 | 39 | save_stats = basic_items.edit_inquiry_code(save_stats) 40 | assert save_stats["inquiry_code"] == "abcdefghi" 41 | -------------------------------------------------------------------------------- /tests/test_edits/test_basic/test_talent_orbs.py: -------------------------------------------------------------------------------- 1 | """Test talent orbs""" 2 | 3 | from typing import Any 4 | from pytest import MonkeyPatch 5 | 6 | from BCSFE_Python.edits.basic import talent_orbs 7 | 8 | 9 | def test_create_orb_list(): 10 | """Test that the list of all possible talent orbs is created correctly""" 11 | 12 | types = talent_orbs.get_talent_orbs_types() 13 | assert len(types) == 155 14 | 15 | assert "Red D Attack" == types[0] 16 | assert "Red C Attack" == types[1] 17 | 18 | assert "Red D Defense" == types[5] 19 | 20 | assert "Floating D Attack" == types[10] 21 | 22 | assert "Red D Strong" == types[65] 23 | assert "Alien S Resistant" == types[139] 24 | 25 | 26 | def test_edit_all_orbs(monkeypatch: MonkeyPatch): 27 | """Test that all talent orbs are edited correctly""" 28 | 29 | inputs = ["10"] 30 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 31 | save_stats: dict[str, Any] = { 32 | "talent_orbs": { 33 | 51: 5, 34 | 0: 10, 35 | 98: 0, 36 | 19: 1, 37 | } 38 | } 39 | types = talent_orbs.get_talent_orbs_types() 40 | save_stats = talent_orbs.edit_all_orbs(save_stats, types) 41 | 42 | assert len(save_stats["talent_orbs"]) == len(types) 43 | for i in range(len(types)): 44 | assert save_stats["talent_orbs"][i] == 10 45 | 46 | 47 | def test_edit_all_orbs_invalid(monkeypatch: MonkeyPatch): 48 | """Test that all talent orbs are edited correctly""" 49 | 50 | inputs = ["aaa", "15"] 51 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 52 | save_stats_b: dict[str, Any] = { 53 | "talent_orbs": { 54 | 51: 5, 55 | 0: 10, 56 | 98: 0, 57 | 19: 1, 58 | } 59 | } 60 | types = talent_orbs.get_talent_orbs_types() 61 | save_stats = talent_orbs.edit_all_orbs(save_stats_b, types) 62 | 63 | assert save_stats_b == save_stats 64 | -------------------------------------------------------------------------------- /tests/test_edits/test_cats/__init__.py: -------------------------------------------------------------------------------- 1 | from . import test_cat_id_selector -------------------------------------------------------------------------------- /tests/test_edits/test_cats/test_cat_id_selector.py: -------------------------------------------------------------------------------- 1 | """Test cat id selector""" 2 | 3 | from pytest import MonkeyPatch 4 | from BCSFE_Python.edits.cats import cat_id_selector 5 | 6 | 7 | def test_get_all_cats(): 8 | """Test that the ids are correct""" 9 | save_stats = {"cats": [1, 1, 1, 0, 1]} 10 | ids = cat_id_selector.get_all_cats(save_stats) 11 | assert ids == [0, 1, 2, 3, 4] 12 | 13 | 14 | def test_select_range(monkeypatch: MonkeyPatch): 15 | """Test that the ids are correct""" 16 | 17 | inputs = ["1-5"] 18 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 19 | save_stats = {"cats": [1, 1, 1, 0, 1]} 20 | 21 | ids = cat_id_selector.select_cats_range(save_stats) 22 | actual_ids = [1, 2, 3, 4, 5] 23 | assert ids == actual_ids 24 | 25 | 26 | def test_select_current(): 27 | """Test that the ids are correct""" 28 | 29 | save_stats = {"cats": [1, 1, 1, 0, 1]} 30 | 31 | ids = cat_id_selector.select_current_cats(save_stats) 32 | actual_ids = [0, 1, 2, 4] 33 | assert ids == actual_ids 34 | 35 | 36 | def test_select_rarity(monkeypatch: MonkeyPatch): 37 | """Test that the ids are correct""" 38 | 39 | inputs = ["1"] 40 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 41 | 42 | save_stats = {"version": "en"} 43 | ids = cat_id_selector.select_cats_rarity(save_stats) 44 | actual_ids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 643] 45 | assert ids == actual_ids 46 | 47 | 48 | def test_select_range_reverse(monkeypatch: MonkeyPatch): 49 | """Test that the ids are correct""" 50 | 51 | inputs = ["5-1"] 52 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 53 | save_stats = {"cats": [1, 1, 1, 0, 1]} 54 | 55 | ids = cat_id_selector.select_cats_range(save_stats) 56 | actual_ids = [1, 2, 3, 4, 5] 57 | assert ids == actual_ids 58 | 59 | 60 | def test_select_range_mult(monkeypatch: MonkeyPatch): 61 | """Test that the ids are correct""" 62 | 63 | inputs = ["1-5 7-10"] 64 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 65 | save_stats = {"cats": [1, 1, 1, 0, 1]} 66 | 67 | ids = cat_id_selector.select_cats_range(save_stats) 68 | actual_ids = [1, 2, 3, 4, 5, 7, 8, 9, 10] 69 | assert ids == actual_ids 70 | 71 | 72 | def test_select_range_spaces(monkeypatch: MonkeyPatch): 73 | """Test that the ids are correct""" 74 | 75 | inputs = ["1 4 7 2"] 76 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 77 | save_stats = {"cats": [1, 1, 1, 0, 1]} 78 | 79 | ids = cat_id_selector.select_cats_range(save_stats) 80 | actual_ids = [1, 4, 7, 2] 81 | assert ids == actual_ids 82 | 83 | 84 | def test_select_range_comb(monkeypatch: MonkeyPatch): 85 | """Test that the ids are correct""" 86 | 87 | inputs = ["1 4 7 2 8-10"] 88 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 89 | save_stats = {"cats": [1, 1, 1, 0, 1]} 90 | 91 | ids = cat_id_selector.select_cats_range(save_stats) 92 | actual_ids = [1, 4, 7, 2, 8, 9, 10] 93 | assert ids == actual_ids 94 | 95 | 96 | def test_select_range_all(monkeypatch: MonkeyPatch): 97 | """Test that the ids are correct""" 98 | 99 | inputs = ["all"] 100 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 101 | save_stats = {"cats": [1, 1, 1, 0, 1]} 102 | 103 | ids = cat_id_selector.select_cats_range(save_stats) 104 | actual_ids = [0, 1, 2, 3, 4] 105 | assert ids == actual_ids 106 | 107 | 108 | def test_gatya_banner(monkeypatch: MonkeyPatch): 109 | """Test that the ids are correct""" 110 | 111 | inputs = ["602"] 112 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 113 | 114 | save_stats = {"version": "en"} 115 | ids = cat_id_selector.select_cats_gatya_banner(save_stats) 116 | ids.sort() 117 | actual_ids = [ 118 | 448, 119 | 449, 120 | 450, 121 | 451, 122 | 455, 123 | 461, 124 | 463, 125 | 478, 126 | 481, 127 | 493, 128 | 544, 129 | 612, 130 | 138, 131 | 259, 132 | 330, 133 | 195, 134 | 496, 135 | 358, 136 | 376, 137 | 502, 138 | 526, 139 | 533, 140 | 564, 141 | 229, 142 | 30, 143 | 31, 144 | 32, 145 | 33, 146 | 35, 147 | 36, 148 | 39, 149 | 40, 150 | 61, 151 | 150, 152 | 151, 153 | 152, 154 | 153, 155 | 199, 156 | 307, 157 | 377, 158 | 522, 159 | 37, 160 | 38, 161 | 41, 162 | 42, 163 | 46, 164 | 47, 165 | 48, 166 | 49, 167 | 50, 168 | 51, 169 | 52, 170 | 55, 171 | 56, 172 | 58, 173 | 106, 174 | 145, 175 | 146, 176 | 147, 177 | 148, 178 | 149, 179 | 197, 180 | 198, 181 | 308, 182 | 325, 183 | 495, 184 | 271, 185 | 523, 186 | ] 187 | actual_ids.sort() 188 | assert ids == actual_ids 189 | -------------------------------------------------------------------------------- /tests/test_helper.py: -------------------------------------------------------------------------------- 1 | """Test helper module""" 2 | 3 | from BCSFE_Python import helper 4 | 5 | 6 | def test_str_to_gv(): 7 | """Test that a game version with . is converted to an int representation""" 8 | assert helper.str_to_gv("1.0.0") == "10000" 9 | assert helper.str_to_gv("11.8.2") == "110802" 10 | assert helper.str_to_gv("1.7") == "10700" 11 | assert helper.str_to_gv("10.87") == "108700" 12 | 13 | 14 | def test_gv_to_str(): 15 | """Test that an int representation is converted to a game version with .""" 16 | assert helper.gv_to_str(10000) == "1.0.0" 17 | assert helper.gv_to_str(110802) == "11.8.2" 18 | assert helper.gv_to_str(10700) == "1.7.0" 19 | assert helper.gv_to_str(108700) == "10.87.0" 20 | -------------------------------------------------------------------------------- /tests/test_item.py: -------------------------------------------------------------------------------- 1 | """Test item""" 2 | 3 | from pytest import MonkeyPatch 4 | from BCSFE_Python import item 5 | 6 | 7 | def test_item_set(monkeypatch: MonkeyPatch): 8 | """Test that the value is set correctly""" 9 | 10 | inputs = ["10"] 11 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 12 | 13 | val = item.Item("name", 0, 15, "value") 14 | val.edit() 15 | assert val.value == 10 16 | 17 | 18 | def test_item_set_max(monkeypatch: MonkeyPatch): 19 | """Test that the value is set correctly, clamping to max""" 20 | 21 | inputs = ["10"] 22 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 23 | 24 | val = item.Item("name", 0, 5, "value") 25 | val.edit() 26 | assert val.value == 5 27 | 28 | 29 | def test_item_set_positive(monkeypatch: MonkeyPatch): 30 | """Test that the value is set to a positive number""" 31 | 32 | inputs = ["-10"] 33 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 34 | 35 | val = item.Item("name", 1, 5, "value") 36 | val.edit() 37 | assert val.value == 0 38 | 39 | 40 | def test_item_set_offset(monkeypatch: MonkeyPatch): 41 | """Test that the value is set with the offset""" 42 | 43 | inputs = ["10"] 44 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 45 | 46 | val = item.Item("name", 1, 15, "value", offset=1) 47 | val.edit() 48 | assert val.value == 9 49 | 50 | 51 | def test_item_set_integer(monkeypatch: MonkeyPatch): 52 | """Test that the value is set to a an integer""" 53 | 54 | inputs = ["AAAA", "10"] 55 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 56 | 57 | val = item.Item("name", 0, 50, "value") 58 | val.edit() 59 | assert val.value == 10 60 | 61 | 62 | def test_item_set_no_max(monkeypatch: MonkeyPatch): 63 | """Test that the value is set to a an integer""" 64 | 65 | inputs = ["1111111111"] 66 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 67 | 68 | val = item.Item("name", 0, None, "value") 69 | val.edit() 70 | assert val.value == 1111111111 71 | 72 | 73 | def test_item_set_too_large(monkeypatch: MonkeyPatch): 74 | """Test that the value is set to a an integer""" 75 | 76 | inputs = ["1234343434343434343434"] 77 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 78 | 79 | val = item.Item("name", 0, None, "value") 80 | val.edit() 81 | assert val.value == 4294967295 82 | 83 | 84 | def test_item_set_str(monkeypatch: MonkeyPatch): 85 | """Test that the value is set to a string""" 86 | 87 | inputs = ["AAAA"] 88 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 89 | 90 | val = item.Item("name", "", 50, "value") 91 | val.edit() 92 | assert val.value == "AAAA" 93 | 94 | 95 | def test_item_group_set(monkeypatch: MonkeyPatch): 96 | """Test that the value is set correctly""" 97 | 98 | inputs = ["1 2 3", "10", "15", "2"] 99 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 100 | 101 | val = item.create_item_group( 102 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 103 | ) 104 | val.edit() 105 | assert val.values == [10, 15, 2] 106 | 107 | 108 | def test_item_group_set_1(monkeypatch: MonkeyPatch): 109 | """Test that the value is set correctly""" 110 | 111 | inputs = ["1", "5"] 112 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 113 | 114 | val = item.create_item_group( 115 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 116 | ) 117 | val.edit() 118 | assert val.values == [5, 1, 5] 119 | 120 | 121 | def test_item_group_set_invalid(monkeypatch: MonkeyPatch): 122 | """Test that the value is set correctly""" 123 | 124 | inputs = ["0", "5"] 125 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 126 | 127 | val = item.create_item_group( 128 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 129 | ) 130 | val.edit() 131 | assert val.values == [0, 1, 5] 132 | 133 | 134 | def test_item_group_set_all(monkeypatch: MonkeyPatch): 135 | """Test that the value is set correctly""" 136 | 137 | inputs = ["4", "5"] 138 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 139 | 140 | val = item.create_item_group( 141 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 142 | ) 143 | val.edit() 144 | assert val.values == [5, 5, 5] 145 | 146 | 147 | def test_item_group_set_max(monkeypatch: MonkeyPatch): 148 | """Test that the value is set correctly""" 149 | 150 | inputs = ["1 2 3", "5", "51", "-1"] 151 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 152 | 153 | val = item.create_item_group( 154 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 155 | ) 156 | val.edit() 157 | assert val.values == [5, 50, 0] 158 | 159 | 160 | def test_item_group_set_order(monkeypatch: MonkeyPatch): 161 | """Test that the value is set correctly""" 162 | 163 | inputs = ["3 2 1", "5", "6", "7"] 164 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 165 | 166 | val = item.create_item_group( 167 | ["name_1", "name_2", "name_3"], [0, 1, 5], [50, 50, 50], "value", "name" 168 | ) 169 | val.edit() 170 | assert val.values == [7, 6, 5] 171 | 172 | 173 | def test_item_group_set_offset(monkeypatch: MonkeyPatch): 174 | """Test that the value is set correctly""" 175 | 176 | inputs = ["1 2 3", "5", "6", "7"] 177 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 178 | 179 | val = item.create_item_group( 180 | ["name_1", "name_2", "name_3"], 181 | [0, 1, 5], 182 | [50, 50, 50], 183 | "value", 184 | "name", 185 | offset=1, 186 | ) 187 | val.edit() 188 | assert val.values == [4, 5, 6] 189 | 190 | 191 | def test_item_group_set_offset_negative(monkeypatch: MonkeyPatch): 192 | """Test that the value is set correctly""" 193 | 194 | inputs = ["1 2 3", "5", "6", "7"] 195 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 196 | 197 | val = item.create_item_group( 198 | ["name_1", "name_2", "name_3"], 199 | [0, 1, 5], 200 | [50, 50, 50], 201 | "value", 202 | "name", 203 | offset=-1, 204 | ) 205 | val.edit() 206 | assert val.values == [6, 7, 8] 207 | 208 | 209 | def test_item_group_set_max_none(monkeypatch: MonkeyPatch): 210 | """Test that the value is set correctly""" 211 | 212 | inputs = ["1 2 3", "500000", "6", "7"] 213 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 214 | 215 | val = item.create_item_group( 216 | ["name_1", "name_2", "name_3"], 217 | [0, 1, 5], 218 | None, 219 | "value", 220 | "name", 221 | ) 222 | val.edit() 223 | assert val.values == [500000, 6, 7] 224 | 225 | 226 | def test_item_group_set_max_singular(monkeypatch: MonkeyPatch): 227 | """Test that the value is set correctly""" 228 | 229 | inputs = ["1 2 3", "500000", "6", "7"] 230 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 231 | 232 | val = item.create_item_group( 233 | ["name_1", "name_2", "name_3"], 234 | [0, 1, 5], 235 | 5, 236 | "value", 237 | "name", 238 | ) 239 | val.edit() 240 | assert val.values == [5, 5, 5] 241 | 242 | 243 | def test_item_group_set_too_large(monkeypatch: MonkeyPatch): 244 | """Test that the value is set correctly""" 245 | 246 | inputs = ["1 2 3", "12333333333333333333", "6", "7"] 247 | monkeypatch.setattr("builtins.input", lambda: inputs.pop(0)) 248 | 249 | val = item.create_item_group( 250 | ["name_1", "name_2", "name_3"], 251 | [0, 1, 5], 252 | None, 253 | "value", 254 | "name", 255 | ) 256 | val.edit() 257 | assert val.values == [4294967295, 6, 7] 258 | -------------------------------------------------------------------------------- /tests/test_parse.py: -------------------------------------------------------------------------------- 1 | import os 2 | from BCSFE_Python import parse_save, patcher, serialise_save 3 | 4 | 5 | def test_parse(): 6 | """Test parse save data""" 7 | 8 | # get all files in the saves dir 9 | save_files: list[str] = [] 10 | for file in os.listdir(os.path.join(os.path.dirname(__file__), "saves")): 11 | path = os.path.join(os.path.dirname(__file__), "saves", file) 12 | if ( 13 | os.path.isfile(path) 14 | and not file.endswith(".bak") 15 | and not file.endswith("_backup") 16 | ): 17 | save_files.append(path) 18 | 19 | _ = [run_testparse(file) for file in save_files] 20 | 21 | 22 | def run_testparse(file: str): 23 | """Run test parse save data""" 24 | data_1 = open(file, "rb").read() 25 | gv = parse_save.get_game_version(data_1) 26 | if gv < 110000: 27 | return 28 | gv_c = patcher.detect_game_version(data_1) 29 | print(f"Parsing {file} - {gv} - {gv_c}") 30 | save_stats = parse_save.parse_save(data_1, gv_c) 31 | data_2 = serialise_save.serialize_save(save_stats) 32 | save_stats = parse_save.parse_save(data_2, gv_c) 33 | data_3 = serialise_save.serialize_save(save_stats) 34 | assert data_2 == data_3 == data_1 35 | --------------------------------------------------------------------------------