├── .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 |
--------------------------------------------------------------------------------