├── src └── bcsfe │ ├── py.typed │ ├── files │ ├── locales │ │ ├── vi │ │ │ ├── metadata.json │ │ │ ├── edits │ │ │ │ ├── gambling.properties │ │ │ │ ├── medals.properties │ │ │ │ ├── scheme_items.properties │ │ │ │ ├── gold_pass.properties │ │ │ │ ├── gatya.properties │ │ │ │ ├── playtime.properties │ │ │ │ ├── missions.properties │ │ │ │ ├── user_rank.properties │ │ │ │ ├── special_skills.properties │ │ │ │ ├── fixes.properties │ │ │ │ ├── enemy.properties │ │ │ │ ├── talent_orbs.properties │ │ │ │ ├── treasures.properties │ │ │ │ ├── bannable_items.properties │ │ │ │ ├── items.properties │ │ │ │ └── gamototo.properties │ │ │ └── core │ │ │ │ ├── files.properties │ │ │ │ ├── updater.properties │ │ │ │ ├── theme.properties │ │ │ │ ├── input.properties │ │ │ │ ├── locale.properties │ │ │ │ ├── server.properties │ │ │ │ ├── config.properties │ │ │ │ └── main.properties │ │ └── en │ │ │ ├── edits │ │ │ ├── gambling.properties │ │ │ ├── medals.properties │ │ │ ├── scheme_items.properties │ │ │ ├── gatya.properties │ │ │ ├── missions.properties │ │ │ ├── user_rank.properties │ │ │ ├── special_skills.properties │ │ │ ├── playtime.properties │ │ │ ├── fixes.properties │ │ │ ├── gold_pass.properties │ │ │ ├── enemy.properties │ │ │ ├── talent_orbs.properties │ │ │ ├── treasures.properties │ │ │ ├── bannable_items.properties │ │ │ ├── gamototo.properties │ │ │ └── items.properties │ │ │ └── core │ │ │ ├── files.properties │ │ │ ├── updater.properties │ │ │ ├── theme.properties │ │ │ ├── input.properties │ │ │ ├── locale.properties │ │ │ ├── server.properties │ │ │ ├── config.properties │ │ │ └── main.properties │ ├── themes │ │ ├── discord.json │ │ └── default.json │ └── max_values.json │ ├── core │ ├── game │ │ ├── __init__.py │ │ ├── battle │ │ │ ├── __init__.py │ │ │ └── enemy.py │ │ ├── gamoto │ │ │ ├── __init__.py │ │ │ ├── catamins.py │ │ │ └── base_materials.py │ │ ├── map │ │ │ ├── __init__.py │ │ │ ├── challenge.py │ │ │ ├── tower.py │ │ │ ├── map_option.py │ │ │ ├── ex_stage.py │ │ │ ├── uncanny.py │ │ │ ├── map_reset.py │ │ │ └── map_names.py │ │ ├── catbase │ │ │ ├── __init__.py │ │ │ ├── my_sale.py │ │ │ ├── stamp.py │ │ │ ├── unlock_popups.py │ │ │ ├── matatabi.py │ │ │ ├── drop_chara.py │ │ │ ├── beacon_base.py │ │ │ ├── officer_pass.py │ │ │ ├── playtime.py │ │ │ ├── gambling.py │ │ │ ├── gatya_item.py │ │ │ └── login_bonuses.py │ │ └── localizable.py │ ├── server │ │ ├── __init__.py │ │ ├── headers.py │ │ ├── client_info.py │ │ ├── updater.py │ │ └── request.py │ ├── io │ │ ├── __init__.py │ │ ├── json_file.py │ │ ├── command.py │ │ ├── yaml.py │ │ ├── thread_helper.py │ │ ├── git_handler.py │ │ ├── root_handler.py │ │ └── waydroid.py │ ├── max_value_helper.py │ ├── country_code.py │ ├── log.py │ └── crypto.py │ ├── __init__.py │ ├── cli │ ├── edits │ │ ├── clear_tutorial.py │ │ ├── aku_realm.py │ │ ├── __init__.py │ │ ├── rare_ticket_trade.py │ │ ├── fixes.py │ │ └── max_all.py │ ├── __init__.py │ ├── server_cli.py │ └── file_dialog.py │ └── __main__.py ├── tests ├── __init__.py └── test_parse.py ├── setup.py ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── .github └── FUNDING.yml ├── pyproject.toml └── LOCALIZATION.md /src/bcsfe/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": ["HungJoesifer"], 3 | "name": "Tiếng Việt (Vietnamese)" 4 | } 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/bcsfe/files * 2 | recursive-exclude src/bcsfe/files/game_data * 3 | recursive-exclude src/bcsfe/files/map_names * -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aenum==3.1.16 2 | colored==1.4.4 3 | PyJWT==2.10.1 4 | PyYAML==6.0.2 5 | Requests==2.32.4 6 | beautifulsoup4==4.13.4 7 | argparse==1.4.0 8 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.game import catbase, battle, map, gamoto, localizable 2 | 3 | __all__ = ["catbase", "battle", "map", "gamoto", "localizable"] 4 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/battle/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.game.battle import slots, battle_items, cleared_slots 2 | 3 | __all__ = ["slots", "battle_items", "cleared_slots"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *SAVE_DATA* 2 | /dist 3 | __pychache__ 4 | *.pyc 5 | /tests/saves/* 6 | .* 7 | *.egg* 8 | src/bcsfe/files/game_data 9 | src/bcsfe/files/map_names 10 | pyrightconfig.json 11 | /test*.py 12 | /save.json 13 | -------------------------------------------------------------------------------- /src/bcsfe/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.2.1" 2 | 3 | from bcsfe import core, cli 4 | 5 | 6 | __all__ = ["core", "cli"] 7 | 8 | 9 | def run(): 10 | from bcsfe import __main__ 11 | 12 | __main__.main() 13 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/gambling.properties: -------------------------------------------------------------------------------- 1 | reset_wildcat_slots=<@su>Successfully reset wildcat slots 2 | reset_cat_scratcher=<@su>Successfully reset cat scratcher lottery 3 | reset_gambling_events=Reset Wildcat Slots and Cat Scratcher Lottery 4 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/gambling.properties: -------------------------------------------------------------------------------- 1 | reset_wildcat_slots=<@su>Đã reset Wildcat Slots thành công 2 | reset_gambling_events=Reset Wildcat Slots và Cat Scratcher Lottery 3 | reset_cat_scratcher=<@su>Đã reset Cat Scratcher Lottery thành công 4 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/gamoto/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.game.gamoto import ( 2 | catamins, 3 | gamatoto, 4 | base_materials, 5 | ototo, 6 | cat_shrine, 7 | ) 8 | 9 | __all__ = ["catamins", "gamatoto", "base_materials", "ototo", "cat_shrine"] 10 | -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/clear_tutorial.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import color 4 | 5 | 6 | def clear_tutorial( 7 | save_file: core.SaveFile, display_already_cleared: bool = True 8 | ): 9 | core.StoryChapters.clear_tutorial(save_file) 10 | if display_already_cleared: 11 | color.ColoredText.localize("tutorial_cleared") 12 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/medals.properties: -------------------------------------------------------------------------------- 1 | medals=Meow Medals 2 | add_medals=Add Medals 3 | remove_medals=Remove Medals 4 | medal_add_remove_dialog=Do you want to <@t>add medals or <@t>remove medals?: 5 | medal_string={medal_name}: <@q>{medal_req} 6 | select_medals=Select medals: 7 | medals_added=<@su>Succesfully added meow medals 8 | medals_removed=<@su>Succesfully removed meow medals -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/scheme_items.properties: -------------------------------------------------------------------------------- 1 | scheme_items_edit_success=<@su>Succesfully edited scheme items 2 | scheme_items_select_gain=Select scheme items to gain 3 | scheme_items_select_remove=Select scheme items to remove 4 | gain_remove_scheme_items=Do you want to <@t>gain or <@t>remove scheme items?: 5 | gain_scheme_items=Gain scheme items 6 | remove_scheme_items=Remove scheme items -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/aku_realm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import color 4 | 5 | 6 | def unlock_aku_realm(save_file: core.SaveFile): 7 | stage_ids = [255, 256, 257, 258, 265, 266, 268] 8 | for stage_id in stage_ids: 9 | save_file.event_stages.clear_map(1, stage_id, 0, False) 10 | 11 | color.ColoredText.localize("aku_realm_unlocked") 12 | -------------------------------------------------------------------------------- /src/bcsfe/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.cli import ( 2 | color, 3 | dialog_creator, 4 | main, 5 | file_dialog, 6 | feature_handler, 7 | save_management, 8 | server_cli, 9 | edits, 10 | ) 11 | 12 | __all__ = [ 13 | "color", 14 | "dialog_creator", 15 | "main", 16 | "file_dialog", 17 | "feature_handler", 18 | "save_management", 19 | "server_cli", 20 | "edits", 21 | ] 22 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/medals.properties: -------------------------------------------------------------------------------- 1 | # filename="medals.properties" 2 | medals=Meow Medals 3 | add_medals=Thêm Medals 4 | remove_medals=Xóa Medals 5 | medal_add_remove_dialog=Bạn muốn <@t>add medals hay <@t>remove medals?: 6 | medal_string={medal_name}: <@q>{medal_req} 7 | select_medals=Chọn medals: 8 | medals_added=<@su>Đã thêm meow medals thành công 9 | medals_removed=<@su>Đã xóa meow medals thành công -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/scheme_items.properties: -------------------------------------------------------------------------------- 1 | # filename="scheme_items.properties" 2 | scheme_items_edit_success=<@su>Đã chỉnh sửa scheme items thành công 3 | scheme_items_select_gain=Chọn scheme items để nhận 4 | scheme_items_select_remove=Chọn scheme items để xóa 5 | gain_remove_scheme_items=Bạn muốn <@t>nhận hay <@t>xóa scheme items?: 6 | gain_scheme_items=Nhận scheme items 7 | remove_scheme_items=Xóa scheme items -------------------------------------------------------------------------------- /src/bcsfe/core/server/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.server import ( 2 | managed_item, 3 | headers, 4 | client_info, 5 | server_handler, 6 | game_data_getter, 7 | request, 8 | updater, 9 | event_data, 10 | ) 11 | 12 | __all__ = [ 13 | "managed_item", 14 | "server_handler", 15 | "headers", 16 | "client_info", 17 | "game_data_getter", 18 | "request", 19 | "updater", 20 | "event_data" 21 | ] 22 | -------------------------------------------------------------------------------- /src/bcsfe/files/themes/discord.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Discord Theme", 3 | "name": "Discord Theme", 4 | "description": "Discord-inspired dark mode theme", 5 | "author": "HungJoesifer", 6 | "version": "1.0.0", 7 | "colors": { 8 | "primary": "#F0F8FF", 9 | "secondary": "#A6B3F8", 10 | "tertiary": "#5865F2", 11 | "quaternary": "#FFFFFF", 12 | "error": "#ED4245", 13 | "warning": "#F1C40F", 14 | "success": "#57F287" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/files.properties: -------------------------------------------------------------------------------- 1 | another_path=Enter path manually 2 | select_files_dir=Select files in directory: 3 | enter_path=Enter file path / location: 4 | enter_path_dir=Enter folder path / location: 5 | enter_path_default=Enter file path / location (default: <@t>{default}): 6 | current_files_dir=Current files in directory <@t>{dir}: 7 | other_dir=Enter other directory 8 | no_files_dir=<@e>No files in directory 9 | path_not_exists=<@e>Path does not exist -------------------------------------------------------------------------------- /src/bcsfe/files/themes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "default", 3 | "name": "Default", 4 | "description": "Default theme of the editor", 5 | "author": "fieryhenry", 6 | "version": "1.0.0", 7 | "colors": { 8 | "primary": "#FFFFFF", 9 | "secondary": "#FFFFFF", 10 | "quaternary": "#008000", 11 | "tertiary": "#00FFFF", 12 | "error": "#FF0000", 13 | "warning": "#FF0000", 14 | "success": "#00FF00" 15 | } 16 | } -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/gatya.properties: -------------------------------------------------------------------------------- 1 | event_tickets=Event Tickets / Lucky Tickets 2 | downloading_gatya_data=Downloading gacha event data... 3 | download_gatya_data_success=<@su>Successfully downloaded gacha event data 4 | download_gatya_data_fail=<@e>Failed to download gacha event data. Maybe try again 5 | save_gatya_error=<@e>Failed to save gatya data due to {error} 6 | gatya_by_id_q=Do you want to select gacha banners by <@t>ID or <@t>name?: 7 | by_id=By ID 8 | by_name=By Name 9 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/gold_pass.properties: -------------------------------------------------------------------------------- 1 | # filename="gold_pass.properties" 2 | gold_pass_dialog=Nhập <@t>officer id bạn muốn (Để <@q>trống cho <@q>random id, hoặc nhập <@q>-1 để <@q>remove gold pass): 3 | gold_pass=Gold Pass / Officer Club 4 | gold_pass_remove_success=<@su>Đã remove gold pass thành công 5 | gold_pass_get_success=<@su>Đã nhận gold pass thành công (id: <@t>{id}) 6 | officer_pass_fixed=<@su>Đã fix officer club khỏi crash thành công 7 | fix_officer_pass_crash=Fix Officer Club Crashing -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/files.properties: -------------------------------------------------------------------------------- 1 | # filename="files.properties" 2 | another_path=Nhập đường dẫn thủ công 3 | select_files_dir=Chọn tệp trong thư mục: 4 | enter_path=Nhập đường dẫn tệp / vị trí: 5 | enter_path_dir=Nhập đường dẫn thư mục / vị trí: 6 | enter_path_default=Nhập đường dẫn tệp / vị trí (mặc định: <@t>{default}): 7 | current_files_dir=Các tệp hiện tại trong thư mục <@t>{dir}: 8 | other_dir=Nhập thư mục khác 9 | no_files_dir=<@e>Không có tệp trong thư mục 10 | path_not_exists=<@e>Đường dẫn không tồn tại -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/gatya.properties: -------------------------------------------------------------------------------- 1 | # filename="gatya.properties" 2 | event_tickets=Event Tickets / Lucky Tickets 3 | downloading_gatya_data=Đang tải dữ liệu sự kiện gacha... 4 | download_gatya_data_success=<@su>Đã tải dữ liệu sự kiện gacha thành công 5 | download_gatya_data_fail=<@e>Không thể tải dữ liệu sự kiện gacha. Có lẽ thử lại 6 | save_gatya_error=<@e>Không thể lưu dữ liệu gatya do {error} 7 | gatya_by_id_q=Bạn có muốn chọn gacha banners theo <@t>ID hay <@t>tên?: 8 | by_name=Theo tên 9 | by_id=Theo ID 10 | -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.cli.edits import ( 2 | basic_items, 3 | cat_editor, 4 | clear_tutorial, 5 | rare_ticket_trade, 6 | fixes, 7 | enemy_editor, 8 | aku_realm, 9 | map, 10 | event_tickets, 11 | max_all, 12 | storage, 13 | ) 14 | 15 | __all__ = [ 16 | "basic_items", 17 | "cat_editor", 18 | "clear_tutorial", 19 | "rare_ticket_trade", 20 | "fixes", 21 | "enemy_editor", 22 | "aku_realm", 23 | "map", 24 | "event_tickets", 25 | "max_all", 26 | "storage", 27 | ] 28 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/missions.properties: -------------------------------------------------------------------------------- 1 | missions=Catnip Challenges / Missions|Cat Missions 2 | complete_reward=Clear Missions and Don't Claim Rewards 3 | complete_claim=Complete Missions and Claim Rewards 4 | uncomplete=Uncomplete Mission 5 | select_mission_claim=Do you want to <@t>complete missions and don't claim the rewards or <@t>complete missions and claim the rewards <@q>(Doesn't actually give you the rewards) or <@t>uncomplete missions if possible? 6 | select_missions=Select missions to edit: 7 | missions_edited=<@su>Succesfully edited missions -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/user_rank.properties: -------------------------------------------------------------------------------- 1 | claim=Claim 2 | unclaim=Unclaim 3 | fix_claimed=Fix Claimed 4 | claim_or_unclaim_ur=Do you want to <@t>claim or <@t>unclaim or <@t>fix claimed (unclaim any rewards that are above the current user rank) user rank rewards?: 5 | select_ur=Select user rank rewards 6 | ur_claimed_success=<@su>Successfully claimed user rank rewards 7 | ur_unclaimed_success=<@su>Successfully unclaimed user rank rewards 8 | ur_string=Rank: <@s>{rank}: {description} 9 | ur_fix_claimed_success=<@su>Successfully fixed claimed user rank rewards -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/playtime.properties: -------------------------------------------------------------------------------- 1 | # filename="playtime.properties" 2 | playtime_str=<@t>{hours} giờ, <@t>{minutes} phút, <@t>{seconds} giây (<@t>{frames} frame) 3 | playtime_current=Thời gian chơi hiện tại: {{playtime_str}} 4 | playtime_edited=Đã chỉnh sửa thời gian chơi thành công thành {{playtime_str}} 5 | playtime_hours_prompt=Nhập số <@t>giờ để đặt thời gian chơi thành: 6 | playtime_minutes_prompt=Nhập số <@t>phút để đặt thời gian chơi thành: 7 | playtime_seconds_prompt=Nhập số <@t>giây để đặt thời gian chơi thành: 8 | playtime=Thời gian chơi -------------------------------------------------------------------------------- /src/bcsfe/core/io/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.io import ( 2 | bc_csv, 3 | path, 4 | data, 5 | command, 6 | yaml, 7 | config, 8 | json_file, 9 | save, 10 | thread_helper, 11 | root_handler, 12 | adb_handler, 13 | git_handler, 14 | waydroid, 15 | ) 16 | 17 | __all__ = [ 18 | "bc_csv", 19 | "path", 20 | "data", 21 | "command", 22 | "yaml", 23 | "config", 24 | "json_file", 25 | "save", 26 | "thread_helper", 27 | "root_handler", 28 | "adb_handler", 29 | "git_handler", 30 | "waydroid", 31 | ] 32 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/special_skills.properties: -------------------------------------------------------------------------------- 1 | special_skills_dialog=Select a base ability to upgrade 2 | upgrade_individual_skill=Input an upgrade for each selected skill 3 | upgrade_all_skills=Input an upgrade to apply to all selected skills 4 | 5 | upgrade_skills_select_mod=Select an option to upgrade skills: 6 | 7 | selected_skill=<@t>{name} is selected 8 | selected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+{plus_level} 9 | selected_skill_upgraded=<@t>{name} is upgraded to <@t>{base_level}<@s>+{plus_level} 10 | skills_edited=Succesfully edited special skills -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/missions.properties: -------------------------------------------------------------------------------- 1 | # filename="missions.properties" 2 | missions=Catnip Challenges / Missions|Cat Missions 3 | complete_reward=Hoàn thành Missions và Không Nhận Phần Thưởng 4 | complete_claim=Hoàn thành Missions và Nhận Phần Thưởng 5 | uncomplete=Không Hoàn thành Mission 6 | select_mission_claim=Bạn muốn <@t>hoàn thành missions và không nhận phần thưởng hay <@t>hoàn thành missions và nhận phần thưởng <@q>(Thực tế không cấp phần thưởng cho bạn) hay <@t>không hoàn thành missions nếu có thể? 7 | select_missions=Chọn missions để chỉnh sửa: 8 | missions_edited=<@su>Đã chỉnh sửa missions thành công -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/user_rank.properties: -------------------------------------------------------------------------------- 1 | # filename="user_rank.properties" 2 | claim=Nhận 3 | unclaim=Không Nhận 4 | fix_claimed=Sửa Đã Nhận 5 | claim_or_unclaim_ur=Bạn muốn <@t>nhận hay <@t>không nhận hay <@t>sửa đã nhận (không nhận bất kỳ phần thưởng nào vượt quá user rank hiện tại) user rank rewards?: 6 | select_ur=Chọn user rank rewards 7 | ur_claimed_success=<@su>Đã nhận user rank rewards thành công 8 | ur_unclaimed_success=<@su>Đã không nhận user rank rewards thành công 9 | ur_string=Rank: <@s>{rank}: {description} 10 | ur_fix_claimed_success=<@su>Đã sửa claimed user rank rewards thành công -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/playtime.properties: -------------------------------------------------------------------------------- 1 | playtime_str=<@t>{hours} $(hours: !=1($hours)$, hour)/$, <@t>{minutes} $(minutes: !=1($minutes)$, minute)/$, <@t>{seconds} $(seconds: !=1($seconds)$, second)/$ (<@t>{frames} $(frames: !=1($frames)$, frame)/$) 2 | playtime_current=Current playtime: {{playtime_str}} 3 | playtime_edited=Successfully edited playtime to {{playtime_str}} 4 | playtime_hours_prompt=Enter the number of <@t>hours to set the playtime to: 5 | playtime_minutes_prompt=Enter the number of <@t>minutes to set the playtime to: 6 | playtime_seconds_prompt=Enter the number of <@t>seconds to set the playtime to: 7 | playtime=Play Time -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/fixes.properties: -------------------------------------------------------------------------------- 1 | fix_gamatoto_crash=Fix gamatoto from crashing the game 2 | fix_time_errors=Fix time related issues 3 | 4 | fix_ototo_crash=Fix ototo from crashing the game 5 | 6 | fix_gamatoto_crash_success=<@su>Sucessfully fixed gamatoto from crashing the game 7 | fix_time_errors_success=<@su>Sucessfully fixed time related issues <@w>(Your device time on both devices must be correct for this to work) 8 | fix_ototo_crash_success=<@su>Successfully fixed ototo from crashing the game 9 | 10 | fixes=Fixes 11 | 12 | unlock_equip_menu=Unlock Equip Menu 13 | equip_menu_unlocked=<@su>Successfully unlocked equip menu 14 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/gold_pass.properties: -------------------------------------------------------------------------------- 1 | gold_pass_dialog=Enter the <@t>officer id you want (Leave <@q>blank for a <@q>random id, or enter <@q>-1 to <@q>remove the gold pass): 2 | gold_pass=Gold Pass / Officer Club 3 | gold_pass_remove_success=<@su>Succesfully removed the gold pass 4 | gold_pass_get_success=<@su>Succesfully gained the gold pass (id: <@t>{id}). <@w>NOTE: The game may remove your gold pass if it realizes you don't actually have one, this is nothing I can fix so please do not report bugs about it. 5 | officer_pass_fixed=<@su>Succesfully fixed the officer club from crashing 6 | fix_officer_pass_crash=Fix Officer Club Crashing 7 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/special_skills.properties: -------------------------------------------------------------------------------- 1 | # filename="special_skills.properties" 2 | special_skills_dialog=Chọn một base ability để nâng cấp 3 | upgrade_individual_skill=Nhập nâng cấp cho từng skill đã chọn 4 | upgrade_all_skills=Nhập nâng cấp để áp dụng cho tất cả skill đã chọn 5 | 6 | upgrade_skills_select_mod=Chọn tùy chọn để nâng cấp skills: 7 | 8 | selected_skill=<@t>{name} đã được chọn 9 | selected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+{plus_level} 10 | selected_skill_upgraded=<@t>{name} đã được nâng cấp lên <@t>{base_level}<@s>+{plus_level} 11 | skills_edited=<@su>Đã chỉnh sửa special skills thành công -------------------------------------------------------------------------------- /src/bcsfe/files/max_values.json: -------------------------------------------------------------------------------- 1 | { 2 | "catfood": 45000, 3 | "xp": 99999999, 4 | "normal_tickets": 2999, 5 | "100_million_tickets": 9999, 6 | "rare_tickets": 299, 7 | "platinum_tickets": 9, 8 | "legend_tickets": 4, 9 | "np": 9999, 10 | "leadership": 9999, 11 | "battle_items": 9999, 12 | "catamins": 9999, 13 | "catseyes": 9999, 14 | "catfruit": { 15 | "old": 128, 16 | "new": 998 17 | }, 18 | "base_materials": 9999, 19 | "labyrinth_medals": 9999, 20 | "talent_orbs": 998, 21 | "treasure_level": 9999, 22 | "stage_clear_count": 9999, 23 | "itf_timed_score": 9999, 24 | "event_tickets": 9999, 25 | "treasure_chests": 9999 26 | } 27 | -------------------------------------------------------------------------------- /tests/test_parse.py: -------------------------------------------------------------------------------- 1 | from bcsfe import core 2 | 3 | 4 | def run(): 5 | saves_path = core.Path(__file__).parent().add("saves") 6 | 7 | for file in saves_path.get_files(): 8 | print(f"Testing {file.basename()}") 9 | data1 = file.read() 10 | 11 | save_1 = core.SaveFile(data1) 12 | data_2 = save_1.to_data() 13 | 14 | assert data1 == data_2 15 | 16 | json_data_1 = save_1.to_dict() 17 | 18 | save_3 = core.SaveFile.from_dict(json_data_1) 19 | json_data_2 = save_3.to_dict() 20 | 21 | assert json_data_1 == json_data_2 22 | 23 | data_3 = save_3.to_data() 24 | 25 | assert data1 == data_3 26 | 27 | print(f"Tested {file.basename()} {save_1.game_version}") 28 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/updater.properties: -------------------------------------------------------------------------------- 1 | local_version=<@q>Local version: <@s>{local_version} 2 | latest_version=<@q>Latest version: <@s>{latest_version} 3 | 4 | update_check_fail=<@e>Failed to check for updates. Maybe check your internet connection? 5 | 6 | update_available= 7 | ><@q>An update is available: <@s>{latest_version} 8 | >Would you like to update? <@t>({{y/n}}): 9 | update_success= 10 | ><@t>Update successful 11 | >Please restart the application 12 | update_fail= 13 | ><@e>Update failed 14 | >Please update manually 15 | >Command: <@s>pip install --upgrade bcsfe 16 | 17 | version_line={{local_version}} | {{latest_version}} 18 | 19 | disable_update_message=Would you like to disable update messages? <@t>({{y/n}}): 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/updater.properties: -------------------------------------------------------------------------------- 1 | # filename="updater.properties" 2 | local_version=<@q>Phiên bản cục bộ: <@s>{local_version} 3 | latest_version=<@q>Phiên bản mới nhất: <@s>{latest_version} 4 | 5 | update_check_fail=<@e>Không thể kiểm tra cập nhật. Có lẽ bạn phải kiểm tra kết nối internet của mình. 6 | 7 | update_available= 8 | ><@q>Có cập nhật khả dụng: <@s>{latest_version} 9 | >Bạn có muốn cập nhật không? <@t>({{y/n}}): 10 | update_success= 11 | ><@t>Cập nhật thành công 12 | >Vui lòng khởi động lại ứng dụng 13 | update_fail= 14 | ><@e>Cập nhật thất bại 15 | >Vui lòng cập nhật thủ công 16 | >Lệnh: <@s>pip install --upgrade bcsfe 17 | 18 | version_line={{local_version}} | {{latest_version}} 19 | 20 | disable_update_message=Bạn có muốn tắt thông báo cập nhật không? <@t>({{y/n}}): -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.game.map import ( 2 | story, 3 | event, 4 | item_reward_stage, 5 | timed_score, 6 | ex_stage, 7 | dojo, 8 | outbreaks, 9 | tower, 10 | challenge, 11 | map_reset, 12 | uncanny, 13 | legend_quest, 14 | gauntlets, 15 | enigma, 16 | aku, 17 | zero_legends, 18 | chapters, 19 | map_names, 20 | map_option, 21 | ) 22 | 23 | __all__ = [ 24 | "story", 25 | "event", 26 | "item_reward_stage", 27 | "timed_score", 28 | "ex_stage", 29 | "dojo", 30 | "outbreaks", 31 | "tower", 32 | "challenge", 33 | "map_reset", 34 | "uncanny", 35 | "legend_quest", 36 | "gauntlets", 37 | "enigma", 38 | "aku", 39 | "zero_legends", 40 | "chapters", 41 | "map_names", 42 | "map_option", 43 | ] 44 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/fixes.properties: -------------------------------------------------------------------------------- 1 | # filename="fixes.properties" 2 | fix_gamatoto_crash=Fix gamatoto gây crashing game 3 | fix_time_errors=Fix vấn đề liên quan đến thời gian (lỗi khi chỉnh thời gian trên thiết bị hay còn gọi là du hành thời gian) 4 | 5 | fix_ototo_crash=Fix ototo gây crashing game 6 | 7 | fix_gamatoto_crash_success=<@su>Đã fix gamatoto không gây crash game thành công 8 | fix_time_errors_success=<@su>Đã fix vấn đề liên quan đến thời gian (lỗi khi chỉnh thời gian trên thiết bị hay còn gọi là du hành thời gian) thành công <@w>(Thời gian thiết bị của bạn trên cả hai thiết bị phải đúng để điều này hoạt động) 9 | fix_ototo_crash_success=<@su>Đã fix ototo không gây crash game thành công 10 | 11 | fixes=Fixes 12 | 13 | unlock_equip_menu=Mở khóa Equip Menu 14 | equip_menu_unlocked=<@su>Đã mở khóa equip menu thành công -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/__init__.py: -------------------------------------------------------------------------------- 1 | from bcsfe.core.game.catbase import ( 2 | gatya_item, 3 | stamp, 4 | cat, 5 | upgrade, 6 | special_skill, 7 | my_sale, 8 | gatya, 9 | user_rank_rewards, 10 | item_pack, 11 | login_bonuses, 12 | scheme_items, 13 | unlock_popups, 14 | beacon_base, 15 | mission, 16 | nyanko_club, 17 | officer_pass, 18 | medals, 19 | talent_orbs, 20 | matatabi, 21 | powerup, 22 | drop_chara, 23 | playtime, 24 | gambling, 25 | ) 26 | 27 | __all__ = [ 28 | "stamp", 29 | "cat", 30 | "upgrade", 31 | "special_skill", 32 | "my_sale", 33 | "gatya", 34 | "user_rank_rewards", 35 | "item_pack", 36 | "login_bonuses", 37 | "scheme_items", 38 | "unlock_popups", 39 | "beacon_base", 40 | "mission", 41 | "nyanko_club", 42 | "officer_pass", 43 | "medals", 44 | "talent_orbs", 45 | "gatya_item", 46 | "matatabi", 47 | "powerup", 48 | "drop_chara", 49 | "playtime", 50 | "gambling", 51 | ] 52 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/localizable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Localizable: 6 | def __init__(self, save_file: core.SaveFile): 7 | self.save_file = save_file 8 | self.localizable = self.get_localizable() 9 | 10 | def get_localizable(self) -> dict[str, str] | None: 11 | gdg = core.core_data.get_game_data_getter(self.save_file) 12 | data = gdg.download("resLocal", "localizable.tsv") 13 | if data is None: 14 | return None 15 | csv = core.CSV(data, "\t") 16 | keys: dict[str, str] = {} 17 | for line in csv: 18 | try: 19 | keys[line[0].to_str()] = line[1].to_str() 20 | except IndexError: 21 | pass 22 | 23 | return keys 24 | 25 | def get(self, key: str) -> str | None: 26 | if self.localizable is None: 27 | return None 28 | return self.localizable.get(key) 29 | 30 | def get_lang(self) -> str | None: 31 | return self.get("lang") 32 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/enemy.properties: -------------------------------------------------------------------------------- 1 | total_selected_enemies=<@t>{total} enemies currently selected 2 | unlock_enemy_guide_success=<@su>Successfully unlocked enemy guide entries 3 | remove_enemy_guide_success=<@su>Successfully removed enemy guide entries 4 | selected_enemy=<@t>{name} (<@t>{id}) is selected 5 | select_enemies_valid=Select all enemies in the enemy guide 6 | select_enemies_invalid=Select all enemies which are not in the enemy guide 7 | select_enemies_all=Select all enemies 8 | select_enemies_id=Select enemies by ID 9 | select_enemies_name=Select enemies by name 10 | select_enemies=Select enemies: 11 | enter_enemy_ids=You can find enemy IDs here: <@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order\nEnter enemy IDs {{range_input}}: 12 | enter_enemy_name=Enter enemy name: 13 | enemy_not_found_name=<@w>No enemies found with name <@s>{name} 14 | unlock_enemy_guide=Unlock enemy guide entries 15 | remove_enemy_guide=Remove enemy guide entries 16 | enemy_guide=Enemy Guide 17 | edit_enemy_guide=Enter an option to edit enemy guide entries: 18 | -------------------------------------------------------------------------------- /src/bcsfe/core/server/headers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import time 3 | from bcsfe import core 4 | 5 | 6 | class AccountHeaders: 7 | def __init__(self, save_file: core.SaveFile, data: str): 8 | self.save_file = save_file 9 | self.data = data 10 | 11 | def get_headers(self) -> dict[str, str]: 12 | return AccountHeaders.get_headers_static( 13 | self.save_file.inquiry_code, self.data 14 | ) 15 | 16 | @staticmethod 17 | def get_headers_static(iq: str, data: str): 18 | return { 19 | "accept-enconding": "gzip", 20 | "connection": "keep-alive", 21 | "content-type": "application/json", 22 | "nyanko-signature": core.NyankoSignature( 23 | iq, data 24 | ).generate_signature(), 25 | "nyanko-timestamp": str(int(time.time())), 26 | "nyanko-signature-version": "1", 27 | "nyanko-signature-algorithm": "HMACSHA256", 28 | "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)", 29 | } 30 | -------------------------------------------------------------------------------- /src/bcsfe/core/server/client_info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class ClientInfo: 7 | def __init__(self, cc: core.CountryCode, gv: core.GameVersion): 8 | self.cc = cc 9 | self.gv = gv 10 | 11 | @staticmethod 12 | def from_save_file(save_file: core.SaveFile): 13 | return ClientInfo(save_file.cc, save_file.game_version) 14 | 15 | def get_client_info(self) -> dict[str, Any]: 16 | cc = self.cc.get_client_info_code() 17 | 18 | data = { 19 | "clientInfo": { 20 | "client": { 21 | "countryCode": cc, 22 | "version": self.gv.game_version, 23 | }, 24 | "device": { 25 | "model": "SM-G955F", 26 | }, 27 | "os": { 28 | "type": "android", 29 | "version": "9", 30 | }, 31 | }, 32 | "nonce": core.Random.get_hex_string(32), 33 | } 34 | return data 35 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/enemy.properties: -------------------------------------------------------------------------------- 1 | # filename="enemy.properties" 2 | total_selected_enemies=<@t>{total} enemies hiện đang được chọn 3 | unlock_enemy_guide_success=<@su>Đã unlock enemy guide entries thành công 4 | remove_enemy_guide_success=<@su>Đã remove enemy guide entries thành công 5 | selected_enemy=<@t>{name} (<@t>{id}) đã được chọn 6 | select_enemies_valid=Chọn tất cả enemies trong enemy guide 7 | select_enemies_invalid=Chọn tất cả enemies không trong enemy guide 8 | select_enemies_all=Chọn tất cả enemies 9 | select_enemies_id=Chọn enemies theo ID 10 | select_enemies_name=Chọn enemies theo name 11 | select_enemies=Chọn enemies: 12 | enter_enemy_ids=Bạn có thể tìm enemy IDs tại đây: <@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order\nNhập enemy IDs {{range_input}}: 13 | enter_enemy_name=Nhập enemy name: 14 | enemy_not_found_name=<@w>Không tìm thấy enemies nào với name <@s>{name} 15 | unlock_enemy_guide=Mở khóa enemy guide entries 16 | remove_enemy_guide=Xóa enemy guide entries 17 | enemy_guide=Enemy Guide 18 | edit_enemy_guide=Nhập tùy chọn để chỉnh sửa enemy guide entries: -------------------------------------------------------------------------------- /src/bcsfe/core/io/json_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import json 3 | from typing import Any 4 | from bcsfe import core 5 | 6 | 7 | class JsonFile: 8 | def __init__(self, data: core.Data): 9 | self.json = json.loads(data.data) 10 | 11 | @staticmethod 12 | def from_path(path: core.Path) -> JsonFile: 13 | return JsonFile(path.read()) 14 | 15 | @staticmethod 16 | def from_object(js: Any) -> JsonFile: 17 | return JsonFile(core.Data(json.dumps(js))) 18 | 19 | @staticmethod 20 | def from_data(data: core.Data) -> JsonFile: 21 | return JsonFile(data) 22 | 23 | def to_data(self, indent: int | None = 4) -> core.Data: 24 | return core.Data(json.dumps(self.json, indent=indent)) 25 | 26 | def to_file(self, path: core.Path) -> None: 27 | path.write(self.to_data()) 28 | 29 | def to_object(self) -> Any: 30 | return self.json 31 | 32 | def get(self, key: str) -> Any: 33 | return self.json[key] 34 | 35 | def set(self, key: str, value: Any) -> None: 36 | self.json[key] = value 37 | 38 | def __str__(self) -> str: 39 | return str(self.json) 40 | 41 | def __getitem__(self, key: str) -> Any: 42 | return self.json[key] 43 | 44 | def __setitem__(self, key: str, value: Any) -> None: 45 | self.json[key] = value 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "bcsfe" 7 | authors = [{ name = "fieryhenry" }] 8 | description = "A save file editor for The Battle Cats" 9 | license = "GPL-3.0-or-later" 10 | readme = "README.md" 11 | requires-python = ">=3.9" 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: End Users/Desktop", 15 | "Topic :: Utilities", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.9", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [ 21 | "aenum", 22 | "colored==1.4.4", 23 | "pyjwt", 24 | "requests", 25 | "pyyaml", 26 | "beautifulsoup4", 27 | "argparse", 28 | ] 29 | dynamic = ["version"] 30 | keywords = ["battle cats", "save editor", "hacking"] 31 | 32 | [project.urls] 33 | Homepage = "https://codeberg.org/fieryhenry/BCSFE-Python" 34 | Repository = "https://codeberg.org/fieryhenry/BCSFE-Python" 35 | Issues = "https://codeberg.org/fieryhenry/BCSFE-Python/issues" 36 | Changelog = "https://codeberg.org/fieryhenry/BCSFE-Python/raw/branch/main/CHANGELOG.md" 37 | 38 | [tool.setuptools.dynamic] 39 | version = { attr = "bcsfe.__version__" } 40 | 41 | [tool.setuptools] 42 | package-dir = { "" = "src" } 43 | 44 | [project.scripts] 45 | bcsfe = "bcsfe:run" 46 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/talent_orbs.properties: -------------------------------------------------------------------------------- 1 | total_current_orbs=Total Current Orbs: <@q>{total_orbs} 2 | total_current_orb_types=Total Current Orb Types: <@q>{total_types} 3 | current_orbs=Current Orbs: 4 | orb_select=Select talent orbs to edit: 5 | selected_orbs=Selected talent Orbs: 6 | edit_orbs_individually=Do you want to edit each orb individually (<@q>1) or all at once (<@q>2)?: 7 | edit_orbs_all=Input a value to edit all selected orbs to (max <@t>{max}): 8 | failed_to_load_orbs=Failed to load talent orbs 9 | 10 | edit_orbs_help= 11 | >Help: 12 | >Available grades: {all_grades_str} 13 | >Available attributes: {all_attributes_str} 14 | >Available effects: {all_effects_str} 15 | ><@w>Note: Not all grades and effects will be available for all attributes. 16 | >Example inputs: 17 | > aku - selects all aku orbs 18 | > red s - selects all red orbs with s grade 19 | > alien d 0 - selects the alien orb with d grade that increases attack. 20 | > c 1 - selects the boost stories of legend orb with grade c 21 | >If you want to select <@q>all orbs then input: 22 | > <@q>* 23 | >If you want to do <@q>multiple selections then separate them with a <@q>comma like this: 24 | > s black 4,d 3,floating 25 | > 26 | 27 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/theme.properties: -------------------------------------------------------------------------------- 1 | theme_text= 2 | >Current Theme: <@s>{theme_name} (Version <@s>{theme_version}) 3 | >Made by <@s>{theme_author} 4 | >Theme File Location: <@s>{theme_path} 5 | 6 | default_theme_text= 7 | >Current Theme: <@s>Default 8 | >Theme File Location: <@s>{theme_path} 9 | 10 | checking_for_theme_updates=Checking for updates to external theme <@t>{theme_name}... 11 | external_theme_updated=<@su>Successfully updated external theme <@t>{theme_name} to version <@t>{version}<@t>.\n{{restart_to_see_changes}} 12 | external_theme_no_update=<@su>No update needed for external theme <@t>{theme_name} latest version is <@t>{version}<@t> 13 | theme_changed=<@su>Successfully changed theme to <@t>{theme_name}.\n{{restart_to_see_changes}} 14 | theme_removed=<@su>Successfully removed theme <@t>{theme_name}.\n{{restart_to_see_changes}} 15 | no_external_themes=<@e>No external themes found 16 | 17 | 18 | theme_dialog=Select a theme: 19 | add_theme=Add theme 20 | remove_theme=Remove theme 21 | theme_remove_dialog=Select themes to remove: 22 | enter_theme_git_repo=Enter the git repository of the theme (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git): 23 | theme_already_exists=<@e>A theme with name <@s>{theme_name} already exists.\nWould you like to overwrite it? ({{y/n}}): 24 | theme_added=<@su>Successfully added theme 25 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/talent_orbs.properties: -------------------------------------------------------------------------------- 1 | # filename="talent_orbs.properties" 2 | total_current_orbs=Tổng Orbs Hiện Tại: <@q>{total_orbs} 3 | total_current_orb_types=Tổng Loại Orbs Hiện Tại: <@q>{total_types} 4 | current_orbs=Orbs Hiện Tại: 5 | orb_select=Chọn talent orbs để chỉnh sửa: 6 | selected_orbs=Talent Orbs Đã Chọn: 7 | edit_orbs_individually=Bạn muốn chỉnh sửa từng orb riêng lẻ (<@q>1) hay tất cả cùng lúc (<@q>2)?: 8 | edit_orbs_all=Nhập giá trị để chỉnh sửa tất cả orbs đã chọn thành (tối đa <@t>{max}): 9 | failed_to_load_orbs=Không thể tải talent orbs 10 | 11 | edit_orbs_help= 12 | >Trợ giúp: 13 | >Các grade khả dụng: {all_grades_str} 14 | >Các attribute khả dụng: {all_attributes_str} 15 | >Các effect khả dụng: {all_effects_str} 16 | ><@w>Lưu ý: Không phải tất cả grade và effect đều khả dụng cho mọi attribute. 17 | >Ví dụ đầu vào: 18 | > aku - chọn tất cả aku orbs 19 | > red s - chọn tất cả red orbs với s grade 20 | > alien d 0 - chọn alien orb với d grade tăng attack. 21 | > c 1 - chọn boost stories of legend orb với grade c 22 | >Nếu bạn muốn chọn <@q>tất cả orbs thì nhập: 23 | > <@q>* 24 | >Nếu bạn muốn thực hiện <@q>nhiều lựa chọn thì phân cách chúng bằng <@q>dấu phẩy như thế này: 25 | > s black 4,d 3,floating -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/theme.properties: -------------------------------------------------------------------------------- 1 | # filename="theme.properties" 2 | theme_text= 3 | >Chủ đề hiện tại: <@s>{theme_name} (Phiên bản <@s>{theme_version}) 4 | >Được tạo bởi <@s>{theme_author} 5 | >Vị trí tệp chủ đề: <@s>{theme_path} 6 | 7 | default_theme_text= 8 | >Chủ đề hiện tại: <@s>Mặc định 9 | >Vị trí tệp chủ đề: <@s>{theme_path} 10 | 11 | checking_for_theme_updates=Đang kiểm tra cập nhật cho chủ đề bên ngoài <@t>{theme_name}... 12 | external_theme_updated=<@su>Đã cập nhật chủ đề bên ngoài thành công <@t>{theme_name} lên phiên bản <@t>{version}<@t>.\n{{restart_to_see_changes}} 13 | external_theme_no_update=<@su>Không cần cập nhật cho chủ đề bên ngoài <@t>{theme_name} phiên bản mới nhất là <@t>{version}<@t> 14 | theme_changed=<@su>Đã thay đổi chủ đề thành công thành <@t>{theme_name}.\n{{restart_to_see_changes}} 15 | theme_removed=<@su>Đã xóa chủ đề thành công <@t>{theme_name}.\n{{restart_to_see_changes}} 16 | no_external_themes=<@e>Không tìm thấy chủ đề bên ngoài 17 | 18 | 19 | theme_dialog=Chọn một chủ đề: 20 | add_theme=Thêm chủ đề 21 | remove_theme=Xóa chủ đề 22 | theme_remove_dialog=Chọn các chủ đề để xóa: 23 | enter_theme_git_repo=Nhập kho git của chủ đề (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git): 24 | theme_already_exists=<@e>Một chủ đề với tên <@s>{theme_name} đã tồn tại.\nBạn có muốn ghi đè lên không? ({{y/n}}): 25 | theme_added=<@su>Đã thêm chủ đề thành công -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/treasures.properties: -------------------------------------------------------------------------------- 1 | whole_chapters=Whole Chapters 2 | individual_stages=Individual Stages 3 | treasure_groups=Treasure Groups / Sets 4 | treasure_dialog=Do you want to edit treasures for <@t>whole chapters at once, <@t>individual stages or individual <@t>treasure groups?: 5 | treasures_edited=<@su>Succesfully edited treasures 6 | per_chapter=Per Chapter 7 | all_selected_chapters=All Selected Chapters 8 | edit_per_chapter=Do you want to edit data for <@t>all selected chapters or <@t>each chapter individually?: 9 | no_treasure=No Treasure 10 | custom_treasure_level=Custom Treasure Level (<@w>Only edit if you know what you're doing!) 11 | treasure_level_dialog=Enter the treasure level you want to set: 12 | custom_treasure_level_dialog=Enter the custom treasure level you want to set: 13 | select_stage_by_id=Select Stages by IDs 14 | select_stage_by_name=Select Stages by Names 15 | select_stage_dialog=Do you want to select stages by <@t>IDs or <@t>Names?: 16 | select_stage_id=Enter the stage IDs you want to select {{range_input}} 17 | select_stages_name=Select stages: 18 | select_treasure_groups=Select the treasure groups you want to edit: 19 | story_treasures=Story Treasures 20 | current_chapter=Current Chapter: <@t>{chapter_name} 21 | current_treasure_group=Current Treasure Group: <@t>{treasure_group_name} 22 | group_individual=Individual Groups 23 | group_all_at_once=All Selected Groups 24 | select_treasure_groups_individual=Do you want to edit the treasure level for each <@t>treasure group individually or for <@t>all selected groups at once?: 25 | -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/rare_ticket_trade.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | from bcsfe.cli import color, dialog_creator 5 | 6 | 7 | class RareTicketTrade: 8 | @staticmethod 9 | def rare_ticket_trade(save_file: core.SaveFile): 10 | current_amount = save_file.rare_tickets 11 | max_amount = max( 12 | core.core_data.max_value_manager.get("rare_tickets") 13 | - current_amount, 14 | 0, 15 | ) 16 | if max_amount == 0: 17 | color.ColoredText.localize("rare_ticket_trade_maxed") 18 | return 19 | to_add = dialog_creator.IntInput(max_amount, 0).get_input_locale_while( 20 | "rare_ticket_trade_enter", 21 | {"max": max_amount, "current": current_amount}, 22 | ) 23 | if to_add is None: 24 | return 25 | 26 | space = False 27 | for storage_item in save_file.cats.storage_items: 28 | if storage_item.item_type == 0 or ( 29 | storage_item.item_id == 1 and storage_item.item_type == 2 30 | ): 31 | storage_item.item_id = 1 32 | storage_item.item_type = 2 33 | space = True 34 | break 35 | 36 | if not space: 37 | color.ColoredText.localize("rare_ticket_trade_storage_full") 38 | return 39 | 40 | amount = to_add * 5 41 | save_file.gatya.trade_progress = amount 42 | 43 | color.ColoredText.localize( 44 | "rare_ticket_successfully_traded", rare_ticket_count=to_add 45 | ) 46 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/treasures.properties: -------------------------------------------------------------------------------- 1 | # filename="treasures.properties" 2 | whole_chapters=Toàn bộ chapters 3 | individual_stages=Từng stages riêng lẻ 4 | treasure_groups=Nhóm treasures / Sets 5 | treasure_dialog=Bạn muốn chỉnh sửa treasures cho <@t>toàn bộ chapters cùng lúc, <@t>từng stages riêng lẻ hay từng <@t>treasure groups?: 6 | treasures_edited=<@su>Đã chỉnh sửa treasures thành công 7 | per_chapter=Theo chapter 8 | all_selected_chapters=Tất cả chapters đã chọn 9 | no_treasure=Không có treasure 10 | custom_treasure_level=Level treasure tùy chỉnh (<@w>Chỉ chỉnh sửa nếu bạn biết mình đang làm gì!) 11 | treasure_level_dialog=Nhập level treasure bạn muốn đặt: 12 | custom_treasure_level_dialog=Nhập level treasure tùy chỉnh bạn muốn đặt: 13 | select_stage_by_id=Chọn stages theo IDs 14 | select_stage_by_name=Chọn stages theo tên 15 | select_stage_dialog=Bạn muốn chọn stages theo <@t>IDs hay <@t>tên?: 16 | select_stage_id=Nhập IDs stage bạn muốn chọn {{range_input}} 17 | select_stages_name=Chọn stages: 18 | select_treasure_groups=Chọn treasure groups bạn muốn chỉnh sửa: 19 | story_treasures=Story Treasures 20 | current_chapter=Chapter hiện tại: <@t>{chapter_name} 21 | current_treasure_group=Nhóm treasure hiện tại: <@t>{treasure_group_name} 22 | group_individual=Nhóm riêng lẻ 23 | group_all_at_once=Tất cả nhóm đã chọn 24 | select_treasure_groups_individual=Bạn muốn chỉnh sửa level treasure cho từng <@t>treasure group riêng lẻ hay cho <@t>tất cả nhóm đã chọn cùng lúc?: 25 | edit_per_chapter=Bạn muốn chỉnh sửa dữ liệu cho <@t>tất cả chapters đã chọn hay <@t>từng chapter riêng lẻ?: 26 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import subprocess 3 | import threading 4 | 5 | 6 | class CommandResult: 7 | def __init__(self, result: str, exit_code: int): 8 | self.result = result 9 | self.exit_code = exit_code 10 | 11 | def __str__(self) -> str: 12 | return self.result 13 | 14 | def __repr__(self) -> str: 15 | return f"Result({self.result!r}, {self.exit_code!r})" 16 | 17 | @property 18 | def success(self) -> bool: 19 | return self.exit_code == 0 20 | 21 | @staticmethod 22 | def create_success(result: str = "") -> CommandResult: 23 | return CommandResult(result, 0) 24 | 25 | @staticmethod 26 | def create_failure(result: str = "") -> CommandResult: 27 | return CommandResult(result, 1) 28 | 29 | 30 | class Command: 31 | def __init__(self, command: str, display_output: bool = True): 32 | self.command = command 33 | self.display_output = display_output 34 | 35 | def run(self, inputData: str = "\n") -> CommandResult: 36 | self.process = subprocess.Popen( 37 | self.command, 38 | stdout=subprocess.PIPE, 39 | stderr=subprocess.STDOUT, 40 | stdin=subprocess.PIPE, 41 | shell=True, 42 | universal_newlines=True, 43 | ) 44 | output, _ = self.process.communicate(inputData) 45 | return_code = self.process.wait() 46 | return CommandResult(output, return_code) 47 | 48 | def run_in_thread(self, inputData: str = "\n") -> None: 49 | self.thread = threading.Thread(target=self.run, args=(inputData,)) 50 | self.thread.start() 51 | -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/fixes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import color 4 | import datetime 5 | 6 | 7 | class Fixes: 8 | @staticmethod 9 | def fix_gamatoto_crash(save_file: core.SaveFile): 10 | save_file.gamatoto.skin = 2 11 | 12 | color.ColoredText.localize("fix_gamatoto_crash_success") 13 | 14 | @staticmethod 15 | def fix_ototo_crash(save_file: core.SaveFile): 16 | save_file.ototo.cannons = core.game.gamoto.ototo.Cannons.init( 17 | save_file.game_version 18 | ) 19 | color.ColoredText.localize("fix_ototo_crash_success") 20 | 21 | @staticmethod 22 | def fix_time_errors(save_file: core.SaveFile): 23 | save_file.date_3 = datetime.datetime.now() 24 | save_file.timestamp = datetime.datetime.now().timestamp() 25 | save_file.energy_penalty_timestamp = datetime.datetime.now().timestamp() 26 | 27 | color.ColoredText.localize("fix_time_errors_success") 28 | 29 | # 10 = 62 / hgt1 = ahead by too much 30 | # 11 = 63 / hgt0 = behind by too much 31 | # 12 = 61 / hgt2 = ahead by too much 32 | 33 | # date_3 - controls gacha errors (hgt2) 34 | # can't be ahead of the device time 35 | 36 | # timestamp - controls gacha errors (hgt1, hgt0) 37 | # can't be ahead by more than 10 minutes to device time 38 | # can't be behind by more than 1.5 days to device time 39 | 40 | # penalty_timestamp - controls energy / gamatoto errors 41 | # can't by ahead of device time 42 | # can't be ahead by more than 1 day to device time 43 | # can't be behind by more than 1 day to device time 44 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/input.properties: -------------------------------------------------------------------------------- 1 | input_int=Input a number between <@q>{min} and <@q>{max}: 2 | select_edit=Select options for <@t>{group_name}: 3 | input_int_default=Input a number between <@q>{min} and <@q>{max} (default <@q>{default}): 4 | input_many=Input numbers between <@q>{min} and <@q>{max} separated by spaces: 5 | input_single=Input a number between <@q>{min} and <@q>{max}: 6 | input=Enter a value for <@t>{name} (current value: <@q>{value}) (max value: <@q>{max}): 7 | input_min=Enter a value for <@t>{name} (current value: <@q>{value}) (range: <@q>{min} - <@q>{max}): 8 | input_non_max=Enter a value for <@t>{name} (current value: <@q>{value}): 9 | input_all=Enter a value for all <@t>{name} (max value: <@q>{max}): 10 | value_changed=<@su>Successfully changed <@s>{name} to <@s>{value} 11 | value_gave=<@su>Successfully gave the <@s>{name} 12 | all_at_once=Select all options at once 13 | invalid_input=<@e>Invalid input. Please try again. 14 | invalid_input_int=<@e>Invalid input. Please enter a number between <@s>{min} and <@s>{max} 15 | select_option=Select option: 16 | finish=Finish 17 | features=Features: 18 | go_back=Go back 19 | yes_key=y 20 | quit_key=q 21 | range_input=separated by spaces (e.g <@t>1 2 3 192), or enter a range (e.g. <@t>1-43) or enter <@t>all: 22 | select_features= 23 | >To select a feature, enter 24 | >- a <@q>number corresponding to the number on the left 25 | >- <@t>text to search for a feature 26 | >You can press <@t>enter to view all features 27 | >Some features are <@t>categories and so when selected, will display all of its <@t>sub-features 28 | >Input: 29 | 30 | individual=Individual 31 | edit_all_at_once=All at once 32 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/gamoto/catamins.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Catamin: 6 | def __init__(self, amount: int): 7 | self.amount = amount 8 | 9 | @staticmethod 10 | def read(stream: core.Data) -> Catamin: 11 | amount = stream.read_int() 12 | return Catamin(amount) 13 | 14 | def write(self, stream: core.Data): 15 | stream.write_int(self.amount) 16 | 17 | def serialize(self) -> int: 18 | return self.amount 19 | 20 | @staticmethod 21 | def deserialize(data: int) -> Catamin: 22 | return Catamin(data) 23 | 24 | def __repr__(self): 25 | return f"Catamin({self.amount})" 26 | 27 | def __str__(self): 28 | return f"Catamin({self.amount})" 29 | 30 | 31 | class Catamins: 32 | def __init__(self, catamins: list[Catamin]): 33 | self.catamins = catamins 34 | 35 | @staticmethod 36 | def read(stream: core.Data) -> Catamins: 37 | total = stream.read_int() 38 | catamins: list[Catamin] = [] 39 | for _ in range(total): 40 | catamins.append(Catamin.read(stream)) 41 | return Catamins(catamins) 42 | 43 | def write(self, stream: core.Data): 44 | stream.write_int(len(self.catamins)) 45 | for catamin in self.catamins: 46 | catamin.write(stream) 47 | 48 | def serialize(self) -> list[int]: 49 | return [catamin.serialize() for catamin in self.catamins] 50 | 51 | @staticmethod 52 | def deserialize(data: list[int]) -> Catamins: 53 | return Catamins([Catamin.deserialize(catamin) for catamin in data]) 54 | 55 | def __repr__(self): 56 | return f"Catamins({self.catamins})" 57 | 58 | def __str__(self): 59 | return f"Catamins({self.catamins})" 60 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/input.properties: -------------------------------------------------------------------------------- 1 | # filename="input.properties" 2 | input_int=Nhập một số giữa <@q>{min} và <@q>{max}: 3 | select_edit=Chọn các tùy chọn cho <@t>{group_name}: 4 | input_int_default=Nhập một số giữa <@q>{min} và <@q>{max} (mặc định <@q>{default}): 5 | input_many=Nhập các số giữa <@q>{min} và <@q>{max} cách nhau bằng dấu cách: 6 | input_single=Nhập một số giữa <@q>{min} và <@q>{max}: 7 | input=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}) (giá trị tối đa: <@q>{max}): 8 | input_min=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}) (phạm vi: <@q>{min} - <@q>{max}): 9 | input_non_max=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}): 10 | input_all=Nhập giá trị cho tất cả <@t>{name} (giá trị tối đa: <@q>{max}): 11 | value_changed=<@su>Đã thay đổi thành công <@s>{name} thành <@s>{value} 12 | value_gave=<@su>Đã cung cấp thành công <@s>{name} 13 | all_at_once=Chọn tất cả các tùy chọn cùng lúc 14 | invalid_input=<@e>Đầu vào không hợp lệ. Vui lòng thử lại. 15 | invalid_input_int=<@e>Đầu vào không hợp lệ. Vui lòng nhập số giữa <@s>{min} và <@s>{max} 16 | features=Tính năng: 17 | go_back=Quay lại 18 | yes_key=y 19 | quit_key=q 20 | range_input=cách nhau bằng dấu cách (ví dụ <@t>1 2 3 192), hoặc nhập phạm vi (ví dụ <@t>1-43) hoặc nhập <@t>all: 21 | select_features= 22 | >Để chọn một tính năng, nhập 23 | >- một <@q>số tương ứng với số bên trái 24 | >- <@t>văn bản để tìm kiếm tính năng 25 | >Bạn có thể nhấn <@t>enter để xem tất cả tính năng 26 | >Một số tính năng là <@t>danh mục và khi chọn, sẽ hiển thị tất cả <@t>tính năng con 27 | >Đầu vào: 28 | 29 | individual=Cá nhân 30 | edit_all_at_once=Tất cả cùng lúc 31 | finish=Hoàn thành 32 | select_option=Chọn tùy chọn: 33 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/bannable_items.properties: -------------------------------------------------------------------------------- 1 | do_you_want_to_continue=Do you want to continue? ({{y/n}}): 2 | 3 | catfood_warning=<@w>WARNING: Editing in cat food can result in a ban. Use at your own risk.\n{{do_you_want_to_continue}} 4 | legend_ticket_warning=<@w>WARNING: Editing in legend tickets can result in a ban. Use at your own risk.\n{{do_you_want_to_continue}} 5 | rare_ticket_warning= 6 | ><@w>WARNING: Editing in rare tickets can result in a ban. Use at your own risk. 7 | >You can use the rare ticket trade feature to get rare tickets with a lower risk of ban. 8 | platinum_ticket_warning= 9 | ><@w>WARNING: Editing in platinum tickets can result in a ban. Use at your own risk. 10 | >You can use the platinum shards feature to get platinum tickets with a lower risk of ban. 11 | 12 | select_an_option_to_continue=Select an option to continue editing {feature_name}: 13 | 14 | continue_editing=Continue editing {feature_name} 15 | go_to_safe_feature=Go to the safer {safer_feature_name} feature 16 | cancel_editing=Cancel editing {feature_name} 17 | 18 | rare_ticket_trade_enter=Enter the number of rare tickets you want to <@q>add (max value: <@q>{max}) (current amount: <@q>{current}): 19 | rare_ticket_trade_storage_full=<@e>ERROR: You don't have enough space in your cat storage, please free 1 space! 20 | rare_ticket_successfully_traded= 21 | ><@su>Successfully gave {rare_ticket_count} rare tickets. 22 | >You now need to enter the cat storage and press the <@q>Use all button and then press the <@q>Trade for Ticket button to get your tickets. 23 | 24 | rare_tickets_l=rare tickets 25 | rare_ticket_trade_l=rare ticket trade 26 | 27 | rare_ticket_trade_maxed=<@e>ERROR: You already have the maximum amount of rare tickets!\nPlease use some before running this feature! 28 | 29 | platinum_tickets_l=platinum tickets 30 | platinum_shards_l=platinum shards -------------------------------------------------------------------------------- /src/bcsfe/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import traceback 3 | 4 | from bcsfe import cli 5 | 6 | from bcsfe import core 7 | 8 | import bcsfe 9 | import argparse 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser("bcsfe") 14 | 15 | parser.add_argument( 16 | "--version", "-v", action="store_true", help="display the version and exit" 17 | ) 18 | parser.add_argument( 19 | "--input-path", "-i", type=str, help="input path to save file to edit" 20 | ) 21 | parser.add_argument( 22 | "--config-path", 23 | "-c", 24 | type=str, 25 | default=None, 26 | help="path to the config file. If unspecified defaults to Documents/bcsfe/config.yaml", 27 | ) 28 | parser.add_argument( 29 | "--log-path", 30 | "-l", 31 | type=str, 32 | default=None, 33 | help="path to the log file. If unspecified defaults to Documents/bcsfe/bcsfe.log", 34 | ) 35 | 36 | args = parser.parse_args() 37 | if args.version: 38 | print(bcsfe.__version__) 39 | exit() 40 | 41 | if args.config_path is not None: 42 | core.set_config_path(core.Path(args.config_path)) 43 | 44 | if args.log_path is not None: 45 | core.set_log_path(core.Path(args.log_path)) 46 | 47 | core.core_data.init_data() 48 | 49 | try: 50 | cli.main.Main().main(args.input_path) 51 | except KeyboardInterrupt: 52 | cli.main.Main.leave() 53 | except Exception as e: 54 | tb = traceback.format_exc() 55 | cli.color.ColoredText.localize( 56 | "error", error=e, version=bcsfe.__version__, traceback=tb 57 | ) 58 | try: 59 | cli.main.Main.exit_editor() 60 | except Exception: 61 | pass 62 | except KeyboardInterrupt: 63 | pass 64 | 65 | 66 | main() 67 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/locale.properties: -------------------------------------------------------------------------------- 1 | available_locales=Available languages: 2 | locale_desc=Language to use {{config_value_txt}} 3 | locale=Language 4 | locale_dialog=Select a language: 5 | add_locale=Add Locale 6 | remove_locale=Remove Locale 7 | locale_remove_dialog=Select locales to remove: 8 | enter_locale_git_repo=Enter the git repository of the locale (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git): 9 | locale_already_exists=<@e>A locale with name <@s>{locale_name} already exists.\nWould you like to overwrite it? ({{y/n}}): 10 | locale_added=<@su>Successfully added localization 11 | checking_for_locale_updates=Checking for updates to external localization <@t>{locale_name}... 12 | external_locale_updated=<@su>Successfully updated external localization <@t>{locale_name} to version <@t>{version}<@t>.\n{{restart_to_see_changes}} 13 | external_locale_no_update=<@su>No update needed for external localization <@t>{locale_name} latest version is <@t>{version}<@t> 14 | invalid_git_repo=<@e>Invalid git repository 15 | locale_cancelled=<@e>Cancelled 16 | restart_to_see_changes=You will need to restart the editor to see all of the changes 17 | locale_changed=<@su>Successfully changed locale to <@t>{locale_name}.\n{{restart_to_see_changes}} 18 | locale_removed=<@su>Successfully removed locale <@t>{locale_name}.\n{{restart_to_see_changes}} 19 | no_external_locales=<@e>No external locales found 20 | 21 | missing_locale_keys=Missing Locale Keys: 22 | extra_locale_keys=Extra Locale Keys: 23 | 24 | locale_text= 25 | >Current Locale: <@s>{locale_name} (Version: <@s>{locale_version}) 26 | >Made by <@s>{locale_author} 27 | >Locale File Location: <@s>{locale_path} 28 | 29 | default_locale_text_authors= 30 | >Current Locale: <@s>{name} 31 | >Made by <@s>{authors} 32 | >Locale File Location: <@s>{path} 33 | 34 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/locale.properties: -------------------------------------------------------------------------------- 1 | # filename="locale.properties" 2 | available_locales=Các ngôn ngữ khả dụng: 3 | locale_desc=Ngôn ngữ sử dụng {{config_value_txt}} 4 | locale=Ngôn ngữ 5 | locale_dialog=Chọn một ngôn ngữ: 6 | add_locale=Thêm ngôn ngữ 7 | remove_locale=Xóa ngôn ngữ 8 | locale_remove_dialog=Chọn các ngôn ngữ để xóa: 9 | enter_locale_git_repo=Nhập kho git của ngôn ngữ (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git): 10 | locale_already_exists=<@e>Một ngôn ngữ với tên <@s>{locale_name} đã tồn tại.\nBạn có muốn ghi đè lên không? ({{y/n}}): 11 | locale_added=<@su>Đã thêm ngôn ngữ thành công 12 | checking_for_locale_updates=Đang kiểm tra cập nhật cho ngôn ngữ bên ngoài <@t>{locale_name}... 13 | external_locale_updated=<@su>Đã cập nhật ngôn ngữ bên ngoài thành công <@t>{locale_name} lên phiên bản <@t>{version}<@t>.\n{{restart_to_see_changes}} 14 | external_locale_no_update=<@su>Không cần cập nhật cho ngôn ngữ bên ngoài <@t>{locale_name} phiên bản mới nhất là <@t>{version}<@t> 15 | invalid_git_repo=<@e>Kho git không hợp lệ 16 | locale_cancelled=<@e>Đã hủy 17 | restart_to_see_changes=Bạn cần khởi động lại trình chỉnh sửa để thấy tất cả thay đổi 18 | locale_changed=<@su>Đã thay đổi ngôn ngữ thành công thành <@t>{locale_name}.\n{{restart_to_see_changes}} 19 | locale_removed=<@su>Đã xóa ngôn ngữ thành công <@t>{locale_name}.\n{{restart_to_see_changes}} 20 | no_external_locales=<@e>Không tìm thấy ngôn ngữ bên ngoài 21 | 22 | missing_locale_keys=Các khóa ngôn ngữ thiếu: 23 | extra_locale_keys=Các khóa ngôn ngữ thêm: 24 | 25 | locale_text= 26 | >Ngôn ngữ hiện tại: <@s>{locale_name} (Phiên bản: <@s>{locale_version}) 27 | >Được tạo bởi <@s>{locale_author} 28 | >Vị trí tệp ngôn ngữ: <@s>{locale_path} 29 | 30 | default_locale_text_authors= 31 | >Ngôn ngữ hiện tại: <@s>{name} 32 | >Được tạo bởi <@s>{authors} 33 | >Vị trí tệp ngôn ngữ: <@s>{path} -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/bannable_items.properties: -------------------------------------------------------------------------------- 1 | # filename="bannable_items.properties" 2 | do_you_want_to_continue=Bạn có muốn tiếp tục không? ({{y/n}}): 3 | 4 | catfood_warning=<@w>CẢNH BÁO: Chỉnh sửa CatFood có thể dẫn đến ban. Chỉnh sửa với rủi ro có thể xảy ra bất cứ lúc nào.\n{{do_you_want_to_continue}} 5 | legend_ticket_warning=<@w>CẢNH BÁO: Chỉnh sửa Legend Tickets có thể dẫn đến ban. Chỉnh sửa với rủi ro có thể xảy ra bất cứ lúc nào.\n{{do_you_want_to_continue}} 6 | rare_ticket_warning= 7 | ><@w>CẢNH BÁO: Chỉnh sửa Rare Tickets có thể dẫn đến ban. Chỉnh sửa với rủi ro có thể xảy ra bất cứ lúc nào. 8 | >Bạn có thể sử dụng tính năng Rare Ticket Trade để nhận Rare Tickets với rủi ro ban thấp hơn. 9 | platinum_ticket_warning= 10 | ><@w>CẢNH BÁO: Chỉnh sửa Platinum Tickets có thể dẫn đến ban. Chỉnh sửa với rủi ro có thể xảy ra bất cứ lúc nào. 11 | >Bạn có thể sử dụng tính năng Platinum Shards để nhận Platinum Tickets với rủi ro ban thấp hơn. 12 | 13 | select_an_option_to_continue=Chọn tùy chọn để tiếp tục chỉnh sửa {feature_name}: 14 | 15 | continue_editing=Tiếp tục chỉnh sửa {feature_name} 16 | go_to_safe_feature=Chuyển đến tính năng an toàn hơn {safer_feature_name} 17 | cancel_editing=Hủy chỉnh sửa {feature_name} 18 | 19 | rare_ticket_trade_enter=Nhập số Rare Tickets bạn muốn <@q>add (max value: <@q>{max}) (current amount: <@q>{current}): 20 | rare_ticket_trade_storage_full=<@e>LỖI: Bạn không có đủ chỗ trong cat storage, vui lòng giải phóng nó! 21 | rare_ticket_successfully_traded= 22 | ><@su>Đã trao {rare_ticket_count} Rare Tickets thành công. 23 | >Bây giờ bạn cần vào cat storage và nhấn nút <@q>Use all rồi nhấn nút <@q>Trade for Ticket để nhận tickets của bạn. 24 | 25 | rare_tickets_l=Rare Tickets 26 | rare_ticket_trade_l=Rare Ticket Trade 27 | 28 | rare_ticket_trade_maxed=<@e>LỖI: Bạn đã có lượng Rare Tickets tối đa!\nVui lòng sử dụng một số trước khi chạy tính năng này! 29 | 30 | platinum_tickets_l=Platinum Tickets 31 | platinum_shards_l=Platinum Shards -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/my_sale.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class MySale: 7 | def __init__(self, dict_1: dict[int, int], dict_2: dict[int, bool]): 8 | self.dict_1 = dict_1 9 | self.dict_2 = dict_2 10 | 11 | @staticmethod 12 | def init() -> MySale: 13 | return MySale({}, {}) 14 | 15 | @staticmethod 16 | def read_bonus_hash(stream: core.Data): 17 | variable_length = stream.read_variable_length_int() 18 | dict_1 = {} 19 | for _ in range(variable_length): 20 | key = stream.read_variable_length_int() 21 | value = stream.read_variable_length_int() 22 | dict_1[key] = value 23 | 24 | variable_length = stream.read_variable_length_int() 25 | dict_2 = {} 26 | for _ in range(variable_length): 27 | key = stream.read_variable_length_int() 28 | value = stream.read_byte() 29 | dict_2[key] = value 30 | 31 | return MySale(dict_1, dict_2) 32 | 33 | def write_bonus_hash(self, stream: core.Data): 34 | stream.write_variable_length_int(len(self.dict_1)) 35 | for key, value in self.dict_1.items(): 36 | stream.write_variable_length_int(key) 37 | stream.write_variable_length_int(value) 38 | 39 | stream.write_variable_length_int(len(self.dict_2)) 40 | for key, value in self.dict_2.items(): 41 | stream.write_variable_length_int(key) 42 | stream.write_byte(value) 43 | 44 | def serialize(self) -> dict[str, Any]: 45 | return { 46 | "dict_1": self.dict_1, 47 | "dict_2": self.dict_2, 48 | } 49 | 50 | @staticmethod 51 | def deserialize(data: dict[str, Any]) -> MySale: 52 | return MySale(data.get("dict_1", {}), data.get("dict_2", {})) 53 | 54 | def __repr__(self) -> str: 55 | return f"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})" 56 | 57 | def __str__(self) -> str: 58 | return f"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})" 59 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/stamp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class StampData: 7 | def __init__( 8 | self, 9 | current_stamp: int, 10 | collected_stamp: list[int], 11 | unknown: int, 12 | daily_reward: int, 13 | ): 14 | self.current_stamp = current_stamp 15 | self.collected_stamp = collected_stamp 16 | self.unknown = unknown 17 | self.daily_reward = daily_reward 18 | 19 | @staticmethod 20 | def init() -> StampData: 21 | return StampData(0, [0] * 30, 0, 0) 22 | 23 | @staticmethod 24 | def read(stream: core.Data) -> StampData: 25 | current_stamp = stream.read_int() 26 | collected_stamp = stream.read_int_list(30) 27 | unknown = stream.read_int() 28 | daily_reward = stream.read_int() 29 | return StampData(current_stamp, collected_stamp, unknown, daily_reward) 30 | 31 | def write(self, stream: core.Data): 32 | stream.write_int(self.current_stamp) 33 | stream.write_int_list(self.collected_stamp, write_length=False) 34 | stream.write_int(self.unknown) 35 | stream.write_int(self.daily_reward) 36 | 37 | def serialize(self) -> dict[str, Any]: 38 | return { 39 | "current_stamp": self.current_stamp, 40 | "collected_stamp": self.collected_stamp, 41 | "unknown": self.unknown, 42 | "daily_reward": self.daily_reward, 43 | } 44 | 45 | @staticmethod 46 | def deserialize(data: dict[str, Any]) -> StampData: 47 | return StampData( 48 | data.get("current_stamp", 0), 49 | data.get("collected_stamp", []), 50 | data.get("unknown", 0), 51 | data.get("daily_reward", 0), 52 | ) 53 | 54 | def __repr__(self): 55 | return f"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})" 56 | 57 | def __str__(self): 58 | return f"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})" 59 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/yaml.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | from bcsfe import cli 5 | import yaml 6 | 7 | 8 | class YamlFile: 9 | def __init__(self, path: core.Path, print_err: bool = True): 10 | self.path = path 11 | self.yaml: dict[str, Any] = {} 12 | if self.path.exists(): 13 | self.data = path.read() 14 | try: 15 | yml = yaml.safe_load(self.data.data) 16 | if not isinstance(yml, dict): 17 | self.yaml = {} 18 | self.save(print_err) 19 | else: 20 | self.yaml = yml 21 | except yaml.YAMLError: 22 | self.yaml = {} 23 | self.save(print_err) 24 | else: 25 | self.yaml = {} 26 | self.save(print_err) 27 | 28 | def save(self, print_err: bool = True) -> None: 29 | self.path.parent().generate_dirs() 30 | 31 | try: 32 | with open(self.path.path, "w", encoding="utf-8") as f: 33 | yaml.dump(self.yaml, f) 34 | except FileNotFoundError: 35 | if print_err: 36 | cli.color.ColoredText.localize("yaml_create_error", path=self.path.path) 37 | 38 | def __getitem__(self, key: str) -> Any: 39 | return self.yaml[key] 40 | 41 | def __setitem__(self, key: str, value: Any) -> None: 42 | self.yaml[key] = value 43 | 44 | def __delitem__(self, key: str) -> None: 45 | del self.yaml[key] 46 | 47 | def __contains__(self, key: str) -> bool: 48 | return key in self.yaml 49 | 50 | def __iter__(self): 51 | return iter(self.yaml) 52 | 53 | def __len__(self) -> int: 54 | return len(self.yaml) 55 | 56 | def __repr__(self) -> str: 57 | return self.yaml.__repr__() 58 | 59 | def __str__(self) -> str: 60 | return self.yaml.__str__() 61 | 62 | def get(self, key: str) -> Any: 63 | return self.yaml.get(key) 64 | 65 | def remove(self) -> None: 66 | self.path.remove() 67 | self.yaml = {} 68 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/unlock_popups.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Popup: 6 | def __init__(self, seen: bool): 7 | self.seen = seen 8 | 9 | @staticmethod 10 | def init() -> Popup: 11 | return Popup(False) 12 | 13 | @staticmethod 14 | def read(stream: core.Data) -> Popup: 15 | seen = stream.read_bool() 16 | return Popup(seen) 17 | 18 | def write(self, stream: core.Data): 19 | stream.write_bool(self.seen) 20 | 21 | def serialize(self) -> bool: 22 | return self.seen 23 | 24 | @staticmethod 25 | def deserialize(data: bool) -> Popup: 26 | return Popup(data) 27 | 28 | def __repr__(self) -> str: 29 | return f"Popup(seen={self.seen!r})" 30 | 31 | def __str__(self) -> str: 32 | return self.__repr__() 33 | 34 | 35 | class UnlockPopups: 36 | def __init__(self, popups: dict[int, Popup]): 37 | self.popups = popups 38 | 39 | @staticmethod 40 | def init() -> UnlockPopups: 41 | return UnlockPopups({}) 42 | 43 | @staticmethod 44 | def read(stream: core.Data) -> UnlockPopups: 45 | total = stream.read_int() 46 | popups: dict[int, Popup] = {} 47 | for _ in range(total): 48 | key = stream.read_int() 49 | popups[key] = Popup.read(stream) 50 | return UnlockPopups(popups) 51 | 52 | def write(self, stream: core.Data): 53 | stream.write_int(len(self.popups)) 54 | for key, popup in self.popups.items(): 55 | stream.write_int(key) 56 | popup.write(stream) 57 | 58 | def serialize(self) -> dict[int, bool]: 59 | return {key: popup.serialize() for key, popup in self.popups.items()} 60 | 61 | @staticmethod 62 | def deserialize(data: dict[int, bool]) -> UnlockPopups: 63 | return UnlockPopups( 64 | {int(key): Popup.deserialize(popup) for key, popup in data.items()} 65 | ) 66 | 67 | def __repr__(self) -> str: 68 | return f"Popups(popups={self.popups!r})" 69 | 70 | def __str__(self) -> str: 71 | return self.__repr__() 72 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/items.properties: -------------------------------------------------------------------------------- 1 | # filename="items.properties" 2 | # Lưu ý rằng không phải tất cả các vật phẩm đều có ở đây 3 | 4 | catamins=Catamins 5 | catfruit=Catfruit 6 | base_materials=Base Materials 7 | inquiry_code=Inquiry Code 8 | rare_gatya_seed=Rare Gacha Seed 9 | normal_gatya_seed=Normal Gacha Seed 10 | event_gatya_seed=Event Gacha Seed 11 | unlocked_slots=Unlocked Slots|Equip Slots|Lineups 12 | password_refresh_token=Password Refresh Token 13 | challenge_score=Điểm Challenge 14 | dojo_score=Điểm Dojo 15 | items=Items 16 | user_rank_rewards=Claim phần thưởng User Rank (Không gửi phần thưởng) 17 | 18 | catfood=CatFood 19 | xp=XP 20 | normal_tickets=Normal Tickets|Basic Tickets|Silver Tickets 21 | rare_tickets=Rare Tickets|Gold Tickets 22 | platinum_tickets=Platinum Tickets 23 | legend_tickets=Legend Tickets 24 | 100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets 25 | 100_million_warn=<@w>Lưu ý: bạn chỉ có thể thấy và sử dụng tickets nếu sự kiện 100 Million Downloads hiện đang active 26 | platinum_shards=Platinum Shards 27 | np=NP 28 | leadership=Leadership 29 | catseyes=Catseyes 30 | battle_items=Battle Items 31 | talent_orbs=Talent Orbs 32 | scheme_items=Scheme Items 33 | labyrinth_medals=Labyrinth Medals 34 | restart_pack=Restart Pack|Returner Mode 35 | engineers=Engineers 36 | gamototo=Gamatoto / Ototo 37 | special_skills=Special Skills / Base Abilities 38 | treasure_chests=Treasure Chests 39 | unknown_treasure_chest_name=Unknown Treasure Chest ({id}) 40 | 41 | rare_ticket_trade=Rare Ticket Trade 42 | rare_ticket_trade_feature_name=Rare Ticket Trade (Cho phép lấy vé không bị ban) 43 | 44 | other=Other 45 | gatya=Gacha 46 | levels=Levels / Story / Treasure 47 | cats_special_skills=Cats / Special Skills 48 | 49 | gatya_item_unknown_name=Unknown Item 50 | unknown_catamin_name=Unknown Catamin <@t>{id} 51 | unknown_catseye_name=Unknown Catseye <@t>{id} 52 | unknown_catfruit_name=Unknown Catfruit <@t>{id} 53 | unknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id} 54 | 55 | reset_golden_cat_cpus_success=<@su>Đã reset số lần sử dụng Golden Cat CPU thành công 56 | reset_golden_cat_cpus=Reset số lần sử dụng Golden Cat CPU 57 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/matatabi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Fruit: 6 | def __init__( 7 | self, 8 | id: int, 9 | seed: bool, 10 | group: int, 11 | sort: int, 12 | require: int | None = None, 13 | text: str | None = None, 14 | grow_up: list[int] | None = None, 15 | ): 16 | self.id = id 17 | self.seed = seed 18 | self.group = group 19 | self.sort = sort 20 | self.require = require 21 | self.text = text 22 | self.grow_up = grow_up 23 | 24 | 25 | class Matatabi: 26 | def __init__(self, save_file: core.SaveFile): 27 | self.save_file = save_file 28 | self.matatabi = self.__get_matatabi() 29 | self.gatya_item_names = core.core_data.get_gatya_item_names( 30 | self.save_file 31 | ) 32 | 33 | def __get_matatabi(self) -> list[Fruit] | None: 34 | gdg = core.core_data.get_game_data_getter(self.save_file) 35 | data = gdg.download("DataLocal", "Matatabi.tsv") 36 | if data is None: 37 | return None 38 | csv = core.CSV(data, "\t") 39 | matatabi: list[Fruit] = [] 40 | for line in csv.lines[1:]: 41 | id = line[0].to_int() 42 | seed = line[1].to_bool() 43 | group = line[2].to_int() 44 | sort = line[3].to_int() 45 | if len(line) > 4: 46 | require = line[4].to_int() 47 | else: 48 | require = None 49 | if len(line) > 5: 50 | text = line[5].to_str() 51 | else: 52 | text = None 53 | if len(line) > 6: 54 | grow_up = [item.to_int() for item in line[6:]] 55 | else: 56 | grow_up = None 57 | matatabi.append( 58 | Fruit(id, seed, group, sort, require, text, grow_up) 59 | ) 60 | 61 | return matatabi 62 | 63 | def get_names(self) -> list[str | None] | None: 64 | if self.matatabi is None: 65 | return None 66 | 67 | ids = [fruit.id for fruit in self.matatabi] 68 | names = [self.gatya_item_names.get_name(id) for id in ids] 69 | return names 70 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/edits/gamototo.properties: -------------------------------------------------------------------------------- 1 | # filename="gamototo.properties" 2 | enter_raw_gamatoto_xp=Nhập Raw Gamatoto XP 3 | enter_gamatoto_level=Nhập Gamatoto Level 4 | edit_gamatoto_level_q=Nhập tùy chọn để chỉnh sửa gamatoto level: 5 | gamatoto_xp=Gamatoto XP 6 | gamatoto_level=Gamatoto Level 7 | gamatoto_level_success=<@su>Đã đặt gamatoto level thành công thành <@s>{level} (XP: <@s>{xp}) 8 | gamatoto_level_current=<@t>Gamatoto level hiện tại là <@q>{level} (XP: <@q>{xp}) 9 | gamatoto_xp_level=Gamatoto XP / Level 10 | 11 | current_gamatoto_helpers=Helpers hiện tại: 12 | gamatoto_helper=Helper: <@t>{name} (rarity: <@t>{rarity_name}) 13 | 14 | new_gamatoto_helpers=New Helpers: 15 | gamatoto_helpers=Gamatoto Helpers 16 | 17 | ototo_cat_cannon=Ototo Cat Cannon 18 | 19 | current_cannon_stats=Cannon Stats hiện tại: 20 | 21 | cannon_part=<@t><@q>{name}{buffer}(level <@s>{level}) 22 | development={buffer}(Development: <@q>{development}) 23 | cannon_stats={parts} 24 | 25 | foundation=Foundation 26 | style=Style 27 | effect=Effect 28 | improved_foundation=Improved Foundation 29 | improved_style=Improved Style 30 | 31 | unknown_stage=Unknown Stage (<@s>{stage}) 32 | 33 | selected_cannon=<@t>Selected cannon: <@q>{name} 34 | selected_cannon_stage=<@t>Cannon: <@q>{name} Current Stage: <@q>{stage} 35 | 36 | cannon_edit_type=Bạn muốn chỉnh sửa từng cannon riêng lẻ hay áp dụng chỉnh sửa cho tất cả các cannon đã chọn cùng lúc?: 37 | 38 | cannon_dev_level_q=Bạn muốn chỉnh sửa development của các cannon hay levels của các cannon?: 39 | development_o=Development 40 | level_o=Levels 41 | 42 | select_development=Chọn development stage: 43 | select_cannon=Chọn Cannon 44 | cannon_level=Cannon Level 45 | 46 | cannon_success=<@su>Đã chỉnh sửa ototo cannons thành công 47 | 48 | cat_shrine=Cat Shrine 49 | shrine_level=Shrine Level 50 | shrine_xp=Shrine XP 51 | current_shrine_xp_level=<@t>Current XP: <@q>{xp} (Level: <@q>{level}) 52 | cat_shrine_choice_dialog=Bạn muốn chỉnh sửa cat shrine <@t>level hay cat shrine <@t>XP?: 53 | shrine_level_dialog=Nhập cat shrine level (max: <@q>{max_level}): 54 | shrine_xp_dialog=Nhập cat shrine XP (max: <@q>{max_xp}): 55 | cat_shrine_edited=<@su>Đã chỉnh sửa cat shrine thành công -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/gamototo.properties: -------------------------------------------------------------------------------- 1 | enter_raw_gamatoto_xp=Enter Raw Gamatoto XP 2 | enter_gamatoto_level=Enter Gamatoto Level 3 | edit_gamatoto_level_q=Enter an option to edit the gamatoto level: 4 | gamatoto_xp=Gamatoto XP 5 | gamatoto_level=Gamatoto Level 6 | gamatoto_level_success=<@su>Succesfully set gamatoto level to <@s>{level} (XP: <@s>{xp}) 7 | gamatoto_level_current=<@t>Current gamatoto level is <@q>{level} (XP: <@q>{xp}) 8 | gamatoto_xp_level=Gamatoto XP / Level 9 | 10 | current_gamatoto_helpers=Current Helpers: 11 | gamatoto_helper=Helper: <@t>{name} (rarity: <@t>{rarity_name}) 12 | 13 | new_gamatoto_helpers=New Helpers: 14 | gamatoto_helpers=Gamatoto Helpers 15 | 16 | ototo_cat_cannon=Ototo Cat Cannon 17 | 18 | current_cannon_stats=Current Cannon Stats: 19 | 20 | cannon_part=<@t><@q>{name}{buffer}(level <@s>{level}) 21 | development={buffer}(Development: <@q>{development}) 22 | cannon_stats={parts} 23 | 24 | foundation=Foundation 25 | style=Style 26 | effect=Effect 27 | improved_foundation=Improved Foundation 28 | improved_style=Improved Style 29 | 30 | unknown_stage=Unknown Stage (<@s>{stage}) 31 | 32 | selected_cannon=<@t>Selected cannon: <@q>{name} 33 | selected_cannon_stage=<@t>Cannon: <@q>{name} Current Stage: <@q>{stage} 34 | 35 | cannon_edit_type=Do you want to edit each cannon individually or apply edits to all selected cannons at once?: 36 | 37 | cannon_dev_level_q=Do you want to edit the development of the cannons or the levels of the cannons?: 38 | development_o=Development 39 | level_o=Levels 40 | 41 | select_development=Select development stage: 42 | select_cannon=Select Cannon 43 | cannon_level=Cannon Level 44 | 45 | cannon_success=<@su>Succesfully edited ototo cannons 46 | 47 | cat_shrine=Cat Shrine 48 | shrine_level=Shrine Level 49 | shrine_xp=Shrine XP 50 | current_shrine_xp_level=<@t>Current XP: <@q>{xp} (Level: <@q>{level}) 51 | cat_shrine_choice_dialog=Do you want to edit the cat shrine <@t>level or the cat shrine <@t>XP?: 52 | shrine_level_dialog=Enter cat shrine level (max: <@q>{max_level}): 53 | shrine_xp_dialog=Enter cat shrine XP (max: <@q>{max_xp}): 54 | cat_shrine_edited=<@su>Succesfully edited cat shrine 55 | make_catshrine_appear=Show Cat Shrine in Game 56 | make_catshrine_disappear=Hide Cat Shrine in Game 57 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/drop_chara.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | 4 | from bcsfe import core 5 | 6 | 7 | @dataclass 8 | class Drop: 9 | stage_id: int 10 | save_id: int 11 | chara_id: int 12 | 13 | 14 | class CharaDrop: 15 | def __init__(self, save_file: core.SaveFile): 16 | self.save_file = save_file 17 | self.drops = self.get_drops() 18 | 19 | def get_drops(self) -> list[Drop] | None: 20 | gdg = core.core_data.get_game_data_getter(self.save_file) 21 | data = gdg.download("DataLocal", "drop_chara.csv") 22 | if data is None: 23 | return None 24 | csv = core.CSV(data) 25 | drops: list[Drop] = [] 26 | for line in csv.lines[1:]: 27 | drops.append( 28 | Drop( 29 | stage_id=line[0].to_int(), 30 | save_id=line[1].to_int(), 31 | chara_id=line[2].to_int(), 32 | ) 33 | ) 34 | 35 | return drops 36 | 37 | def get_drop(self, stage_id: int) -> Drop | None: 38 | if self.drops is None: 39 | return None 40 | for drop in self.drops: 41 | if drop.stage_id == stage_id: 42 | return drop 43 | 44 | return None 45 | 46 | def get_drops_from_chara_id(self, chara_id: int) -> list[Drop] | None: 47 | if self.drops is None: 48 | return None 49 | drops: list[Drop] = [] 50 | for drop in self.drops: 51 | if drop.chara_id == chara_id: 52 | drops.append(drop) 53 | 54 | return drops 55 | 56 | def unlock_drops_from_cat_id(self, cat_id: int) -> None: 57 | drops = self.get_drops_from_chara_id(cat_id) 58 | if drops is None: 59 | return 60 | for drop in drops: 61 | try: 62 | self.save_file.unit_drops[drop.save_id] = 1 63 | except IndexError: 64 | pass 65 | 66 | def remove_drops_from_cat_id(self, cat_id: int) -> None: 67 | drops = self.get_drops_from_chara_id(cat_id) 68 | if drops is None: 69 | return 70 | for drop in drops: 71 | try: 72 | self.save_file.unit_drops[drop.save_id] = 0 73 | except IndexError: 74 | pass 75 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/thread_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Callable, Any, Iterable 3 | import threading 4 | 5 | 6 | class Thread: 7 | def __init__( 8 | self, 9 | name: str, 10 | target: Callable[..., Any], 11 | args: Iterable[Any] | None = None, 12 | ): 13 | self.name = name 14 | self.target = target 15 | self.args: Iterable[Any] = args if args is not None else [] 16 | self._thread: threading.Thread | None = None 17 | 18 | def start(self): 19 | self._thread = threading.Thread( 20 | target=self.target, args=self.args, name=self.name 21 | ) 22 | self._thread.start() 23 | 24 | def join(self): 25 | if self._thread is not None: 26 | self._thread.join() 27 | 28 | def is_alive(self) -> bool: 29 | if self._thread is not None: 30 | return self._thread.is_alive() 31 | return False 32 | 33 | @staticmethod 34 | def run(name: str, target: Callable[..., None], args: Any): 35 | thread = Thread(name, target, args) 36 | thread.start() 37 | return thread 38 | 39 | 40 | def thread_run_many_helper(funcs: list[Callable[..., Any]], *args: list[Any]): 41 | for i in range(len(funcs)): 42 | args_ = args[i] 43 | funcs[i](*args_) 44 | return 45 | 46 | 47 | def thread_run_many( 48 | funcs: list[Callable[..., Any]], args: Any = None, max_threads: int = 16 49 | ) -> list[Thread]: 50 | chunk_size = len(funcs) // max_threads 51 | if chunk_size == 0: 52 | chunk_size = 1 53 | callable_chunks: list[list[Callable[..., Any]]] = [] 54 | args_chunks: list[list[Any]] = [] 55 | for i in range(0, len(funcs), chunk_size): 56 | callable_chunks.append(funcs[i : i + chunk_size]) 57 | args_chunks.append(args[i : i + chunk_size]) 58 | 59 | threads: list[Thread] = [] 60 | for i in range(len(callable_chunks)): 61 | args_ = args_chunks[i] 62 | if args is None: 63 | args_ = [] 64 | 65 | threads.append( 66 | Thread.run( 67 | "run_many_helper", 68 | thread_run_many_helper, 69 | (callable_chunks[i], *args_), 70 | ) 71 | ) 72 | 73 | for thread in threads: 74 | thread.join() 75 | 76 | return threads 77 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/challenge.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | from bcsfe.cli import dialog_creator 5 | 6 | 7 | class ChallengeChapters: 8 | def __init__(self, chapters: core.Chapters): 9 | self.chapters = chapters 10 | self.scores: list[int] = [] 11 | self.shown_popup: bool = False 12 | 13 | @staticmethod 14 | def init() -> ChallengeChapters: 15 | return ChallengeChapters(core.Chapters.init()) 16 | 17 | @staticmethod 18 | def read(data: core.Data) -> ChallengeChapters: 19 | ch = core.Chapters.read(data) 20 | return ChallengeChapters(ch) 21 | 22 | def write(self, data: core.Data): 23 | self.chapters.write(data) 24 | 25 | def read_scores(self, data: core.Data): 26 | total_scores = data.read_int() 27 | self.scores = [data.read_int() for _ in range(total_scores)] 28 | 29 | def write_scores(self, data: core.Data): 30 | data.write_int(len(self.scores)) 31 | for score in self.scores: 32 | data.write_int(score) 33 | 34 | def read_popup(self, data: core.Data): 35 | self.shown_popup = data.read_bool() 36 | 37 | def write_popup(self, data: core.Data): 38 | data.write_bool(self.shown_popup) 39 | 40 | def serialize(self) -> dict[str, Any]: 41 | return { 42 | "chapters": self.chapters.serialize(), 43 | "scores": self.scores, 44 | "shown_popup": self.shown_popup, 45 | } 46 | 47 | @staticmethod 48 | def deserialize(data: dict[str, Any]) -> ChallengeChapters: 49 | challenge = ChallengeChapters( 50 | core.Chapters.deserialize(data.get("chapters", {})), 51 | ) 52 | challenge.scores = data.get("scores", []) 53 | challenge.shown_popup = data.get("shown_popup", False) 54 | return challenge 55 | 56 | def __repr__(self): 57 | return f"Challenge({self.chapters})" 58 | 59 | def __str__(self): 60 | return self.__repr__() 61 | 62 | def edit_score(self): 63 | if not self.scores: 64 | self.scores = [0] 65 | self.scores[0] = dialog_creator.SingleEditor( 66 | "challenge_score", self.scores[0], None, localized_item=True 67 | ).edit() 68 | self.shown_popup = True 69 | self.chapters.clear_stage(0, 0, 0, False) 70 | 71 | 72 | def edit_challenge_score(save_file: core.SaveFile): 73 | save_file.challenge.edit_score() 74 | -------------------------------------------------------------------------------- /src/bcsfe/core/max_value_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import enum 3 | from typing import Any 4 | from bcsfe import core 5 | 6 | 7 | class MaxValueType(enum.Enum): 8 | CATFOOD = "catfood" 9 | XP = "xp" 10 | NORMAL_TICKETS = "normal_tickets" 11 | HUNDRED_MILLION_TICKETS = "100_million_tickets" 12 | RARE_TICKETS = "rare_tickets" 13 | PLATINUM_TICKETS = "platinum_tickets" 14 | LEGEND_TICKETS = "legend_tickets" 15 | NP = "np" 16 | LEADERSHIP = "leadership" 17 | BATTLE_ITEMS = "battle_items" 18 | CATAMINS = "catamins" 19 | CATSEYES = "catseyes" 20 | CATFRUIT = "catfruit" 21 | BASE_MATERIALS = "base_materials" 22 | LABYRINTH_MEDALS = "labyrinth_medals" 23 | TALENT_ORBS = "talent_orbs" 24 | TREASURE_LEVEL = "treasure_level" 25 | STAGE_CLEAR_COUNT = "stage_clear_count" 26 | ITF_TIMED_SCORE = "itf_timed_score" 27 | EVENT_TICKETS = "event_tickets" 28 | TREASURE_CHESTS = "treasure_chests" 29 | 30 | 31 | class MaxValueHelper: 32 | def __init__(self): 33 | self.max_value_data = self.get_max_value_data() 34 | 35 | @staticmethod 36 | def convert_val_code(value_code: MaxValueType | str) -> str: 37 | if isinstance(value_code, MaxValueType): 38 | value_code = value_code.value 39 | return value_code 40 | 41 | def get_max_value_data(self) -> dict[str, Any]: 42 | file_path = core.Path("max_values.json", True) 43 | if not file_path.exists(): 44 | return {} 45 | try: 46 | return core.JsonFile.from_data(file_path.read()).to_object() 47 | except core.JSONDecodeError: 48 | return {} 49 | 50 | def get(self, value_code: str | MaxValueType) -> int: 51 | try: 52 | return int(self.max_value_data.get(self.convert_val_code(value_code), 0)) 53 | except ValueError: 54 | return 0 55 | 56 | def get_property(self, value_code: str | MaxValueType, property: str) -> int: 57 | try: 58 | return int( 59 | self.max_value_data.get(self.convert_val_code(value_code), {}).get( 60 | property, 0 61 | ) 62 | ) 63 | except ValueError: 64 | return 0 65 | 66 | def get_old(self, value_code: str | MaxValueType) -> int: 67 | return self.get_property(value_code, "old") 68 | 69 | def get_new(self, value_code: str | MaxValueType) -> int: 70 | return self.get_property(value_code, "new") 71 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/beacon_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class BeaconEventListScene: 7 | def __init__( 8 | self, 9 | int_dict: dict[int, int], 10 | str_dict: dict[int, list[str]], 11 | bool_dict: dict[int, bool], 12 | ): 13 | self.int_array = int_dict 14 | self.str_array = str_dict 15 | self.bool_array = bool_dict 16 | 17 | @staticmethod 18 | def init() -> BeaconEventListScene: 19 | return BeaconEventListScene({}, {}, {}) 20 | 21 | @staticmethod 22 | def read(stream: core.Data) -> BeaconEventListScene: 23 | int_dict = {} 24 | str_dict = {} 25 | bool_dict = {} 26 | for _ in range(stream.read_int()): 27 | int_dict[stream.read_int()] = stream.read_int() 28 | for _ in range(stream.read_int()): 29 | str_dict[stream.read_int()] = stream.read_string_list() 30 | for _ in range(stream.read_int()): 31 | bool_dict[stream.read_int()] = stream.read_bool() 32 | return BeaconEventListScene(int_dict, str_dict, bool_dict) 33 | 34 | def write(self, stream: core.Data): 35 | stream.write_int(len(self.int_array)) 36 | for key, value in self.int_array.items(): 37 | stream.write_int(key) 38 | stream.write_int(value) 39 | stream.write_int(len(self.str_array)) 40 | for key, value in self.str_array.items(): 41 | stream.write_int(key) 42 | stream.write_string_list(value) 43 | stream.write_int(len(self.bool_array)) 44 | for key, value in self.bool_array.items(): 45 | stream.write_int(key) 46 | stream.write_bool(value) 47 | 48 | def serialize(self) -> dict[str, Any]: 49 | return { 50 | "int_array": self.int_array, 51 | "str_array": self.str_array, 52 | "bool_array": self.bool_array, 53 | } 54 | 55 | @staticmethod 56 | def deserialize(data: dict[str, Any]) -> BeaconEventListScene: 57 | return BeaconEventListScene( 58 | data.get("int_array", []), 59 | data.get("str_array", []), 60 | data.get("bool_array", []), 61 | ) 62 | 63 | def __repr__(self): 64 | return f"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})" 65 | 66 | def __str__(self): 67 | return f"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})" 68 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/tower.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class TowerChapters: 7 | def __init__(self, chapters: core.Chapters): 8 | self.chapters = chapters 9 | self.item_obtain_states: list[list[bool]] = [] 10 | 11 | @staticmethod 12 | def init() -> TowerChapters: 13 | return TowerChapters(core.Chapters.init()) 14 | 15 | @staticmethod 16 | def read(data: core.Data) -> TowerChapters: 17 | ch = core.Chapters.read(data) 18 | return TowerChapters(ch) 19 | 20 | def write(self, data: core.Data): 21 | self.chapters.write(data) 22 | 23 | def read_item_obtain_states(self, data: core.Data): 24 | total_stars = data.read_int() 25 | total_stages = data.read_int() 26 | self.item_obtain_states: list[list[bool]] = [] 27 | for _ in range(total_stars): 28 | self.item_obtain_states.append(data.read_bool_list(total_stages)) 29 | 30 | def write_item_obtain_states(self, data: core.Data): 31 | data.write_int(len(self.item_obtain_states)) 32 | try: 33 | data.write_int(len(self.item_obtain_states[0])) 34 | except IndexError: 35 | data.write_int(0) 36 | for item_obtain_state in self.item_obtain_states: 37 | data.write_bool_list(item_obtain_state, write_length=False) 38 | 39 | def serialize(self) -> dict[str, Any]: 40 | return { 41 | "chapters": self.chapters.serialize(), 42 | "item_obtain_states": self.item_obtain_states, 43 | } 44 | 45 | @staticmethod 46 | def deserialize(data: dict[str, Any]) -> TowerChapters: 47 | tower = TowerChapters( 48 | core.Chapters.deserialize(data.get("chapters", {})), 49 | ) 50 | tower.item_obtain_states = data.get("item_obtain_states", []) 51 | return tower 52 | 53 | def __repr__(self): 54 | return f"Tower({self.chapters}, {self.item_obtain_states})" 55 | 56 | def __str__(self): 57 | return self.__repr__() 58 | 59 | def get_total_stars(self, chapter_id: int) -> int: 60 | return len(self.chapters.chapters[chapter_id].chapters) 61 | 62 | def get_total_stages(self, chapter_id: int, star: int) -> int: 63 | return len(self.chapters.chapters[chapter_id].chapters[star].stages) 64 | 65 | @staticmethod 66 | def edit_towers(save_file: core.SaveFile): 67 | towers = save_file.tower 68 | towers.chapters.edit_chapters(save_file, "V", 7000) 69 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/map_option.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from bcsfe import core 4 | 5 | 6 | class MapOptionLine: 7 | def __init__( 8 | self, 9 | map_id: int, 10 | crown_count: int, 11 | crown_mults: list[int], 12 | guerrilla_set: int, 13 | reset_type: int, 14 | one_time_display: bool, 15 | display_order: int, 16 | interval: int, 17 | challenge_flag: bool, 18 | difficulty_mask: int, 19 | hide_after_clear: bool, 20 | name: str, 21 | ): 22 | self.map_id = map_id 23 | self.crown_count = crown_count 24 | self.crown_mults = crown_mults 25 | self.guerrilla_set = guerrilla_set 26 | self.reset_type = reset_type 27 | self.one_time_display = one_time_display 28 | self.display_order = display_order 29 | self.interval = interval 30 | self.challenge_flag = challenge_flag 31 | self.difficulty_mask = difficulty_mask 32 | self.hide_after_clear = hide_after_clear 33 | self.name = name 34 | 35 | @staticmethod 36 | def from_line(line: core.Row) -> MapOptionLine: 37 | return MapOptionLine( 38 | line.next_int(), 39 | line.next_int(), 40 | [line.next_int() for _ in range(4)], 41 | line.next_int(), 42 | line.next_int(), 43 | line.next_bool(), 44 | line.next_int(), 45 | line.next_int(), 46 | line.next_bool(), 47 | line.next_int(), 48 | line.next_bool(), 49 | line.next_str(), 50 | ) 51 | 52 | 53 | class MapOption: 54 | def __init__(self, maps: dict[int, MapOptionLine]): 55 | self.maps = maps 56 | 57 | @staticmethod 58 | def from_csv(csv: core.CSV) -> MapOption: 59 | data: dict[int, MapOptionLine] = {} 60 | 61 | for line in csv.lines[1:]: # skip headers 62 | item = MapOptionLine.from_line(line) 63 | data[item.map_id] = item 64 | 65 | return MapOption(data) 66 | 67 | @staticmethod 68 | def from_save(save_file: core.SaveFile) -> MapOption | None: 69 | gdg = core.core_data.get_game_data_getter(save_file) 70 | data = gdg.download("DataLocal", "Map_option.csv") 71 | if data is None: 72 | return None 73 | 74 | csv = core.CSV(data) 75 | 76 | return MapOption.from_csv(csv) 77 | 78 | def get_map(self, map_id: int) -> MapOptionLine | None: 79 | return self.maps.get(map_id) 80 | -------------------------------------------------------------------------------- /src/bcsfe/cli/server_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe.cli import dialog_creator, main, color, file_dialog 3 | from bcsfe import core 4 | 5 | 6 | class ServerCLI: 7 | def __init__(self): 8 | pass 9 | 10 | def download_save( 11 | self, 12 | ) -> tuple[core.Path, core.CountryCode] | None: 13 | transfer_code = dialog_creator.StringInput().get_input_locale_while( 14 | "enter_transfer_code", {} 15 | ) 16 | if transfer_code is None: 17 | return None 18 | confirmation_code = dialog_creator.StringInput().get_input_locale_while( 19 | "enter_confirmation_code", {} 20 | ) 21 | if confirmation_code is None: 22 | return None 23 | cc = core.CountryCode.select() 24 | if cc is None: 25 | return None 26 | gv = core.GameVersion(120200) # not important 27 | 28 | color.ColoredText.localize( 29 | "downloading_save_file", 30 | transfer_code=transfer_code, 31 | confirmation_code=confirmation_code, 32 | country_code=cc, 33 | ) 34 | 35 | server_handler, result = core.ServerHandler.from_codes( 36 | transfer_code, 37 | confirmation_code, 38 | cc, 39 | gv, 40 | ) 41 | if server_handler is None and result is not None: 42 | color.ColoredText.localize("invalid_codes_error") 43 | if dialog_creator.YesNoInput().get_input_once( 44 | "display_response_debug_info_q" 45 | ): 46 | if result.response is not None: 47 | color.ColoredText.localize( 48 | "response_text_display", 49 | url=result.url, 50 | request_headers=result.headers, 51 | request_body=result.data, 52 | response_headers=result.response.headers, 53 | response_body=result.response.text, 54 | ) 55 | return 56 | if server_handler is None: 57 | return 58 | 59 | save_file = server_handler.save_file 60 | if file_dialog.FileDialog().filedialog is None: 61 | path = core.SaveFile.get_saves_path().add("SAVE_DATA") 62 | else: 63 | path = main.Main().save_save_dialog(save_file) 64 | if path is None: 65 | return None 66 | 67 | save_file.to_file(path) 68 | 69 | color.ColoredText.localize("save_downloaded", path=path.to_str()) 70 | 71 | return path, cc 72 | -------------------------------------------------------------------------------- /LOCALIZATION.md: -------------------------------------------------------------------------------- 1 | # Localization 2 | 3 | Small tutorial on how to localize the editor into a different language. 4 | 5 | ## Disclaimer 6 | 7 | Please do not use machine or AI translated text, they will likely make mistakes, especially since 8 | there is specific terminology unique to The Battle Cats and the save editor, and if you do not 9 | know the language you will not be able to correct them. There are also many other ethical and legal 10 | issues when using AI that I would like to avoid. 11 | 12 | Thank you for understanding 13 | 14 | ## How To 15 | 16 | 0. If you want to submit a pull request later you should fork the editor (make sure to fork the codeberg repo: ) 17 | 18 | 1. Install the editor from source by following [these instructions](https://codeberg.org/fieryhenry/BCSFE-Python#install-from-source) 19 | (make sure to change the git clone url to be your fork if you have one) 20 | 21 | 2. Inside the `src/bcsfe/files/locales/` folder you will find the pre-existing locales, copy the 22 | one named `en` and rename it to the code of the language you are translating to 23 | 24 | 3. Create a file called `metadata.json` inside the folder and edit it to contain the following info: 25 | 26 | ```json 27 | { 28 | "authors": ["author-1", "author2", "cool-person3"], 29 | "name": "Name of language (english name of language)" 30 | } 31 | ``` 32 | 33 | For example the one for Vietnamese looks like this: 34 | 35 | ```json 36 | { 37 | "authors": ["HungJoesifer"], 38 | "name": "Tiếng Việt (Vietnamese)" 39 | } 40 | ```` 41 | 42 | 4. Edit each of the .properties file, translating each value, try to keep the colors the same as 43 | the original text. Anything in `{..}` should stay exactly how it is. Anything in `{{..}}` references 44 | another key and so can be changed if you want. For more details see [here](https://codeberg.org/fieryhenry/ExampleEditorLocale/). 45 | 46 | 5. Once you think you have finished, open the editor and edit the config value `Language` and 47 | select your language from the list 48 | 49 | 6. Restart the editor and check that it works, you should also see the details you specified 50 | in the `metadata.json` file in the opening text 51 | 52 | 7. Enable the config option to display missing locale keys then restart the editor 53 | 54 | 8. If everything is correct you shouldn't see any missing keys (extra keys are fine). 55 | 56 | 9. Once done, push your changes to your fork if you have one and feel free to submit a pull request 57 | to the codeberg repo. Alternatively you can just zip your locale folder and send it to me or in 58 | the #localization channel on discord. (or [matrix](https://matrix.to/#/@fieryhenry:matrix.battlecatsmodding.org)) 59 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/officer_pass.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | from bcsfe.cli import color 5 | 6 | 7 | class OfficerPass: 8 | def __init__(self, play_time: int): 9 | self.play_time = play_time 10 | self.gold_pass = core.NyankoClub.init() 11 | self.cat_id = 0 12 | self.cat_form = 0 13 | 14 | @staticmethod 15 | def init() -> OfficerPass: 16 | return OfficerPass(0) 17 | 18 | @staticmethod 19 | def read(data: core.Data) -> OfficerPass: 20 | play_time = data.read_int() 21 | return OfficerPass(play_time) 22 | 23 | def write(self, data: core.Data): 24 | if self.play_time > 2**31 - 1: 25 | self.play_time = 2**31 - 1 26 | data.write_int(self.play_time) 27 | 28 | def read_gold_pass(self, data: core.Data, gv: core.GameVersion): 29 | self.gold_pass = core.NyankoClub.read(data, gv) 30 | 31 | def write_gold_pass(self, data: core.Data, gv: core.GameVersion): 32 | self.gold_pass.write(data, gv) 33 | 34 | def read_cat_data(self, data: core.Data): 35 | self.cat_id = data.read_short() 36 | self.cat_form = data.read_short() 37 | 38 | def write_cat_data(self, data: core.Data): 39 | data.write_short(self.cat_id) 40 | data.write_short(self.cat_form) 41 | 42 | def serialize(self) -> dict[str, Any]: 43 | return { 44 | "play_time": self.play_time, 45 | "gold_pass": self.gold_pass.serialize(), 46 | "cat_id": self.cat_id, 47 | "cat_form": self.cat_form, 48 | } 49 | 50 | @staticmethod 51 | def deserialize(data: dict[str, Any]) -> OfficerPass: 52 | officer_pass = OfficerPass( 53 | data.get("play_time", 0), 54 | ) 55 | officer_pass.gold_pass = core.NyankoClub.deserialize( 56 | data.get("gold_pass", {}) 57 | ) 58 | officer_pass.cat_id = data.get("cat_id", 0) 59 | officer_pass.cat_form = data.get("cat_form", 0) 60 | return officer_pass 61 | 62 | def __repr__(self): 63 | return f"OfficerPass({self.play_time}, {self.gold_pass}, {self.cat_id}, {self.cat_form})" 64 | 65 | def __str__(self): 66 | return self.__repr__() 67 | 68 | def reset(self, save_file: core.SaveFile): 69 | self.cat_id = 0 70 | self.cat_form = 0 71 | self.play_time = 0 72 | self.gold_pass.remove_gold_pass(save_file) 73 | 74 | @staticmethod 75 | def fix_crash(save_file: core.SaveFile): 76 | officer_pass = save_file.officer_pass 77 | officer_pass.reset(save_file) 78 | 79 | color.ColoredText.localize("officer_pass_fixed") 80 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/edits/items.properties: -------------------------------------------------------------------------------- 1 | # Note that not all items are here 2 | 3 | catamins=Catamins 4 | catfruit=Catfruit 5 | base_materials=Base Materials 6 | inquiry_code=Inquiry Code 7 | rare_gatya_seed=Rare Gacha Seed 8 | normal_gatya_seed=Normal Gacha Seed 9 | event_gatya_seed=Event Gacha Seed 10 | unlocked_slots=Unlocked Slots|Equip Slots|Lineups 11 | password_refresh_token=Password Refresh Token 12 | challenge_score=Challenge Score 13 | dojo_score=Dojo Score 14 | items=Items 15 | user_rank_rewards=Claim User Rank Rewards (Does not give rewards) 16 | 17 | catfood=Cat Food 18 | xp=XP 19 | normal_tickets=Normal Tickets|Basic Tickets|Silver Tickets 20 | rare_tickets=Rare Tickets|Gold Tickets 21 | platinum_tickets=Platinum Tickets 22 | legend_tickets=Legend Tickets 23 | 100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets 24 | 100_million_warn=<@w>Note: you will only be able to see and use the tickets if the 100 Million Downloads event is currently active 25 | platinum_shards=Platinum Shards 26 | np=NP 27 | leadership=Leadership 28 | catseyes=Catseyes 29 | battle_items=Battle Items 30 | duration=<@t>{days} days, <@t>{hours} hours, <@t>{minutes} minutes, <@t>{seconds} seconds 31 | endless_item_item=<@s>{item} : <@s>{int} 32 | endless_items_success=<@su>Successfully edited endless items 33 | invalid_minute_count=<@e>Invalid minute amount 34 | enter_duration_minutes=Enter the duration in minutes for the endless items to last for (if you enter <@t>infinity the items last forever): 35 | infinity_duration=<@t>infinity 36 | infinity=Infinity 37 | enter_duration_minutes_item=Enter the duration in minutes for the endless <@t>{item} to last for (if you enter <@t>infinity the items last forever): 38 | battle_items_endless=Endless Battle Items 39 | talent_orbs=Talent Orbs 40 | scheme_items=Scheme Items 41 | labyrinth_medals=Labyrinth Medals 42 | restart_pack=Restart Pack|Returner Mode 43 | engineers=Engineers 44 | gamototo=Gamatoto / Ototo 45 | special_skills=Special Skills / Base Abilities 46 | treasure_chests=Treasure Chests 47 | unknown_treasure_chest_name=Unknown Treasure Chest ({id}) 48 | 49 | rare_ticket_trade=Rare Ticket Trade 50 | rare_ticket_trade_feature_name=Rare Ticket Trade (Allows for unbannable rare tickets) 51 | 52 | other=Other 53 | gatya=Gacha 54 | levels=Levels / Story / Treasure 55 | cats_special_skills=Cats / Special Skills 56 | 57 | gatya_item_unknown_name=Unknown Item 58 | unknown_catamin_name=Unknown Catamin <@t>{id} 59 | unknown_catseye_name=Unknown Catseye <@t>{id} 60 | unknown_catfruit_name=Unknown Catfruit <@t>{id} 61 | unknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id} 62 | 63 | reset_golden_cat_cpus_success=<@su>Successfully reset golden cat CPU uses 64 | reset_golden_cat_cpus=Reset Golden Cat CPU Uses 65 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/playtime.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | 4 | from bcsfe import core 5 | from bcsfe.cli import color, dialog_creator 6 | 7 | 8 | @dataclass 9 | class PlayTime: 10 | frames: int 11 | 12 | @staticmethod 13 | def get_fps() -> int: 14 | return 30 15 | 16 | @property 17 | def seconds(self) -> int: 18 | return self.frames // self.get_fps() 19 | 20 | @property 21 | def minutes(self) -> int: 22 | return self.seconds // 60 23 | 24 | @property 25 | def hours(self) -> int: 26 | return self.minutes // 60 27 | 28 | @property 29 | def just_seconds(self) -> int: 30 | return self.seconds % 60 31 | 32 | @property 33 | def just_minutes(self) -> int: 34 | return self.minutes % 60 35 | 36 | @property 37 | def just_hours(self) -> int: 38 | return self.hours % 60 39 | 40 | @staticmethod 41 | def from_hours(hours: int) -> PlayTime: 42 | return PlayTime(hours * 60 * 60 * PlayTime.get_fps()) 43 | 44 | @staticmethod 45 | def from_minutes(minutes: int) -> PlayTime: 46 | return PlayTime(minutes * 60 * PlayTime.get_fps()) 47 | 48 | @staticmethod 49 | def from_seconds(seconds: int) -> PlayTime: 50 | return PlayTime(seconds * PlayTime.get_fps()) 51 | 52 | @staticmethod 53 | def from_hours_mins_secs( 54 | hours: int, minutes: int, seconds: int 55 | ) -> PlayTime: 56 | return ( 57 | PlayTime.from_hours(hours) 58 | + PlayTime.from_minutes(minutes) 59 | + PlayTime.from_seconds(seconds) 60 | ) 61 | 62 | def __add__(self, other: PlayTime) -> PlayTime: 63 | return PlayTime(self.frames + other.frames) 64 | 65 | 66 | def edit(save_file: core.SaveFile): 67 | play_time = PlayTime(save_file.officer_pass.play_time) 68 | color.ColoredText.localize( 69 | "playtime_current", 70 | hours=play_time.hours, 71 | minutes=play_time.just_minutes, 72 | seconds=play_time.just_seconds, 73 | frames=play_time.frames, 74 | ) 75 | hours, _ = dialog_creator.IntInput().get_input("playtime_hours_prompt", {}) 76 | if hours is None: 77 | return 78 | minutes, _ = dialog_creator.IntInput().get_input( 79 | "playtime_minutes_prompt", {} 80 | ) 81 | if minutes is None: 82 | return 83 | seconds, _ = dialog_creator.IntInput().get_input( 84 | "playtime_seconds_prompt", {} 85 | ) 86 | if seconds is None: 87 | return 88 | 89 | play_time = PlayTime.from_hours_mins_secs(hours, minutes, seconds) 90 | save_file.officer_pass.play_time = play_time.frames 91 | color.ColoredText.localize( 92 | "playtime_edited", 93 | hours=play_time.hours, 94 | minutes=play_time.just_minutes, 95 | seconds=play_time.just_seconds, 96 | frames=play_time.frames, 97 | ) 98 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/gamoto/base_materials.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import dialog_creator 4 | 5 | 6 | class Material: 7 | def __init__(self, amount: int): 8 | self.amount = amount 9 | 10 | @staticmethod 11 | def init() -> Material: 12 | return Material(0) 13 | 14 | @staticmethod 15 | def read(stream: core.Data) -> Material: 16 | amount = stream.read_int() 17 | return Material(amount) 18 | 19 | def write(self, stream: core.Data): 20 | stream.write_int(self.amount) 21 | 22 | def serialize(self) -> int: 23 | return self.amount 24 | 25 | @staticmethod 26 | def deserialize(data: int) -> Material: 27 | return Material(data) 28 | 29 | def __repr__(self) -> str: 30 | return f"Material(amount={self.amount!r})" 31 | 32 | def __str__(self) -> str: 33 | return self.__repr__() 34 | 35 | 36 | class BaseMaterials: 37 | def __init__(self, materials: list[Material]): 38 | self.materials = materials 39 | 40 | @staticmethod 41 | def init() -> BaseMaterials: 42 | return BaseMaterials([]) 43 | 44 | @staticmethod 45 | def read(stream: core.Data) -> BaseMaterials: 46 | total = stream.read_int() 47 | materials: list[Material] = [] 48 | for _ in range(total): 49 | materials.append(Material.read(stream)) 50 | return BaseMaterials(materials) 51 | 52 | def write(self, stream: core.Data): 53 | stream.write_int(len(self.materials)) 54 | for material in self.materials: 55 | material.write(stream) 56 | 57 | def serialize(self) -> list[int]: 58 | return [material.serialize() for material in self.materials] 59 | 60 | @staticmethod 61 | def deserialize(data: list[int]) -> BaseMaterials: 62 | return BaseMaterials( 63 | [Material.deserialize(material) for material in data] 64 | ) 65 | 66 | def __repr__(self) -> str: 67 | return f"Materials(materials={self.materials!r})" 68 | 69 | def __str__(self) -> str: 70 | return self.__repr__() 71 | 72 | def edit_base_materials(self, save_file: core.SaveFile): 73 | names = core.core_data.get_gatya_item_names(save_file).names 74 | items = core.core_data.get_gatya_item_buy(save_file).get_by_category(7) 75 | if items is None: 76 | return 77 | if names is None: 78 | return 79 | names = [names[item.id] for item in items] 80 | base_materials = [ 81 | base_material.amount for base_material in self.materials 82 | ] 83 | values = dialog_creator.MultiEditor.from_reduced( 84 | "base_materials", 85 | names, 86 | base_materials, 87 | core.core_data.max_value_manager.get("base_materials"), 88 | group_name_localized=True, 89 | ).edit() 90 | self.materials = [Material(value) for value in values] 91 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/server.properties: -------------------------------------------------------------------------------- 1 | transfer_code=Transfer Code 2 | enter_transfer_code=Enter Transfer Code: 3 | confirmation_code=Confirmation Code 4 | enter_confirmation_code=Enter Confirmation Code: 5 | country_code=Country Code 6 | country_code_select=Select country code: 7 | invalid_codes_error=<@e>Failed to download save file. Please check your transfer code and confirmation code and country code and try again. 8 | display_response_debug_info_q=Do you want to display the response debug info? ({{y/n}}): 9 | response_text_display= 10 | >URL: <@q>{url} 11 | >Request Headers: <@q>{request_headers} 12 | >Request Body: <@q>{request_body} 13 | > 14 | >Response Headers: <@q>{response_headers} 15 | >Response Body: <@q>{response_body} 16 | 17 | downloading_save_file=Downloading save file from server (transfer code: <@q>{transfer_code}, confirmation code: <@q>{confirmation_code}, country code: <@q>{country_code})... 18 | upload_result= 19 | ><@su> 20 | >Transfer Code: <@s>{transfer_code} 21 | >Confirmation Code: <@s>{confirmation_code} 22 | > 23 | 24 | upload_fail=<@e>Failed to upload save file. {{try_again_message}} {{see_log}} 25 | unban_fail=<@e>Failed to unban account. {{try_again_message}} {{see_log}} 26 | unban_success=<@su>Account unbanned successfully. 27 | upload_items_checker_confirm=Some managed items have not yet been tracked for your current save file. Do you want to upload them now? ({{y/n}}): 28 | strict_ban_prevention_enabled=<@w>Strict Ban Prevention Enabled. A new account will be created before uploading save file / managed items. 29 | create_new_account_success=<@su>Account created successfully. 30 | create_new_account_fail=<@e>Failed to create account. {{try_again_message}} {{see_log}} 31 | 32 | uploading_save_file=<@q>Uploading save file to server... 33 | getting_codes=<@q>Getting transfer code and confirmation code... 34 | getting_auth_token=<@q>Getting account auth token... 35 | refreshing_password=<@q>Refreshing account password... 36 | getting_password=<@q>Getting account password... 37 | getting_save_key=<@q>Getting account save key... 38 | 39 | inquiry_code_warning=<@w>WARNING: Editing your inquiry code can result in your account being unplayable. Use at your own risk.\n{{do_you_want_to_continue}} 40 | password_refresh_token_warning=<@w>WARNING: Editing your password refresh token can result in your account being unplayable. Use at your own risk.\n{{do_you_want_to_continue}} 41 | 42 | no_internet=<@e>No internet connection. Please check your internet connection and try again. 43 | 44 | transfer_backup=<@su>Saved backup transfer save file to <@t>{path} 45 | transfer_backup_fail=<@e>Failed to save transfer backup file to <@t>{path} due to {error} 46 | 47 | retry_auth_token=<@e>Failed to get auth token, retrying... 48 | 49 | downloading_compressed_data=<@su>Downloading game data from <@s>{url} 50 | clear_game_data_q=Do you want to clear all downloaded game data? ({{y/n}}): 51 | cleared_game_data=<@su>Successfully cleared game data 52 | -------------------------------------------------------------------------------- /src/bcsfe/core/server/updater.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import sys 3 | from typing import Any 4 | from bcsfe import core 5 | import bcsfe 6 | 7 | 8 | class Updater: 9 | package_name = "bcsfe" 10 | 11 | def __init__(self): 12 | pass 13 | 14 | def get_local_version(self) -> str: 15 | return bcsfe.__version__ 16 | 17 | def get_pypi_json(self) -> dict[str, Any] | None: 18 | url = f"https://pypi.org/pypi/{self.package_name}/json" 19 | # add a User-Agent since pypi started to block the default requests user-agent 20 | # this probably won't be needed in the future as i assume this block is temporary 21 | response = core.RequestHandler( 22 | url, headers={"User-Agent": "BCSFE-Updater"} 23 | ).get() 24 | if response is None: 25 | return None 26 | try: 27 | return response.json() 28 | except core.JSONDecodeError: 29 | return None 30 | 31 | def get_releases(self) -> list[str] | None: 32 | pypi_json = self.get_pypi_json() 33 | if pypi_json is None: 34 | return None 35 | releases = pypi_json.get("releases") 36 | if releases is None: 37 | return None 38 | return list(releases.keys()) 39 | 40 | def get_latest_version(self, prereleases: bool = False) -> str | None: 41 | releases = self.get_releases() 42 | if releases is None: 43 | return None 44 | 45 | releases.reverse() 46 | if prereleases: 47 | return releases[0] 48 | else: 49 | for release in releases: 50 | if "b" not in release: 51 | return release 52 | return releases[0] 53 | 54 | def get_latest_version_info( 55 | self, prereleases: bool = False 56 | ) -> dict[str, Any] | None: 57 | pypi_json = self.get_pypi_json() 58 | if pypi_json is None: 59 | return None 60 | releases = pypi_json.get("releases") 61 | if releases is None: 62 | return None 63 | return releases.get(self.get_latest_version(prereleases)) 64 | 65 | def update(self, target_version: str) -> bool: 66 | binary = sys.orig_argv[0] 67 | python_aliases = [binary, "py", "python", "python3"] 68 | for python_alias in python_aliases: 69 | cmd = f"{python_alias} -m pip install --upgrade {self.package_name}=={target_version}" 70 | result = core.Path().run(cmd) 71 | if result.exit_code == 0: 72 | break 73 | else: 74 | pip_aliases = ["pip", "pip3"] 75 | for pip_alias in pip_aliases: 76 | cmd = f"{pip_alias} install --upgrade {self.package_name}=={target_version}" 77 | result = core.Path().run(cmd) 78 | if result.exit_code == 0: 79 | break 80 | else: 81 | return False 82 | return True 83 | 84 | def has_enabled_pre_release(self) -> bool: 85 | return core.core_data.config.get_bool(core.ConfigKey.UPDATE_TO_BETA) 86 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/git_handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import color 4 | 5 | 6 | class Repo: 7 | def __init__(self, url: str, output_error: bool = True): 8 | self.url = url 9 | self.output_error = output_error 10 | self.success = self.clone() 11 | 12 | def get_repo_name(self) -> str: 13 | return self.url.split("/")[-1] 14 | 15 | def get_path(self) -> core.Path: 16 | path = GitHandler.get_repo_folder().add(self.get_repo_name()) 17 | path.generate_dirs() 18 | return path 19 | 20 | def run_cmd(self, cmd: str) -> bool: 21 | result = core.Command(cmd).run() 22 | success = result.exit_code == 0 23 | if not success and self.output_error: 24 | color.ColoredText.localize("failed_to_run_git_cmd", cmd=cmd) 25 | return success 26 | 27 | def clone_to_temp(self, path: core.Path) -> bool: 28 | cmd = f"git clone {self.url} {path}" 29 | return self.run_cmd(cmd) 30 | 31 | def clone(self) -> bool: 32 | if self.is_cloned(): 33 | return True 34 | cmd = f"git clone {self.url} {self.get_path()}" 35 | success = self.run_cmd(cmd) 36 | if not success: 37 | self.get_path().remove() 38 | return success 39 | 40 | def pull(self) -> bool: 41 | cmd = f"git -C {self.get_path()} pull" 42 | return self.run_cmd(cmd) 43 | 44 | def fetch(self) -> bool: 45 | cmd = f"git -C {self.get_path()} fetch" 46 | return self.run_cmd(cmd) 47 | 48 | def get_file(self, file_path: core.Path) -> core.Data | None: 49 | path = self.get_path().add(file_path) 50 | try: 51 | return path.read() 52 | except FileNotFoundError: 53 | return None 54 | 55 | def get_temp_file( 56 | self, temp_folder: core.Path, file_path: core.Path 57 | ) -> core.Data: 58 | path = temp_folder.add(file_path) 59 | return path.read() 60 | 61 | def get_folder(self, folder_path: core.Path) -> core.Path | None: 62 | path = self.get_path().add(folder_path) 63 | if path.exists(): 64 | return path 65 | return None 66 | 67 | def is_cloned(self) -> bool: 68 | return ( 69 | len(self.get_path().get_dirs()) > 0 70 | or len(self.get_path().get_paths_dir()) > 0 71 | ) 72 | 73 | 74 | class GitHandler: 75 | @staticmethod 76 | def get_repo_folder() -> core.Path: 77 | repo_folder = core.Path.get_documents_folder().add("repos") 78 | repo_folder.generate_dirs() 79 | return repo_folder 80 | 81 | def get_repo(self, repo_url: str, output_error: bool = True) -> Repo | None: 82 | repo = Repo(repo_url) 83 | if repo.success: 84 | return repo 85 | if output_error: 86 | color.ColoredText.localize("failed_to_get_repo", url=repo_url) 87 | return None 88 | 89 | @staticmethod 90 | def is_git_installed() -> bool: 91 | cmd = "git --version" 92 | return core.Command(cmd).run().exit_code == 0 93 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/server.properties: -------------------------------------------------------------------------------- 1 | # filename="server.properties" 2 | transfer_code=Transfer Code 3 | enter_transfer_code=Nhập Transfer Code: 4 | confirmation_code=Confirmation Code 5 | enter_confirmation_code=Nhập Confirmation Code: 6 | country_code=Country Code 7 | country_code_select=Chọn Country Code: 8 | invalid_codes_error=<@e>Không thể tải xuống tệp lưu. Vui lòng kiểm tra Transfer Code, Confirmation Code và Country Code rồi thử lại. 9 | display_response_debug_info_q=Bạn có muốn hiển thị thông tin gỡ lỗi phản hồi không? ({{y/n}}): 10 | response_text_display= 11 | >URL: <@q>{url} 12 | >Tiêu đề Yêu cầu: <@q>{request_headers} 13 | >Thân Yêu cầu: <@q>{request_body} 14 | > 15 | >Tiêu đề Phản hồi: <@q>{response_headers} 16 | >Thân Phản hồi: <@q>{response_body} 17 | 18 | downloading_save_file=Đang tải xuống save file từ máy chủ (Transfer Code: <@q>{transfer_code}, Confirmation Code: <@q>{confirmation_code}, Country Code: <@q>{country_code})... 19 | upload_result= 20 | ><@su> 21 | >Transfer Code: <@s>{transfer_code} 22 | >Confirmation Code: <@s>{confirmation_code} 23 | > 24 | 25 | upload_fail=<@e>Không thể tải lên save file. {{try_again_message}} {{see_log}} 26 | unban_fail=<@e>Không thể gỡ cấm tài khoản. {{try_again_message}} {{see_log}} 27 | unban_success=<@su>Tài khoản đã gỡ cấm thành công. 28 | upload_items_checker_confirm=Một số vật phẩm được quản lý chưa được theo dõi cho save file hiện tại. Bạn có muốn tải chúng lên ngay không? ({{y/n}}): 29 | strict_ban_prevention_enabled=<@w>Ngăn chặn cấm nghiêm ngặt đã bật. Một tài khoản mới sẽ được tạo trước khi tải lên save file / vật phẩm được quản lý. 30 | create_new_account_success=<@su>Tài khoản đã tạo thành công. 31 | create_new_account_fail=<@e>Không thể tạo tài khoản. {{try_again_message}} {{see_log}} 32 | 33 | uploading_save_file=<@q>Đang tải lên save file đến máy chủ... 34 | getting_codes=<@q>Đang lấy Transfer Code và Confirmation Code... 35 | getting_auth_token=<@q>Đang lấy mã xác thực tài khoản... 36 | refreshing_password=<@q>Đang làm mới mật khẩu tài khoản... 37 | getting_password=<@q>Đang lấy mật khẩu tài khoản... 38 | getting_save_key=<@q>Đang lấy khóa lưu tài khoản... 39 | 40 | inquiry_code_warning=<@w>CẢNH BÁO: Chỉnh sửa mã inquiry có thể khiến tài khoản không chơi được. Sử dụng với rủi ro của riêng bạn.\n{{do_you_want_to_continue}} 41 | password_refresh_token_warning=<@w>CẢNH BÁO: Chỉnh sửa mã làm mới mật khẩu có thể khiến tài khoản không chơi được. Sử dụng với rủi ro của riêng bạn.\n{{do_you_want_to_continue}} 42 | 43 | no_internet=<@e>Không có kết nối internet. Vui lòng kiểm tra kết nối internet và thử lại. 44 | 45 | transfer_backup=<@su>Đã lưu save file chuyển giao sao lưu đến <@t>{path} 46 | transfer_backup_fail=<@e>Không thể lưu save file chuyển giao sao lưu đến <@t>{path} do {error} 47 | 48 | retry_auth_token=<@e>Không thể lấy mã xác thực, đang thử lại... 49 | downloading_compressed_data=<@su>Đang tải dữ liệu game nén từ <@s>{url} 50 | clear_game_data_q=Bạn có muốn xóa tất cả dữ liệu game đã tải xuống? ({{y/n}}): 51 | cleared_game_data=<@su>Đã xóa dữ liệu game thành công 52 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/ex_stage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Stage: 6 | def __init__(self, clear_amount: int): 7 | self.clear_amount = clear_amount 8 | 9 | @staticmethod 10 | def init() -> Stage: 11 | return Stage(0) 12 | 13 | @staticmethod 14 | def read(stream: core.Data) -> Stage: 15 | clear_amount = stream.read_int() 16 | return Stage(clear_amount) 17 | 18 | def write(self, stream: core.Data): 19 | stream.write_int(self.clear_amount) 20 | 21 | def serialize(self) -> int: 22 | return self.clear_amount 23 | 24 | @staticmethod 25 | def deserialize(data: int) -> Stage: 26 | return Stage(data) 27 | 28 | def __repr__(self) -> str: 29 | return f"Stage(clear_amount={self.clear_amount!r})" 30 | 31 | def __str__(self) -> str: 32 | return f"Stage(clear_amount={self.clear_amount!r})" 33 | 34 | 35 | class Chapter: 36 | def __init__(self, stages: list[Stage]): 37 | self.stages = stages 38 | 39 | @staticmethod 40 | def init() -> Chapter: 41 | return Chapter([Stage.init() for _ in range(12)]) 42 | 43 | @staticmethod 44 | def read(stream: core.Data) -> Chapter: 45 | total = 12 46 | stages: list[Stage] = [] 47 | for _ in range(total): 48 | stages.append(Stage.read(stream)) 49 | return Chapter(stages) 50 | 51 | def write(self, stream: core.Data): 52 | for stage in self.stages: 53 | stage.write(stream) 54 | 55 | def serialize(self) -> list[int]: 56 | return [stage.serialize() for stage in self.stages] 57 | 58 | @staticmethod 59 | def deserialize(data: list[int]) -> Chapter: 60 | return Chapter([Stage.deserialize(stage) for stage in data]) 61 | 62 | def __repr__(self) -> str: 63 | return f"Chapter(stages={self.stages!r})" 64 | 65 | def __str__(self) -> str: 66 | return f"Chapter(stages={self.stages!r})" 67 | 68 | 69 | class ExChapters: 70 | def __init__(self, chapters: list[Chapter]): 71 | self.chapters = chapters 72 | 73 | @staticmethod 74 | def init() -> ExChapters: 75 | return ExChapters([]) 76 | 77 | @staticmethod 78 | def read(stream: core.Data) -> ExChapters: 79 | total = stream.read_int() 80 | chapters: list[Chapter] = [] 81 | for _ in range(total): 82 | chapters.append(Chapter.read(stream)) 83 | 84 | return ExChapters(chapters) 85 | 86 | def write(self, stream: core.Data): 87 | stream.write_int(len(self.chapters)) 88 | for chapter in self.chapters: 89 | chapter.write(stream) 90 | 91 | def serialize(self) -> list[list[int]]: 92 | return [chapter.serialize() for chapter in self.chapters] 93 | 94 | @staticmethod 95 | def deserialize(data: list[list[int]]) -> ExChapters: 96 | return ExChapters([Chapter.deserialize(chapter) for chapter in data]) 97 | 98 | def __repr__(self) -> str: 99 | return f"Chapters(chapters={self.chapters!r})" 100 | 101 | def __str__(self) -> str: 102 | return f"Chapters(chapters={self.chapters!r})" 103 | -------------------------------------------------------------------------------- /src/bcsfe/core/country_code.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import enum 3 | from bcsfe.cli import dialog_creator 4 | from bcsfe import core 5 | 6 | 7 | class CountryCodeType(enum.Enum): 8 | EN = "en" 9 | JP = "jp" 10 | KR = "kr" 11 | TW = "tw" 12 | 13 | 14 | class CountryCode: 15 | def __init__(self, cc: str | CountryCodeType): 16 | self.value = cc.value if isinstance(cc, CountryCodeType) else cc 17 | self.value = self.value.lower() 18 | 19 | def get_code(self) -> str: 20 | return self.value 21 | 22 | def get_client_info_code(self) -> str: 23 | code = self.get_code() 24 | if code == "jp": 25 | return "ja" 26 | return code 27 | 28 | def get_patching_code(self) -> str: 29 | code = self.get_code() 30 | if code == "jp": 31 | return "" 32 | return code 33 | 34 | @staticmethod 35 | def from_patching_code(code: str) -> CountryCode: 36 | if code == "": 37 | return CountryCode(CountryCodeType.JP) 38 | return CountryCode(code) 39 | 40 | @staticmethod 41 | def from_code(code: str) -> CountryCode: 42 | return CountryCode(code) 43 | 44 | @staticmethod 45 | def get_all() -> list["CountryCode"]: 46 | return [CountryCode(cc) for cc in CountryCodeType] 47 | 48 | @staticmethod 49 | def get_all_str() -> list[str]: 50 | ccts = CountryCode.get_all() 51 | return [cc.get_code() for cc in ccts] 52 | 53 | def __str__(self) -> str: 54 | return self.get_code() 55 | 56 | def __repr__(self) -> str: 57 | return self.get_code() 58 | 59 | def copy(self) -> CountryCode: 60 | return self 61 | 62 | @staticmethod 63 | def select() -> CountryCode | None: 64 | index = dialog_creator.ChoiceInput.from_reduced( 65 | CountryCode.get_all_str(), 66 | dialog="country_code_select", 67 | single_choice=True, 68 | ).single_choice() 69 | if index is None: 70 | return None 71 | return CountryCode.get_all()[index - 1] 72 | 73 | @staticmethod 74 | def select_from_ccs(ccs: list[CountryCode]) -> CountryCode | None: 75 | index = dialog_creator.ChoiceInput.from_reduced( 76 | [cc.get_code() for cc in ccs], 77 | dialog="country_code_select", 78 | single_choice=True, 79 | ).single_choice() 80 | if index is None: 81 | return None 82 | return ccs[index - 1] 83 | 84 | def __eq__(self, o: object) -> bool: 85 | if isinstance(o, CountryCode): 86 | return self.get_code() == o.get_code() 87 | elif isinstance(o, str): 88 | return self.get_code() == o 89 | elif isinstance(o, CountryCodeType): 90 | return self.get_code() == o.value 91 | return False 92 | 93 | def get_cc_lang(self) -> core.CountryCode: 94 | if core.core_data.config.get_bool(core.ConfigKey.FORCE_LANG_GAME_DATA): 95 | locale = core.core_data.config.get_str(core.ConfigKey.LOCALE) 96 | return core.CountryCode.from_code(locale) 97 | return self 98 | 99 | @staticmethod 100 | def get_langs() -> list[str]: 101 | return ["de", "it", "es", "fr", "th"] 102 | 103 | def is_lang(self) -> bool: 104 | return self.get_code() in CountryCode.get_langs() 105 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/gambling.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from typing import Any 4 | 5 | from bcsfe.cli import color 6 | 7 | 8 | class GamblingEvent: 9 | def __init__( 10 | self, 11 | completed: dict[int, bool], 12 | values: dict[int, dict[int, int]], 13 | start_times: dict[int, int | float], 14 | ): 15 | self.completed = completed 16 | self.values = values 17 | self.start_times = start_times 18 | 19 | @staticmethod 20 | def init() -> GamblingEvent: 21 | return GamblingEvent({}, {}, {}) 22 | 23 | @staticmethod 24 | def read(data: core.Data, game_version: core.GameVersion) -> GamblingEvent: 25 | total = data.read_short() 26 | completed: dict[int, bool] = {} 27 | 28 | for _ in range(total): 29 | key = data.read_short() 30 | completed[key] = data.read_bool() 31 | 32 | total = data.read_short() 33 | values: dict[int, dict[int, int]] = {} 34 | 35 | for _ in range(total): 36 | key = data.read_short() 37 | if key not in values: 38 | values[key] = {} 39 | 40 | total2 = data.read_short() 41 | for _ in range(total2): 42 | key2 = data.read_short() 43 | 44 | values[key][key2] = data.read_short() 45 | 46 | total = data.read_short() 47 | start_times: dict[int, int | float] = {} 48 | 49 | for _ in range(total): 50 | key = data.read_short() 51 | 52 | if game_version < 90100: 53 | value = data.read_double() 54 | else: 55 | value = data.read_int() 56 | 57 | start_times[key] = value 58 | 59 | return GamblingEvent(completed, values, start_times) 60 | 61 | def write(self, data: core.Data, game_version: core.GameVersion): 62 | data.write_short(len(self.completed)) 63 | data.write_short_bool_dict(self.completed, write_length=False) 64 | 65 | data.write_short(len(self.values)) 66 | 67 | for key, value in self.values.items(): 68 | data.write_short(key) 69 | data.write_short(len(value)) 70 | 71 | for key2, value2 in value.items(): 72 | data.write_short(key2) 73 | data.write_short(value2) 74 | 75 | data.write_short(len(self.start_times)) 76 | for key, value in self.start_times.items(): 77 | data.write_short(key) 78 | 79 | # this is a bad conversion, since float is timestamp i assume and int as the date as YYYMMDD. FIXME 80 | if game_version < 90100: 81 | data.write_double(float(value)) 82 | else: 83 | data.write_int(int(value)) 84 | 85 | def serialize(self) -> dict[str, Any]: 86 | return { 87 | "completed": self.completed, 88 | "values": self.values, 89 | "start_times": self.start_times, 90 | } 91 | 92 | @staticmethod 93 | def deserialize(data: dict[str, Any]) -> GamblingEvent: 94 | return GamblingEvent( 95 | data.get("completed", {}), 96 | data.get("values", {}), 97 | data.get("start_times", {}), 98 | ) 99 | 100 | def reset(self): 101 | self.completed = {} 102 | self.values = {} 103 | # TODO: check start times 104 | self.start_times = {} 105 | 106 | @staticmethod 107 | def reset_events(save_file: core.SaveFile): 108 | save_file.wildcat_slots.reset() 109 | color.ColoredText.localize("reset_wildcat_slots") 110 | save_file.cat_scratcher.reset() 111 | color.ColoredText.localize("reset_cat_scratcher") 112 | -------------------------------------------------------------------------------- /src/bcsfe/core/server/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import requests 4 | 5 | from bcsfe import core 6 | 7 | 8 | class MultiPartFile: 9 | def __init__(self, content: bytes, content_type: str, filename: str | None = None): 10 | self.content = content 11 | self.content_type = content_type 12 | self.filename = filename 13 | 14 | 15 | class MultipartForm: 16 | def __init__(self): 17 | self.data: dict[str, MultiPartFile] = {} 18 | 19 | def into_files( 20 | self, 21 | ) -> dict[str, tuple[str | None, bytes, str]]: 22 | out = {} 23 | for name, data in self.data.items(): 24 | out[name] = (data.filename, data.content, data.content_type) 25 | 26 | return out 27 | 28 | def add_key( 29 | self, key: str, content: bytes, content_type: str, filename: str | None = None 30 | ): 31 | self.data[key] = MultiPartFile(content, content_type, filename) 32 | 33 | def get_all_type(self, content_type: str) -> str: 34 | data = "" 35 | for key, file in self.data.items(): 36 | if file.content_type == content_type: 37 | content = file.content.decode("utf-8", errors="ignore") 38 | data += f"key: {key}, data: {content}\n" 39 | 40 | return data 41 | 42 | 43 | class RequestHandler: 44 | """Handles HTTP requests.""" 45 | 46 | def __init__( 47 | self, 48 | url: str, 49 | headers: dict[str, str] | None = None, 50 | data: core.Data | None = None, 51 | form: MultipartForm | None = None, 52 | ): 53 | """Initializes a new instance of the RequestHandler class. 54 | 55 | Args: 56 | url (str): URL to request. 57 | headers (dict[str, str] | None, optional): Headers to send with the request. Defaults to None. 58 | data (core.Data | None, optional): Data to send with the request. Defaults to None. 59 | """ 60 | if data is None: 61 | data = core.Data() 62 | self.url = url 63 | self.headers = headers 64 | self.data = data 65 | self.form = form 66 | 67 | def get( 68 | self, 69 | stream: bool = False, 70 | no_timeout: bool = False, 71 | ) -> requests.Response | None: 72 | """Sends a GET request. 73 | 74 | Returns: 75 | requests.Response: Response from the server. 76 | """ 77 | try: 78 | return requests.get( 79 | self.url, 80 | headers=self.headers, 81 | timeout=( 82 | None 83 | if no_timeout 84 | else core.core_data.config.get_int( 85 | core.ConfigKey.MAX_REQUEST_TIMEOUT 86 | ) 87 | ), 88 | stream=stream, 89 | files=None if self.form is None else self.form.into_files(), 90 | ) 91 | except requests.exceptions.ConnectionError: 92 | return None 93 | 94 | def post(self, no_timeout: bool = False) -> requests.Response | None: 95 | """Sends a POST request. 96 | 97 | Returns: 98 | requests.Response: Response from the server. 99 | """ 100 | try: 101 | return requests.post( 102 | self.url, 103 | headers=self.headers, 104 | data=self.data.data, 105 | timeout=( 106 | None 107 | if no_timeout 108 | else core.core_data.config.get_int( 109 | core.ConfigKey.MAX_REQUEST_TIMEOUT 110 | ) 111 | ), 112 | files=None if self.form is None else self.form.into_files(), 113 | ) 114 | except requests.exceptions.ConnectionError: 115 | return None 116 | -------------------------------------------------------------------------------- /src/bcsfe/core/log.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """Module for handling logging""" 4 | import traceback 5 | from bcsfe import core 6 | import time 7 | 8 | 9 | class Logger: 10 | def __init__(self, path: core.Path | None): 11 | """ 12 | Initializes a Logger object 13 | """ 14 | if path is None: 15 | path = core.Path.get_documents_folder().add("bcsfe.log") 16 | self.log_file = path 17 | try: 18 | self.log_data = self.log_file.read(True).split(b"\n") 19 | except Exception as e: 20 | self.log_data = None 21 | 22 | def is_log_enabled(self) -> bool: 23 | return self.log_data is not None 24 | 25 | 26 | def get_time(self) -> str: 27 | """ 28 | Returns the current time in the format: "HH:MM:SS" 29 | 30 | Returns: 31 | str: The current time 32 | """ 33 | return time.strftime("%d/%m/%Y %H:%M:%S", time.localtime()) 34 | 35 | def log_debug(self, message: str): 36 | """ 37 | Logs a debug message 38 | 39 | Args: 40 | message (str): The message to log 41 | """ 42 | if self.log_data is None: 43 | return 44 | self.log_data.append( 45 | core.Data(f"[DEBUG]::{self.get_time()} - {message}") 46 | ) 47 | self.write() 48 | 49 | def log_info(self, message: str): 50 | """ 51 | Logs an info message 52 | 53 | Args: 54 | message (str): The message to log 55 | """ 56 | if self.log_data is None: 57 | return 58 | self.log_data.append( 59 | core.Data(f"[INFO]::{self.get_time()} - {message}") 60 | ) 61 | self.write() 62 | 63 | def log_warning(self, message: str): 64 | """ 65 | Logs a warning message 66 | 67 | Args: 68 | message (str): The message to log 69 | """ 70 | if self.log_data is None: 71 | return 72 | self.log_data.append( 73 | core.Data(f"[WARNING]::{self.get_time()} - {message}") 74 | ) 75 | self.write() 76 | 77 | def log_error(self, message: str): 78 | """ 79 | Logs an error message 80 | 81 | Args: 82 | message (str): The message to log 83 | """ 84 | if self.log_data is None: 85 | return 86 | self.log_data.append( 87 | core.Data(f"[ERROR]::{self.get_time()} - {message}") 88 | ) 89 | self.write() 90 | 91 | def log_exception(self, exception: Exception, extra_msg: str = ""): 92 | tb = traceback.format_exc() 93 | if tb == "NoneType: None\n": 94 | try: 95 | raise exception 96 | except Exception: 97 | tb = traceback.format_exc() 98 | 99 | self.log_error( 100 | f"{extra_msg}: {exception.__class__.__name__}: {exception}\n{tb}" 101 | ) 102 | 103 | def write(self): 104 | """ 105 | Writes the log data to the log file 106 | """ 107 | if self.log_data is None: 108 | return 109 | self.log_file.write( 110 | core.Data.from_many(self.log_data, core.Data("\n")).strip() 111 | ) 112 | 113 | def log_no_file_found(self, file_name: str): 114 | """ 115 | Logs that a file was not found 116 | 117 | Args: 118 | fileName (str): The name of the file 119 | """ 120 | self.log_warning(f"Could not find {file_name}") 121 | 122 | @staticmethod 123 | def get_traceback() -> str: 124 | """ 125 | Gets the traceback of the last exception 126 | 127 | Returns: 128 | str: The traceback 129 | """ 130 | tb = traceback.format_exc() 131 | if tb == "NoneType: None\n": 132 | return "" 133 | return tb 134 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/root_handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | import tempfile 4 | 5 | 6 | class PackageNameNotSet(Exception): 7 | pass 8 | 9 | 10 | class RootHandler: 11 | def __init__(self): 12 | self.package_name = None 13 | 14 | def is_android(self) -> bool: 15 | return core.Path.get_root().add("system").exists() 16 | 17 | def set_package_name(self, package_name: str): 18 | self.package_name = package_name 19 | 20 | def is_rooted(self) -> bool: 21 | try: 22 | core.Path.get_root().add("data").add("data").get_dirs() 23 | except PermissionError: 24 | return False 25 | return True 26 | 27 | def get_battlecats_packages(self) -> list[str]: 28 | packages = core.Path.get_root().add("data").add("data").get_dirs() 29 | packages = [ 30 | package.basename() 31 | for package in packages 32 | if package.add("files").add("SAVE_DATA").exists() 33 | ] 34 | return packages 35 | 36 | def get_package_name(self) -> str: 37 | if self.package_name is None: 38 | raise PackageNameNotSet("Package name is not set") 39 | return self.package_name 40 | 41 | def get_battlecats_path(self) -> core.Path: 42 | return core.Path.get_root().add("data").add("data").add(self.get_package_name()) 43 | 44 | def get_battlecats_save_path(self) -> core.Path: 45 | return self.get_battlecats_path().add("files").add("SAVE_DATA") 46 | 47 | def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult: 48 | self.get_battlecats_save_path().copy(local_path) 49 | return core.CommandResult.create_success() 50 | 51 | def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult: 52 | local_path.copy(self.get_battlecats_save_path()) 53 | return core.CommandResult.create_success() 54 | 55 | def close_game(self) -> core.CommandResult: 56 | cmd = core.Command( 57 | f"sudo pkill -f {self.get_package_name()}", 58 | ) 59 | return cmd.run() 60 | 61 | def run_game(self) -> core.CommandResult: 62 | cmd = core.Command( 63 | f"sudo monkey -p {self.get_package_name()} -c android.intent.category.LAUNCHER 1", 64 | ) 65 | return cmd.run() 66 | 67 | def rerun_game(self) -> core.CommandResult: 68 | result = self.close_game() 69 | if not result.success: 70 | return result 71 | result = self.run_game() 72 | if not result.success: 73 | return result 74 | 75 | return core.CommandResult.create_success() 76 | 77 | def save_locally( 78 | self, local_path: core.Path | None = None 79 | ) -> tuple[core.Path | None, core.CommandResult]: 80 | if local_path is None: 81 | local_path = core.Path.get_documents_folder().add("saves").add("SAVE_DATA") 82 | local_path.parent().generate_dirs() 83 | result = self.save_battlecats_save(local_path) 84 | if not result.success: 85 | return None, result 86 | 87 | return local_path, result 88 | 89 | def load_locally(self, local_path: core.Path) -> core.CommandResult: 90 | success = self.load_battlecats_save(local_path) 91 | if not success: 92 | return core.CommandResult.create_failure() 93 | 94 | success = self.rerun_game() 95 | if not success: 96 | return core.CommandResult.create_failure() 97 | 98 | return core.CommandResult.create_success() 99 | 100 | def load_save( 101 | self, save: core.SaveFile, rerun_game: bool = True 102 | ) -> core.CommandResult: 103 | with tempfile.TemporaryDirectory() as temp_dir: 104 | local_path = core.Path(temp_dir).add("SAVE_DATA") 105 | save.to_data().to_file(local_path) 106 | result = self.load_battlecats_save(local_path) 107 | if not result.success: 108 | return result 109 | if rerun_game: 110 | result = self.rerun_game() 111 | 112 | return result 113 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/uncanny.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | from bcsfe.cli import color, dialog_creator 5 | 6 | 7 | class UncannyChapters: 8 | def __init__(self, chapters: core.Chapters, unknown: list[int]): 9 | self.chapters = chapters 10 | self.unknown = unknown 11 | 12 | @staticmethod 13 | def init() -> UncannyChapters: 14 | return UncannyChapters(core.Chapters.init(), []) 15 | 16 | @staticmethod 17 | def read(data: core.Data) -> UncannyChapters: 18 | ch = core.Chapters.read(data, read_every_time=False) 19 | unknown = data.read_int_list(length=len(ch.chapters)) 20 | return UncannyChapters(ch, unknown) 21 | 22 | def write(self, data: core.Data): 23 | self.chapters.write(data, write_every_time=False) 24 | data.write_int_list(self.unknown, write_length=False) 25 | 26 | def serialize(self) -> dict[str, Any]: 27 | return { 28 | "chapters": self.chapters.serialize(), 29 | "unknown": self.unknown, 30 | } 31 | 32 | @staticmethod 33 | def deserialize(data: dict[str, Any]) -> UncannyChapters: 34 | return UncannyChapters( 35 | core.Chapters.deserialize(data.get("chapters", {})), 36 | data.get("unknown", []), 37 | ) 38 | 39 | def __repr__(self): 40 | return f"Uncanny({self.chapters}, {self.unknown})" 41 | 42 | def __str__(self): 43 | return self.__repr__() 44 | 45 | @staticmethod 46 | def edit_uncanny(save_file: core.SaveFile): 47 | uncanny = save_file.uncanny 48 | uncanny.chapters.edit_chapters(save_file, "NA", 13000) 49 | 50 | @staticmethod 51 | def edit_catamin_stages(save_file: core.SaveFile): 52 | choice = dialog_creator.ChoiceInput.from_reduced( 53 | ["change_clear_amount_catamin", "clear_unclear_stage_catamin"], 54 | dialog="catamin_stage_clear_q", 55 | ).single_choice() 56 | if choice is None: 57 | return None 58 | 59 | if choice == 1: 60 | names = core.MapNames(save_file, "B") 61 | map_ids = core.EventChapters.select_map_names(names.map_names) 62 | if map_ids is None: 63 | return None 64 | if len(map_ids) >= 2: 65 | choice2 = dialog_creator.ChoiceInput.from_reduced( 66 | ["individual", "all_at_once"], dialog="catamin_clear_amounts_q" 67 | ).single_choice() 68 | if choice2 is None: 69 | return None 70 | else: 71 | choice2 = 1 72 | 73 | if choice2 == 2: 74 | clear_amount = dialog_creator.IntInput().get_input( 75 | "enter_clear_amount_catamin", {} 76 | )[0] 77 | if clear_amount is None: 78 | return None 79 | for map_id in map_ids: 80 | save_file.event_stages.chapter_completion_count[14_000 + map_id] = ( 81 | clear_amount 82 | ) 83 | elif choice == 1: 84 | for map_id in map_ids: 85 | name = names.map_names.get(map_id) or core.localize("unknown_map") 86 | clear_amount = dialog_creator.IntInput().get_input( 87 | "enter_clear_amount_catamin_map", {"name": name, "id": map_id} 88 | )[0] 89 | if clear_amount is None: 90 | return None 91 | save_file.event_stages.chapter_completion_count[14_000 + map_id] = ( 92 | clear_amount 93 | ) 94 | 95 | color.ColoredText.localize("catamin_stage_success") 96 | 97 | elif choice == 2: 98 | completed_chapters = save_file.catamin_stages.chapters.edit_chapters( 99 | save_file, "B", 14000 100 | ) 101 | if completed_chapters is None: 102 | return None 103 | 104 | # TODO: maybe in the future ask if the user wants to modify the chapter clear amounts 105 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/map_reset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class MapResetData: 6 | def __init__( 7 | self, 8 | yearly_end_timestamp: float, 9 | monthly_end_timestamp: float, 10 | weekly_end_timestamp: float, 11 | daily_end_timestamp: float, 12 | ): 13 | self.yearly_end_timestamp = yearly_end_timestamp 14 | self.monthly_end_timestamp = monthly_end_timestamp 15 | self.weekly_end_timestamp = weekly_end_timestamp 16 | self.daily_end_timestamp = daily_end_timestamp 17 | 18 | @staticmethod 19 | def init() -> MapResetData: 20 | return MapResetData( 21 | 0.0, 22 | 0.0, 23 | 0.0, 24 | 0.0, 25 | ) 26 | 27 | @staticmethod 28 | def read(stream: core.Data) -> MapResetData: 29 | yearly_end_timestamp = stream.read_double() 30 | monthly_end_timestamp = stream.read_double() 31 | weekly_end_timestamp = stream.read_double() 32 | daily_end_timestamp = stream.read_double() 33 | return MapResetData( 34 | yearly_end_timestamp, 35 | monthly_end_timestamp, 36 | weekly_end_timestamp, 37 | daily_end_timestamp, 38 | ) 39 | 40 | def write(self, stream: core.Data): 41 | stream.write_double(self.yearly_end_timestamp) 42 | stream.write_double(self.monthly_end_timestamp) 43 | stream.write_double(self.weekly_end_timestamp) 44 | stream.write_double(self.daily_end_timestamp) 45 | 46 | def serialize(self) -> dict[str, float]: 47 | return { 48 | "yearly_end_timestamp": self.yearly_end_timestamp, 49 | "monthly_end_timestamp": self.monthly_end_timestamp, 50 | "weekly_end_timestamp": self.weekly_end_timestamp, 51 | "daily_end_timestamp": self.daily_end_timestamp, 52 | } 53 | 54 | @staticmethod 55 | def deserialize(data: dict[str, float]) -> MapResetData: 56 | return MapResetData( 57 | data.get("yearly_end_timestamp", 0.0), 58 | data.get("monthly_end_timestamp", 0.0), 59 | data.get("weekly_end_timestamp", 0.0), 60 | data.get("daily_end_timestamp", 0.0), 61 | ) 62 | 63 | def __str__(self) -> str: 64 | return f"MapResetData(yearly_end_timestamp={self.yearly_end_timestamp!r}, monthly_end_timestamp={self.monthly_end_timestamp!r}, weekly_end_timestamp={self.weekly_end_timestamp!r}, daily_end_timestamp={self.daily_end_timestamp!r})" 65 | 66 | def __repr__(self) -> str: 67 | return str(self) 68 | 69 | 70 | class MapResets: 71 | def __init__(self, data: dict[int, list[MapResetData]]): 72 | self.data = data 73 | 74 | @staticmethod 75 | def init() -> MapResets: 76 | return MapResets({}) 77 | 78 | @staticmethod 79 | def read(stream: core.Data) -> MapResets: 80 | data: dict[int, list[MapResetData]] = {} 81 | for _ in range(stream.read_int()): 82 | key = stream.read_int() 83 | value: list[MapResetData] = [] 84 | for _ in range(stream.read_int()): 85 | value.append(MapResetData.read(stream)) 86 | data[key] = value 87 | return MapResets(data) 88 | 89 | def write(self, stream: core.Data): 90 | stream.write_int(len(self.data)) 91 | for key, value in self.data.items(): 92 | stream.write_int(key) 93 | stream.write_int(len(value)) 94 | for item in value: 95 | item.write(stream) 96 | 97 | def serialize(self) -> dict[int, list[dict[str, float]]]: 98 | return { 99 | key: [item.serialize() for item in value] 100 | for key, value in self.data.items() 101 | } 102 | 103 | @staticmethod 104 | def deserialize(data: dict[int, list[dict[str, float]]]) -> MapResets: 105 | return MapResets( 106 | { 107 | key: [MapResetData.deserialize(item) for item in value] 108 | for key, value in data.items() 109 | } 110 | ) 111 | 112 | def __str__(self) -> str: 113 | return f"MapResets(data={self.data!r})" 114 | 115 | def __repr__(self) -> str: 116 | return str(self) 117 | -------------------------------------------------------------------------------- /src/bcsfe/core/io/waydroid.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from bcsfe import core 4 | from bcsfe.cli import color 5 | from bcsfe.core import io 6 | from bcsfe.core.io.command import CommandResult 7 | 8 | 9 | class WayDroidNotInstalledError(Exception): 10 | def __init__(self, result: CommandResult): 11 | self.result = result 12 | 13 | 14 | class WayDroidHandler(io.root_handler.RootHandler): 15 | def __init__(self): 16 | self.check_waydroid_installed() 17 | 18 | self.adb_handler = io.adb_handler.AdbHandler(root=False) 19 | 20 | self.package_name = None 21 | 22 | def set_package_name(self, package_name: str): 23 | self.package_name = package_name 24 | self.adb_handler.set_package_name(self.package_name) 25 | 26 | @staticmethod 27 | def display_waydroid_not_installed(e: WayDroidNotInstalledError): 28 | color.ColoredText.localize("waydroid_not_installed", error=e) 29 | return 30 | 31 | @staticmethod 32 | def check_waydroid_installed(): 33 | result = io.command.Command("waydroid -V").run() 34 | if not result.success: 35 | raise WayDroidNotInstalledError(result) 36 | 37 | def run_shell_cmd(self, command: str) -> core.CommandResult: 38 | cmd = "waydroid shell" 39 | use_pkexec = core.core_data.config.get_bool(core.ConfigKey.USE_PKEXEC_WAYDROID) 40 | if use_pkexec: 41 | cmd = "pkexec " + cmd 42 | return io.command.Command(cmd).run(f"{command}") 43 | 44 | def pull_file( 45 | self, device_path: core.Path, local_path: core.Path 46 | ) -> core.CommandResult: 47 | # copy file to sdcard 48 | 49 | result = self.run_shell_cmd( 50 | f"cp {device_path.to_str_forwards()} /sdcard/{device_path.basename()} && chmod o+rw /sdcard/{device_path.basename()}" 51 | ) 52 | 53 | if not result.success: 54 | return result 55 | 56 | device_path = core.Path("/sdcard/").add(device_path.basename()) 57 | 58 | # adb pull 59 | 60 | result = self.adb_handler.adb_pull_file(device_path, local_path) 61 | if not result.success: 62 | return result 63 | 64 | # delete /sdcard file again 65 | # 66 | return self.adb_handler.run_shell(f"rm /sdcard/{device_path.basename()}") 67 | 68 | def push_file( 69 | self, local_path: core.Path, device_path: core.Path 70 | ) -> core.CommandResult: 71 | original_device_path = device_path.copy_object() 72 | 73 | device_path = core.Path("/sdcard/").add(device_path.basename()) 74 | 75 | # push to /sdcard with adb 76 | 77 | import time 78 | 79 | time.sleep(0.25) 80 | result = self.adb_handler.adb_push_file(local_path, device_path) 81 | 82 | if not result.success: 83 | return result 84 | 85 | result = self.run_shell_cmd( 86 | f"cp '/sdcard/{device_path.basename()}' '{original_device_path.to_str_forwards()}' && chmod o+rw '{original_device_path.to_str_forwards()}'" 87 | ) 88 | 89 | if not result.success: 90 | return result 91 | 92 | # remove temp file 93 | # 94 | return self.adb_handler.run_shell(f"rm '/sdcard/{device_path.basename()}'") 95 | 96 | def get_battlecats_packages(self) -> list[str]: 97 | cmd = "find /data/data/ -name SAVE_DATA -mindepth 3 -maxdepth 3" 98 | result = self.run_shell_cmd(cmd) 99 | 100 | if not result.success: 101 | return [] 102 | 103 | packages: list[str] = [] 104 | 105 | for package in result.result.split("\n"): 106 | parts = package.split("/") 107 | if len(parts) < 4: 108 | continue 109 | 110 | packages.append(package.split("/")[3]) 111 | 112 | return packages 113 | 114 | def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult: 115 | return self.pull_file(self.get_battlecats_save_path(), local_path) 116 | 117 | def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult: 118 | return self.push_file(local_path, self.get_battlecats_save_path()) 119 | 120 | def run_game(self) -> core.CommandResult: 121 | return self.adb_handler.run_game() 122 | 123 | def close_game(self) -> core.CommandResult: 124 | return self.adb_handler.close_game() 125 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/battle/enemy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | 4 | 5 | class Enemy: 6 | def __init__(self, id: int): 7 | self.id = id 8 | 9 | def unlock_enemy_guide(self, save_file: core.SaveFile): 10 | save_file.enemy_guide[self.id] = 1 11 | 12 | def reset_enemy_guide(self, save_file: core.SaveFile): 13 | save_file.enemy_guide[self.id] = 0 14 | 15 | def get_name(self, save_file: core.SaveFile) -> str | None: 16 | return core.core_data.get_enemy_names(save_file).get_name(self.id) 17 | 18 | 19 | class EnemyDictionaryItem: 20 | def __init__(self, enemy_id: int, scale: int, first_seen: int | None): 21 | self.enemy_id = enemy_id 22 | self.scale = scale 23 | self.first_seen = first_seen 24 | 25 | 26 | class EnemyDictionary: 27 | def __init__(self, save_file: core.SaveFile): 28 | self.save_file = save_file 29 | self.dictionary = self.__get_dictionary() 30 | 31 | def __get_dictionary(self) -> list[EnemyDictionaryItem] | None: 32 | gdg = core.core_data.get_game_data_getter(self.save_file) 33 | csv_data = gdg.download("DataLocal", "enemy_dictionary_list.csv") 34 | if csv_data is None: 35 | return None 36 | 37 | csv = core.CSV(csv_data) 38 | data: list[EnemyDictionaryItem] = [] 39 | 40 | for row in csv: 41 | first_seen = None 42 | if len(row) >= 3: 43 | first_seen = row[2].to_int() 44 | data.append( 45 | EnemyDictionaryItem(row[0].to_int(), row[1].to_int(), first_seen) 46 | ) 47 | 48 | return data 49 | 50 | def get_valid_enemies(self) -> list[int] | None: 51 | if self.dictionary is None: 52 | return None 53 | 54 | return [enemy.enemy_id for enemy in self.dictionary] 55 | 56 | def get_invalid_enemies(self, total_enemies: int) -> list[int] | None: 57 | valid_enemies = self.get_valid_enemies() 58 | if valid_enemies is None: 59 | return None 60 | 61 | valid_enemies = set(valid_enemies) 62 | 63 | return list(filter(lambda i: i not in valid_enemies, range(total_enemies))) 64 | 65 | 66 | class EnemyDescription: 67 | def __init__(self, trait_str: str, description: list[str] | None): 68 | self.trait_str = trait_str 69 | self.description = description 70 | 71 | 72 | class EnemyDescriptions: 73 | def __init__(self, save_file: core.SaveFile): 74 | self.save_file = save_file 75 | self.descriptions = self.__get_descriptions() 76 | 77 | def __get_descriptions(self) -> list[EnemyDescription] | None: 78 | gdg = core.core_data.get_game_data_getter(self.save_file) 79 | data = gdg.download( 80 | "resLocal", 81 | f"EnemyPictureBook_{core.core_data.get_lang(self.save_file)}.csv", 82 | ) 83 | if data is None: 84 | return None 85 | 86 | csv = core.CSV(data, core.Delimeter.from_country_code_res(self.save_file.cc)) 87 | descriptions: list[EnemyDescription] = [] 88 | 89 | for i, row in enumerate(csv): 90 | if len(row) == 1: 91 | descriptions.append(EnemyDescription(row[0].to_str(), None)) 92 | else: 93 | descriptions.append( 94 | EnemyDescription(row[0].to_str(), row[1:].to_str_list()) 95 | ) 96 | 97 | return descriptions 98 | 99 | 100 | class EnemyNames: 101 | def __init__(self, save_file: core.SaveFile): 102 | self.save_file = save_file 103 | self.names = self.get_names() 104 | 105 | def get_names(self) -> list[str] | None: 106 | gdg = core.core_data.get_game_data_getter(self.save_file) 107 | data = gdg.download("resLocal", "Enemyname.tsv") 108 | if data is None: 109 | return None 110 | csv = core.CSV( 111 | data, 112 | "\t", 113 | remove_empty=False, 114 | ) 115 | names: list[str] = [] 116 | for row in csv: 117 | names.append(row[0].to_str()) 118 | 119 | return names 120 | 121 | def get_name(self, id: int) -> str | None: 122 | if self.names is None: 123 | return None 124 | try: 125 | name = self.names[id] 126 | if not name: 127 | return core.core_data.local_manager.get_key( 128 | "enemy_not_in_name_list", id=id 129 | ) 130 | except IndexError: 131 | return core.core_data.local_manager.get_key("enemy_unknown_name", id=id) 132 | return name 133 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/config.properties: -------------------------------------------------------------------------------- 1 | config=Config 2 | edit_config=Edit config 3 | default_value=(default value: <@q>{default_value}) 4 | current_value=(current value: <@q>{current_value}) 5 | config_value_txt=<@s>{{current_value}} {{default_value}} 6 | 7 | config_dialog=Select a config option to edit: 8 | 9 | update_to_beta_desc=Check for updates to beta versions {{config_value_txt}} 10 | update_to_beta=Update to Beta Versions 11 | 12 | show_update_message_desc=Show a message when a new version is available {{config_value_txt}} 13 | show_update_message=Show update message 14 | 15 | config_full=<@t>{key_desc} 16 | 17 | disable_maxes_desc=Disable maximum values when editing {{config_value_txt}} 18 | disable_maxes=Disable maximum values 19 | 20 | max_backups_desc=Maximum number of backups of save files to keep {{config_value_txt}} 21 | max_backups=Maximum save backups 22 | 23 | available_themes=Available themes: 24 | theme_desc=Theme to use {{config_value_txt}} 25 | theme=Theme 26 | 27 | show_missing_locale_keys=Show missing locale keys 28 | show_missing_locale_keys_desc=Display all locale keys which are in the en locale, but not in the current locale. Useful for debugging purposes: {{config_value_txt}} 29 | 30 | reset_cat_data_desc=Reset all cat data when removing a cat from the save file {{config_value_txt}} 31 | reset_cat_data=Reset cat data on cat removal 32 | 33 | filter_current_cats_desc=When selecting cats to edit, filter out cats that are not in the save file {{config_value_txt}} 34 | filter_current_cats=Filter current cats on cat selection 35 | 36 | set_cat_current_forms_desc=When true forming cats, set the cat's current form to the newly unlocked form {{config_value_txt}} 37 | set_cat_current_forms=Set cat current forms on form unlock 38 | 39 | strict_upgrade_desc=When upgrading cats, check for things such as user rank and game progression so that you can only upgrade cats to a certain level {{config_value_txt}} 40 | strict_upgrade=Strict upgrade checks 41 | 42 | separate_cat_edit_options_desc=Separate the cat edit options into multiple features {{config_value_txt}} 43 | separate_cat_edit_options=Separate cat edit options 44 | 45 | strict_ban_prevention_desc=When doing anything server related, create a new account to reduce the chance of getting banned {{config_value_txt}} 46 | strict_ban_prevention=Strict ban prevention 47 | 48 | max_request_timeout_desc=Maximum time to wait for a request to complete (in seconds) {{config_value_txt}} 49 | max_request_timeout=Maximum request timeout 50 | 51 | game_data_repo_desc=Repository to use for game data {{config_value_txt}} 52 | game_data_repo=Game data repository 53 | game_data_repo_dialog=Enter a game data repository to use: 54 | 55 | force_lang_game_data_desc=Force the editor to use the game data for the current locale even if the save file is for a different version {{config_value_txt}} 56 | force_lang_game_data=Force use game data for current locale 57 | 58 | clear_tutorial_on_load_desc=Clear the tutorial when you load a save file into the editor {{config_value_txt}} 59 | clear_tutorial_on_load=Clear tutorial on save load 60 | 61 | remove_ban_message_on_load_desc=Remove the ban message when you load a save file into the editor {{config_value_txt}} 62 | remove_ban_message_on_load=Remove ban message on save load 63 | 64 | unlock_cat_on_edit_desc=Unlock the cat when you edit its level, talents, form, etc. {{config_value_txt}} 65 | unlock_cat_on_edit=Unlock cat on edit 66 | 67 | use_file_dialog_desc=Use the tkinter file dialog to open and save files instead of the file input {{config_value_txt}} 68 | use_file_dialog=Use file dialog 69 | 70 | adb_path_desc=Path to the adb executable {{config_value_txt}} 71 | adb_path=ADB path 72 | 73 | use_waydroid=Use waydroid shell rather than adb 74 | use_waydroid_desc=Waydroid doesn't support adb root, so use waydroid shell instead {{config_value_txt}} 75 | 76 | use_pkexec_waydroid=Use the pkexec binary to run waydroid commands 77 | use_pkexec_waydroid_desc=Running <@s>waydroid shell requires root access. Use <@s>pkexec to avoid running the whole editor as root {{config_value_txt}} 78 | 79 | ignore_parse_error_desc=Ignore parsing errors and just skip parsing the rest of the save data. <@w>WARNING only really do this if your save file is corrupted, any parsing issues should be reported to the discord server {{config_value_txt}} 80 | ignore_parse_error=Ignore Save Parsing Errors 81 | 82 | string_config_dialog=Enter a new value for <@q>{val}: 83 | 84 | 85 | enable_disable_dialog=Do you want to <@q>enable or <@q>disable this feature?: 86 | 87 | 88 | enable=Enable 89 | disable=Disable 90 | 91 | enabled=Enabled 92 | disabled=Disabled 93 | 94 | config_success=<@su>Successfully updated config 95 | 96 | yaml_create_error=<@e>Failed to create yaml file at <@s>{path}<@s>, this is likely a permission issue on your end, maybe try running the editor as root/Administrator? 97 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/config.properties: -------------------------------------------------------------------------------- 1 | # filename="config.properties" 2 | config=Cấu hình 3 | edit_config=Chỉnh sửa cấu hình 4 | default_value=(giá trị mặc định: <@q>{default_value}) 5 | current_value=(giá trị hiện tại: <@q>{current_value}) 6 | config_value_txt=<@s>{{current_value}} {{default_value}} 7 | 8 | config_dialog=Chọn một tùy chọn cấu hình để chỉnh sửa: 9 | 10 | update_to_beta_desc=Kiểm tra cập nhật cho phiên bản beta {{config_value_txt}} 11 | update_to_beta=Cập nhật lên phiên bản beta 12 | 13 | show_update_message_desc=Hiển thị thông báo khi có phiên bản mới {{config_value_txt}} 14 | show_update_message=Hiển thị thông báo cập nhật 15 | 16 | config_full=<@t>{key_desc} 17 | 18 | disable_maxes_desc=Tắt giá trị tối đa khi chỉnh sửa {{config_value_txt}} 19 | disable_maxes=Tắt giá trị tối đa 20 | 21 | max_backups_desc=Số lượng tối đa sao lưu save file giữ {{config_value_txt}} 22 | max_backups=Sao lưu save tối đa 23 | 24 | available_themes=Các chủ đề khả dụng: 25 | theme_desc=Chủ đề sử dụng {{config_value_txt}} 26 | theme=Chủ đề 27 | 28 | show_missing_locale_keys=Hiển thị các khóa ngôn ngữ thiếu 29 | show_missing_locale_keys_desc=Hiển thị tất cả các khóa ngôn ngữ có trong ngôn ngữ en nhưng không có trong ngôn ngữ hiện tại. Hữu ích cho mục đích gỡ lỗi: {{config_value_txt}} 30 | 31 | reset_cat_data_desc=Đặt lại tất cả dữ liệu cat khi xóa cat khỏi save file {{config_value_txt}} 32 | reset_cat_data=Đặt lại dữ liệu cat khi xóa cat 33 | 34 | filter_current_cats_desc=Khi chọn cat để chỉnh sửa, lọc ra các cat không có trong save file {{config_value_txt}} 35 | filter_current_cats=Lọc cat hiện tại khi chọn cat 36 | 37 | set_cat_current_forms_desc=Khi true form cats, đặt hình dạng hiện tại của cat thành hình dạng mới mở khóa {{config_value_txt}} 38 | set_cat_current_forms=Đặt hình dạng hiện tại của cat khi mở khóa hình dạng 39 | 40 | strict_upgrade_desc=Khi nâng cấp cats, kiểm tra các yếu tố như user rank và tiến trình trò chơi để chỉ nâng cấp cats lên mức độ nhất định {{config_value_txt}} 41 | strict_upgrade=Kiểm tra nâng cấp nghiêm ngặt 42 | 43 | separate_cat_edit_options_desc=Tách các tùy chọn chỉnh sửa cat thành nhiều tính năng {{config_value_txt}} 44 | separate_cat_edit_options=Tách tùy chọn chỉnh sửa cats 45 | 46 | strict_ban_prevention_desc=Khi thực hiện bất kỳ hoạt động liên quan đến máy chủ, tạo tài khoản mới để giảm nguy cơ bị cấm {{config_value_txt}} 47 | strict_ban_prevention=Ngăn chặn cấm nghiêm ngặt 48 | 49 | max_request_timeout_desc=Thời gian tối đa chờ yêu cầu hoàn thành (tính bằng giây) {{config_value_txt}} 50 | max_request_timeout=Thời gian chờ yêu cầu tối đa 51 | 52 | game_data_repo_desc=Kho sử dụng cho dữ liệu trò chơi {{config_value_txt}} 53 | game_data_repo=Kho dữ liệu trò chơi 54 | game_data_repo_dialog=Nhập kho dữ liệu trò chơi để sử dụng: 55 | 56 | force_lang_game_data_desc=Bắt buộc trình chỉnh sửa sử dụng dữ liệu trò chơi cho ngôn ngữ hiện tại ngay cả khi save file dành cho phiên bản khác {{config_value_txt}} 57 | force_lang_game_data=Bắt buộc sử dụng dữ liệu trò chơi cho ngôn ngữ hiện tại 58 | 59 | clear_tutorial_on_load_desc=Xóa tutorial khi tải save file vào trình chỉnh sửa {{config_value_txt}} 60 | clear_tutorial_on_load=Xóa tutorial khi tải save 61 | 62 | remove_ban_message_on_load_desc=Xóa thông báo cấm khi tải save file vào trình chỉnh sửa {{config_value_txt}} 63 | remove_ban_message_on_load=Xóa thông báo cấm khi tải save 64 | 65 | unlock_cat_on_edit_desc=Mở khóa cat khi chỉnh sửa level, talents, form, v.v. {{config_value_txt}} 66 | unlock_cat_on_edit=Mở khóa cat khi chỉnh sửa 67 | 68 | use_file_dialog_desc=Sử dụng hộp thoại tệp tkinter để mở và lưu tệp thay vì đầu vào tệp {{config_value_txt}} 69 | use_file_dialog=Sử dụng hộp thoại tệp 70 | 71 | adb_path_desc=Đường dẫn đến executable adb {{config_value_txt}} 72 | adb_path=Đường dẫn ADB 73 | 74 | use_waydroid=Sử dụng shell waydroid thay vì adb 75 | use_waydroid_desc=Waydroid không hỗ trợ adb root, vì vậy sử dụng shell waydroid thay thế {{config_value_txt}} 76 | 77 | ignore_parse_error_desc=Bỏ qua lỗi phân tích và chỉ bỏ qua việc phân tích phần còn lại của dữ liệu lưu. <@w>CẢNH BÁO chỉ thực hiện điều này nếu save file của bạn bị hỏng, bất kỳ vấn đề phân tích nào cũng nên báo cáo cho máy chủ discord {{config_value_txt}} 78 | ignore_parse_error=Bỏ qua lỗi phân tích lưu 79 | 80 | string_config_dialog=Nhập giá trị mới cho <@q>{val}: 81 | 82 | 83 | enable_disable_dialog=Bạn muốn <@q>bật hay <@q>tắt tính năng này?: 84 | 85 | 86 | enable=Bật 87 | disable=Tắt 88 | 89 | enabled=Đã bật 90 | disabled=Đã tắt 91 | 92 | config_success=<@su>Đã cập nhật cấu hình thành công 93 | 94 | yaml_create_error=<@e>Không thể tạo tệp yaml tại <@s>{path}<@s>, có lẽ là vấn đề quyền truy cập ở phía bạn, hãy thử chạy trình chỉnh sửa với quyền root/Administrator? 95 | 96 | use_pkexec_waydroid_desc=Chạy <@s>waydroid shell yêu cầu quyền root. Sử dụng <@s>pkexec để tránh chạy toàn bộ trình chỉnh sửa với quyền root {{config_value_txt}} 97 | use_pkexec_waydroid=Sử dụng binary pkexec để chạy các lệnh waydroid 98 | -------------------------------------------------------------------------------- /src/bcsfe/cli/edits/max_all.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from bcsfe import core 5 | 6 | 7 | def max_catfood(save_file: core.SaveFile): 8 | orig = save_file.catfood 9 | save_file.catfood = core.core_data.max_value_manager.get(core.MaxValueType.CATFOOD) 10 | core.BackupMetaData(save_file).add_managed_item( 11 | core.ManagedItem.from_change( 12 | save_file.catfood - orig, core.ManagedItemType.CATFOOD 13 | ) 14 | ) 15 | 16 | 17 | def max_rare_tickets(save_file: core.SaveFile): 18 | orig = save_file.rare_tickets 19 | save_file.rare_tickets = core.core_data.max_value_manager.get( 20 | core.MaxValueType.RARE_TICKETS 21 | ) 22 | core.BackupMetaData(save_file).add_managed_item( 23 | core.ManagedItem.from_change( 24 | save_file.rare_tickets - orig, core.ManagedItemType.RARE_TICKET 25 | ) 26 | ) 27 | 28 | 29 | def max_plat_tickets(save_file: core.SaveFile): 30 | orig = save_file.platinum_tickets 31 | save_file.platinum_tickets = core.core_data.max_value_manager.get( 32 | core.MaxValueType.PLATINUM_TICKETS 33 | ) 34 | core.BackupMetaData(save_file).add_managed_item( 35 | core.ManagedItem.from_change( 36 | save_file.platinum_tickets - orig, core.ManagedItemType.PLATINUM_TICKET 37 | ) 38 | ) 39 | 40 | 41 | def max_plat_shards(save_file: core.SaveFile): 42 | save_file.platinum_shards = 10 * core.core_data.max_value_manager.get( 43 | core.MaxValueType.PLATINUM_TICKETS 44 | ) 45 | 46 | 47 | def max_legend_tickets(save_file: core.SaveFile): 48 | orig = save_file.legend_tickets 49 | save_file.legend_tickets = core.core_data.max_value_manager.get( 50 | core.MaxValueType.LEGEND_TICKETS 51 | ) 52 | core.BackupMetaData(save_file).add_managed_item( 53 | core.ManagedItem.from_change( 54 | save_file.legend_tickets - orig, core.ManagedItemType.LEGEND_TICKET 55 | ) 56 | ) 57 | 58 | 59 | def max_xp(save_file: core.SaveFile): 60 | save_file.xp = core.core_data.max_value_manager.get(core.MaxValueType.XP) 61 | 62 | 63 | def max_np(save_file: core.SaveFile): 64 | save_file.np = core.core_data.max_value_manager.get(core.MaxValueType.NP) 65 | 66 | 67 | def max_100_million_ticket(save_file: core.SaveFile): 68 | save_file.hundred_million_ticket = core.core_data.max_value_manager.get( 69 | core.MaxValueType.HUNDRED_MILLION_TICKETS 70 | ) 71 | 72 | 73 | def max_leadership(save_file: core.SaveFile): 74 | save_file.leadership = core.core_data.max_value_manager.get( 75 | core.MaxValueType.LEADERSHIP 76 | ) 77 | 78 | 79 | def max_battle_items(save_file: core.SaveFile): 80 | for item in save_file.battle_items.items: 81 | item.amount = core.core_data.max_value_manager.get( 82 | core.MaxValueType.BATTLE_ITEMS 83 | ) 84 | 85 | 86 | def max_catseyes(save_file: core.SaveFile): 87 | for id in range(len(save_file.catseyes)): 88 | save_file.catseyes[id] = core.core_data.max_value_manager.get( 89 | core.MaxValueType.CATSEYES 90 | ) 91 | 92 | 93 | def max_treasure_chests(save_file: core.SaveFile): 94 | for id in range(len(save_file.treasure_chests)): 95 | save_file.treasure_chests[id] = core.core_data.max_value_manager.get( 96 | core.MaxValueType.TREASURE_CHESTS 97 | ) 98 | 99 | 100 | def max_catamins(save_file: core.SaveFile): 101 | for id in range(len(save_file.catseyes)): 102 | save_file.catamins[id] = core.core_data.max_value_manager.get( 103 | core.MaxValueType.CATAMINS 104 | ) 105 | 106 | 107 | def max_labyrinth_medals(save_file: core.SaveFile): 108 | for id in range(len(save_file.labyrinth_medals)): 109 | save_file.labyrinth_medals[id] = core.core_data.max_value_manager.get( 110 | core.MaxValueType.LABYRINTH_MEDALS 111 | ) 112 | 113 | 114 | # def max_catfruit(save_file: core.SaveFile): 115 | # for id in range(len(save_file.catfruit)): 116 | # save_file.catfruit[id] = core.core_data.max_value_manager.get_new( 117 | # core.MaxValueType.CATFRUIT 118 | # ) 119 | 120 | 121 | def max_normal_tickets(save_file: core.SaveFile): 122 | save_file.normal_tickets = core.core_data.max_value_manager.get( 123 | core.MaxValueType.NORMAL_TICKETS 124 | ) 125 | 126 | 127 | def max_all(save_file: core.SaveFile): 128 | maxes = core.core_data.max_value_manager 129 | features: dict[str, Callable[[core.SaveFile], None]] = { 130 | "catfood": max_catfood, 131 | "xp": max_xp, 132 | "normal_tickets": max_normal_tickets, 133 | "rare_tickets": max_rare_tickets, 134 | "platinum_tickets": max_plat_tickets, 135 | "legend_tickets": max_legend_tickets, 136 | "platinum_shards": max_plat_shards, 137 | "np": max_np, 138 | "leadership": max_leadership, 139 | "battle_items": max_battle_items, 140 | "catseyes": max_catseyes, 141 | "catamins": max_catamins, 142 | "labyrinth_medals": max_labyrinth_medals, 143 | "100_million_ticket": max_100_million_ticket, 144 | "treasure_chests": max_treasure_chests, 145 | } 146 | # TODO: finish 147 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/gatya_item.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import enum 3 | from bcsfe import core 4 | 5 | 6 | class GatyaItemNames: 7 | def __init__(self, save_file: core.SaveFile): 8 | self.save_file = save_file 9 | self.names = self.__get_names() 10 | 11 | def __get_names(self) -> list[str] | None: 12 | gdg = core.core_data.get_game_data_getter(self.save_file) 13 | data = gdg.download("resLocal", "GatyaitemName.csv") 14 | if data is None: 15 | return None 16 | csv = core.CSV( 17 | data, core.Delimeter.from_country_code_res(self.save_file.cc) 18 | ) 19 | names: list[str] = [] 20 | for line in csv: 21 | names.append(line[0].to_str()) 22 | 23 | return names 24 | 25 | def get_name(self, index: int) -> str | None: 26 | if self.names is None: 27 | return None 28 | try: 29 | return self.names[index] 30 | except IndexError: 31 | return core.core_data.local_manager.get_key( 32 | "gatya_item_unknown_name", index=index 33 | ) 34 | 35 | 36 | class GatyaItemBuyItem: 37 | def __init__( 38 | self, 39 | id: int, 40 | rarity: int, 41 | reflect_or_storage: bool, 42 | price: int, 43 | stage_drop_id: int, 44 | quantity: int, 45 | server_id: int, 46 | category: int, 47 | index: int, 48 | src_item_id: int, 49 | main_menu_type: int, 50 | gatya_ticket_id: int, 51 | comment: str, 52 | ): 53 | self.id = id 54 | self.rarity = rarity 55 | self.reflect_or_storage = reflect_or_storage 56 | self.price = price 57 | self.stage_drop_id = stage_drop_id 58 | self.quantity = quantity 59 | self.server_id = server_id 60 | self.category = category 61 | self.index = index 62 | self.src_item_id = src_item_id 63 | self.main_menu_type = main_menu_type 64 | self.gatya_ticket_id = gatya_ticket_id 65 | self.comment = comment 66 | 67 | class GatyaItemCategory(enum.Enum): 68 | MISC = 0 69 | EVENT_TICKETS = 1 70 | SPECIAL_SKILLS = 2 71 | BATTLE_ITEMS = 3 72 | EVOLVE_ITEMS = 4 73 | CATSEYES = 5 74 | CATAMINS = 6 75 | BASE_MATERIALS = 7 76 | LUCKY_TICKETS_1 = 8 77 | ENDLESS_ITEMS = 9 78 | LUCKY_TICKETS_2 = 10 79 | LABYRINTH_MEDALS = 11 80 | TREASURE_CHESTS = 12 81 | 82 | class GatyaItemBuy: 83 | def __init__(self, save_file: core.SaveFile): 84 | self.save_file = save_file 85 | self.buy = self.get_buy() 86 | 87 | def get_buy(self) -> list[GatyaItemBuyItem] | None: 88 | gdg = core.core_data.get_game_data_getter(self.save_file) 89 | data = gdg.download("DataLocal", "Gatyaitembuy.csv") 90 | if data is None: 91 | return None 92 | csv = core.CSV(data) 93 | buy: list[GatyaItemBuyItem] = [] 94 | for i, line in enumerate(csv.lines[1:]): 95 | try: 96 | buy.append( 97 | GatyaItemBuyItem( 98 | i, 99 | line[0].to_int(), 100 | line[1].to_bool(), 101 | line[2].to_int(), 102 | line[3].to_int(), 103 | line[4].to_int(), 104 | line[5].to_int(), 105 | line[6].to_int(), 106 | line[7].to_int(), 107 | line[8].to_int(), 108 | line[9].to_int(), 109 | line[10].to_int(), 110 | line[11].to_str(), 111 | ) 112 | ) 113 | except IndexError: 114 | pass 115 | 116 | return buy 117 | 118 | def sort_by_index(self, items: list[GatyaItemBuyItem]): 119 | items.sort(key=lambda x: x.index) 120 | return items 121 | 122 | def get_by_category(self, category: int | GatyaItemCategory) -> list[GatyaItemBuyItem] | None: 123 | if self.buy is None: 124 | return None 125 | if isinstance(category, GatyaItemCategory): 126 | category = category.value 127 | return self.sort_by_index( 128 | [item for item in self.buy if item.category == category] 129 | ) 130 | 131 | def get_names_by_category(self, category: int | GatyaItemCategory) -> list[tuple[GatyaItemBuyItem, str | None]] | None: 132 | items = self.get_by_category(category) 133 | if items is None: 134 | return None 135 | 136 | names = GatyaItemNames(self.save_file) 137 | 138 | return [(item, names.get_name(item.id)) for item in items] 139 | 140 | def get(self, item_id: int) -> GatyaItemBuyItem | None: 141 | if self.buy is None: 142 | return None 143 | if item_id < 0 or item_id >= len(self.buy): 144 | return None 145 | 146 | return self.buy[item_id] 147 | 148 | def get_by_server_id(self, server_id: int) -> GatyaItemBuyItem | None: 149 | if self.buy is None: 150 | return None 151 | for item in self.buy: 152 | if item.server_id == server_id: 153 | return item 154 | 155 | return None 156 | -------------------------------------------------------------------------------- /src/bcsfe/core/crypto.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import enum 3 | import hashlib 4 | import hmac 5 | import random 6 | from bcsfe import core 7 | 8 | 9 | class HashAlgorithm(enum.Enum): 10 | """An enum representing a hash algorithm.""" 11 | 12 | MD5 = enum.auto() 13 | SHA1 = enum.auto() 14 | SHA256 = enum.auto() 15 | 16 | 17 | class Hash: 18 | """A class to hash data.""" 19 | 20 | def __init__(self, algorithm: HashAlgorithm): 21 | """Initializes a new instance of the Hash class. 22 | 23 | Args: 24 | algorithm (HashAlgorithm): The hash algorithm to use. 25 | """ 26 | self.algorithm = algorithm 27 | 28 | def get_hash( 29 | self, 30 | data: core.Data, 31 | length: int | None = None, 32 | ) -> core.Data: 33 | """Gets the hash of the given data. 34 | 35 | Args: 36 | data (core.Data): The data to hash. 37 | length (int | None, optional): The length of the hash. Defaults to None. 38 | 39 | Raises: 40 | ValueError: Invalid hash algorithm. 41 | 42 | Returns: 43 | core.Data: The hash of the data. 44 | """ 45 | if self.algorithm == HashAlgorithm.MD5: 46 | hash = hashlib.md5() 47 | elif self.algorithm == HashAlgorithm.SHA1: 48 | hash = hashlib.sha1() 49 | elif self.algorithm == HashAlgorithm.SHA256: 50 | hash = hashlib.sha256() 51 | else: 52 | raise ValueError("Invalid hash algorithm") 53 | hash.update(data.get_bytes()) 54 | if length is None: 55 | return core.Data(hash.digest()) 56 | return core.Data(hash.digest()[:length]) 57 | 58 | 59 | class Random: 60 | """A class to get random data""" 61 | 62 | @staticmethod 63 | def get_bytes(length: int) -> bytes: 64 | """Gets random bytes. 65 | 66 | Args: 67 | length (int): The length of the bytes. 68 | 69 | Returns: 70 | bytes: The random bytes. 71 | """ 72 | return bytes(random.getrandbits(8) for _ in range(length)) 73 | 74 | @staticmethod 75 | def get_alpha_string(length: int) -> str: 76 | """Gets a random string of the given length. 77 | 78 | Args: 79 | length (int): The length of the string. 80 | 81 | Returns: 82 | str: The random string. 83 | """ 84 | characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 85 | return "".join(random.choice(characters) for _ in range(length)) 86 | 87 | @staticmethod 88 | def get_hex_string(length: int) -> str: 89 | """Gets a random hex string of the given length. 90 | 91 | Args: 92 | length (int): The length of the string. 93 | 94 | Returns: 95 | str: The random string. 96 | """ 97 | characters = "0123456789abcdef" 98 | return "".join(random.choice(characters) for _ in range(length)) 99 | 100 | @staticmethod 101 | def get_digits_string(length: int) -> str: 102 | """Gets a random digits string of the given length. 103 | 104 | Args: 105 | length (int): The length of the string. 106 | 107 | Returns: 108 | str: The random string. 109 | """ 110 | characters = "0123456789" 111 | return "".join(random.choice(characters) for _ in range(length)) 112 | 113 | 114 | class Hmac: 115 | def __init__(self, algorithm: HashAlgorithm): 116 | self.algorithm = algorithm 117 | 118 | def get_hmac(self, key: core.Data, data: core.Data) -> core.Data: 119 | if self.algorithm == HashAlgorithm.MD5: 120 | alg = hashlib.md5 121 | elif self.algorithm == HashAlgorithm.SHA1: 122 | alg = hashlib.sha1 123 | elif self.algorithm == HashAlgorithm.SHA256: 124 | alg = hashlib.sha256 125 | else: 126 | raise ValueError("Invalid hash algorithm") 127 | hmac_data = hmac.new( 128 | key.get_bytes(), data.get_bytes(), digestmod=alg 129 | ).digest() 130 | return core.Data(hmac_data) 131 | 132 | 133 | class NyankoSignature: 134 | def __init__(self, inquiry_code: str, data: str): 135 | self.inquiry_code = inquiry_code 136 | self.data = data 137 | 138 | def generate_signature(self) -> str: 139 | """Generates a signature from the inquiry code and data. 140 | 141 | Returns: 142 | str: The signature. 143 | """ 144 | random_data = Random.get_hex_string(64) 145 | key = self.inquiry_code + random_data 146 | hmac_ = Hmac(HashAlgorithm.SHA256) 147 | signature = hmac_.get_hmac(core.Data(key), core.Data(self.data)) 148 | 149 | return random_data + signature.to_hex() 150 | 151 | def generate_signature_v1(self) -> str: 152 | """Generates a signature from the inquiry code and data. 153 | 154 | Returns: 155 | str: The signature. 156 | """ 157 | 158 | data = self.data + self.data # repeat data for some reason 159 | random_data = Random.get_hex_string(40) 160 | key = self.inquiry_code + random_data 161 | hmac_ = Hmac(HashAlgorithm.SHA1) 162 | signature = hmac_.get_hmac(core.Data(key), core.Data(data)) 163 | 164 | return random_data + signature.to_hex() 165 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/map/map_names.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any, Callable 3 | 4 | import bs4 5 | from bcsfe import core 6 | from bcsfe.cli import color 7 | 8 | 9 | class MapNames: 10 | def __init__( 11 | self, 12 | save_file: core.SaveFile, 13 | code: str, 14 | output: bool = True, 15 | no_r_prefix: bool = False, 16 | base_index: int | None = None, 17 | ): 18 | self.save_file = save_file 19 | self.out = output 20 | self.code = code 21 | self.base_index = base_index 22 | self.no_r_prefix = no_r_prefix 23 | self.gdg = core.core_data.get_game_data_getter(self.save_file) 24 | self.map_names: dict[int, str | None] = {} 25 | self.stage_names: dict[int, list[str]] = {} 26 | self.get_map_names() 27 | self.save_map_names() 28 | 29 | def get_file_path(self) -> core.Path: 30 | return ( 31 | core.Path("map_names", True) 32 | .add(self.code) 33 | .generate_dirs() 34 | .add(f"{self.gdg.cc.get_code()}.json") 35 | ) 36 | 37 | def read_map_names(self) -> dict[int, str | None]: 38 | file_path = self.get_file_path() 39 | if file_path.exists(): 40 | names = core.JsonFile(file_path.read()).to_object() 41 | for id in names.keys(): 42 | self.map_names[int(id)] = names[id] 43 | return names 44 | return {} 45 | 46 | def download_map_name(self, id: int): 47 | file_name = f"{self.code}{str(id).zfill(3)}.html" 48 | if self.gdg.cc != core.CountryCodeType.JP: 49 | url = f"https://ponosgames.com/information/appli/battlecats/stage/{self.gdg.cc.get_code()}/{file_name}" 50 | else: 51 | url = ( 52 | f"https://ponosgames.com/information/appli/battlecats/stage/{file_name}" 53 | ) 54 | data = core.RequestHandler(url).get() 55 | if data is None: 56 | return None 57 | if data.status_code != 200: 58 | return None 59 | html = data.content.decode("utf-8") 60 | bs = bs4.BeautifulSoup(html, "html.parser") 61 | name = bs.find("h2") 62 | if name is None: 63 | return None 64 | name = name.text.strip() 65 | if name: 66 | self.map_names[id] = name 67 | else: 68 | self.map_names[id] = None 69 | return name 70 | 71 | def get_map_names_in_game( 72 | self, base_index: int, total_stages: int 73 | ) -> dict[int, str | None] | None: 74 | gdg = core.core_data.get_game_data_getter(self.save_file) 75 | map_name_data = gdg.download("resLocal", "Map_Name.csv") 76 | if map_name_data is None: 77 | return None 78 | 79 | csv = core.CSV( 80 | map_name_data, core.Delimeter.from_country_code_res(self.save_file.cc) 81 | ) 82 | names: dict[int, str | None] = {} 83 | for row in csv: 84 | id = row[0].to_int() 85 | name = row[1].to_str().strip() 86 | 87 | for i in range(total_stages): 88 | index = i + base_index 89 | if id == index: 90 | if name: 91 | names[i] = name 92 | else: 93 | names[i] = None 94 | break 95 | 96 | return names 97 | 98 | def get_map_names(self) -> dict[int, str | None] | None: 99 | gdg = core.core_data.get_game_data_getter(self.save_file) 100 | r_prefix = "" if self.no_r_prefix else "R" 101 | stage_names = gdg.download( 102 | "resLocal", 103 | f"StageName_{r_prefix}{self.code}_{core.core_data.get_lang(self.save_file)}.csv", 104 | ) 105 | if stage_names is None: 106 | return None 107 | csv = core.CSV( 108 | stage_names, 109 | core.Delimeter.from_country_code_res(self.save_file.cc), 110 | ) 111 | for i, row in enumerate(csv): 112 | stage_names_row = row.to_str_list() 113 | if not stage_names_row: 114 | continue 115 | self.stage_names[i] = stage_names_row 116 | 117 | if self.base_index is None: 118 | names = self.read_map_names() 119 | total_downloaded = len(names) 120 | funcs: list[Callable[..., Any]] = [] 121 | args: list[tuple[Any]] = [] 122 | for i in range(len(csv)): 123 | if i < total_downloaded: 124 | continue 125 | funcs.append(self.download_map_name) 126 | args.append((i,)) 127 | 128 | if self.out: 129 | color.ColoredText.localize("downloading_map_names", code=self.code) 130 | core.thread_run_many(funcs, args) 131 | else: 132 | names = self.get_map_names_in_game(self.base_index, len(self.stage_names)) 133 | if names is None: 134 | return None 135 | self.map_names = names 136 | return self.map_names 137 | 138 | def save_map_names(self): 139 | file_path = self.get_file_path() 140 | self.map_names = dict(sorted(self.map_names.items(), key=lambda item: item[0])) 141 | core.JsonFile.from_object(self.map_names).to_file(file_path) 142 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/en/core/main.properties: -------------------------------------------------------------------------------- 1 | # Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files 2 | # color formatting 3 | # 4 | # <@p> = primary color 5 | # <@s> = secondary color 6 | # <@t> = tertiary color 7 | # <@q> = quaternary color 8 | # <@e> = error color 9 | # <@w> = warning color 10 | # <@su> = success color 11 | # 12 | # = close current color 13 | # When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes. 14 | # You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red. 15 | # If you want to write < or > or / in the text, escape them with a backslash (\) e.g. \< or \> or \/ 16 | # 17 | # <#rrggbb> = hex color 18 | # 19 | # = white 20 | # = black 21 | # = red 22 | # = green 23 | # = blue 24 | # = yellow 25 | # = magenta 26 | # = cyan 27 | # = dark yellow 28 | # = dark grey 29 | # = dark blue 30 | # = dark cyan 31 | # = dark magenta 32 | # = dark red 33 | # = dark green 34 | # = light grey 35 | # = orange 36 | 37 | downloading=<@su>Downloading <@s>{file_name} from <@s>{pack_name} with version <@s>{version} and country code <@s>{country_code} 38 | failed_to_download_game_data=<@e>Failed to download game data <@s>{file_name} from <@s>{pack_name} with version <@s>{version} with country code <@s>{country_code}. Url: <@s>{url} Maybe check your internet connection. 39 | failed_to_get_game_versions=<@e>Failed to get game versions. Maybe check your internet connection. 40 | no_device_error=<@e>No connected devices found 41 | no_package_name_error=<@e>No battle cats packages found. Your device may not be rooted or you may need to try again and make sure you have entered the catbase at least once. 42 | exit=Exit 43 | tkinter_not_found=<@e>tkinter was not found. If you are not on mobile, please install it and try again. 44 | tkinter_not_found_enter_path_file=Please enter the path/location of the {initialfile} file: 45 | tkinter_not_found_enter_path_file_save=Please enter the path/location to save the {initialfile} file: 46 | tkinter_not_found_enter_path_dir=Please enter the path/location of the {initialdir} folder instead: 47 | discord_url=https://discord.gg/DvmMgvn5ZB 48 | 49 | welcome= 50 | ><@t>Welcome to the <@s>Battle Cats Save File Editor! 51 | >Made by <@s>fieryhenry 52 | > 53 | >Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python 54 | >Discord: <@s>{{discord_url}} - Please report any bugs to <@s>#bug-reports and suggestions to <@s>#suggestions 55 | >Donate: <@s>https://ko-fi.com/fieryhenry 56 | > 57 | >Config File Location: <@s>{config_path} 58 | > 59 | >{theme_text} 60 | > 61 | >{locale_text} 62 | > 63 | ><@q>Thanks To: 64 | >- <@s>Lethal's editor for giving me inspiration and helping me work out how to orignally patch the save data and edit cf/xp: <@s>https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren/ 65 | >- <@s>Beeven and <@s>csehydrogen's code, which helped me figure out how to patch save data: https://github.com/beeven/battlecats and https://github.com/csehydrogen/BattleCatsHacker 66 | >- Anyone who has supported my work for giving me motivation to keep working on this and similar projects: <@s>https://ko-fi.com/fieryhenry 67 | >- Everyone in the discord for giving me saves, reporting bugs, suggesting new features, and for being an amazing community: <@s>{{discord_url}} 68 | > 69 | ><@w>If you paid for this program, you have been scammed. This program is free and open source. 70 | > 71 | ><@w>Use this tool at your own risk. I am not responsible for any bans or damage caused to your save file. 72 | >Obviously, the save editor does try to prevent this from happening, but I cannot guarantee that your save is safe. 73 | >Though if your save does get corrupted please do still report it to the discord. 74 | >I recommend you to make backups of your save file before editing it. 75 | 76 | report_message=Please report this to <@s>#bug-reports on the discord: <@s>{{discord_url}} 77 | report_message_l=please report this to <@s>#bug-reports on the discord: <@s>{{discord_url}} 78 | try_again_message=Please try again. If error persists {{report_message_l}} 79 | all=All 80 | 81 | error=<@e>An error has occurred (<@s>{error}, editor version: <@s>{version}) {{report_message_l}}\n{traceback} 82 | see_log=<@e>Please see the log file for more details. 83 | max=max 84 | none=None 85 | unknown=Unknown 86 | 87 | leave=\n<@q>Thank you for using the Battle Cats Save File Editor! 88 | checking_for_changes=<@t>Checking for changes... 89 | no_changes=<@su>No changes found. 90 | changes_found=<@su>Changes found. 91 | 92 | y/n=y/n 93 | yes=yes 94 | 95 | git_not_installed=<@e>Git is not installed. Please install it, add it to PATH, and try again. 96 | failed_to_get_repo=<@e>Failed to get repo: "<@t>{url}". Maybe it doesn't exist, or you have no internet connection 97 | failed_to_run_git_cmd=<@e>Failed to run git command: "<@t>{cmd}". Maybe check your internet connection 98 | cancel=Cancel 99 | 100 | update_external=Update External Content 101 | updating_external_content=<@q>Updating external content... 102 | 103 | downloading_map_names=<@q>Getting map names... (code: <@t>{code}). This may take a while... 104 | 105 | select_device=Select device: 106 | 107 | continue_q=Continue? ({{y/n}}): 108 | 109 | no_data_version=<@e>The latest available game data version is not available. This is probably due to internet issues. Please try again. 110 | 111 | no_feature_with_name=<@e>No feature found with name: <@s>{name} 112 | -------------------------------------------------------------------------------- /src/bcsfe/cli/file_dialog.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from bcsfe import core 3 | from bcsfe.cli import color, dialog_creator 4 | 5 | 6 | class FileDialog: 7 | def load_tk(self): 8 | try: 9 | import tkinter as tk 10 | from tkinter import filedialog 11 | 12 | self.tk = tk 13 | self.filedialog = filedialog 14 | except ImportError: 15 | self.tk = None 16 | self.filedialog = None 17 | 18 | def __init__(self): 19 | self.load_tk() 20 | if self.tk is not None: 21 | try: 22 | self.root = self.tk.Tk() 23 | except self.tk.TclError: 24 | self.tk = None 25 | self.filedialog = None 26 | return 27 | 28 | self.root.withdraw() 29 | self.root.wm_attributes("-topmost", 1) # type: ignore 30 | 31 | def select_files_in_dir( 32 | self, path: core.Path, ignore_json: bool 33 | ) -> str | None: 34 | """Print current files in directory. 35 | 36 | Args: 37 | path (core.Path): Path to directory. 38 | """ 39 | color.ColoredText.localize("current_files_dir", dir=path) 40 | path.generate_dirs() 41 | files = path.get_files() 42 | if not files: 43 | color.ColoredText.localize("no_files_dir") 44 | 45 | files.sort(key=lambda file: file.basename()) 46 | 47 | # remove files with .json extension 48 | if ignore_json: 49 | files = [file for file in files if file.get_extension() != "json"] 50 | 51 | files_str_ls = [file.basename() for file in files] 52 | options = files_str_ls + [core.localize("other_dir"), core.localize("another_path")] 53 | 54 | choice = dialog_creator.ChoiceInput.from_reduced( 55 | options, 56 | dialog="select_files_dir", 57 | single_choice=True, 58 | localize_options=False, 59 | ).single_choice() 60 | if choice is None: 61 | return 62 | 63 | choice -= 1 64 | if choice == len(files): 65 | path_input = color.ColoredInput().localize("enter_path_dir") 66 | path_obj = core.Path(path_input) 67 | if path_obj.is_relative(): 68 | path_obj = path.add(path_obj) 69 | if not path_obj.exists(): 70 | color.ColoredText.localize("path_not_exists", path=path_obj) 71 | return self.select_files_in_dir(path, ignore_json) 72 | return self.select_files_in_dir(path_obj, ignore_json) 73 | if choice == len(files) + 1: 74 | path_input = color.ColoredInput().localize("enter_path") 75 | return path_input or None 76 | return files[choice].to_str() 77 | 78 | def use_tk(self) -> bool: 79 | return ( 80 | self.tk is not None 81 | and self.filedialog is not None 82 | and core.core_data.config.get_bool(core.ConfigKey.USE_FILE_DIALOG) 83 | ) 84 | 85 | def get_file( 86 | self, 87 | title: str, 88 | initialdir: str, 89 | initialfile: str, 90 | filetypes: list[tuple[str, str]] | None = None, 91 | ignore_json: bool = False, 92 | ) -> str | None: 93 | if filetypes is None: 94 | filetypes = [] 95 | title = core.core_data.local_manager.get_key(title) 96 | color.ColoredText.localize(title) 97 | if not self.use_tk(): 98 | curr_path = core.Path(initialdir).add(initialfile) 99 | file = self.select_files_in_dir(curr_path.parent(), ignore_json) 100 | if file is None: 101 | return None 102 | path_obj = core.Path(file) 103 | if path_obj.exists(): 104 | return file 105 | color.ColoredText.localize("path_not_exists", path=path_obj) 106 | return None 107 | 108 | return ( 109 | self.filedialog.askopenfilename( # type: ignore 110 | title=title, 111 | filetypes=filetypes, 112 | initialdir=initialdir, 113 | initialfile=initialfile, 114 | ) 115 | or None 116 | ) 117 | 118 | def save_file( 119 | self, 120 | title: str, 121 | initialdir: str, 122 | initialfile: str, 123 | filetypes: list[tuple[str, str]] | None = None, 124 | ) -> str | None: 125 | """Save file dialog 126 | 127 | Args: 128 | title (str): Title of dialog. 129 | filetypes (list[tuple[str, str]] | None, optional): File types. Defaults to None. 130 | initialdir (str, optional): Initial directory. Defaults to "". 131 | initialfile (str, optional): Initial file. Defaults to "". 132 | 133 | Returns: 134 | str | None: Path to file. 135 | """ 136 | if filetypes is None: 137 | filetypes = [] 138 | title = core.core_data.local_manager.get_key(title) 139 | color.ColoredText.localize(title) 140 | if not self.use_tk(): 141 | def_path = core.Path(initialdir).add(initialfile).to_str() 142 | path = color.ColoredInput().localize( 143 | "enter_path_default", default=def_path 144 | ) 145 | return path.strip().strip("'").strip('"') if path else def_path 146 | return ( 147 | self.filedialog.asksaveasfilename( # type: ignore 148 | title=title, 149 | filetypes=filetypes, 150 | initialdir=initialdir, 151 | initialfile=initialfile, 152 | ) 153 | or None 154 | ) 155 | -------------------------------------------------------------------------------- /src/bcsfe/files/locales/vi/core/main.properties: -------------------------------------------------------------------------------- 1 | # filename="main.properties" 2 | # Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files 3 | # color formatting 4 | # 5 | # <@p> = primary color 6 | # <@s> = secondary color 7 | # <@t> = tertiary color 8 | # <@q> = quaternary color 9 | # <@e> = error color 10 | # <@w> = warning color 11 | # <@su> = success color 12 | # 13 | # = close current color 14 | # When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes. 15 | # You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red. 16 | # If you want to write < or > or / in the text, escape them with a backslash (\) e.g. \< or \> or \/ 17 | # 18 | # <#rrggbb> = hex color 19 | # 20 | # = white 21 | # = black 22 | # = red 23 | # = green 24 | # = blue 25 | # = yellow 26 | # = magenta 27 | # = cyan 28 | # = dark yellow 29 | # = dark grey 30 | # = dark blue 31 | # = dark cyan 32 | # = dark magenta 33 | # = dark red 34 | # = dark green 35 | # = light grey 36 | # = orange 37 | 38 | downloading=<@su>Đang tải xuống <@s>{file_name} từ <@s>{pack_name} với phiên bản <@s>{version} 39 | failed_to_download_game_data=<@e>Không thể tải xuống dữ liệu trò chơi <@s>{file_name} từ <@s>{pack_name} với phiên bản <@s>{version}. Có lẽ bạn phải kiểm tra kết nối internet của mình. 40 | no_device_error=<@e>Không tìm thấy thiết bị kết nối 41 | no_package_name_error=<@e>Không tìm thấy gói Battle Cats. Thiết bị của bạn có thể không được root hoặc bạn cần thử lại và đảm bảo đã vào catbase ít nhất một lần. 42 | exit=Thoát 43 | tkinter_not_found=<@e>tkinter không được tìm thấy. Nếu bạn không dùng trên di động, vui lòng cài đặt và thử lại. 44 | tkinter_not_found_enter_path_file=Vui lòng nhập đường dẫn/vị trí của tệp {initialfile}: 45 | tkinter_not_found_enter_path_file_save=Vui lòng nhập đường dẫn/vị trí để lưu tệp {initialfile}: 46 | tkinter_not_found_enter_path_dir=Vui lòng nhập đường dẫn/vị trí của thư mục {initialdir} thay thế: 47 | discord_url=https://discord.gg/DvmMgvn5ZB 48 | 49 | welcome= 50 | ><@t>Chào mừng đến với <@s>Trình chỉnh sửa save file Battle Cats! 51 | >Được tạo bởi <@s>fieryhenry 52 | > 53 | >Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python 54 | >Discord: <@s>{{discord_url}} - Vui lòng báo lỗi đến <@s>#bug-reports và gợi ý đến <@s>#suggestions 55 | >Ủng hộ: <@s>https://ko-fi.com/fieryhenry 56 | > 57 | >Vị trí tệp cấu hình: <@s>{config_path} 58 | > 59 | >{theme_text} 60 | > 61 | >{locale_text} 62 | > 63 | ><@q>Cảm ơn: 64 | >- <@s>Trình chỉnh sửa của Lethal đã truyền cảm hứng và giúp tôi tìm cách vá dữ liệu lưu ban đầu và chỉnh sửa cf/xp: <@s>https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren/ 65 | >- <@s>Beeven và code của <@s>csehydrogen, giúp tôi tìm cách vá dữ liệu lưu: https://github.com/beeven/battlecats và https://github.com/csehydrogen/BattleCatsHacker 66 | >- Những người ủng hộ tôi đã cho tôi động lực tiếp tục làm và các dự án tương tự: <@s>https://ko-fi.com/fieryhenry 67 | >- Những người trong discord đã cung cấp save file, báo lỗi, gợi ý tính năng mới, và là cộng đồng tuyệt vời: <@s>{{discord_url}} 68 | > 69 | ><@w>Nếu bạn trả tiền cho chương trình này thì bạn đã bị lừa. Chương trình này miễn phí và là mã nguồn mở. 70 | > 71 | ><@w>Sử dụng công cụ này với rủi ro của riêng bạn. Tôi không chịu trách nhiệm cho bất kỳ lệnh cấm hoặc hỏng hóc nào gây ra cho save file của bạn. 72 | >Trình chỉnh sửa sẽ cố gắng ngăn chặn điều đó xảy ra, nhưng tôi không thể đảm bảo save file của bạn an toàn. 73 | >Nếu save file bị hỏng, vui lòng vẫn báo cáo trong discord. 74 | >Tôi khuyên bạn nên sao lưu save file trước khi chỉnh sửa. 75 | 76 | report_message=Vui lòng báo cáo điều này đến <@s>#bug-reports trên discord: <@s>{{discord_url}} 77 | report_message_l=Vui lòng báo cáo điều này đến <@s>#bug-reports trên discord: <@s>{{discord_url}} 78 | try_again_message=Vui lòng thử lại. Nếu lỗi vẫn tiếp diễn {{report_message_l}} 79 | all=Tất cả 80 | 81 | error=<@e>Đã xảy ra lỗi (<@s>{error}) {{report_message_l}}\n{traceback} 82 | see_log=<@e>Vui lòng xem tệp nhật ký để biết thêm chi tiết. 83 | max=Tối đa 84 | none=Không có 85 | unknown=Không xác định 86 | 87 | leave=\n<@q>Cảm ơn bạn đã sử dụng Trình chỉnh sửa save file The Battle Cats! 88 | checking_for_changes=<@t>Đang kiểm tra thay đổi... 89 | no_changes=<@su>Không tìm thấy thay đổi. 90 | changes_found=<@su>Đã tìm thấy thay đổi. 91 | 92 | y/n=y/n 93 | 94 | git_not_installed=<@e>Git chưa được cài đặt. Vui lòng cài đặt, thêm vào PATH, và thử lại. 95 | failed_to_get_repo=<@e>Không thể lấy kho: "<@t>{url}". Có lẽ nó không tồn tại, hoặc bạn không có kết nối internet 96 | failed_to_run_git_cmd=<@e>Không thể chạy lệnh git: "<@t>{cmd}". Có lẽ bạn phải kiểm tra kết nối internet của mình. 97 | cancel=Hủy 98 | 99 | update_external=Cập nhật nội dung bên ngoài 100 | updating_external_content=<@q>Đang cập nhật nội dung bên ngoài... 101 | 102 | downloading_map_names=<@q>Đang lấy tên bản đồ... (mã: <@t>{code}). Có thể mất một lúc... 103 | 104 | select_device=Chọn thiết bị: 105 | 106 | continue_q=Tiếp tục? ({{y/n}}): 107 | 108 | no_data_version=<@e>Phiên bản dữ liệu trò chơi mới nhất không khả dụng. Có lẽ do vấn đề internet. Vui lòng thử lại. 109 | 110 | failed_to_get_game_versions=<@e>Không thể lấy phiên bản game. Có lẽ kiểm tra kết nối internet của bạn. 111 | 112 | no_feature_with_name=<@e>Không tìm thấy tính năng với tên: <@s>{name} 113 | yes=Có 114 | -------------------------------------------------------------------------------- /src/bcsfe/core/game/catbase/login_bonuses.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any 3 | from bcsfe import core 4 | 5 | 6 | class Login: 7 | def __init__(self, count: int): 8 | self.count = count 9 | 10 | @staticmethod 11 | def init() -> Login: 12 | return Login(0) 13 | 14 | @staticmethod 15 | def read(stream: core.Data) -> Login: 16 | count = stream.read_int() 17 | return Login(count) 18 | 19 | def write(self, stream: core.Data): 20 | stream.write_int(self.count) 21 | 22 | def serialize(self) -> int: 23 | return self.count 24 | 25 | @staticmethod 26 | def deserialize(data: int) -> Login: 27 | return Login(data) 28 | 29 | def __repr__(self): 30 | return f"Login({self.count})" 31 | 32 | def __str__(self): 33 | return f"Login({self.count})" 34 | 35 | 36 | class Logins: 37 | def __init__(self, logins: list[Login]): 38 | self.logins = logins 39 | 40 | @staticmethod 41 | def init() -> Logins: 42 | return Logins([]) 43 | 44 | @staticmethod 45 | def read(stream: core.Data) -> Logins: 46 | total = stream.read_int() 47 | logins: list[Login] = [] 48 | for _ in range(total): 49 | logins.append(Login.read(stream)) 50 | return Logins(logins) 51 | 52 | def write(self, stream: core.Data): 53 | stream.write_int(len(self.logins)) 54 | for login in self.logins: 55 | login.write(stream) 56 | 57 | def serialize(self) -> list[int]: 58 | return [login.serialize() for login in self.logins] 59 | 60 | @staticmethod 61 | def deserialize(data: list[int]) -> Logins: 62 | return Logins([Login.deserialize(login) for login in data]) 63 | 64 | def __repr__(self): 65 | return f"Logins({self.logins})" 66 | 67 | def __str__(self): 68 | return f"Logins({self.logins})" 69 | 70 | 71 | class LoginSets: 72 | def __init__(self, logins: list[Logins]): 73 | self.logins = logins 74 | 75 | @staticmethod 76 | def init() -> LoginSets: 77 | return LoginSets([]) 78 | 79 | @staticmethod 80 | def read(stream: core.Data) -> LoginSets: 81 | total = stream.read_int() 82 | logins: list[Logins] = [] 83 | for _ in range(total): 84 | logins.append(Logins.read(stream)) 85 | return LoginSets(logins) 86 | 87 | def write(self, stream: core.Data): 88 | stream.write_int(len(self.logins)) 89 | for login in self.logins: 90 | login.write(stream) 91 | 92 | def serialize(self) -> list[list[int]]: 93 | return [login.serialize() for login in self.logins] 94 | 95 | @staticmethod 96 | def deserialize(data: list[list[int]]) -> LoginSets: 97 | return LoginSets([Logins.deserialize(login) for login in data]) 98 | 99 | def __repr__(self): 100 | return f"LoginSets({self.logins})" 101 | 102 | def __str__(self): 103 | return f"LoginSets({self.logins})" 104 | 105 | 106 | class LoginBonus: 107 | def __init__( 108 | self, 109 | old_logins: LoginSets | None = None, 110 | logins: dict[int, Login] | None = None, 111 | ): 112 | self.old_logins = old_logins 113 | self.logins = logins 114 | 115 | @staticmethod 116 | def init(gv: core.GameVersion) -> LoginBonus: 117 | if gv < 80000: 118 | return LoginBonus(old_logins=LoginSets.init()) 119 | else: 120 | return LoginBonus(logins={}) 121 | 122 | @staticmethod 123 | def read(stream: core.Data, gv: core.GameVersion) -> LoginBonus: 124 | if gv < 80000: 125 | logins_old = LoginSets.read(stream) 126 | return LoginBonus(logins_old) 127 | else: 128 | total = stream.read_int() 129 | logins: dict[int, Login] = {} 130 | for _ in range(total): 131 | id = stream.read_int() 132 | logins[id] = Login.read(stream) 133 | return LoginBonus(logins=logins) 134 | 135 | def write(self, stream: core.Data, gv: core.GameVersion): 136 | if gv < 80000 and self.old_logins is not None: 137 | self.old_logins.write(stream) 138 | elif gv >= 80000 and self.logins is not None: 139 | stream.write_int(len(self.logins)) 140 | for id, login in self.logins.items(): 141 | stream.write_int(id) 142 | login.write(stream) 143 | 144 | def serialize( 145 | self, 146 | ) -> dict[str, Any]: 147 | if self.old_logins is not None: 148 | return {"old_logins": self.old_logins.serialize()} 149 | elif self.logins is not None: 150 | return { 151 | "logins": { 152 | id: login.serialize() for id, login in self.logins.items() 153 | } 154 | } 155 | else: 156 | return {} 157 | 158 | @staticmethod 159 | def deserialize(data: dict[str, Any]) -> LoginBonus: 160 | if "old_logins" in data: 161 | return LoginBonus( 162 | old_logins=LoginSets.deserialize(data["old_logins"]) 163 | ) 164 | elif "logins" in data: 165 | return LoginBonus( 166 | logins={ 167 | int(id): Login.deserialize(login) 168 | for id, login in data["logins"].items() 169 | } 170 | ) 171 | else: 172 | return LoginBonus() 173 | 174 | def __repr__(self): 175 | return f"LoginBonus({self.old_logins}, {self.logins})" 176 | 177 | def __str__(self): 178 | return f"LoginBonus({self.old_logins}, {self.logins})" 179 | 180 | def get_login(self, id: int) -> Login | None: 181 | if self.logins is not None: 182 | return self.logins.get(id) 183 | else: 184 | return None 185 | --------------------------------------------------------------------------------