Repository: fieryhenry/BCSFE-Python Branch: main Commit: 93355c12b931 Files: 208 Total size: 1.1 MB Directory structure: gitextract_nh3e0iid/ ├── CHANGELOG.md ├── LICENSE ├── LOCALIZATION.md ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src/ │ └── bcsfe/ │ ├── __init__.py │ ├── __main__.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── color.py │ │ ├── dialog_creator.py │ │ ├── edits/ │ │ │ ├── __init__.py │ │ │ ├── aku_realm.py │ │ │ ├── basic_items.py │ │ │ ├── cat_editor.py │ │ │ ├── clear_tutorial.py │ │ │ ├── enemy_editor.py │ │ │ ├── event_tickets.py │ │ │ ├── fixes.py │ │ │ ├── map.py │ │ │ ├── max_all.py │ │ │ ├── rare_ticket_trade.py │ │ │ └── storage.py │ │ ├── feature_handler.py │ │ ├── file_dialog.py │ │ ├── main.py │ │ ├── recent_saves.py │ │ ├── save_management.py │ │ └── server_cli.py │ ├── core/ │ │ ├── __init__.py │ │ ├── country_code.py │ │ ├── crypto.py │ │ ├── game/ │ │ │ ├── __init__.py │ │ │ ├── battle/ │ │ │ │ ├── __init__.py │ │ │ │ ├── battle_items.py │ │ │ │ ├── cleared_slots.py │ │ │ │ ├── enemy.py │ │ │ │ └── slots.py │ │ │ ├── catbase/ │ │ │ │ ├── __init__.py │ │ │ │ ├── beacon_base.py │ │ │ │ ├── cat.py │ │ │ │ ├── drop_chara.py │ │ │ │ ├── gambling.py │ │ │ │ ├── gatya.py │ │ │ │ ├── gatya_item.py │ │ │ │ ├── item_pack.py │ │ │ │ ├── login_bonuses.py │ │ │ │ ├── matatabi.py │ │ │ │ ├── medals.py │ │ │ │ ├── mission.py │ │ │ │ ├── my_sale.py │ │ │ │ ├── nyanko_club.py │ │ │ │ ├── officer_pass.py │ │ │ │ ├── playtime.py │ │ │ │ ├── powerup.py │ │ │ │ ├── scheme_items.py │ │ │ │ ├── special_skill.py │ │ │ │ ├── stamp.py │ │ │ │ ├── talent_orbs.py │ │ │ │ ├── unlock_popups.py │ │ │ │ ├── upgrade.py │ │ │ │ └── user_rank_rewards.py │ │ │ ├── gamoto/ │ │ │ │ ├── __init__.py │ │ │ │ ├── base_materials.py │ │ │ │ ├── cat_shrine.py │ │ │ │ ├── catamins.py │ │ │ │ ├── gamatoto.py │ │ │ │ └── ototo.py │ │ │ ├── localizable.py │ │ │ └── map/ │ │ │ ├── __init__.py │ │ │ ├── aku.py │ │ │ ├── challenge.py │ │ │ ├── chapters.py │ │ │ ├── dojo.py │ │ │ ├── enigma.py │ │ │ ├── event.py │ │ │ ├── ex_stage.py │ │ │ ├── gauntlets.py │ │ │ ├── item_reward_stage.py │ │ │ ├── legend_quest.py │ │ │ ├── map_names.py │ │ │ ├── map_option.py │ │ │ ├── map_reset.py │ │ │ ├── outbreaks.py │ │ │ ├── story.py │ │ │ ├── timed_score.py │ │ │ ├── tower.py │ │ │ ├── uncanny.py │ │ │ └── zero_legends.py │ │ ├── game_version.py │ │ ├── io/ │ │ │ ├── __init__.py │ │ │ ├── adb_handler.py │ │ │ ├── bc_csv.py │ │ │ ├── command.py │ │ │ ├── config.py │ │ │ ├── data.py │ │ │ ├── git_handler.py │ │ │ ├── json_file.py │ │ │ ├── path.py │ │ │ ├── root_handler.py │ │ │ ├── save.py │ │ │ ├── thread_helper.py │ │ │ ├── waydroid.py │ │ │ └── yaml.py │ │ ├── locale_handler.py │ │ ├── log.py │ │ ├── max_value_helper.py │ │ ├── server/ │ │ │ ├── __init__.py │ │ │ ├── client_info.py │ │ │ ├── event_data.py │ │ │ ├── game_data_getter.py │ │ │ ├── headers.py │ │ │ ├── managed_item.py │ │ │ ├── request.py │ │ │ ├── server_handler.py │ │ │ └── updater.py │ │ └── theme_handler.py │ ├── files/ │ │ ├── locales/ │ │ │ ├── en/ │ │ │ │ ├── core/ │ │ │ │ │ ├── config.properties │ │ │ │ │ ├── files.properties │ │ │ │ │ ├── input.properties │ │ │ │ │ ├── locale.properties │ │ │ │ │ ├── main.properties │ │ │ │ │ ├── save.properties │ │ │ │ │ ├── server.properties │ │ │ │ │ ├── theme.properties │ │ │ │ │ └── updater.properties │ │ │ │ └── edits/ │ │ │ │ ├── bannable_items.properties │ │ │ │ ├── cats.properties │ │ │ │ ├── enemy.properties │ │ │ │ ├── fixes.properties │ │ │ │ ├── gambling.properties │ │ │ │ ├── gamototo.properties │ │ │ │ ├── gatya.properties │ │ │ │ ├── gold_pass.properties │ │ │ │ ├── items.properties │ │ │ │ ├── map.properties │ │ │ │ ├── medals.properties │ │ │ │ ├── missions.properties │ │ │ │ ├── playtime.properties │ │ │ │ ├── scheme_items.properties │ │ │ │ ├── special_skills.properties │ │ │ │ ├── talent_orbs.properties │ │ │ │ ├── treasures.properties │ │ │ │ └── user_rank.properties │ │ │ ├── tw/ │ │ │ │ ├── core/ │ │ │ │ │ ├── config.properties │ │ │ │ │ ├── files.properties │ │ │ │ │ ├── input.properties │ │ │ │ │ ├── locale.properties │ │ │ │ │ ├── main.properties │ │ │ │ │ ├── save.properties │ │ │ │ │ ├── server.properties │ │ │ │ │ ├── theme.properties │ │ │ │ │ └── updater.properties │ │ │ │ ├── edits/ │ │ │ │ │ ├── bannable_items.properties │ │ │ │ │ ├── cats.properties │ │ │ │ │ ├── enemy.properties │ │ │ │ │ ├── fixes.properties │ │ │ │ │ ├── gambling.properties │ │ │ │ │ ├── gamototo.properties │ │ │ │ │ ├── gatya.properties │ │ │ │ │ ├── gold_pass.properties │ │ │ │ │ ├── items.properties │ │ │ │ │ ├── map.properties │ │ │ │ │ ├── medals.properties │ │ │ │ │ ├── missions.properties │ │ │ │ │ ├── playtime.properties │ │ │ │ │ ├── scheme_items.properties │ │ │ │ │ ├── special_skills.properties │ │ │ │ │ ├── talent_orbs.properties │ │ │ │ │ ├── treasures.properties │ │ │ │ │ └── user_rank.properties │ │ │ │ └── metadata.json │ │ │ └── vi/ │ │ │ ├── core/ │ │ │ │ ├── config.properties │ │ │ │ ├── files.properties │ │ │ │ ├── input.properties │ │ │ │ ├── locale.properties │ │ │ │ ├── main.properties │ │ │ │ ├── save.properties │ │ │ │ ├── server.properties │ │ │ │ ├── theme.properties │ │ │ │ └── updater.properties │ │ │ ├── edits/ │ │ │ │ ├── bannable_items.properties │ │ │ │ ├── cats.properties │ │ │ │ ├── enemy.properties │ │ │ │ ├── fixes.properties │ │ │ │ ├── gambling.properties │ │ │ │ ├── gamototo.properties │ │ │ │ ├── gatya.properties │ │ │ │ ├── gold_pass.properties │ │ │ │ ├── items.properties │ │ │ │ ├── map.properties │ │ │ │ ├── medals.properties │ │ │ │ ├── missions.properties │ │ │ │ ├── playtime.properties │ │ │ │ ├── scheme_items.properties │ │ │ │ ├── special_skills.properties │ │ │ │ ├── talent_orbs.properties │ │ │ │ ├── treasures.properties │ │ │ │ └── user_rank.properties │ │ │ └── metadata.json │ │ ├── max_values.json │ │ └── themes/ │ │ ├── default.json │ │ └── discord.json │ └── py.typed └── tests/ ├── __init__.py └── test_parse.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [3.3.0] - 2026-04-01 ### Added - Display basic save info when a save is loaded (region, version, partial inquiry code) - Prompt to ask if you want to change game data repo to gitlab if battlecatsmodding.org is not loading ### Fixed - JP 15.3.0 save parsing - Version checking for version numbers >= 10 - Talent orb effect colours being broken on some terminals - Maybe fixed cat shrine not appearing when using the feature to make it appear - Some issues when reading/writing invalid cat talents - Editor crashing when inputing an option too high if disable maxes is enabled ## [3.2.2] - 2025-12-29 ### Added - Options within the cat shrine feature to make it appear/disappear in-game ### Changed - Reworked how clearing maps/stages works for non story chapters - setting map progress and clear count are now done separately. ### Fixed - Fixed a few more issues with game data downloading - Fixed some issues where disabling max values didn't work for some features - Fixed unknwon map names sometimes showing as blank rather than "Unknown Map Name" - Improved speed of getting map names for most map clearing features - Special skill upgrading crashing if you disable max values and you enter an upgrade value which is too large - Game data repo config value URL is validated when trying to change it ## [3.2.1] - 2025-11-23 ### Added - Feature to add endless battle items ### Fixed - Fixed game data downloading when upgrading from a previous editor version - Fixed disable max values not working in some situations - Fixed an issue where maps not present in the save file could be selected, causing an error ## [3.2.0] - 2025-11-21 ### Added - Feature to edit the cat storage - Feature to reset the golden cat cpu uses - Feature to clear enigma stages - Feature to clear catclaw championships - Feature to reset cat scratcher and wildcat slots - Cats of the Cosmos Chapter 3 outbreaks - Features to push to root storage and rerun game - Option to select cats by the game version they were released in ### Fixed - Fixed issue of not being able to add enigma stages if they have already been cleared - Fixed issue on some platforms where it couldn't find JSONDecodeError - Fixed a few text and input issues - Fixed issue where editing event tickets could cause your inquiry code to change ### Changed - Game data repo is now at - Game data is now downloaded and saved all at once, rather than file by file which should speed up some features - When running the Update External Content feature, it asks you if you want to clear the current game data ## [3.1.0] - 2025-09-03 ### Added - Added a way to choose a gacha banner by id rather than by name - Feature to clear catamin stages and set the clear times to whatever you want - When selecting cats you can now filter down your selection, e.g Select current rare cats from this specific gacha banner - Better error message if your device is not rooted when trying to pull from root storage - Better error message when failing to create a config file ### Changed - Changed default game data repo from to - Changed a few other urls to point to the Codeberg repo rather than the GitHub repo - Increased speed of downloading all cat names - Made a few more option selections work if you enter the text of the option rather than the number associated with it ### Fixed - Fixed talent parsing issue for some save files - Fixed gacha banner selection using old banners rather than newer ones for banners which have the same name - Disable maxes config option now works for upgrading cats - Fixed regression where editing scheme items raised an exception - Increased default timeout for requests and made some requests have no timeout to fix issues with slow internet connection - Issue where an exception would be raised if the editor couldn't auto-detect your country code when downloading save data ## [3.0.1] - 2025-08-04 ### Fixed - Installing the editor on certain platforms due to a dependency issue ## [3.0.0] - 2025-08-04 This is a full re-write of the editor, so many things were added, changed and fixed, and I didn't really document the changes that well, so here's a summary: ### Added - Game Data for es, it, de, th, fr locales and a way to change what repo to use for game data - Better color and localization support - Better adb usage - Waydroid support - More config options - Lucky tickets - Treasure Chests - Support for new talent orbs - Ultra Form Support - Better save backups _ More support for older game versions - Labyrinth medals - Many more things ### Changed - Improved the wording on a few features - Cats will be auto-unlocked by default when upgrading / true forming, etc - Many more things ## [2.7.2.3] - 2023-06-19 ### Fixed - New version of colored crashing the editor by forcing the editor to use the old version (new colored version renamed stuff and also raises an exception when not using a specific set of colors) - Max value for equip slots being too high, for some reason ponos has allocated space for 18 equip slots but has only allocated space for 17 slot names ## [2.7.2.2] - 2023-06-08 ### Fixed - The editor crashing when editing meow medals or event stages ## [2.7.2.1] - 2023-05-28 ### Fixed - The editor crashing if user info not found ## [2.7.2] - 2023-05-28 ### Added - Ultra Talent Support - 12.2.0 cannon support ### Changed - Improved item tracking and user info tracking so your inquiry code shouldn't change as much ### Fixed - Issues with the max values for some multi items - Disable maxes config option not being checked - Gold pass not being able to be removed ## [2.7.1] - 2023-03-22 ### Added - A feature to convert save versions e.g en to jp - might give issues and only works if both apps are the same version ### Changed - Base material names are no longer hardcoded and so jp base material names exist now - New cats no longer cause the cat capsule machine still thinking the cat is new - Talent orbs editing works better now + aku orbs ### Fixed - Things like treasures and gold pass id crashing the editor when entering too large of a number - Jp 12.2.0 save parsing - The first mission not showing up in mission clearing - Gatya seed not being able to set above the 32 signed int limit - Outbreaks crashing ## [2.7.0] - 2023-01-08 ### Added - Features to clear legend quest, behemoth culling stages, and collab gauntlets - Feature to get scheme item rewards (e.g go go pogo cat mission rewards) - More support for rooted android devices (pull and push directly to root folder + re-run game) - The ability to remove talents - The ability to select / download a new save without having to restart the editor ### Changed - Catseye editing will now use the game data for names - means i don't need to update the whole editor to put another catseye type in - When uploading the managed items, a save key is added (idk if this changes anything / reduces bans but newer game versions do this) - The editor will never ask if you want to exit, to exit enter the option to exit or do `ctrl+c` - Renamed feature `Create a new account` to `Generate a new inquiry code and token` to better reflect what it does ### Fixed - Cat name selection for jp - Evolve cats and upgrade cats crashing if game data is outdated - Main story crashing and chapter names being offset sometimes - Dojo score not being able to be edited if you haven't been to the dojo yet - Max value for some items being an unsigned int even though the game reads signed ints - Outbreak clearing not setting all stages - Jp timed score rewards being parsed and serialized incorrectly leading to incorrect timed scores being edited in ### Removed - The `pick` module due to issues with python 3.11 ## [2.6.0] - 2022-10-24 ### Added - Editor support for android. Using termux you can now run and install the editor - On crash, the editor will ask if you want to save your changes and upload managed item changes to the servers - A way to remove meow medals - A feature to play the CotC 3 filibuster stage again - A way to remove outbreaks - A feature to unlock the aku realm ### Changed - When upgrading cats, if you upgrade past the normal max for that cat then the level cap of the cat will also increase / decrease to match. (E.g if you upgrade a cat to level 35 using the editor, then use a catseye in game then it will unlock level 36 instead of level 31) - How selecting stages to clear works. Instead of selecting stage ids you enter a stage to complete the progress to (e.g entering 5 clears the first 5 stages, and entering 48 clears them all and then if you then enter 5 again it will clear the level progress for the levels 6-48) ### Fixed - Crash if using an older game version and getting cats by rarity / gatya id - Talents crashing - CotC 2 and 3 appearing in the outbreaks feature when they don't have outbreaks - The editor crashing if you don't have an internet connection ## [2.5.0] - 2022-10-14 ### Added - A feature to fix time related issues (HGT, no energy recovery, etc) ### Changed - Features that fix things (fix time related issues, fix gamatoto crashing the game, fix equip menu not unlocked, etc) have been moved / copied to their own category called `Fixes` ### Fixed - Having a very high playtime not allowing you to transfer - Having corrupted cat unlock flags messing up user rank calculation and not letting you transfer - Cat shrine not appearing when editing it - Selecting cats from name not letting you select cats - Transfer error messages not appearing in some cases ## [2.4.0] - 2022-10-05 ### Added - An option in save management to save the save data without opening the file selection dialog - Option to edit where the config file is located - A way to enter an officer id or generate a random one when getting the gold pass. Entering -1 for the officer id will remove the gold pass ### Changed - Platinum shards max amounts now takes into account your current platinum ticket amount to make sure you can't go over 9 tickets - Made catshrine appear when using the edit catshrine level feature and the level up dialogs are now skipped - When pulling using adb the editor will automatically detect currently installed game versions and let you select one to pull. If only 1 game version is installed it will just default to that one. ### Fixed - Selecting cats based on name crashing if entering a cat id too large - Upgrade cats / special skills crashing the editor if setting the base level to 0 or a level to be larger than 65535 - Being unable to download a save / pull saves if your default country code is longer than 2 characters. The editor will just ask you to manually enter it - Treasure groups chapter selection ids being off by 1 ## [2.3.0] - 2022-09-14 ### Added - Feature to add enigma stages - Feature to edit Gamatoto shrine xp / level - Replaced some unknown values in the save stats + updated parsing for 11.3.0 and up ### Changed - Get gold pass will now give the paid version instead of the free trial and each subsequent use of the feature will increase the total renewal times by 1 and wipe the daily catfood stamp count ### Fixed - File not found error if item_tracker.json is not present ## [2.2.2] - 2022-09-04 ### Added - A new config option to select options with the arrow keys or j and k to select some options. `EDITOR` -> `USE_ARROW_KEYS_FOR_FEATURE_SELECT` ### Fixed - Default save path being empty, causing the editor to not be able to pull saves unless changed ## [2.2.1] - 2022-09-04 ### Fixed - Editor sometimes crashing when saving a file when the file dialog ## [2.2.0] - 2022-09-03 ### Added - Option when selecting cats to only get obtainable cats (Only the cats that show up in the cat guide) - Option to select cats by name when selecting cats ### Changed - Config file will now be located in the app data folder / home folder - Character drop, evolve cats and talents will now be able to use the normal cat selecting menu - You can now select all chapters at once when editing treasure groups ### Fixed - Wrong chapter being shown when selecting levels - Editor crashing when entering the name of a category when selecting a feature ## [2.1.1] - 2022-08-17 ### Changed - Split up some features into subcategories e.g Treasures / Levels -> Treasures -> Treasure groups. Or Items -> Tickets -> Normal Tickets ### Fixed - Gamatoto helpers ## [2.1.0] - 2022-08-16 ### Added - The ability to unlock the equip menu - The ability to upload catfood and other bannable item changes to the ponos servers - this is done automatically whenever your save data is saved / uploaded. This should in theory prevent bans from catfood and other items, but it seems a bit unreliable so I've kept the warning in the editor - A feature to claim all user rank rewards (Doesn't give any items) - A way to select specific gacha banner cats - you need to go to the wiki for the banner you want, and look at the name of the image e.g royal fest = 602 - The ability to get the gold pass - A feature to create a new account - new iq and token - A way to clear specific aku stages - Some configuration options , e.g options to remove max limits, automatically save changes after each edit, etc, the path to the config file is shown at the top of the editor ### Changed - You can now exit, catfood, rare, plat, and legend tickets after the warning is shown - The editor will now display "Press enter to exit" when exiting - Whenever your inquiry code changes, the editor will upload your catfood and other bannable item amounts to the servers - this should prevent bans - When entering a transfer code, the editor will check for a hex number and when entering a confirmation code it will check for a dec number. This should prevent people confusing 0 for O - Game data will now be downloaded from [here](https://github.com/fieryhenry/BCData) when needed so that if I want to update the data in the editor, I don't have to do a new release ### Fixed - Select cats based on rarity being off by 1 - Evolve cats setting some cats to the first form ## [2.0.2] - 2022-07-08 ### Fixed - Jp not being able to upload save data ## [2.0.1] - 2022-07-04 ### Fixed - Upgrade cats and unlock event stages not working properly when editing all at once ## [2.0.0] - 2022-07-04 ### Added - The ability to upload your save data to the ponos servers and get transfer and confirmation codes. (The editor's root requirement is now gone). Although, you'll still need root access if you get banned / elsewhere popup. I haven't tested the feature too much so it could lead to bans - An option to go back in the feature menu - An automatic updater, if there is a new update, it will ask if you want to update and if you say yes then it'll try to update automatically - A way to select `all` talent orbs to edit all at once - A new tutorial video that shows you how to use the transfer system stuff and unban an account [here](https://www.youtube.com/watch?v=Kr6VaLTXOSY) ### Changed - The fix elsewhere / unban feature, it no longer needs another account. You can still use the old one, now named `Old Fix elsewhere error / Unban account (needs 2 save files)` if you want - A bunch of the source code. You should now be able to import BCSFE_Python in another python file and access the parser, serialiser, etc. Due to the rewrite, some stuff may be broken. This, and testing, is where the majority of the time went to - The order of few options, to make the server stuff closer to the top as that's what most people will be selecting now that no root is needed ### Fixed - Some adb issues - More save parsing issues - Edit dojo score crashing if you haven't been to the dojo yet - Adding adb to path issue - Ototo cat cannon not setting the correct value when editing all at once - Individual treasures feature giving you 49/48 treasures ## [1.8.0.1] - 2022-05-24 ### Removed - Import from a random module that got imported automatically by vscode ## [1.8.0] - 2022-05-24 ### Added - New behemoth stones to get catfruit feature - The ability to fix gamatoto from crashing the game ### Fixed - Some adb issues thanks to [!j0](https://github.com/j0912345) - More save parsing issues ## [1.7.1] - 2022-05-20 ### Fixed - Save parsing issue with en 11.5 ## [1.7.0] - 2022-05-20 ### Added - The ability to clear catnip challenges / missions - The ability to complete cat cannons to certain stages (e.g foundation, style, cannon) - The ability to set the Catclaw dojo score (only `Hall of Initiates` atm - don't know if ranked stuff can be save edited) - The ability to remove the `Clear "{stage_name}" for a chance to get the Special unit {cat_name}` stage clear rewards when entering Legend Stages - The ability to set the `maxed upgrades --> rare tickets` conversion thing to allow for unbannable rare tickets to be generated. Run the `trade progress` feature, enter the number of rare tickets you want, go into game and press the `Use All` button in cat storage and then press `Trade for Ticket` . There appears to be nothing in your storage because there is an unobtainable blue upgrade / special skill between `power` and `range` and the editor adds that to your storage to allow you to use the `trade` thing, although any other blue upgrade also works, as long as it is max level. ### Fixed - More save parsing issues ## [1.6.2] - 2022-05-03 ### Fixed - Upgrade cats and upgrade blue upgrades crashing the editor ## [1.6.1] - 2022-05-03 ### Fixed - Gauntlets from crashing the editor ## [1.6.0] - 2022-05-03 ### Added - The ability to edit specific treasures for each stage - The ability to edit groups of treasures (e.g energy drink, aqua crystal) ### Fixed - More save parsing issues - Event stages crashing when selecting `all` for the stage ids ## [1.5.0] - 2022-04-28 ### Added - When exporting to json, the current editor version will be included and so if json data from a different editor version is being imported a warning message will show. - Option to edit specific stages in a main story chapter - Option to remove enemy guide entries ### Fixed - More save parsing issues - Meow medals not writing properly - The enemy ids in `unlock/remove enemy guide entries` not being the same as the ones on the wiki ## [1.4.8] - 2022-04-23 ### Fixed - More save parsing issues - Event stages, uncanny, gauntlets not unlocking the next subchapter - Outbreaks crashing the editor after being edited ## [1.4.7] - 2022-04-22 ### Changed - It seems like the adb included in the editor doesn't work, and so I've removed it, you now need to have adb in your Path environment variable. Tutorial in the help videos's description ## [1.4.6] - 2022-04-22 ### Changed - When the editor detects a new version, it will display where to see the changelog ### Fixed - A small issue relating to meow medals - Some more parsing errors ## [1.4.5] - 2022-04-22 ### Changed - `adb.exe` is now included in the project, so you should be able to auto-pull and push saves without adding it to your `PATH` ### Fixed - Catfruit crashing ## [1.4.4] - 2022-04-21 ### Fixed - Some saves getting an error when parsing ## [1.4.2 & 1.4.3] - 2022-04-21 ### Fixed - It should correctly auto-install required packages ## [1.4.1] - 2022-04-21 ### Fixed - It should auto-install required packages ## [1.4.0] - 2022-04-21 ### Added - Ability to unlock enemy guide - Ability to clear cat guide rewards ### Changed - Made clear tutorial also beat Korea ## [1.3.0] - 2022-04-21 ### Added - Ability to add, upgrade cats, and true form cats in a certain rarity category. ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: LOCALIZATION.md ================================================ # Localization Small tutorial on how to localize the editor into a different language. ## Disclaimer Please do not use machine or AI translated text, they will likely make mistakes, especially since there is specific terminology unique to The Battle Cats and the save editor, and if you do not know the language you will not be able to correct them. There are also many other ethical and legal issues when using AI that I would like to avoid. Thank you for understanding ## How To 0. If you want to submit a pull request later you should fork the editor (make sure to fork the codeberg repo: ) 1. Install the editor from source by following [these instructions](https://codeberg.org/fieryhenry/BCSFE-Python#install-from-source) (make sure to change the git clone url to be your fork if you have one) 2. Inside the `src/bcsfe/files/locales/` folder you will find the pre-existing locales, copy the one named `en` and rename it to the code of the language you are translating to 3. Create a file called `metadata.json` inside the folder and edit it to contain the following info: ```json { "authors": ["author-1", "author2", "cool-person3"], "name": "Name of language (english name of language)" } ``` For example the one for Vietnamese looks like this: ```json { "authors": ["HungJoesifer"], "name": "Tiếng Việt (Vietnamese)" } ```` 4. Edit each of the .properties file, translating each value, try to keep the colors the same as the original text. Anything in `{..}` should stay exactly how it is. Anything in `{{..}}` references another key and so can be changed if you want. For more details see [here](https://codeberg.org/fieryhenry/ExampleEditorLocale/). 5. Once you think you have finished, open the editor and edit the config value `Language` and select your language from the list 6. Restart the editor and check that it works, you should also see the details you specified in the `metadata.json` file in the opening text 7. Enable the config option to display missing locale keys then restart the editor 8. If everything is correct you shouldn't see any missing keys (extra keys are fine). 9. Once done, push your changes to your fork if you have one and feel free to submit a pull request to the codeberg repo. Alternatively you can just zip your locale folder and send it to me or in the #localization channel on discord. (or [matrix](https://matrix.to/#/@fieryhenry:matrix.battlecatsmodding.org)) ================================================ FILE: MANIFEST.in ================================================ recursive-include src/bcsfe/files * recursive-exclude src/bcsfe/files/game_data * recursive-exclude src/bcsfe/files/map_names * ================================================ FILE: README.md ================================================ # Battle Cats Save File Editor [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/fieryhenry) [![LiberaPay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/fieryhenry) BCSFE is a python command line save editor for The Battle Cats. Join the [discord server](https://discord.gg/DvmMgvn5ZB) if you want to suggest new features, report bugs or get help on how to use the editor (please read the below tutorials first before asking for help). ## Thanks to Lethal's editor for giving me inspiration to start the project and it helped me work out how to patch the save data and edit cf/xp: Beeven and csehydrogen's free and open source code, which helped me figure out how to patch save data: [beeven/battlecats](https://github.com/beeven/battlecats), [csehydrogen/BattleCatsHacker](https://github.com/csehydrogen/BattleCatsHacker) Anyone who has supported my on [Ko-Fi](https://ko-fi.com/fieryhenry) or [LiberaPay](https://liberapay.com/fieryhenry) Everyone who's given me saves, which helped to test save loading/saving and to test/develop new features ### Localization - HungJoesifer for Vietnamese localization - LinYuAn for Traditional Chinese localization ### Themes - HungJoesifer for the `discord` inspired theme ## Installation Note the following tutorials are for the device you wish to run the editor on, not the device that you have the game installed on. For example just because you have the game on an android device, does not mean you have to run the editor on it. It is easier to run the editor on a PC / laptop rather than on a mobile device. ### Windows / MacOS 1. Install Python 3.9 or later if you don't already have it: 2. Open a terminal such as PowerShell or Command Prompt 3. Run the following command: ```powershell py -m pip install bcsfe ``` 4. If you get an error saying that `py` is not a recongnised command, then try: ```powershell python -m pip install bcsfe ``` or ```powershell python3 -m pip install bcsfe ``` 5. If you get an error saying `No module named pip`, then run: ```powershell py -m ensurepip --upgrade ``` Again change `py` for `python` or `python3` if needed. I won't mention this again, so just remember the one which works at keep using that. 5. To run the editor, as long as Python is in your PATH, you should be able to run: ```powershell bcsfe ``` 6. If Python is not in your path you'll need to run: ```powershell py -m bcsfe ``` If you are using Windows and you are still struggling, try watching this video [here](https://codeberg.org/fieryhenry/videos/media/branch/main/bcsfe_windows_help.webm). 7. To update the editor run: ```powershell py -m pip install -U bcsfe ``` 8. To uninstall the editor run: ```powershell py -m pip uninstall bcsfe ``` ### Linux 1. Install Python 3.9 or later using your system's package manager if you don't already have it 2. You might have to install pip seperately with a package called `python-pip` or something similar or you can run the following command: ```sh python3 -m ensurepip --upgrade ``` 3. Depending on your distro you might not be able to install the editor directly using the system pip and you might need to use pipx (python-pipx) or create a virtual environment manually. 4. Using pipx: ```sh pipx install bcsfe ``` 5. If `~/.local/bin/` is in your path you should be able to run the editor with the command: ```sh bcsfe ``` 6. You may also need to install `tk` with your system package manager to open the file selection dialog. This package may be called `tk` or `python-tk` or `python3-tk`. 7. To update the editor if you are using pipx run: ```sh pipx upgrade bcsfe ``` 8. To uninstall the editor if you are using pipx run: ```sh pipx uninstall bcsfe ``` If anyone wants to put the editor on the AUR or another package repo, feel free, I'll be happy to help if needed. ### Android You need to install a terminal emulator to be able to install and run Python packages. [Termux](https://termux.dev/en/) is a good option and is what this tutorial will use. 1. Download Termux, you can either get it from [F-Droid](https://f-droid.org/), or the APK directly from [GitHub](https://github.com/termux/termux-app?tab=readme-ov-file#github). DO NOT use the Google Play Store version, as it does not fully work. I recommend using F-Droid since it can update Termux for you (and it's just a better alternative than using the Google Play Store). On F-Droid Termux is called `Termux Terminal emulator with packages` 2. Once Termux is installed, open it and run the following commands: ```sh termux-setup-storage termux-change-repo pkg update pkg upgrade pkg install python python-pip ``` When it asks for a mirror, it doesn't really matter which one you pick, the default single mirror works fine. 3. Install the editor with the following command: ```sh pip install bcsfe ``` Or if that doesn't work try: ```sh python -m pip install bcsfe ``` 4. Run the editor with the following command: ```sh bcsfe ``` Or if that doesn't work try: ```sh python -m bcsfe ``` Note that the editor might give you warnings about tkinter not being installed, you can just ignore those as tkinter will not work on mobile. This just means that instead of a graphical file selection dialog, you just have to type the file path manually. For example to save your save file to your downloads directory, the path might look something like `/storage/emulated/0/Download/SAVE_DATA` or `/sdcard/Download/SAVE_DATA` 5. To update the editor run: ```sh pip install -U bcsfe ``` Or ```sh python -m pip install -U bcsfe ``` 5. To uninstall the editor run: ```sh pip uninstall bcsfe ``` Or ```sh python -m pip uninstall bcsfe ``` ### iOS I do not have an iOS device, so there is no tutorial. The video that was recommended is now outdated. But for a general overview of what you need to do: 1. Download a-Shell from the App Store 2. Install the editor with: ```sh pip install bcsfe ``` 3. Run the editor with: ```sh bcsfe ``` Or if that doesn't work try: ```sh python -m bcsfe ``` Or ```sh python3 -m bcsfe ``` 4. To update the editor run: ```sh pip install -U bcsfe ``` 5. To uninstall the editor run: ```sh pip uninstall bcsfe ``` ## Terms of Use I don't like that I have to have Terms of use but these terms are designed to prevent scams and the exploitation of users. By using the editor you agree to the following: If you are using the editor to run a paid service that profits off of the editor (e.g a service to provide people with hacked accounts, or a paid discord bot to edit people's accounts, etc) you must make it very clear that you are using this save editor. This should be done by linking this Codeberg page, and explicitly stating that the tool you are using is available for free and that they don't need to use your service to hack their account. This information needs to be visible and something the customer agrees to **before** any payment is made. This also includes paid services which claim to teach people "How To Hack The Battle Cats". In those cases, this still applies, so you still need to state and have the customer acknowledge the things I said above. Free services / derivative works (such as a third party discord bot or editor gui) are fine to use the editor under the hood as long as you abide by the [License](#license). Basically if you are distributing a program which uses the editor, you need to license your own program under the GPL or a compatible license (basically make it open source / free software too). Also if you **are** profiting from the editor, it would be greatly appreciated if you could give back something and support me. ## Usage Once you have installed and ran the editor, you can now begin to edit your save file! 1. In `The Battle Cats` enter the `Change Account / Device` menu in the `Settings` on the main menu. 2. Then enter the `Change Device` -> `Retrieve Data from Old Device` menu. 3. Then click / tap `Save Data to Server`, this should give you a transfer code and a confirmation code. 4. In the editor use the option called `Download save file using transfer and confirmation code` by entering the number `1` 5. Enter your transfer code 6. Enter your confirmation code 7. Select the country code that you are using, `en`=english, `kr`=korean, `jp`=japanese, `tw`=taiwanese. Note that `en` also includes the `it`, `es`, `fr`, `th`, and `de` translations. 8. Edit what you want. Note that in most cases, if you want to exit the current input you can enter `q` and press enter to go back to the previous menu 9. In the editor, go into the `Save Management` category and select `Save changes and upload to game servers (get transfer and confirmation codes)`. It may take some time, it may also fail, if it does then try again. 10. This should give you a new transfer code and a new confirmation code. 11. Back in-game, tap the `Close Game` button, then tap `Cancel Data Transfer` (and also possibly `Start Game From Beginning`) 12. Go back into the `Change Account / Device` menu and then go into the `Resume Data Transfer` -> `Transfer Data to New Device` menu 13. Enter the new codes, and tap `Resume Transfer` 14. Then done! You should see your edits in-game. 15. Every time that you want to make an edit to your save, you will have to re-upload it to the game servers in the editor and re-download it in-game, the saves aren't automatically linked together. Apparently doing the Google Account / Apple Account link limits the number of data transfers you can do within a certain time. So to be safe, I would avoid linking your account. ### Using a rooted device via adb 1. Add adb to your PATH environment variable, or edit the config to set ADB path editor config option to the full path of the adb executable. You can download adb from [platform-tools](https://developer.android.com/studio/releases/platform-tools) 1. Open the editor and select the option named `Pull save file from device using adb` and enter your game version, or select the option named `Select save file from file` and select a copy of your save data 1. Edit what you want 1. Go into save management and select an option to push save data to the game 1. Enter the game and you should see changes ### Using a rooted device directly 1. You need to be running the editor on the device itself, so you'll need to follow the [Android tutorial](#android) to install the editor 1. You may have to run the editor with `sudo python -m bcsfe` or something, so you might have to setup the termux root repo and run `pkg install sudo` 1. In the editor select the option named `Pull save file from root storage` 1. Edit what you want 1. Go into save management and select an option to push save data to the game 1. Enter the game and you should see changes ### How to unban your account 1. Select the option in `Account` to `Unban account` or just upload the save data to the game servers again 1. It may take some time but after, you should be able to choose one of the options in save management to push the save data to the game. #### How to prevent a ban in the future - Instead of editing in platinum tickets use the `Platinum Shards` feature - Instead of editing in rare tickets use the `Normal Ticket Max Trade Progress (allows for unbannable rare tickets)` feature - Instead of hacking in cat food, just edit everything in that you can buy with cat food, e.g battle items, catamins, xp, energy refills (leaderships), etc. If you really want catfood then you can clear and unclear catnip missions with the feature `Catnip Challenges / Missions` then entering 1 when asked. You'll need to collect the catfood in-game after each clear though - Instead of hacking in tickets, just hack in the cats/upgrades you want directly ### Install from source If you want the latest features then you can install the editor from the git repo. 1. Download git: - Windows: [Git](https://git-scm.com/downloads) - Linux: (use package manager, e.g `sudo apt-get install git` or `sudo pacman -S git`) - Android: Termux: `pkg install git` - iOS: a-Shell should already include it 2. Run the following commands: (You may have to replace `py` with `python` or `python3`) ```sh git clone https://codeberg.org/fieryhenry/BCSFE-Python.git cd BCSFE-Python py -m pip install -e . py -m bcsfe ``` Then if you want the latest changes you only need to run `git pull` in the downloaded `BCSFE-Python` folder. (use `cd` to change the folder) Alternatively you can use pip directly, although it won't auto-update with the latest git commits. ```sh py -m pip install -U git+https://codeberg.org/fieryhenry/BCSFE-Python.git py -m bcsfe ``` Again, you might need change `py` for `python` or `python3` If you want to use the editor again all you need to do is run the `py -m bcsfe` command ## Documentation - [Custom Editor Locales](https://codeberg.org/fieryhenry/ExampleEditorLocale) - [Custom Editor Themes](https://codeberg.org/fieryhenry/ExampleEditorTheme) I only have documentation for the locales and themes atm, but I will probably add more documentation in the future. ## Contributing If you want to contribute to the BCSFE, I recommend joining the [Discord Server](https://discord.gg/DvmMgvn5ZB) and starting a discussion in #dev-chat, or create an issue in this repo, or a draft pull request. If you need help with reverse engineering the save file, I have a basic starting guide here: . If you want to localize the editor see [here](./LOCALIZATION.md). ## License BCSFE is licensed under the GNU GPLv3 which can be read [here](https://www.gnu.org/licenses/gpl-3.0.en.html). ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project] name = "bcsfe" authors = [{ name = "fieryhenry" }] description = "A save file editor for The Battle Cats" license = "GPL-3.0-or-later" readme = "README.md" requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: End Users/Desktop", "Topic :: Utilities", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Operating System :: OS Independent", ] dependencies = [ "aenum", "colored==1.4.4", "pyjwt", "requests", "pyyaml", "beautifulsoup4", "argparse", ] dynamic = ["version"] keywords = ["battle cats", "save editor", "hacking"] [project.urls] Homepage = "https://codeberg.org/fieryhenry/BCSFE-Python" Repository = "https://codeberg.org/fieryhenry/BCSFE-Python" Issues = "https://codeberg.org/fieryhenry/BCSFE-Python/issues" Changelog = "https://codeberg.org/fieryhenry/BCSFE-Python/raw/branch/main/CHANGELOG.md" [tool.setuptools.dynamic] version = { attr = "bcsfe.__version__" } [tool.setuptools] package-dir = { "" = "src" } [project.scripts] bcsfe = "bcsfe:run" ================================================ FILE: requirements.txt ================================================ aenum==3.1.16 colored==1.4.4 PyJWT==2.12.1 PyYAML==6.0.2 Requests==2.33.1 beautifulsoup4==4.13.4 argparse==1.4.0 ================================================ FILE: setup.py ================================================ from setuptools import setup setup() ================================================ FILE: src/bcsfe/__init__.py ================================================ __version__ = "3.3.0" from bcsfe import core, cli __all__ = ["core", "cli"] def run(): from bcsfe import __main__ __main__.main() ================================================ FILE: src/bcsfe/__main__.py ================================================ from __future__ import annotations import traceback from bcsfe import cli from bcsfe import core import bcsfe import argparse def main(): parser = argparse.ArgumentParser("bcsfe") parser.add_argument( "--version", "-v", action="store_true", help="display the version and exit" ) parser.add_argument( "--input-path", "-i", type=str, help="input path to save file to edit" ) parser.add_argument( "--game-data-dir", "-g", type=str, help="path to store the game data to" ) parser.add_argument( "--transfer-backup-path", type=str, help="path to save the backup SAVE_DATA after transfering to", ) parser.add_argument( "--config-path", "-c", type=str, default=None, help=f"path to the config file. If unspecified defaults to {core.Config.get_config_path()}", ) parser.add_argument( "--log-path", "-l", type=str, default=None, help=f"path to the log file. If unspecified defaults to {core.Logger.get_log_path()}", ) args = parser.parse_args() if args.version: print(bcsfe.__version__) exit() if args.config_path is not None: core.set_config_path(core.Path(args.config_path)) if args.log_path is not None: core.set_log_path(core.Path(args.log_path)) if args.transfer_backup_path is not None: core.set_transfer_backup_path(core.Path(args.transfer_backup_path)) if args.game_data_dir is not None: core.set_game_data_path(core.Path(args.game_data_dir)) core.core_data.init_data() try: cli.main.Main().main(args.input_path) except KeyboardInterrupt: cli.main.Main.leave() except Exception as e: tb = traceback.format_exc() cli.color.ColoredText.localize( "error", error=e, version=bcsfe.__version__, traceback=tb ) try: cli.main.Main.exit_editor() except Exception: pass except KeyboardInterrupt: pass main() ================================================ FILE: src/bcsfe/cli/__init__.py ================================================ from bcsfe.cli import ( color, dialog_creator, main, file_dialog, feature_handler, save_management, server_cli, edits, recent_saves, ) __all__ = [ "color", "dialog_creator", "main", "file_dialog", "feature_handler", "save_management", "server_cli", "edits", "recent_saves", ] ================================================ FILE: src/bcsfe/cli/color.py ================================================ from __future__ import annotations from typing import Any from aenum import NamedConstant # type: ignore import colored # type: ignore from bcsfe import core class ColorHex(NamedConstant): GREEN = "#008000" G = GREEN RED = "#FF0000" R = RED DARK_YELLOW = "#D7C32A" DY = DARK_YELLOW BLACK = "#000000" BL = BLACK WHITE = "#FFFFFF" W = WHITE CYAN = "#00FFFF" C = CYAN DARK_GREY = "#A9A9A9" DG = DARK_GREY BLUE = "#0000FF" B = BLUE YELLOW = "#FFFF00" Y = YELLOW MAGENTA = "#FF00FF" M = MAGENTA DARK_BLUE = "#00008B" DB = DARK_BLUE DARK_CYAN = "#008B8B" DC = DARK_CYAN DARK_MAGENTA = "#8B008B" DM = DARK_MAGENTA DARK_RED = "#8B0000" DR = DARK_RED DARK_GREEN = "#006400" DGN = DARK_GREEN LIGHT_GREY = "#D3D3D3" LG = LIGHT_GREY ORANGE = "#FFA500" O = ORANGE @staticmethod def from_name(name: str) -> str: if name == "": return "" return getattr(ColorHex, name.upper()) class ColorHelper: def __init__(self): self.theme_handler = core.core_data.theme_manager def get_color(self, color_name: str) -> str: try: first_char = color_name[0] except IndexError: return "" if first_char == "#": return color_name if first_char == "@": try: second_char = color_name[1] except IndexError: return "" try: third_char = color_name[2] except IndexError: third_char = "" if second_char == "p": return self.theme_handler.get_primary_color() if second_char == "s" and third_char != "u": return self.theme_handler.get_secondary_color() if second_char == "t": return self.theme_handler.get_tertiary_color() if second_char == "q": return self.theme_handler.get_quaternary_color() if second_char == "e": return self.theme_handler.get_error_color() if second_char == "w": return self.theme_handler.get_warning_color() if second_char == "s" and third_char == "u": return self.theme_handler.get_success_color() return self.theme_handler.get_theme_color(color_name[1:]) try: return ColorHex.from_name(color_name) except AttributeError: return "" class ColoredText: def __init__(self, string: str, end: str = "\n") -> None: string = string.replace("\\n", "\n") self.string = string self.end = end self.color_helper = ColorHelper() self.display(string) def display(self, string: str) -> None: text_data = self.parse(string) for i, (text, color) in enumerate(text_data): if i == len(text_data) - 1: text += self.end if color == "": print(text, end="") else: try: fg = colored.fg(color) # type: ignore except Exception: print(text, end="") continue print(colored.stylize(text, fg), end="") # type: ignore @staticmethod def localize(string: str, escape: bool = True, **kwargs: Any) -> ColoredText: return ColoredText( core.core_data.local_manager.get_key(string, escape=escape, **kwargs) ) def parse(self, txt: str) -> list[tuple[str, str]]: txt = "<@p>" + txt + "" output: list[tuple[str, str]] = [] i = 0 tags: list[str] = [] inside_tag = False in_closing_tag = False tag_text = "" text = "" special_chars = core.LocalManager.get_special_chars() while i < len(txt): char = txt[i] if char == "\\" and i + 1 < len(txt) and txt[i + 1] in special_chars: i += 1 char = txt[i] text += char i += 1 continue if tags: tag = tags[-1] else: tag = "" if char == ">" and inside_tag: inside_tag = False if not in_closing_tag: tags.append(tag_text) if in_closing_tag: in_closing_tag = False tag_text = "" if char == "<" and not inside_tag: inside_tag = True if text: color = self.color_helper.get_color(tag) output.append((text, color)) text = "" tag_text = "" if char == "/" and inside_tag: in_closing_tag = True if tags: tags.pop() if not inside_tag and char != ">" and char != "<": text += char if inside_tag and char != "<" and char != ">": tag_text += char i += 1 return output class ColoredInput: def __init__(self, end: str = "") -> None: self.end = end def get(self, display_string: str) -> str: ColoredText(display_string, end=self.end) return input() def localize(self, string: str, escape: bool = True, **kwargs: Any) -> str: text = core.core_data.local_manager.get_key(string, escape=escape, **kwargs) return self.get(text) ================================================ FILE: src/bcsfe/cli/dialog_creator.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color class RangeInput: def __init__(self, max: int | None = None, min: int = 0): self.max = max self.min = min def clamp_value(self, value: int) -> int: if self.max is None: return max(value, self.min) return max(min(value, self.max), self.min) def get_input_locale( self, dialog: str, perameters: dict[str, int | str], escape: bool = True, ) -> list[int] | None: user_input = color.ColoredInput(end="").localize( dialog, escape=escape, **perameters ) return self.parse(user_input) def parse(self, user_input: str) -> list[int] | None: if user_input == "": return [] if user_input == core.core_data.local_manager.get_key("quit_key"): return None parts = user_input.split(" ") ids: list[int] = [] all_text = core.core_data.local_manager.get_key("all") for part in parts: if "-" in part and len(part.split("-")) == 2: lower, upper = part.split("-") try: lower = int(lower) upper = int(upper) except ValueError: continue if lower > upper: lower, upper = upper, lower if self.max is not None: lower = max(lower, self.min) upper = min(upper, self.max) else: lower = max(lower, self.min) ids.extend(range(lower, upper + 1)) elif part.lower() == all_text.lower() and self.max is not None: ids.extend(range(self.min, self.max + 1)) else: try: part = int(part) except ValueError: continue if self.max is not None: part = max(part, self.min) part = min(part, self.max) else: part = max(part, self.min) ids.append(part) return ids class IntInput: def __init__( self, max: int | None = None, min: int = 0, default: int | None = None, signed: bool = True, bit_count: int = 32, ensure_max: bool = False, ): self.signed = signed self.bit_count = bit_count self.max = self.get_max_value(max, signed, bit_count, ensure_max) self.min = min self.default = default @staticmethod def get_max_value( max: int | None, signed: bool = True, bit_count: int = 32, ensure_max: bool = False, ) -> int: disable_maxes = ( core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES) and not ensure_max ) if signed: bit_count -= 1 max_int = (2**bit_count) - 1 if disable_maxes or max is None: return max_int return min(max, max_int) def clamp_value(self, value: int) -> int: return max(min(value, self.max), self.min) def get_input( self, localization_key: str, perameters: dict[str, int | str], escape: bool = True, ) -> tuple[int | None, str]: user_input = color.ColoredInput(end="").localize( localization_key, escape=escape, **perameters ) if user_input == "" and self.default is not None: return self.default, user_input try: user_input_i = int(user_input) except ValueError: return None, user_input return self.clamp_value(user_input_i), user_input def get_input_locale_while( self, dialog: str, perameters: dict[str, int | str], escape: bool = True ) -> int | None: while True: int_val, user_input = self.get_input(dialog, perameters, escape=escape) if int_val is not None: return int_val if user_input == core.core_data.local_manager.get_key("quit_key"): return None def get_input_locale( self, localization_key: str | None, perameters: dict[str, int | str] ) -> tuple[int | None, str]: if localization_key is None: if self.default is not None: perameters = { "min": self.min, "max": self.max, "default": self.default, } localization_key = "input_int_default" else: perameters = {"min": self.min, "max": self.max} localization_key = "input_int" return self.get_input(localization_key, perameters) def get_basic_input_locale(self, localization_key: str, perameters: dict[str, Any]): try: user_input = int( color.ColoredInput(end="").localize(localization_key, **perameters) ) except ValueError: return None return user_input class ListOutput: def __init__( self, strings: list[str], ints: list[int] | list[str], dialog: str | None = None, perameters: dict[str, Any] | None = None, start_index: int = 1, localize_elements: bool = True, ): self.strings = strings self.ints = ints self.dialog = dialog if perameters is None: perameters = {} self.perameters = perameters self.start_index = start_index self.localize_elements = localize_elements def get_output(self, dialog: str | None, strings: list[str]) -> str: end_string = "" if dialog is not None: end_string = core.core_data.local_manager.get_key(dialog, **self.perameters) end_string += "\n" for i, string in enumerate(strings): try: int_string = str(self.ints[i]) except IndexError: int_string = "" string = string.replace("{int}", int_string) end_string += f" <@s>{i + self.start_index}. <@t>{string}\n" end_string = end_string.strip("\n") return end_string def display(self, dialog: str | None, strings: list[str]) -> None: output = self.get_output(dialog, strings) color.ColoredText(output) def display_locale(self, remove_alias: bool = False) -> None: dialog = "" if self.dialog is not None: dialog = core.core_data.local_manager.get_key(self.dialog) new_strings: list[str] = [] for string in self.strings: if self.localize_elements: string_ = core.core_data.local_manager.get_key(string) else: string_ = string if remove_alias: string_ = core.core_data.local_manager.get_all_aliases(string_)[0] new_strings.append(string_) self.display(dialog, new_strings) def display_non_locale(self) -> None: self.display(self.dialog, self.strings) class ChoiceInput: def __init__( self, items: list[str], strings: list[str], ints: list[int] | list[str], perameters: dict[str, int | str], dialog: str, single_choice: bool = False, remove_alias: bool = False, display_all_at_once: bool = True, start_index: int = 1, localize_options: bool = True, ): self.items = items self.strings = strings self.ints = ints self.perameters = perameters self.dialog = dialog self.is_single_choice = single_choice self.remove_alias = remove_alias self.display_all_at_once = display_all_at_once self.start_index = start_index self.localize_options = localize_options @staticmethod def from_reduced( items: list[str], ints: list[int] | list[str] | None = None, perameters: dict[str, int | str] | None = None, dialog: str | None = None, single_choice: bool = False, remove_alias: bool = False, display_all_at_once: bool = True, start_index: int = 1, localize_options: bool = True, ) -> ChoiceInput: if perameters is None: perameters = {} if ints is None: ints = [] if dialog is None: dialog = "" return ChoiceInput( items.copy(), items.copy(), ints.copy(), perameters.copy(), dialog, single_choice, remove_alias, display_all_at_once, start_index, localize_options, ) def get_input(self) -> tuple[int | None, str]: if len(self.strings) == 0: return None, "" if len(self.strings) == 1: return self.get_min_value(), "" ListOutput( self.strings, self.ints, start_index=self.start_index, localize_elements=self.localize_options, ).display_locale(self.remove_alias) return IntInput( self.get_max_value(), self.get_min_value(), ensure_max=True ).get_input_locale(self.dialog, self.perameters) def get_input_while(self) -> int | None: if len(self.strings) == 0: return None while True: int_val, user_input = self.get_input() if int_val is not None: return int_val if user_input == core.core_data.local_manager.get_key("quit_key"): return None for i, string in enumerate(self.strings): if self.localize_options: string = core.core_data.local_manager.get_key(string) if string.lower().strip() == user_input.lower().strip(): return i + self.start_index def get_max_value(self) -> int: return len(self.strings) + self.start_index - 1 def get_min_value(self) -> int: return self.start_index def get_input_locale(self, localized: bool = True) -> tuple[list[int] | None, bool]: if len(self.strings) == 0: return [], False if len(self.strings) == 1: return [self.get_min_value()], False if not self.is_single_choice and self.display_all_at_once: if localized: self.strings.append("all_at_once") else: self.strings.append(core.core_data.local_manager.get_key("all_at_once")) if localized: ListOutput( self.strings, self.ints, start_index=self.start_index, localize_elements=self.localize_options, ).display_locale() else: ListOutput( self.strings, self.ints, start_index=self.start_index, localize_elements=self.localize_options, ).display_non_locale() key = "input_many" if self.is_single_choice: key = "input_single" dialog = core.core_data.local_manager.get_key(key).format( min=self.get_min_value(), max=self.get_max_value() ) usr_input = color.ColoredInput().get(dialog).strip().split(" ") int_vals: list[int] = [] for inp in usr_input: try: value = int(inp) if value > self.get_max_value() or value < self.get_min_value(): raise ValueError int_vals.append(value) except ValueError: if inp == core.core_data.local_manager.get_key("quit_key"): return None, False cont = False for i, string in enumerate(self.strings): if self.localize_options: string = core.core_data.local_manager.get_key(string) if string.lower().strip() == inp.lower().strip(): int_vals.append(i + self.start_index) cont = True break if cont: continue color.ColoredText.localize( "invalid_input_int", min=self.get_min_value(), max=self.get_max_value(), ) if ( self.get_max_value() in int_vals and not self.is_single_choice and self.display_all_at_once ): return list(range(self.get_min_value(), self.get_max_value())), True if self.is_single_choice and len(int_vals) > 1: int_vals = [int_vals[0]] return int_vals, False def get_input_locale_while(self) -> list[int] | None: if len(self.strings) == 0: return [] if len(self.strings) == 1: return [self.get_min_value()] while True: int_vals, all_at_once = self.get_input_locale() if int_vals is None: return None if all_at_once: return int_vals if len(int_vals) == 0: continue if len(int_vals) == 1 and int_vals[0] == 0: return [] return int_vals def multiple_choice( self, localized_options: bool = True ) -> tuple[list[int] | None, bool]: color.ColoredText.localize(self.dialog, True, **self.perameters) user_input, all_at_once = self.get_input_locale(localized_options) if user_input is None: return None, all_at_once return [i - self.start_index for i in user_input], all_at_once def single_choice(self) -> int | None: return self.get_input_while() def get(self) -> tuple[int | None | list[int], bool]: if self.is_single_choice: return self.single_choice(), False return self.multiple_choice() class MultiEditor: def __init__( self, group_name: str, items: list[str], strings: list[str], ints: list[int] | None, max_values: list[int] | int | None, perameters: dict[str, int | str] | None, dialog: str, single_choice: bool = False, signed: bool = True, group_name_localized: bool = False, cumulative_max: bool = False, bit_count: int = 32, ): self.items = items self.strings = strings self.ints = ints self.bit_count = bit_count if self.ints is not None: total_ints = len(self.ints) else: total_ints = len(self.strings) if max_values is None: max_values_ = [None] * total_ints elif isinstance(max_values, int): max_values_ = [max_values] * total_ints else: max_values_ = max_values self.max_values = max_values_ if perameters is None: perameters = {} self.perameters = perameters if group_name_localized: self.perameters["group_name"] = core.core_data.local_manager.get_key( group_name ) else: self.perameters["group_name"] = group_name self.dialog = dialog self.is_single_choice = single_choice self.signed = signed self.cumulative_max = cumulative_max @staticmethod def from_reduced( group_name: str, items: list[str], ints: list[int] | None, max_values: list[int] | int | None, group_name_localized: bool = False, dialog: str = "input", cumulative_max: bool = False, items_localized: bool = False, ): if items_localized: for i, item in enumerate(items): items[i] = core.core_data.local_manager.get_key(item) text: list[str] = [] for item_name in items: if ints is not None: text.append(f"{item_name} <@q>: {{int}}") else: text.append(f"{item_name}") return MultiEditor( group_name, items, text, ints, max_values, None, dialog, group_name_localized=group_name_localized, cumulative_max=cumulative_max, ) def edit(self) -> list[int]: choices, all_at_once = ChoiceInput( self.items, self.strings, self.ints or [], # type: ignore self.perameters, "select_edit", ).get() if choices is None: return self.ints or [] if isinstance(choices, int): choices = [choices] if all_at_once: return self.edit_all(choices) return self.edit_one(choices) def edit_all(self, choices: list[int]) -> list[int]: max_max_value = 0 for choice in choices: if choice >= len(self.max_values): max_value = None else: max_value = self.max_values[choice] if max_value is None: max_value = IntInput.get_max_value( max_value, self.signed, self.bit_count ) max_max_value = max(max_max_value, max_value) if self.cumulative_max: max_max_value = max_max_value // len(choices) usr_input = IntInput(max_max_value, default=None).get_input_locale_while( self.dialog + "_all", { "name": self.perameters["group_name"], "max": max_max_value, }, ) if usr_input is None: return self.ints or [] ints = self.ints or [0] * len(self.strings) for choice in choices: if choice >= len(self.max_values): max_value = None else: max_value = self.max_values[choice] max_value = IntInput.get_max_value(max_value, self.signed, self.bit_count) value = min(usr_input, max_value) ints[choice] = value if self.ints is not None: color.ColoredText.localize( "value_changed", name=self.items[choice], value=value, ) return ints def edit_one(self, choices: list[int]) -> list[int]: ints = self.ints or [0] * len(self.strings) for choice in choices: if choice >= len(self.max_values): max_value = None else: max_value = self.max_values[choice] if max_value is None: max_value = IntInput.get_max_value( max_value, self.signed, self.bit_count ) if self.cumulative_max: max_value -= sum(ints) - ints[choice] max_value = max(max_value, 0) item = self.items[choice] usr_input = IntInput( max_value, default=ints[choice] ).get_input_locale_while( self.dialog, {"name": item, "value": ints[choice], "max": max_value}, escape=False, ) if usr_input is None: continue ints[choice] = usr_input color.ColoredText.localize( "value_changed", name=item, value=ints[choice], escape=False ) return ints class SingleEditor: def __init__( self, item: str, value: int, max_value: int | None = None, min_value: int = 0, signed: bool = True, localized_item: bool = False, remove_alias: bool = False, bit_count: int = 32, ): if localized_item: item = core.core_data.local_manager.get_key(item) if remove_alias: item = core.core_data.local_manager.get_all_aliases(item)[0] self.item = item self.value = value self.max_value = max_value self.min_value = min_value self.signed = signed self.bit_count = bit_count def edit(self, escape_text: bool = True) -> int: max_value = IntInput.get_max_value(self.max_value, self.signed, self.bit_count) if self.max_value is None: dialog = "input_non_max" elif self.min_value != 0: dialog = "input_min" else: dialog = "input" usr_input = IntInput( max_value, self.min_value, default=self.value, signed=self.signed, bit_count=self.bit_count, ).get_input_locale_while( dialog, { "name": self.item, "value": self.value, "max": max_value, "min": self.min_value, }, escape=escape_text, ) if usr_input is None: return self.value print() color.ColoredText.localize( "value_changed", name=self.item, value=usr_input, escape=escape_text ) return usr_input class StringInput: def __init__(self, default: str = ""): self.default = default def get_input_locale_while( self, key: str, perameters: dict[str, Any] ) -> str | None: while True: usr_input = self.get_input_locale(key, perameters) if usr_input is None: return None if usr_input == "": return self.default if usr_input == " ": continue return usr_input def get_input_locale( self, key: str, perameters: dict[str, Any] | None = None, escape: bool = True, ) -> str | None: if perameters is None: perameters = {} usr_input = color.ColoredInput().localize(key, escape, **perameters) quit_key = core.core_data.local_manager.get_key("quit_key") if usr_input == "" or usr_input == quit_key: return None if usr_input == f"\\{quit_key}": return quit_key return usr_input class StringEditor: def __init__(self, item: str, value: str, item_localized: bool = False): if item_localized: item = core.core_data.local_manager.get_key(item) self.item = item self.value = value def edit(self) -> str: usr_input = StringInput(default=self.value).get_input_locale_while( "input_non_max", {"name": self.item, "value": self.value}, ) if usr_input is None: return self.value color.ColoredText.localize( "value_changed", name=self.item, value=usr_input, ) return usr_input class YesNoInput: def __init__(self, default: bool = False): self.default = default def get_input_once( self, key: str, perameters: dict[str, Any] | None = None ) -> bool | None: if perameters is None: perameters = {} usr_input = color.ColoredInput().localize(key, **perameters) if usr_input == "": return self.default if usr_input == core.core_data.local_manager.get_key("quit_key"): return None return ( usr_input == core.core_data.local_manager.get_key("yes_key") or usr_input.lower().strip() == core.core_data.local_manager.get_key("yes").lower().strip() ) class DialogBuilder: def __init__(self, dialog_structure: dict[Any, Any]): self.dialog_structure = dialog_structure ================================================ FILE: src/bcsfe/cli/edits/__init__.py ================================================ from bcsfe.cli.edits import ( basic_items, cat_editor, clear_tutorial, rare_ticket_trade, fixes, enemy_editor, aku_realm, map, event_tickets, max_all, storage, ) __all__ = [ "basic_items", "cat_editor", "clear_tutorial", "rare_ticket_trade", "fixes", "enemy_editor", "aku_realm", "map", "event_tickets", "max_all", "storage", ] ================================================ FILE: src/bcsfe/cli/edits/aku_realm.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color def unlock_aku_realm(save_file: core.SaveFile): stage_ids = [255, 256, 257, 258, 265, 266, 268] for stage_id in stage_ids: save_file.event_stages.clear_map(1, stage_id, 0, False) color.ColoredText.localize("aku_realm_unlocked") ================================================ FILE: src/bcsfe/cli/edits/basic_items.py ================================================ from __future__ import annotations import random from bcsfe import core from bcsfe.cli import dialog_creator, color, edits from bcsfe.core.game.catbase.gatya_item import GatyaItemCategory class BasicItems: @staticmethod def get_name(name: str | None, key: str) -> str: if name is None: return core.core_data.local_manager.get_key(key) return name.strip() @staticmethod def reset_golden_cat_cpus(save_file: core.SaveFile): save_file.golden_cpu_count = 0 color.ColoredText.localize("reset_golden_cat_cpus_success") @staticmethod def edit_catfood(save_file: core.SaveFile): should_exit = not dialog_creator.YesNoInput().get_input_once("catfood_warning") if should_exit: return name = core.core_data.get_gatya_item_names(save_file).get_name(22) original_amount = save_file.catfood save_file.catfood = dialog_creator.SingleEditor( BasicItems.get_name(name, "catfood"), save_file.catfood, core.core_data.max_value_manager.get("catfood"), ).edit() change = save_file.catfood - original_amount core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change(change, core.ManagedItemType.CATFOOD) ) @staticmethod def edit_xp(save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(6) save_file.xp = dialog_creator.SingleEditor( BasicItems.get_name(name, "xp"), save_file.xp, core.core_data.max_value_manager.get("xp"), ).edit() @staticmethod def edit_normal_tickets(save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(20) save_file.normal_tickets = dialog_creator.SingleEditor( BasicItems.get_name(name, "normal_tickets"), save_file.normal_tickets, core.core_data.max_value_manager.get("normal_tickets"), ).edit() @staticmethod def edit_100_million_ticket(save_file: core.SaveFile): color.ColoredText.localize("100_million_warn") name = core.core_data.get_gatya_item_names(save_file).get_name(212) save_file.hundred_million_ticket = dialog_creator.SingleEditor( BasicItems.get_name(name, "100_million_tickets"), save_file.hundred_million_ticket, core.core_data.max_value_manager.get("100_million_tickets"), ).edit() @staticmethod def get_bannable_feature_options(feature_name: str, safe_feature_name: str) -> int: feature_name = core.core_data.local_manager.get_key(feature_name) safe_feature_name = core.core_data.local_manager.get_key(safe_feature_name) options = [ core.core_data.local_manager.get_key( "continue_editing", feature_name=feature_name ), core.core_data.local_manager.get_key( "go_to_safe_feature", safer_feature_name=safe_feature_name ), core.core_data.local_manager.get_key( "cancel_editing", feature_name=feature_name ), ] option = dialog_creator.ChoiceInput( options, options, [], {"feature_name": feature_name}, "select_an_option_to_continue", ).single_choice() if option is None: return 2 option -= 1 return option @staticmethod def edit_rare_tickets(save_file: core.SaveFile): color.ColoredText.localize("rare_ticket_warning") name = core.core_data.get_gatya_item_names(save_file).get_name(21) option = BasicItems.get_bannable_feature_options( "rare_tickets_l", "rare_ticket_trade_l" ) if option == 2: return if option == 1: return edits.rare_ticket_trade.RareTicketTrade.rare_ticket_trade(save_file) original_amount = save_file.rare_tickets save_file.rare_tickets = dialog_creator.SingleEditor( BasicItems.get_name(name, "rare_tickets"), save_file.rare_tickets, core.core_data.max_value_manager.get("rare_tickets"), ).edit() change = save_file.rare_tickets - original_amount core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change(change, core.ManagedItemType.RARE_TICKET) ) @staticmethod def edit_platinum_tickets(save_file: core.SaveFile): color.ColoredText.localize("platinum_ticket_warning") name = core.core_data.get_gatya_item_names(save_file).get_name(29) option = BasicItems.get_bannable_feature_options( "platinum_tickets_l", "platinum_shards_l" ) if option == 2: return if option == 1: return edits.basic_items.BasicItems.edit_platinum_shards(save_file) original_amount = save_file.platinum_tickets save_file.platinum_tickets = dialog_creator.SingleEditor( BasicItems.get_name(name, "platinum_tickets"), save_file.platinum_tickets, core.core_data.max_value_manager.get("platinum_tickets"), ).edit() change = save_file.platinum_tickets - original_amount core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change(change, core.ManagedItemType.PLATINUM_TICKET) ) @staticmethod def edit_legend_tickets(save_file: core.SaveFile): should_exit = not dialog_creator.YesNoInput().get_input_once( "legend_ticket_warning" ) if should_exit: return name = core.core_data.get_gatya_item_names(save_file).get_name(145) original_amount = save_file.legend_tickets save_file.legend_tickets = dialog_creator.SingleEditor( BasicItems.get_name(name, "legend_tickets"), save_file.legend_tickets, core.core_data.max_value_manager.get("legend_tickets"), ).edit() change = save_file.legend_tickets - original_amount core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change(change, core.ManagedItemType.LEGEND_TICKET) ) @staticmethod def edit_platinum_shards(save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(157) platinum_ticket_amount = save_file.platinum_tickets max_value = ( core.core_data.max_value_manager.get("platinum_tickets") - platinum_ticket_amount ) * 10 + 9 max_value = max(0, max_value) save_file.platinum_shards = dialog_creator.SingleEditor( BasicItems.get_name(name, "platinum_shards"), save_file.platinum_shards, max_value, ).edit() @staticmethod def edit_np(save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(7) save_file.np = dialog_creator.SingleEditor( BasicItems.get_name(name, "np"), save_file.np, core.core_data.max_value_manager.get("np"), ).edit() @staticmethod def edit_leadership(save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(105) save_file.leadership = dialog_creator.SingleEditor( BasicItems.get_name(name, "leadership"), save_file.leadership, core.core_data.max_value_manager.get("leadership"), ).edit() @staticmethod def edit_battle_items(save_file: core.SaveFile): save_file.battle_items.edit(save_file) @staticmethod def edit_battle_items_endless(save_file: core.SaveFile): save_file.battle_items.edit_endless_items(save_file) @staticmethod def edit_catamins(save_file: core.SaveFile): names_o = core.core_data.get_gatya_item_names(save_file) items = core.core_data.get_gatya_item_buy(save_file).get_by_category(6) if items is None: return names: list[str] = [] for item in items: name = names_o.get_name(item.id) if name is None: name = core.core_data.local_manager.get_key( "unknown_catamin_name", id=item.id ) names.append(name) values = dialog_creator.MultiEditor.from_reduced( "catamins", names, save_file.catamins, core.core_data.max_value_manager.get("catamins"), group_name_localized=True, ).edit() save_file.catamins = values @staticmethod def edit_catseyes(save_file: core.SaveFile): names_o = core.core_data.get_gatya_item_names(save_file) items = core.core_data.get_gatya_item_buy(save_file).get_by_category(5) if items is None: return names: list[str] = [] for item in items: name = names_o.get_name(item.id) if name is None: name = core.core_data.local_manager.get_key( "unknown_catseye_name", id=item.id ) names.append(name) values = dialog_creator.MultiEditor.from_reduced( "catseyes", names, save_file.catseyes, core.core_data.max_value_manager.get("catseyes"), group_name_localized=True, ).edit() save_file.catseyes = values @staticmethod def edit_treasure_chests(save_file: core.SaveFile): names_o = core.core_data.get_gatya_item_names(save_file) items = core.core_data.get_gatya_item_buy(save_file).get_by_category( GatyaItemCategory.TREASURE_CHESTS ) if items is None: return names: list[str] = [] for item in items[: len(save_file.treasure_chests)]: name = names_o.get_name(item.id) if name is None: name = core.core_data.local_manager.get_key( "unknown_treasure_chest_name", id=item.id ) names.append(name) values = dialog_creator.MultiEditor.from_reduced( "treasure_chests", names, save_file.treasure_chests, core.core_data.max_value_manager.get("treasure_chests"), group_name_localized=True, ).edit() save_file.treasure_chests = values @staticmethod def edit_catfruit(save_file: core.SaveFile): names = core.Matatabi(save_file).get_names() if names is None: return new_names: list[str] = [] for i, name in enumerate(names): if name is None: name = core.core_data.local_manager.get_key( "unknown_catfruit_name", id=i ) new_names.append(name) names = new_names extra = len(save_file.catfruit) - len(names) if extra > 0: for i in range(extra): names.append( core.core_data.local_manager.get_key( "unknown_catfruit_name", id=i + len(names) ) ) if save_file.game_version < 110400: max_value = core.core_data.max_value_manager.get_old("catfruit") cumulative_max = True else: max_value = core.core_data.max_value_manager.get_new("catfruit") cumulative_max = False names = names[: len(save_file.catfruit)] values = dialog_creator.MultiEditor.from_reduced( "catfruit", names, save_file.catfruit, max_value, group_name_localized=True, cumulative_max=cumulative_max, ).edit() save_file.catfruit = values @staticmethod def set_restart_pack(save_file: core.SaveFile): save_file.restart_pack = 1 name = core.core_data.get_gatya_item_names(save_file).get_name(123) color.ColoredText.localize("value_gave", name=name) @staticmethod def edit_inquiry_code(save_file: core.SaveFile): should_exit = not dialog_creator.YesNoInput().get_input_once( "inquiry_code_warning" ) if should_exit: return item_name = save_file.get_localizable().get("autoSave_txt5") save_file.inquiry_code = dialog_creator.StringEditor( BasicItems.get_name(item_name, "inquiry_code"), save_file.inquiry_code, ).edit() @staticmethod def edit_password_refresh_token(save_file: core.SaveFile): should_exit = not dialog_creator.YesNoInput().get_input_once( "password_refresh_token_warning" ) if should_exit: return save_file.password_refresh_token = dialog_creator.StringEditor( "password_refresh_token", save_file.password_refresh_token, item_localized=True, ).edit() @staticmethod def edit_scheme_items(save_file: core.SaveFile): save_file.scheme_items.edit(save_file) @staticmethod def edit_engineers(save_file: core.SaveFile): save_file.ototo.edit_engineers(save_file) @staticmethod def edit_base_materials(save_file: core.SaveFile): save_file.ototo.base_materials.edit_base_materials(save_file) @staticmethod def edit_rare_gatya_seed(save_file: core.SaveFile): save_file.gatya.edit_rare_gatya_seed() @staticmethod def edit_normal_gatya_seed(save_file: core.SaveFile): save_file.gatya.edit_normal_gatya_seed() @staticmethod def edit_event_gatya_seed(save_file: core.SaveFile): save_file.gatya.edit_event_gatya_seed() @staticmethod def edit_unlocked_slots(save_file: core.SaveFile): save_file.lineups.edit_unlocked_slots() @staticmethod def edit_labyrinth_medals(save_file: core.SaveFile): names_o = core.core_data.get_gatya_item_names(save_file) items = core.core_data.get_gatya_item_buy(save_file).get_by_category(11) if items is None: return names: list[str] = [] for item in items: name = names_o.get_name(item.id) if name is None: name = core.core_data.local_manager.get_key( "unknown_labyrinth_medal_name", id=item.id ) names.append(name) values = dialog_creator.MultiEditor.from_reduced( "labyrinth_medals", names, save_file.labyrinth_medals, core.core_data.max_value_manager.get("labyrinth_medals"), group_name_localized=True, ).edit() save_file.labyrinth_medals = values @staticmethod def edit_special_skills(save_file: core.SaveFile): save_file.special_skills.edit(save_file) @staticmethod def unlock_equip_menu(save_file: core.SaveFile): save_file.unlock_equip_menu() color.ColoredText.localize("equip_menu_unlocked") @staticmethod def allow_filibuster_stage_reclearing(save_file: core.SaveFile): save_file.filibuster_stage_enabled = True save_file.filibuster_stage_id = random.randint(0, 47) color.ColoredText.localize("filibuster_stage_reclearing_allowed") ================================================ FILE: src/bcsfe/cli/edits/cat_editor.py ================================================ from __future__ import annotations import enum from typing import Any, Callable from bcsfe import core from bcsfe.cli import color, dialog_creator class SelectMode(enum.Enum): AND = 0 OR = 1 REPLACE = 2 class CatEditor: def __init__(self, save_file: core.SaveFile): self.save_file = save_file def get_current_cats(self): return self.save_file.cats.get_unlocked_cats() def get_non_unlocked_cats(self): return self.save_file.cats.get_non_unlocked_cats() def get_non_gacha_cats(self): return self.save_file.cats.get_non_gacha_cats(self.save_file) def filter_cats(self, cats: list[core.Cat]) -> list[core.Cat]: unlocked_cats = self.get_current_cats() return [cat for cat in cats if cat in unlocked_cats] def get_cats_rarity(self, rarity: int) -> list[core.Cat]: return self.save_file.cats.get_cats_rarity(self.save_file, rarity) def get_cats_name(self, name: str) -> list[core.Cat]: return self.save_file.cats.get_cats_name(self.save_file, name) def get_cats_obtainable(self) -> list[core.Cat] | None: return self.save_file.cats.get_cats_obtainable(self.save_file) def get_cats_unobtainable(self) -> list[core.Cat] | None: return self.save_file.cats.get_cats_non_obtainable(self.save_file) def get_cats_gatya_banner(self, gatya_id: int) -> list[core.Cat] | None: return self.save_file.cats.get_cats_gatya_banner(self.save_file, gatya_id) def print_selected_cats(self, current_cats: list[core.Cat]): if len(current_cats) > 50: color.ColoredText.localize("total_selected_cats", total=len(current_cats)) else: for cat in current_cats: names = cat.get_names_cls(self.save_file) if not names: names = [str(cat.id)] color.ColoredText.localize("selected_cat", id=cat.id, name=names[0]) def select( self, current_cats: list[core.Cat] | None = None, finish_option: bool = True ) -> tuple[list[core.Cat], bool]: if current_cats is None: current_cats = [] options: dict[str, Callable[[], Any]] = { "select_cats_all": self.save_file.cats.get_all_cats, "select_cats_current": self.get_current_cats, "select_cats_obtainable": self.get_cats_obtainable, "select_cats_id": self.select_id, "select_cats_name": self.select_name, "select_cats_rarity": self.select_rarity, "select_cats_gatya_banner": self.select_gatya_banner, "select_cats_not_unlocked": self.get_non_unlocked_cats, "select_cats_not_obtainable": self.get_cats_unobtainable, "select_cats_non_gatya": self.get_non_gacha_cats, "select_cats_game_version": self.select_cats_game_version, } if finish_option: options["finish"] = lambda: None option_id = dialog_creator.ChoiceInput( list(options), list(options), [], {}, "select_cats", True ).single_choice() if option_id is None: return current_cats, False option_id -= 1 if option_id == len(options) - 1 and finish_option: return current_cats, True func = options[list(options)[option_id]] new_cats = func() if new_cats is None: return current_cats, False if current_cats: mode_id = dialog_creator.IntInput().get_basic_input_locale("and_mode_q", {}) if mode_id is None: mode = SelectMode.OR elif mode_id == 1: mode = SelectMode.AND elif mode_id == 2: mode = SelectMode.OR elif mode_id == 3: mode = SelectMode.REPLACE else: mode = SelectMode.OR else: mode = SelectMode.OR if mode == SelectMode.AND: return list(set(current_cats) & set(new_cats)), False if mode == SelectMode.OR: return list(set(current_cats) | set(new_cats)), False if mode == SelectMode.REPLACE: return new_cats, False return new_cats, False def select_id(self) -> list[core.Cat] | None: cat_ids = dialog_creator.RangeInput( len(self.save_file.cats.cats) - 1 ).get_input_locale("enter_cat_ids", {}) if cat_ids is None: return None return self.save_file.cats.get_cats_by_ids(cat_ids) def select_cats_game_version(self) -> list[core.Cat] | None: unitbuy = core.UnitBuy(self.save_file) if unitbuy.unit_buy is None: return None versions_set: set[int] = set() for cat in unitbuy.unit_buy: if cat.game_version == -1: continue versions_set.add(cat.game_version) if not versions_set: return None versions = list(versions_set) versions.sort() color.ColoredText.localize("possible_gvs") cur_major_v = -1 for version in versions: gv = core.GameVersion(version) major_v = gv.get_parts()[0] if major_v != cur_major_v: if cur_major_v != -1: print() cur_major_v = major_v else: color.ColoredText(", ", end="") color.ColoredText(f"<@t>{gv.format()}", end="") print() usr_input = dialog_creator.StringInput().get_input_locale("select_gv") if usr_input is None: return None chunks = usr_input.split(" ") versions_selected: list[int] = [] for chunk in chunks: parts = chunk.split("-") if len(parts) == 2: min = parts[0] max = parts[1] v1 = core.GameVersion.from_string(min) v2 = core.GameVersion.from_string(max) for v in range(v1.game_version, v2.game_version + 1): versions_selected.append(v) else: v = core.GameVersion.from_string(chunk) versions_selected.append(v.game_version) valid_versions: set[int] = set() for version in versions_selected: if version in versions_set: valid_versions.add(version) if not valid_versions: color.ColoredText.localize("no_valid_gvs_entered") cats: list[core.Cat] = [] for cat in self.save_file.cats.cats: row = unitbuy.get_unit_buy(cat.id) if row is None: continue if row.game_version in valid_versions: cats.append(cat) return cats def select_rarity(self) -> list[core.Cat] | None: rarity_names = self.save_file.cats.get_rarity_names(self.save_file) rarity_ids, _ = dialog_creator.ChoiceInput( rarity_names, rarity_names, [], {}, "select_rarity" ).multiple_choice() if rarity_ids is None: return None cats: list[core.Cat] = [] for rarity_id in rarity_ids: rarity_cats = self.get_cats_rarity(rarity_id) cats = list(set(cats + rarity_cats)) return cats def select_name(self) -> list[core.Cat] | None: usr_name = dialog_creator.StringInput().get_input_locale("enter_name", {}) if usr_name is None: return [] cats = self.get_cats_name(usr_name) if not cats: color.ColoredText.localize("no_cats_found_name", name=usr_name) return None cat_names: list[str] = [] cat_list: list[core.Cat] = [] for cat in cats: names = cat.get_names_cls(self.save_file) if not names: names = [str(cat.id)] for name in names: if usr_name.lower() in name.lower(): cat_names.append(name) cat_list.append(cat) break if len(cat_names) == 1: color.ColoredText(f"<@t>{cat_names[0]}") cat_option_ids, _ = dialog_creator.ChoiceInput( cat_names, cat_names, [], {}, "select_name" ).multiple_choice() if cat_option_ids is None: return None cats_selected: list[core.Cat] = [] for cat_option_id in cat_option_ids: cats_selected.append(cat_list[cat_option_id]) return cats_selected def select_obtainable(self) -> list[core.Cat] | None: return self.get_cats_obtainable() def select_gatya_banner_name(self) -> list[int] | None: filter_down = dialog_creator.YesNoInput().get_input_once("filter_down_q_gatya") if filter_down is None: return None all_names = core.GatyaInfos(self.save_file).get_all_names() ids = list(all_names.keys()) ids.sort() names: list[str] = [] for id in ids: names.append(all_names[id]) new_names: list[str] = [] new_ids: list[int] = [] unknown_name = core.core_data.local_manager.get_key("unknown_banner") if filter_down: ids.reverse() for id in ids: name = all_names[id] if name in new_names or name == unknown_name: continue new_names.append(name) new_ids.append(id) new_ids.reverse() new_names.reverse() else: new_names = names new_ids = ids ids = new_ids formatted_names: list[str] = [] for name in new_names: formatted_name = core.core_data.local_manager.get_key( "banner_txt", name=name ) formatted_names.append(formatted_name) gatya_option_ids, _ = dialog_creator.ChoiceInput.from_reduced( formatted_names, ints=ids, dialog="select_gatya_banner", start_index=0, ).multiple_choice(False) if gatya_option_ids is None: return None gatya_ids: list[int] = [] for gatya_option_id in gatya_option_ids: gatya_ids.append(ids[gatya_option_id]) return gatya_ids def select_gatya_banner(self) -> list[core.Cat] | None: gset = self.save_file.gatya.read_gatya_data_set(self.save_file).gatya_data_set if gset is None: return None by_id = dialog_creator.ChoiceInput.from_reduced( ["by_id", "by_name"], dialog="gatya_by_id_q" ).single_choice() if by_id is None: return None if by_id == 1: gatya_ids = dialog_creator.RangeInput(len(gset) - 1).get_input_locale( "select_gatya_banner", {} ) else: gatya_ids = self.select_gatya_banner_name() if gatya_ids is None: return None cats: list[core.Cat] = [] for gatya_id in gatya_ids: gatya_cats = self.get_cats_gatya_banner(gatya_id) if gatya_cats is None: continue cats = list(set(cats + gatya_cats)) return cats def unlock_cats(self, cats: list[core.Cat]): cats = self.get_save_cats(cats) for cat in cats: cat.unlock(self.save_file) color.ColoredText.localize("unlock_success") def remove_cats(self, cats: list[core.Cat]): reset = core.core_data.config.get_bool(core.ConfigKey.RESET_CAT_DATA) cats = self.get_save_cats(cats) for cat in cats: cat.remove(reset=reset, save_file=self.save_file) color.ColoredText.localize("remove_success") def get_save_cats(self, cats: list[core.Cat]): ct_cats: list[core.Cat] = [] for cat in cats: ct = self.save_file.cats.get_cat_by_id(cat.id) if ct is None: continue ct_cats.append(ct) return ct_cats def true_form_cats(self, cats: list[core.Cat], force: bool = False): cats = self.get_save_cats(cats) set_current_forms = core.core_data.config.get_bool( core.ConfigKey.SET_CAT_CURRENT_FORMS ) self.save_file.cats.true_form_cats( self.save_file, cats, force, set_current_forms ) color.ColoredText.localize("true_form_success") def fourth_form_cats(self, cats: list[core.Cat], force: bool = False): cats = self.get_save_cats(cats) set_current_forms = core.core_data.config.get_bool( core.ConfigKey.SET_CAT_CURRENT_FORMS ) self.save_file.cats.fourth_form_cats( self.save_file, cats, force, set_current_forms ) color.ColoredText.localize("fourth_form_success") def remove_true_form_cats(self, cats: list[core.Cat]): cats = self.get_save_cats(cats) for cat in cats: cat.remove_true_form() color.ColoredText.localize("remove_true_form_success") def remove_fourth_form_cats(self, cats: list[core.Cat]): cats = self.get_save_cats(cats) for cat in cats: cat.remove_fourth_form() color.ColoredText.localize("remove_fourth_form_success") def upgrade_cats(self, cats: list[core.Cat]): cats = self.get_save_cats(cats) if not cats: return if len(cats) == 1: option_id = 0 else: options: list[str] = [ "upgrade_individual", "upgrade_all", ] option_id = dialog_creator.ChoiceInput( options, options, [], {}, "upgrade_cats_select_mod", True ).single_choice() if option_id is None: return option_id -= 1 success = False if option_id == 0: for cat in cats: names = cat.get_names_cls(self.save_file) if not names: names = [str(cat.id)] color.ColoredText.localize( "selected_cat_upgrades", name=names[0], id=cat.id, base_level=cat.upgrade.base + 1, plus_level=cat.upgrade.plus, ) power_up = core.PowerUpHelper(cat, self.save_file) upgrade, should_exit = core.Upgrade.get_user_upgrade( power_up.get_max_possible_base() - 1, power_up.get_max_possible_plus(), ) if should_exit: return if upgrade is not None: power_up.reset_upgrade() power_up.upgrade_by(upgrade.base) cat.set_upgrade(self.save_file, upgrade, True) color.ColoredText.localize( "selected_cat_upgraded", name=names[0], id=cat.id, base_level=cat.upgrade.base + 1, plus_level=cat.upgrade.plus, ) success = True else: power_up = core.PowerUpHelper(cats[0], self.save_file) upgrade, should_exit = core.Upgrade.get_user_upgrade( power_up.get_max_max_base_upgrade_level() - 1, power_up.get_max_max_plus_upgrade_level(), ) if upgrade is None or should_exit: return success = True for cat in cats: power_up = core.PowerUpHelper(cat, self.save_file) power_up.reset_upgrade() power_up.upgrade_by(upgrade.base) cat.set_upgrade(self.save_file, upgrade, True) if success: color.ColoredText.localize("upgrade_success") def remove_talents_cats(self, cats: list[core.Cat]): for cat in cats: if cat.talents is None: continue for talent in cat.talents: talent.level = 0 color.ColoredText.localize("talents_remove_success") def unlock_cat_guide(self, cats: list[core.Cat]): for cat in cats: if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): cat.unlock(self.save_file) cat.catguide_collected = True color.ColoredText.localize("unlock_cat_guide_success") def remove_cat_guide(self, cats: list[core.Cat]): for cat in cats: cat.catguide_collected = False color.ColoredText.localize("remove_cat_guide_success") def upgrade_talents_cats(self, cats: list[core.Cat]): cats = self.get_save_cats(cats) if not cats: return gdg = core.core_data.get_game_data_getter(self.save_file) is_good_version = gdg.does_save_version_match(self.save_file) if not is_good_version: data_version = gdg.version if data_version is None: color.ColoredText.localize("no_data_version") return color.ColoredText.localize( "talents_version_warning", save_version=self.save_file.game_version.to_string(), data_version=data_version, ) should_stay = dialog_creator.YesNoInput().get_input_once("continue_q") if not should_stay: return if len(cats) == 1: option_id = 0 else: options: list[str] = [ "talents_individual", "talents_all", ] option_id = dialog_creator.ChoiceInput( options, options, [], {}, "upgrade_talents_select_mod", True ).single_choice() if option_id is None: return option_id -= 1 talent_data = self.save_file.cats.read_talent_data(self.save_file) if talent_data is None: return if option_id == 0: for cat in cats: if cat.talents is None: continue names = cat.get_names_cls(self.save_file) if not names: names = [str(cat.id)] color.ColoredText.localize( "selected_cat", name=names[0], id=cat.id, ) data = talent_data.get_cat_talents(cat) if data is None: color.ColoredText.localize("no_talent_data", id=cat.id) continue if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): cat.unlock(self.save_file) talent_names, max_levels, current_levels, ids = data values = dialog_creator.MultiEditor.from_reduced( "talents", talent_names, current_levels, max_levels, group_name_localized=True, ).edit() current_levels = values for i, id in enumerate(ids): talent = cat.get_talent_from_id(id) if talent is None: continue talent.level = current_levels[i] else: for cat in cats: if cat.talents is None: continue data = talent_data.get_cat_talents(cat) if data is None: continue if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): cat.unlock(self.save_file) talent_names, max_levels, current_levels, ids = data for i, id in enumerate(ids): talent = cat.get_talent_from_id(id) if talent is None: continue talent.level = max_levels[i] color.ColoredText.localize("talents_success") @staticmethod def edit_cats(save_file: core.SaveFile): cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return while True: should_exit, current_cats = cat_editor.run_edit_cats(current_cats) if should_exit: break @staticmethod def unlock_remove_cats_run( save_file: core.SaveFile, current_cats: list[core.Cat] | None = None, cat_editor: CatEditor | None = None, ): if cat_editor is None or current_cats is None: cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return choice = dialog_creator.ChoiceInput( ["unlock_cats", "remove_cats"], ["unlock_cats", "remove_cats"], [], {}, "unlock_remove_q", True, remove_alias=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: cat_editor.unlock_cats(current_cats) elif choice == 1: cat_editor.remove_cats(current_cats) CatEditor.set_rank_up_sale(save_file) @staticmethod def true_form_remove_form_cats_run( save_file: core.SaveFile, current_cats: list[core.Cat] | None = None, cat_editor: CatEditor | None = None, ): if cat_editor is None or current_cats is None: cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return choice = dialog_creator.ChoiceInput.from_reduced( ["true_form_cats", "remove_true_form_cats"], dialog="true_form_remove_form_q", single_choice=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: cat_editor.true_form_cats(current_cats) elif choice == 1: cat_editor.remove_true_form_cats(current_cats) @staticmethod def fourth_form_remove_form_cats_run( save_file: core.SaveFile, current_cats: list[core.Cat] | None = None, cat_editor: CatEditor | None = None, ): if cat_editor is None or current_cats is None: cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return choice = dialog_creator.ChoiceInput.from_reduced( ["fourth_form_cats", "remove_fourth_form_cats"], dialog="fourth_form_remove_form_q", single_choice=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: cat_editor.fourth_form_cats(current_cats) elif choice == 1: cat_editor.remove_fourth_form_cats(current_cats) @staticmethod def force_true_form_cats_run(save_file: core.SaveFile): color.ColoredText.localize("force_true_form_cats_warning") cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return cat_editor.true_form_cats(current_cats, force=True) @staticmethod def force_fourth_form_cats_run(save_file: core.SaveFile): color.ColoredText.localize("force_fourth_form_cats_warning") cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return cat_editor.fourth_form_cats(current_cats, force=True) @staticmethod def upgrade_cats_run(save_file: core.SaveFile): cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return cat_editor.upgrade_cats(current_cats) CatEditor.set_rank_up_sale(save_file) @staticmethod def upgrade_talents_remove_talents_cats_run( save_file: core.SaveFile, current_cats: list[core.Cat] | None = None, cat_editor: CatEditor | None = None, ): if cat_editor is None or current_cats is None: cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return choice = dialog_creator.ChoiceInput( ["upgrade_talents_cats", "remove_talents_cats"], ["upgrade_talents_cats", "remove_talents_cats"], [], {}, "upgrade_talents_remove_talents_q", True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: cat_editor.upgrade_talents_cats(current_cats) elif choice == 1: cat_editor.remove_talents_cats(current_cats) @staticmethod def unlock_cat_guide_remove_guide_run( save_file: core.SaveFile, current_cats: list[core.Cat] | None = None, cat_editor: CatEditor | None = None, ): if cat_editor is None or current_cats is None: cat_editor, current_cats = CatEditor.from_save_file(save_file) if cat_editor is None: return choice = dialog_creator.ChoiceInput( ["unlock_cat_guide", "remove_cat_guide"], ["unlock_cat_guide", "remove_cat_guide"], [], {}, "unlock_cat_guide_remove_guide_q", True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: cat_editor.unlock_cat_guide(current_cats) elif choice == 1: cat_editor.remove_cat_guide(current_cats) @staticmethod def from_save_file( save_file: core.SaveFile, ) -> tuple[CatEditor | None, list[core.Cat]]: cat_editor = CatEditor(save_file) stop = False cats = [] while not stop: current_cats, finished = cat_editor.select(cats) cats = current_cats cat_editor.print_selected_cats(cats) if finished: stop = True continue finished = dialog_creator.YesNoInput().get_input_once( "finished_cats_selection" ) if finished is None: return None, [] stop = finished return cat_editor, cats def run_edit_cats( self, cats: list[core.Cat], ) -> tuple[bool, list[core.Cat]]: self.print_selected_cats(cats) options: list[str] = [ "select_cats_again", "unlock_remove_cats", "upgrade_cats", "true_form_remove_form_cats", "force_true_form_cats", "fourth_form_remove_form_cats", "force_fourth_form_cats", "upgrade_talents_remove_talents_cats", "unlock_remove_cat_guide", "finish_edit_cats", ] option_id = dialog_creator.ChoiceInput( options, options, [], {}, "select_edit_cats_option", True, remove_alias=True, ).single_choice() if option_id is None: return False, cats option_id -= 1 if option_id == 0: cats_, _ = self.select(cats, False) cats = cats_ elif option_id == 1: self.unlock_remove_cats_run(self.save_file, cats, self) elif option_id == 2: self.upgrade_cats(cats) elif option_id == 3: self.true_form_remove_form_cats_run(self.save_file, cats, self) elif option_id == 4: color.ColoredText.localize("force_true_form_cats_warning") self.true_form_cats(cats, force=True) elif option_id == 5: self.fourth_form_remove_form_cats_run(self.save_file, cats, self) elif option_id == 6: color.ColoredText.localize("force_fourth_form_cats_warning") self.fourth_form_cats(cats, force=True) elif option_id == 7: self.upgrade_talents_remove_talents_cats_run(self.save_file, cats, self) elif option_id == 8: self.unlock_cat_guide_remove_guide_run(self.save_file, cats, self) CatEditor.set_rank_up_sale(self.save_file) if option_id == 9: return True, cats return False, cats @staticmethod def set_rank_up_sale(save_file: core.SaveFile): save_file.rank_up_sale_value = 0x7FFFFFFF ================================================ FILE: src/bcsfe/cli/edits/clear_tutorial.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color def clear_tutorial( save_file: core.SaveFile, display_already_cleared: bool = True ): core.StoryChapters.clear_tutorial(save_file) if display_already_cleared: color.ColoredText.localize("tutorial_cleared") ================================================ FILE: src/bcsfe/cli/edits/enemy_editor.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator from bcsfe.cli.edits.cat_editor import SelectMode from bcsfe.core.game.battle.enemy import EnemyNames class EnemyEditor: def __init__(self, save_file: core.SaveFile) -> None: self.save_file = save_file def unlock_enemy_guide(self, enemies: list[core.Enemy]): for enemy in enemies: enemy.unlock_enemy_guide(self.save_file) color.ColoredText.localize("unlock_enemy_guide_success") def remove_enemy_guide(self, enemies: list[core.Enemy]): for enemy in enemies: enemy.reset_enemy_guide(self.save_file) color.ColoredText.localize("remove_enemy_guide_success") def print_selected_enemies(self, enemies: list[core.Enemy]): if not enemies: return if len(enemies) > 50: color.ColoredText.localize("total_selected_enemies", total=len(enemies)) else: for enemy in enemies: color.ColoredText.localize( "selected_enemy", id=enemy.id, name=enemy.get_name(self.save_file), ) def select(self, current_enemies: list[core.Enemy] | None): if current_enemies is None: current_enemies = [] self.print_selected_enemies(current_enemies) options: dict[str, Any] = { "select_enemies_valid": self.get_all_valid_enemies, "select_enemies_all": self.get_all_enemies, "select_enemies_id": self.select_id, "select_enemies_name": self.select_name, "select_enemies_invalid": self.get_all_invalid_enemies, } option_id = dialog_creator.ChoiceInput.from_reduced( list(options), dialog="select_enemies", single_choice=True ).single_choice() if option_id is None: return current_enemies option_id -= 1 func = options[list(options)[option_id]] new_enemies = func() if new_enemies is None: return None if current_enemies: mode_id = dialog_creator.IntInput().get_basic_input_locale("and_mode_q", {}) if mode_id is None: mode = SelectMode.OR elif mode_id == 1: mode = SelectMode.AND elif mode_id == 2: mode = SelectMode.OR elif mode_id == 3: mode = SelectMode.REPLACE else: mode = SelectMode.OR else: mode = SelectMode.OR if mode == SelectMode.AND: return [enemy for enemy in new_enemies if enemy in current_enemies] if mode == SelectMode.OR: return list(set(current_enemies + new_enemies)) if mode == SelectMode.REPLACE: return new_enemies return new_enemies def get_all_enemies(self) -> list[core.Enemy]: enemies: list[core.Enemy] = [] for i in range(len(self.save_file.enemy_guide)): enemies.append(core.Enemy(i)) return enemies def get_all_valid_enemies(self) -> list[core.Enemy] | None: valid_ids = core.EnemyDictionary(self.save_file).get_valid_enemies() if valid_ids is None: return None return [core.Enemy(id) for id in valid_ids] def get_all_invalid_enemies(self) -> list[core.Enemy] | None: invalid_ids = core.EnemyDictionary(self.save_file).get_invalid_enemies( len(self.save_file.enemy_guide) ) if invalid_ids is None: return None return [core.Enemy(id) for id in invalid_ids] def select_id(self) -> list[core.Enemy] | None: enemy_ids = dialog_creator.RangeInput( len(self.save_file.enemy_guide) - 1 ).get_input_locale("enter_enemy_ids", {}) if enemy_ids is None: return None enemy_ids = [enemy_id - 2 for enemy_id in enemy_ids] return self.get_enemies_by_id(enemy_ids) def get_enemies_by_id(self, ids: list[int]) -> list[core.Enemy]: enemies: list[core.Enemy] = [] for enemy in self.get_all_enemies(): if enemy.id in ids: enemies.append(enemy) return enemies def select_name(self) -> list[core.Enemy] | None: usr_name = dialog_creator.StringInput().get_input_locale("enter_enemy_name", {}) if usr_name is None: return None enemies = self.get_enemies_by_name(usr_name) if not enemies: color.ColoredText.localize("enemy_not_found_name", name=usr_name) return None enemy_names = [enemy.get_name(self.save_file) for enemy in enemies] new_enemy_names: list[str] = [] for enemy_name in enemy_names: if enemy_name is None: return None new_enemy_names.append(enemy_name) enemy_option_ids, _ = dialog_creator.ChoiceInput.from_reduced( new_enemy_names, dialog="select_enemies", single_choice=False ).multiple_choice() if enemy_option_ids is None: return None enemies_selected: list[core.Enemy] = [] for enemy_option_id in enemy_option_ids: enemies_selected.append(enemies[enemy_option_id]) return enemies_selected def get_enemies_by_name(self, name: str) -> list[core.Enemy]: enemies: list[core.Enemy] = [] for enemy in self.get_all_enemies(): enemy_name = enemy.get_name(self.save_file) if enemy_name is None: continue if name.lower() in enemy_name.lower(): enemies.append(enemy) return enemies @staticmethod def from_save_file( save_file: core.SaveFile, ) -> tuple[EnemyEditor | None, list[core.Enemy]]: enemy_editor = EnemyEditor(save_file) current_enemies = enemy_editor.select([]) if current_enemies is None: return None, [] return enemy_editor, current_enemies @staticmethod def edit_enemy_guide( save_file: core.SaveFile, current_enemies: list[core.Enemy] | None = None, enemy_editor: EnemyEditor | None = None, ): if enemy_editor is None or current_enemies is None: enemy_editor, current_enemies = EnemyEditor.from_save_file(save_file) if enemy_editor is None or not current_enemies: return choice = dialog_creator.ChoiceInput.from_reduced( ["unlock_enemy_guide", "remove_enemy_guide"], dialog="edit_enemy_guide", single_choice=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: enemy_editor.unlock_enemy_guide(current_enemies) elif choice == 1: enemy_editor.remove_enemy_guide(current_enemies) ================================================ FILE: src/bcsfe/cli/edits/event_tickets.py ================================================ from __future__ import annotations from bcsfe import cli, core from bcsfe.core.game.catbase.gatya import GatyaEventType from bcsfe.core.server.event_data import split_hhmm, split_yyyymmdd class EventTickets: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.gatya_item_buy = core.core_data.get_gatya_item_buy(self.save_file) self.gatya_item_names = core.core_data.get_gatya_item_names(self.save_file) self.gatya_option_n = core.GatyaDataOption.read( self.save_file, GatyaEventType.NORMAL ) self.gatya_option_r = core.GatyaDataOption.read( self.save_file, GatyaEventType.RARE ) self.gatya_option_e = core.GatyaDataOption.read( self.save_file, GatyaEventType.EVENT ) cli.color.ColoredText.localize("downloading_gatya_data") temp_save_file = core.SaveFile(cc=save_file.cc, gv=save_file.game_version) gatya_event_data = core.ServerHandler(temp_save_file).download_gatya_data() if gatya_event_data is None: cli.color.ColoredText.localize("download_gatya_data_fail") self.gatya_event_data = None else: cli.color.ColoredText.localize("download_gatya_data_success") self.gatya_event_data = core.ServerGatyaData.from_data(gatya_event_data) @staticmethod def edit(save_file: core.SaveFile): event_tickets = EventTickets(save_file) if event_tickets.gatya_event_data is None: return event_ticket_items: list[ tuple[ core.ServerGatyaDataItem, core.ServerGatyaDataSet, core.GatyaItemBuyItem ] ] = [] if ( event_tickets.gatya_option_n is None or event_tickets.gatya_option_r is None or event_tickets.gatya_option_e is None ): return for item in event_tickets.gatya_event_data.items: for gset in item.sets: if gset.number == -1: continue gset_opt = None if item.get_normal_flag(): gset_opt = event_tickets.gatya_option_n.get(gset.number) elif item.get_rare_flag(): gset_opt = event_tickets.gatya_option_r.get(gset.number) elif item.get_collab_flag(): gset_opt = event_tickets.gatya_option_e.get(gset.number) if gset_opt is None: continue gatya_item = event_tickets.gatya_item_buy.get(gset_opt.ticket_item_id) if gatya_item is None: continue category = gatya_item.category if category in [ core.GatyaItemCategory.EVENT_TICKETS.value, core.GatyaItemCategory.LUCKY_TICKETS_1.value, core.GatyaItemCategory.LUCKY_TICKETS_2.value, ]: event_ticket_items.append((item, gset, gatya_item)) event_names: list[str] = [] values: list[int] = [] for event_item, gset, gatya_item in event_ticket_items: start_y, start_m, start_d = split_yyyymmdd(event_item.filter.start_yyyymmdd) start_h, start_min = split_hhmm(event_item.filter.start_hhmm) end_y, end_m, end_d = split_yyyymmdd(event_item.filter.end_yyyymmdd) end_h, end_min = split_hhmm(event_item.filter.end_hhmm) time_str = f"{start_y}-{start_m:02}-{start_d:02} {start_h:02}:{start_min:02} -> {end_y}-{end_m:02}-{end_d:02} {end_h:02}:{end_min:02}" event_message = gset.message.replace("
", "\n") base_msg = f"{time_str}" item_name = event_tickets.gatya_item_names.get_name(gatya_item.id) if item_name is not None: base_msg += f" - {item_name}" if event_message: base_msg += f" - {event_message}" current_amount = event_tickets.get_ticket(gatya_item.id) if current_amount is not None: event_names.append(base_msg) values.append(current_amount) values = cli.dialog_creator.MultiEditor.from_reduced( "event_tickets", event_names, ints=values, max_values=core.core_data.max_value_manager.get("event_tickets"), group_name_localized=True, ).edit() for (event_item, gset, gatya_item), value in zip(event_ticket_items, values): event_tickets.edit_ticket(gatya_item.id, value) def get_ticket(self, item_id: int) -> int | None: item = self.gatya_item_buy.get(item_id) if item is None: return if item.category == core.GatyaItemCategory.EVENT_TICKETS.value: if item.index < len(self.save_file.event_capsules): return self.save_file.event_capsules[item.index] if item.category == core.GatyaItemCategory.LUCKY_TICKETS_1.value: if item.index < len(self.save_file.lucky_tickets): return self.save_file.lucky_tickets[item.index] if item.category == core.GatyaItemCategory.LUCKY_TICKETS_2.value: if item.index < len(self.save_file.event_capsules_2): return self.save_file.event_capsules_2[item.index] return None def edit_ticket(self, item_id: int, amount: int): item = self.gatya_item_buy.get(item_id) if item is None: return if item.category == core.GatyaItemCategory.EVENT_TICKETS.value: if item.index < len(self.save_file.event_capsules): self.save_file.event_capsules[item.index] = amount if item.category == core.GatyaItemCategory.LUCKY_TICKETS_1.value: if item.index < len(self.save_file.lucky_tickets): self.save_file.lucky_tickets[item.index] = amount if item.category == core.GatyaItemCategory.LUCKY_TICKETS_2.value: if item.index < len(self.save_file.event_capsules_2): self.save_file.event_capsules_2[item.index] = amount ================================================ FILE: src/bcsfe/cli/edits/fixes.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color import datetime class Fixes: @staticmethod def fix_gamatoto_crash(save_file: core.SaveFile): save_file.gamatoto.skin = 2 color.ColoredText.localize("fix_gamatoto_crash_success") @staticmethod def fix_ototo_crash(save_file: core.SaveFile): save_file.ototo.cannons = core.game.gamoto.ototo.Cannons.init( save_file.game_version ) color.ColoredText.localize("fix_ototo_crash_success") @staticmethod def fix_time_errors(save_file: core.SaveFile): save_file.date_3 = datetime.datetime.now() save_file.timestamp = datetime.datetime.now().timestamp() save_file.energy_penalty_timestamp = datetime.datetime.now().timestamp() color.ColoredText.localize("fix_time_errors_success") # 10 = 62 / hgt1 = ahead by too much # 11 = 63 / hgt0 = behind by too much # 12 = 61 / hgt2 = ahead by too much # date_3 - controls gacha errors (hgt2) # can't be ahead of the device time # timestamp - controls gacha errors (hgt1, hgt0) # can't be ahead by more than 10 minutes to device time # can't be behind by more than 1.5 days to device time # penalty_timestamp - controls energy / gamatoto errors # can't by ahead of device time # can't be ahead by more than 1 day to device time # can't be behind by more than 1 day to device time ================================================ FILE: src/bcsfe/cli/edits/map.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color, dialog_creator from typing import Union ChaptersType = Union[ "core.EventChapters", "core.GauntletChapters", "core.LegendQuestChapters", "core.ZeroLegendsChapters", "core.Chapters", ] def get_total_maps(chapters: ChaptersType) -> int: if isinstance(chapters, core.EventChapters): return chapters.get_lengths()[1] return len(chapters.chapters) def unclear_stage( chapters: ChaptersType, map: int, star: int, stage: int, type: int | None = None, ) -> bool: if isinstance(chapters, core.EventChapters): if type is None: raise ValueError("Type must be specified for EventChapters!") return chapters.unclear_stage(type, map, star, stage) else: return chapters.unclear_stage(map, star, stage) def clear_stage( chapters: ChaptersType, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, type: int | None = None, ensure_cleared_only: bool = False, ) -> bool: if isinstance(chapters, core.EventChapters): if type is None: raise ValueError("Type must be specified for EventChapters!") return chapters.clear_stage( type, map, star, stage, clear_amount, overwrite_clear_progress ) else: return chapters.clear_stage( map, star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only=ensure_cleared_only, ) def unclear_rest( chapters: ChaptersType, stages: list[int], stars: int, id: int, type: int | None = None, ): if isinstance(chapters, core.EventChapters): if type is None: raise ValueError("Type must be specified for EventChapters!") chapters.unclear_rest(stages, stars, id, type) else: chapters.unclear_rest(stages, stars, id) def get_total_stars( map_option: core.MapOption, base_index: int, chapters: ChaptersType, id: int, type: int | None = None, ) -> int: max_stars = get_max_stars(chapters, id, type) map_option_stars = map_option.get_map(base_index + id) if map_option_stars is not None: return min(max_stars, map_option_stars.crown_count) return max_stars def get_max_max_stars( map_option: core.MapOption, base_index: int, ids: list[int], chapters: ChaptersType, type: int | None = None, ) -> int: m = 0 for id in ids: val = get_total_stars(map_option, base_index, chapters, id, type) if val > m: m = val return m def get_max_stars( chapters: ChaptersType, id: int, type: int | None = None, ) -> int: if isinstance(chapters, core.EventChapters): if type is None: raise ValueError("Type must be specified for EventChapters!") max_stars = chapters.get_total_stars(type, id) else: max_stars = chapters.get_total_stars(id) return max_stars def get_total_stages( chapters: ChaptersType, id: int, star: int, type: int | None = None ): if isinstance(chapters, core.EventChapters): if type is None: raise ValueError("Type must be specified for EventChapters!") total_stars = chapters.get_total_stages(type, id, star) else: total_stars = chapters.get_total_stages(id, star) return total_stars def select_maps( save_file: core.SaveFile, chapters: ChaptersType, letter_code: str, base_index: int, no_r_prefix: bool = False, ) -> list[int] | None: map_names = core.MapNames( save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index ) names: dict[int, str | None] = {} for id, name in map_names.map_names.items(): if id >= get_total_maps(chapters): continue names[id] = name return core.EventChapters.select_map_names(names) def select_maps_stars( save_file: core.SaveFile, map_option: core.MapOption, chapters: ChaptersType, letter_code: str, base_index: int, type: int | None = None, no_r_prefix: bool = False, ) -> list[tuple[int, int]] | None: map_names = core.MapNames( save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index ) names: dict[int, str | None] = {} for id, name in map_names.map_names.items(): if id >= get_total_maps(chapters): continue for star in range(get_total_stars(map_option, base_index, chapters, id, type)): names[id * 10 + star] = core.localize( "map_name_star", name=name, star=star + 1 ) ids = core.EventChapters.select_map_names(names) if ids is None: return None new_ids: list[tuple[int, int]] = [] for id in ids: map_id = id // 10 star_index = id % 10 new_ids.append((map_id, star_index)) return new_ids def edit_chapters2_clear_count( save_file: core.SaveFile, chapters: ChaptersType, letter_code: str, base_index: int, type: int | None = None, no_r_prefix: bool = False, ): map_names = core.MapNames( save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index ) map_option = core.MapOption.from_save(save_file) if map_option is None: return None map_choices = select_maps_stars( save_file, map_option, chapters, letter_code, base_index, type, no_r_prefix ) if map_choices is None: return None clear_all = edit_all_or_handle_ind(len(map_choices)) if clear_all is None: return None if clear_all == 0: clear_count = core.EventChapters.ask_clear_amount() if clear_count is None: return None for local_map_id, star in map_choices: total_stages = get_total_stages(chapters, local_map_id, star, type) for stage in range(total_stages): clear_stage(chapters, local_map_id, star, stage, clear_count, type=type) else: for local_map_id, star in map_choices: print() core.EventChapters.print_current_chapter( core.localize( "map_name_star", star=star, name=map_names.map_names.get(local_map_id), ), local_map_id, ) clear_whole = dialog_creator.ChoiceInput.from_reduced( ["edit_whole_chapter", "edit_specific_stages"], dialog="edit_chapter_q" ).single_choice() if clear_whole is None: return None clear_whole -= 1 if clear_whole == 0: clear_count = core.EventChapters.ask_clear_amount() if clear_count is None: return None for stage in range( get_total_stages(chapters, local_map_id, star, type) ): clear_stage( chapters, local_map_id, star, stage, clear_count, type=type ) else: stage_ids = core.EventChapters.ask_stages(map_names, local_map_id) if stage_ids is None: return None all_selected_stages = dialog_creator.ChoiceInput.from_reduced( ["each_stage_individually", "stage_all_at_once"], dialog="set_clear_count_stage_q", ).single_choice() if all_selected_stages is None: return None all_selected_stages -= 1 stage_names = core.EventChapters.get_stage_names( map_names, local_map_id ) if stage_names is None: stage_names = [] if all_selected_stages == 0: for stage in stage_ids: print() if stage < len(stage_names): stage_name = stage_names[stage] else: stage_name = None core.EventChapters.print_current_stage(stage_name, stage) clear_count = core.EventChapters.ask_clear_amount() if clear_count is None: return None clear_stage( chapters, local_map_id, star, stage, clear_count, type=type ) else: clear_count = core.EventChapters.ask_clear_amount() if clear_count is None: return None for stage in stage_ids: clear_stage( chapters, local_map_id, star, stage, clear_count, type=type ) def clear_all_or_handle_ind(map_choices_len: int) -> int | None: if map_choices_len <= 1: clear_all = 1 else: clear_all = dialog_creator.ChoiceInput.from_reduced( ["clear_all", "handle_individually"], dialog="clear_chapters_q" ).single_choice() if clear_all is None: return None clear_all -= 1 return clear_all def unclear_all_or_handle_ind(map_choices_len: int) -> int | None: if map_choices_len <= 1: clear_all = 1 else: clear_all = dialog_creator.ChoiceInput.from_reduced( ["unclear_all", "handle_individually"], dialog="unclear_chapters_q" ).single_choice() if clear_all is None: return None clear_all -= 1 return clear_all def edit_all_or_handle_ind(map_choices_len: int) -> int | None: if map_choices_len <= 1: clear_all = 1 else: clear_all = dialog_creator.ChoiceInput.from_reduced( ["edit_map_all", "handle_individually"], dialog="edit_chapters_q_all" ).single_choice() if clear_all is None: return None clear_all -= 1 return clear_all def edit_chapters2_progress( save_file: core.SaveFile, chapters: ChaptersType, letter_code: str, base_index: int, type: int | None = None, no_r_prefix: bool = False, allow_unclear: bool = False, ): map_names = core.MapNames( save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index ) map_choices = select_maps(save_file, chapters, letter_code, base_index, no_r_prefix) if map_choices is None: return None clear_all = clear_all_or_handle_ind(len(map_choices)) if clear_all is None: return None map_option = core.MapOption.from_save(save_file) if map_option is None: return None if clear_all == 0: max_stars = get_max_max_stars( map_option, base_index, map_choices, chapters, type ) if allow_unclear: stars = core.EventChapters.ask_stars_unclear(max_stars, "max_stars") else: stars = core.EventChapters.ask_stars(max_stars, "max_stars") if stars is None: return None for local_map_id in map_choices: unclear_rest( chapters, [0], max(0, stars - 1), local_map_id, type, ) for star in range(stars): total_stages = get_total_stages(chapters, local_map_id, star, type) for stage in range(total_stages): clear_stage( chapters, local_map_id, star, stage, type=type, ensure_cleared_only=True, ) return map_choices for local_map_id in map_choices: name = map_names.map_names.get(local_map_id) core.EventChapters.print_current_chapter(name, local_map_id) clear_whole = dialog_creator.ChoiceInput.from_reduced( ["clear_whole_chapter", "clear_to_specific_stage"], dialog="clear_whole_q" ).single_choice() if clear_whole is None: return None clear_whole -= 1 if clear_whole == 0: max_stars = get_total_stars( map_option, base_index, chapters, local_map_id, type ) if allow_unclear: stars = core.EventChapters.ask_stars_unclear(max_stars) else: stars = core.EventChapters.ask_stars(max_stars) if stars is None: return None unclear_rest( chapters, [0], max(stars - 1, 0), local_map_id, type, ) for star in range(stars): total_stages = get_total_stages(chapters, local_map_id, star, type) for stage in range(total_stages): clear_stage( chapters, local_map_id, star, stage, type=type, ensure_cleared_only=True, ) else: stage_names = map_names.stage_names.get(local_map_id) stage_names = [ stage_name for stage_name in stage_names or [] if stage_name and stage_name != "@" ] stage_id = core.EventChapters.ask_stages_stage_names_one(stage_names) if stage_id is None: return None max_stars = get_total_stars( map_option, base_index, chapters, local_map_id, type ) if allow_unclear: stars = core.EventChapters.ask_stars_unclear(max_stars) else: stars = core.EventChapters.ask_stars(max_stars) if stars is None: return None unclear_rest( chapters, list(range(stage_id)), max(stars - 1, 0), local_map_id, type ) for star in range(stars - 1): total_stages = get_total_stages(chapters, local_map_id, star, type) for stage in range(total_stages): clear_stage( chapters, local_map_id, star, stage, type=type, ensure_cleared_only=True, ) for stage in range(stage_id + 1): clear_stage( chapters, local_map_id, stars - 1, stage, type=type, ensure_cleared_only=True, ) def edit_chapters( save_file: core.SaveFile, chapters: ChaptersType, letter_code: str, base_index: int, type: int | None = None, no_r_prefix: bool = False, ) -> dict[int, bool] | None: while True: choice = dialog_creator.ChoiceInput.from_reduced( [ "edit_progress_clear", "edit_progress_unclear", "edit_clear_counts", "finish", ], dialog="edit_chapters_q", ).single_choice() if choice is None: return None choice -= 1 if choice == 0: edit_chapters2_progress( save_file, chapters, letter_code, base_index, type, no_r_prefix ) elif choice == 1: edit_chapters2_progress( save_file, chapters, letter_code, base_index, type, no_r_prefix, allow_unclear=True, ) elif choice == 2: edit_chapters2_clear_count( save_file, chapters, letter_code, base_index, type, no_r_prefix ) else: break color.ColoredText.localize("map_chapters_edited") color.ColoredText.localize("map_chapters_edited") return None ================================================ FILE: src/bcsfe/cli/edits/max_all.py ================================================ from __future__ import annotations from collections.abc import Callable from bcsfe import core def max_catfood(save_file: core.SaveFile): orig = save_file.catfood save_file.catfood = core.core_data.max_value_manager.get(core.MaxValueType.CATFOOD) core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change( save_file.catfood - orig, core.ManagedItemType.CATFOOD ) ) def max_rare_tickets(save_file: core.SaveFile): orig = save_file.rare_tickets save_file.rare_tickets = core.core_data.max_value_manager.get( core.MaxValueType.RARE_TICKETS ) core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change( save_file.rare_tickets - orig, core.ManagedItemType.RARE_TICKET ) ) def max_plat_tickets(save_file: core.SaveFile): orig = save_file.platinum_tickets save_file.platinum_tickets = core.core_data.max_value_manager.get( core.MaxValueType.PLATINUM_TICKETS ) core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change( save_file.platinum_tickets - orig, core.ManagedItemType.PLATINUM_TICKET ) ) def max_plat_shards(save_file: core.SaveFile): save_file.platinum_shards = 10 * core.core_data.max_value_manager.get( core.MaxValueType.PLATINUM_TICKETS ) def max_legend_tickets(save_file: core.SaveFile): orig = save_file.legend_tickets save_file.legend_tickets = core.core_data.max_value_manager.get( core.MaxValueType.LEGEND_TICKETS ) core.BackupMetaData(save_file).add_managed_item( core.ManagedItem.from_change( save_file.legend_tickets - orig, core.ManagedItemType.LEGEND_TICKET ) ) def max_xp(save_file: core.SaveFile): save_file.xp = core.core_data.max_value_manager.get(core.MaxValueType.XP) def max_np(save_file: core.SaveFile): save_file.np = core.core_data.max_value_manager.get(core.MaxValueType.NP) def max_100_million_ticket(save_file: core.SaveFile): save_file.hundred_million_ticket = core.core_data.max_value_manager.get( core.MaxValueType.HUNDRED_MILLION_TICKETS ) def max_leadership(save_file: core.SaveFile): save_file.leadership = core.core_data.max_value_manager.get( core.MaxValueType.LEADERSHIP ) def max_battle_items(save_file: core.SaveFile): for item in save_file.battle_items.items: item.amount = core.core_data.max_value_manager.get( core.MaxValueType.BATTLE_ITEMS ) def max_catseyes(save_file: core.SaveFile): for id in range(len(save_file.catseyes)): save_file.catseyes[id] = core.core_data.max_value_manager.get( core.MaxValueType.CATSEYES ) def max_treasure_chests(save_file: core.SaveFile): for id in range(len(save_file.treasure_chests)): save_file.treasure_chests[id] = core.core_data.max_value_manager.get( core.MaxValueType.TREASURE_CHESTS ) def max_catamins(save_file: core.SaveFile): for id in range(len(save_file.catseyes)): save_file.catamins[id] = core.core_data.max_value_manager.get( core.MaxValueType.CATAMINS ) def max_labyrinth_medals(save_file: core.SaveFile): for id in range(len(save_file.labyrinth_medals)): save_file.labyrinth_medals[id] = core.core_data.max_value_manager.get( core.MaxValueType.LABYRINTH_MEDALS ) # def max_catfruit(save_file: core.SaveFile): # for id in range(len(save_file.catfruit)): # save_file.catfruit[id] = core.core_data.max_value_manager.get_new( # core.MaxValueType.CATFRUIT # ) def max_normal_tickets(save_file: core.SaveFile): save_file.normal_tickets = core.core_data.max_value_manager.get( core.MaxValueType.NORMAL_TICKETS ) def max_all(save_file: core.SaveFile): maxes = core.core_data.max_value_manager features: dict[str, Callable[[core.SaveFile], None]] = { "catfood": max_catfood, "xp": max_xp, "normal_tickets": max_normal_tickets, "rare_tickets": max_rare_tickets, "platinum_tickets": max_plat_tickets, "legend_tickets": max_legend_tickets, "platinum_shards": max_plat_shards, "np": max_np, "leadership": max_leadership, "battle_items": max_battle_items, "catseyes": max_catseyes, "catamins": max_catamins, "labyrinth_medals": max_labyrinth_medals, "100_million_ticket": max_100_million_ticket, "treasure_chests": max_treasure_chests, } # TODO: finish ================================================ FILE: src/bcsfe/cli/edits/rare_ticket_trade.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color, dialog_creator class RareTicketTrade: @staticmethod def rare_ticket_trade(save_file: core.SaveFile): current_amount = save_file.rare_tickets max_amount = max( core.core_data.max_value_manager.get("rare_tickets") - current_amount, 0, ) if max_amount == 0: color.ColoredText.localize("rare_ticket_trade_maxed") return to_add = dialog_creator.IntInput(max_amount, 0).get_input_locale_while( "rare_ticket_trade_enter", {"max": max_amount, "current": current_amount}, ) if to_add is None: return space = False for storage_item in save_file.cats.storage_items: if storage_item.item_type == 0 or ( storage_item.item_id == 1 and storage_item.item_type == 2 ): storage_item.item_id = 1 storage_item.item_type = 2 space = True break if not space: color.ColoredText.localize("rare_ticket_trade_storage_full") return amount = to_add * 5 save_file.gatya.trade_progress = amount color.ColoredText.localize( "rare_ticket_successfully_traded", rare_ticket_count=to_add ) ================================================ FILE: src/bcsfe/cli/edits/storage.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color, dialog_creator from bcsfe.cli.edits import cat_editor def display_storage(save_file: core.SaveFile, storage: list[core.StorageItem]): color.ColoredText.localize("current_storage_items") index = 0 for item in storage: if item.item_type == 0: continue index += 1 color.ColoredText(f"{index}. ", end="") display_item(item, save_file) if index == 0: color.ColoredText.localize("storage_is_empty") available_slots = len(storage) - index color.ColoredText.localize("available_storage", slots=available_slots) def display_item(item: core.StorageItem, save_file: core.SaveFile): color.ColoredText(get_item_str(item, save_file)) def get_item_str(item: core.StorageItem, save_file: core.SaveFile) -> str: if item.item_type == 1: cat_id = item.item_id names = core.Cat.get_names(cat_id, save_file) if not names: names = [str(cat_id)] return core.localize("cat", name=names[0], id=cat_id) elif item.item_type == 2: skill_id = item.item_id skill_names = ( core.core_data.get_gatya_item_buy(save_file).get_names_by_category( core.GatyaItemCategory.SPECIAL_SKILLS ) or [] ) if skill_id >= len(skill_names) or skill_id < 0: name = str(skill_id) else: name = skill_names[skill_id][1] return core.localize("special_skill", name=name, id=skill_id) elif item.item_type == 3: item_id = item.item_id name = core.core_data.get_gatya_item_names(save_file).get_name(item_id) if name is None: name = str(item_id) return core.localize("item", name=name, id=item_id) else: return core.localize( "unrecognised_storage_item", item_type=item.item_type, id=item.item_id ) def clear_storage(storage: list[core.StorageItem]): for item in storage: item.item_id = 0 item.item_type = 0 def add_item(storage: list[core.StorageItem], item: core.StorageItem) -> bool: for citem in storage: if citem.item_type == 0: citem.item_type = item.item_type citem.item_id = item.item_id return True return False def get_storage_space(storage: list[core.StorageItem]) -> int: space = 0 for item in storage: if item.item_type == 0: space += 1 return space def edit_storage(save_file: core.SaveFile): display_storage(save_file, save_file.cats.storage_items) exit = False while not exit: exit = edit_loop(save_file) color.ColoredText.localize("storage_success") def edit_loop(save_file: core.SaveFile) -> bool: storage = save_file.cats.storage_items options = [ "display_storage", "clear_storage", "add_cats", "add_special_skills", "remove_items", "finish", ] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_option" ).single_choice() if choice is None: return False choice -= 1 if choice == 0: display_storage(save_file, storage) if choice == 1: clear_storage(storage) elif choice == 2: editor, cats = cat_editor.CatEditor.from_save_file(save_file) if editor is None: return False space = get_storage_space(storage) if len(cats) > len(storage): color.ColoredText.localize( "too_many_cats_selected", max=len(storage), current=len(cats) ) return False needs = len(cats) - space if needs > 0: color.ColoredText.localize("need_x_more_space", needs=needs) return False color.ColoredText.localize("added_cats") for cat in cats: item = core.StorageItem.from_cat(cat.id) add_item(storage, item) display_item(item, save_file) elif choice == 3: skill_names: list[str] = list( map( lambda sk: sk[1] or str(sk[0].id), core.core_data.get_gatya_item_buy(save_file).get_names_by_category( core.GatyaItemCategory.SPECIAL_SKILLS ) or [], ) ) options, _ = dialog_creator.ChoiceInput.from_reduced( skill_names, localize_options=False, dialog="select_special_skills" ).multiple_choice(False) if options is None: return False space = get_storage_space(storage) if len(options) > len(storage): color.ColoredText.localize( "too_many_skills_selected", max=len(storage), current=len(options) ) return False needs = len(options) - space if needs > 0: color.ColoredText.localize("need_x_more_space", needs=needs) return False color.ColoredText.localize("added_special_skills") for choice in options: item = core.StorageItem.from_special_skill(choice) add_item(storage, item) display_item(item, save_file) elif choice == 4: options2: list[str] = [] for item in storage: if item.item_type == 0: continue options2.append(get_item_str(item, save_file)) choices, _ = dialog_creator.ChoiceInput.from_reduced( options2, localize_options=False ).multiple_choice(False) if choices is None: return False color.ColoredText.localize("removed_items") index = 0 for item in storage: if item.item_type == 0: continue if index in choices: display_item(item, save_file) item.item_type = 0 item.item_id = 0 index += 1 elif choice == 5: return True return False ================================================ FILE: src/bcsfe/cli/feature_handler.py ================================================ from __future__ import annotations from typing import Any, Callable from bcsfe import core from bcsfe.cli import dialog_creator, color, edits, save_management, main class FeatureHandler: def __init__(self, save_file: core.SaveFile): self.save_file = save_file def get_features(self) -> dict[str, Any]: cat_features = {"cats": edits.cat_editor.CatEditor.edit_cats} if core.core_data.config.get_bool(core.ConfigKey.SEPARATE_CAT_EDIT_OPTIONS): cat_features = { "unlock_remove_cats": edits.cat_editor.CatEditor.unlock_remove_cats_run, "upgrade_cats": edits.cat_editor.CatEditor.upgrade_cats_run, "true_form_remove_form_cats": edits.cat_editor.CatEditor.true_form_remove_form_cats_run, "force_true_form_cats": edits.cat_editor.CatEditor.force_true_form_cats_run, "fourth_form_remove_form_cats": edits.cat_editor.CatEditor.fourth_form_remove_form_cats_run, "force_fourth_form_cats": edits.cat_editor.CatEditor.force_fourth_form_cats_run, "upgrade_talents_remove_talents_cats": edits.cat_editor.CatEditor.upgrade_talents_remove_talents_cats_run, "unlock_remove_cat_guide": edits.cat_editor.CatEditor.unlock_cat_guide_remove_guide_run, } cat_features["special_skills"] = ( edits.basic_items.BasicItems.edit_special_skills ) cat_features["cat_storage"] = edits.storage.edit_storage features: dict[str, Any] = { "save_management": { "save_save": save_management.SaveManagement.save_save, "save_upload": save_management.SaveManagement.save_upload, "save_save_file": save_management.SaveManagement.save_save_dialog, core.localize( "save_save_documents", path=core.SaveFile.get_save_path() ): save_management.SaveManagement.save_save_data_dir, "waydroid_push": save_management.SaveManagement.waydroid_push, "waydroid_push_rerun": save_management.SaveManagement.waydroid_push_rerun, "adb_push": save_management.SaveManagement.adb_push, "adb_push_rerun": save_management.SaveManagement.adb_push_rerun, "root_push": save_management.SaveManagement.root_push, "root_push_rerun": save_management.SaveManagement.root_push_rerun, "export_save": save_management.SaveManagement.export_save, "load_save": save_management.SaveManagement.load_save, # "init_save": save_management.SaveManagement.init_save, "convert_region": save_management.SaveManagement.convert_save_cc, "convert_version": save_management.SaveManagement.convert_save_gv, }, "items": { "catfood": edits.basic_items.BasicItems.edit_catfood, "xp": edits.basic_items.BasicItems.edit_xp, "normal_tickets": edits.basic_items.BasicItems.edit_normal_tickets, "rare_tickets": edits.basic_items.BasicItems.edit_rare_tickets, "rare_ticket_trade_feature_name": edits.rare_ticket_trade.RareTicketTrade.rare_ticket_trade, "platinum_tickets": edits.basic_items.BasicItems.edit_platinum_tickets, "legend_tickets": edits.basic_items.BasicItems.edit_legend_tickets, "platinum_shards": edits.basic_items.BasicItems.edit_platinum_shards, "np": edits.basic_items.BasicItems.edit_np, "leadership": edits.basic_items.BasicItems.edit_leadership, "battle_items": edits.basic_items.BasicItems.edit_battle_items, "battle_items_endless": edits.basic_items.BasicItems.edit_battle_items_endless, "catseyes": edits.basic_items.BasicItems.edit_catseyes, "catfruit": edits.basic_items.BasicItems.edit_catfruit, "talent_orbs": core.game.catbase.talent_orbs.SaveOrbs.edit_talent_orbs, "catamins": edits.basic_items.BasicItems.edit_catamins, "scheme_items": edits.basic_items.BasicItems.edit_scheme_items, "labyrinth_medals": edits.basic_items.BasicItems.edit_labyrinth_medals, "100_million_tickets": edits.basic_items.BasicItems.edit_100_million_ticket, "event_tickets": edits.event_tickets.EventTickets.edit, "treasure_chests": edits.basic_items.BasicItems.edit_treasure_chests, "reset_golden_cat_cpus": edits.basic_items.BasicItems.reset_golden_cat_cpus, }, "cats_special_skills": cat_features, "levels": { "clear_tutorial": edits.clear_tutorial.clear_tutorial, "clear_story": core.game.map.story.StoryChapters.clear_story, "challenge_score": core.game.map.challenge.edit_challenge_score, "dojo_score": core.game.map.dojo.edit_dojo_score, "add_enigma_stages": core.game.map.enigma.edit_enigma, "clear_enigma_stages": core.game.map.gauntlets.GauntletChapters.edit_enigma_stages, "unlock_aku_realm": edits.aku_realm.unlock_aku_realm, "story_treasures": core.game.map.story.StoryChapters.edit_treasures, "outbreaks": core.game.map.outbreaks.Outbreaks.edit_outbreaks, "aku_chapters": core.game.map.aku.AkuChapters.edit_aku_chapters, "itf_timed_scores": core.game.map.story.StoryChapters.edit_itf_timed_scores, "filibuster_reclearing": edits.basic_items.BasicItems.allow_filibuster_stage_reclearing, "sol": core.game.map.event.EventChapters.edit_sol_chapters, "event": core.game.map.event.EventChapters.edit_event_chapters, "collab": core.game.map.event.EventChapters.edit_collab_chapters, "gauntlets": core.game.map.gauntlets.GauntletChapters.edit_gauntlets, "collab_gauntlets": core.game.map.gauntlets.GauntletChapters.edit_collab_gauntlets, "uncanny": core.game.map.uncanny.UncannyChapters.edit_uncanny, "catamin_stages": core.game.map.uncanny.UncannyChapters.edit_catamin_stages, "behemoth_culling": core.game.map.gauntlets.GauntletChapters.edit_behemoth_culling, "legend_quest": core.game.map.legend_quest.LegendQuestChapters.edit_legend_quest, "towers": core.game.map.tower.TowerChapters.edit_towers, "zero_legends": core.game.map.zero_legends.ZeroLegendsChapters.edit_zero_legends, "dojo_catclaw_championships": core.game.map.zero_legends.ZeroLegendsChapters.edit_catclaw_championships, }, "gamototo": { "engineers": edits.basic_items.BasicItems.edit_engineers, "base_materials": edits.basic_items.BasicItems.edit_base_materials, "gamatoto_xp_level": core.game.gamoto.gamatoto.edit_xp, "gamatoto_helpers": core.game.gamoto.gamatoto.edit_helpers, "ototo_cat_cannon": core.game.gamoto.ototo.edit_cannon, "cat_shrine": core.game.gamoto.cat_shrine.CatShrine.edit_catshrine, }, "account": { "unban_account": save_management.SaveManagement.unban_account, "upload_items": save_management.SaveManagement.upload_items, "inquiry_code": edits.basic_items.BasicItems.edit_inquiry_code, "password_refresh_token": edits.basic_items.BasicItems.edit_password_refresh_token, }, "gatya": { "rare_gatya_seed": edits.basic_items.BasicItems.edit_rare_gatya_seed, "normal_gatya_seed": edits.basic_items.BasicItems.edit_normal_gatya_seed, "event_gatya_seed": edits.basic_items.BasicItems.edit_event_gatya_seed, }, "fixes": { "fix_gamatoto_crash": edits.fixes.Fixes.fix_gamatoto_crash, "fix_ototo_crash": edits.fixes.Fixes.fix_ototo_crash, "fix_time_errors": edits.fixes.Fixes.fix_time_errors, "unlock_equip_menu": edits.basic_items.BasicItems.unlock_equip_menu, "fix_officer_pass_crash": core.OfficerPass.fix_crash, }, "other": { "unlocked_slots": edits.basic_items.BasicItems.edit_unlocked_slots, "reset_gambling_events": core.GamblingEvent.reset_events, "restart_pack": edits.basic_items.BasicItems.set_restart_pack, "special_skills": edits.basic_items.BasicItems.edit_special_skills, "playtime": core.game.catbase.playtime.edit, "enemy_guide": edits.enemy_editor.EnemyEditor.edit_enemy_guide, "user_rank_rewards": core.game.catbase.user_rank_rewards.edit_user_rank_rewards, "unlock_equip_menu": edits.basic_items.BasicItems.unlock_equip_menu, "gold_pass": core.game.catbase.nyanko_club.NyankoClub.edit_gold_pass, "medals": core.game.catbase.medals.Medals.edit_medals, "missions": core.game.catbase.mission.Missions.edit_missions, }, "config": core.core_data.config.edit_config, "update_external": core.update_external_content, "exit": main.Main.exit_editor, } return features def get_feature(self, feature_path: list[str]): feature_dict = self.get_features() feature = feature_dict for path in feature_path: feature = feature[path] return feature def search_features( self, name: str, current_path: list[str], features: dict[str, Any] | None = None, found_features: dict[tuple[str, ...], int] | None = None, ) -> dict[tuple[str, ...], int]: name = name.lower() if features is None: features = self.get_features() if found_features is None: found_features = {} for feature_name_key, feature in features.items(): feature_name = core.core_data.local_manager.get_key(feature_name_key) path = current_path.copy() path.append(feature_name_key) if isinstance(feature, dict): found_features.update( self.search_features( name, path, feature, # type: ignore found_features, ) ) for alias in core.LocalManager.get_all_aliases(feature_name): if not name: found_features[*path] = 100 break alias = alias.lower() name = name.replace(" ", "") alias = alias.replace(" ", "") if alias in name or name in alias: found_features[*path] = 100 break return found_features def display_features(self, features: list[list[str]]): feature_names: list[str] = [] for feature_name in features: feature_names.append(feature_name[-1]) print() dialog_creator.ListOutput(feature_names, [], "features", {}).display_locale( remove_alias=True ) def select_features( self, features: list[list[str]], current_path: list[str] ) -> list[list[str]]: if features != list(self.get_features().keys()): features.insert(0, ["go_back"]) self.display_features(features) print() usr_input = color.ColoredInput().localize("select_features").strip() selected_features: list[list[str]] = [] if usr_input.isdigit(): usr_input = int(usr_input) if usr_input > len(features): color.ColoredText.localize("invalid_input") elif usr_input < 1: color.ColoredText.localize("invalid_input") else: feature_name_top = features[usr_input - 1] if feature_name_top == ["go_back"]: return [[k] for k in self.get_features().keys()] feature = self.get_feature(feature_name_top) if isinstance(feature, dict): for feature_name in feature.keys(): # type: ignore feature_path: list[str] = current_path.copy() feature_path.extend(feature_name_top + [feature_name]) selected_features.append(feature_path) else: feature_path = current_path.copy() feature_path.extend(feature_name_top) selected_features.append(feature_path) else: feats = self.search_features(usr_input, []) if not feats: color.ColoredText.localize("no_feature_with_name", name=usr_input) kv_map = list(feats.items()) kv_map.sort(key=lambda v: v[1], reverse=True) selected_features = [list(v[0]) for v in kv_map] return selected_features def select_features_run(self): features_dict = self.get_features() features: list[list[str]] = [[k] for k in features_dict.keys()] self.save_file.to_file_thread(self.save_file.get_temp_path()) edits.clear_tutorial.clear_tutorial(self.save_file, False) self.save_file.show_ban_message = False while True: features = self.select_features(features, []) new_features: list[list[str]] = [] found_strs: list[str] = [] for feature_ in features: if feature_[-1] in found_strs: continue found_strs.append(feature_[-1]) new_features.append(feature_) features = new_features feature = None if len(features) == 1: feature = features[0] if len(features) == 2 and features[0] == ["go_back"]: feature = features[1] if not feature: continue feature = self.get_feature(feature) if isinstance(feature, Callable): self.do_save_actions() feature(self.save_file) self.save_file.to_file_thread(self.save_file.get_temp_path()) features_dict = self.get_features() features = [[k] for k in features_dict.keys()] core.core_data.game_data_getter = None # reset game data getter so that if an old version is removed, it will download the new version def do_save_actions(self): if core.core_data.config.get_bool(core.ConfigKey.CLEAR_TUTORIAL_ON_LOAD): edits.clear_tutorial.clear_tutorial(self.save_file, False) if core.core_data.config.get_bool(core.ConfigKey.REMOVE_BAN_MESSAGE_ON_LOAD): self.save_file.show_ban_message = False ================================================ FILE: src/bcsfe/cli/file_dialog.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color, dialog_creator class FileDialog: def load_tk(self): try: import tkinter as tk from tkinter import filedialog self.tk = tk self.filedialog = filedialog except ImportError: self.tk = None self.filedialog = None def __init__(self): self.load_tk() if self.tk is not None: try: self.root = self.tk.Tk() except self.tk.TclError: self.tk = None self.filedialog = None return self.root.withdraw() self.root.wm_attributes("-topmost", 1) # type: ignore def select_files_in_dir( self, path: core.Path, ignore_json: bool ) -> str | None: """Print current files in directory. Args: path (core.Path): Path to directory. """ color.ColoredText.localize("current_files_dir", dir=path) path.generate_dirs() files = path.get_files() if not files: color.ColoredText.localize("no_files_dir") files.sort(key=lambda file: file.basename()) # remove files with .json extension if ignore_json: files = [file for file in files if file.get_extension() != "json"] files_str_ls = [file.basename() for file in files] options = files_str_ls + [core.localize("other_dir"), core.localize("another_path")] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_files_dir", single_choice=True, localize_options=False, ).single_choice() if choice is None: return choice -= 1 if choice == len(files): path_input = color.ColoredInput().localize("enter_path_dir") path_obj = core.Path(path_input) if path_obj.is_relative(): path_obj = path.add(path_obj) if not path_obj.exists(): color.ColoredText.localize("path_not_exists", path=path_obj) return self.select_files_in_dir(path, ignore_json) return self.select_files_in_dir(path_obj, ignore_json) if choice == len(files) + 1: path_input = color.ColoredInput().localize("enter_path") return path_input or None return files[choice].to_str() def use_tk(self) -> bool: return ( self.tk is not None and self.filedialog is not None and core.core_data.config.get_bool(core.ConfigKey.USE_FILE_DIALOG) ) def get_file( self, title: str, initialdir: str, initialfile: str, filetypes: list[tuple[str, str]] | None = None, ignore_json: bool = False, ) -> str | None: if filetypes is None: filetypes = [] title = core.core_data.local_manager.get_key(title) color.ColoredText.localize(title) if not self.use_tk(): curr_path = core.Path(initialdir).add(initialfile) file = self.select_files_in_dir(curr_path.parent(), ignore_json) if file is None: return None path_obj = core.Path(file) if path_obj.exists(): return file color.ColoredText.localize("path_not_exists", path=path_obj) return None return ( self.filedialog.askopenfilename( # type: ignore title=title, filetypes=filetypes, initialdir=initialdir, initialfile=initialfile, ) or None ) def save_file( self, title: str, initialdir: str, initialfile: str, filetypes: list[tuple[str, str]] | None = None, ) -> str | None: """Save file dialog Args: title (str): Title of dialog. filetypes (list[tuple[str, str]] | None, optional): File types. Defaults to None. initialdir (str, optional): Initial directory. Defaults to "". initialfile (str, optional): Initial file. Defaults to "". Returns: str | None: Path to file. """ if filetypes is None: filetypes = [] title = core.core_data.local_manager.get_key(title) color.ColoredText.localize(title) if not self.use_tk(): def_path = core.Path(initialdir).add(initialfile).to_str() path = color.ColoredInput().localize( "enter_path_default", default=def_path ) return path.strip().strip("'").strip('"') if path else def_path return ( self.filedialog.asksaveasfilename( # type: ignore title=title, filetypes=filetypes, initialdir=initialdir, initialfile=initialfile, ) or None ) ================================================ FILE: src/bcsfe/cli/main.py ================================================ from __future__ import annotations """Main class for the CLI.""" import sys import traceback from typing import Any, NoReturn from bcsfe.cli import ( file_dialog, color, feature_handler, save_management, dialog_creator, ) from bcsfe import core class Main: """Main class for the CLI.""" def __init__(self): self.save_file = None self.exit = False self.save_path = None self.fh = None def wipe_temp_save(self): """Wipe the temp save.""" core.SaveFile.get_temp_path().remove() def main(self, input_path: str | None = None): """Main function for the CLI.""" self.wipe_temp_save() core.GameDataGetter.delete_old_versions(5) self.check_update() print() self.print_start_text() while not self.exit: stop = self.load_save_options(input_path) if stop: break def version_check(self, v1: str, v2: str) -> bool: v1_p = v1.split(".") v2_p = v2.split(".") for p1, p2 in zip(v1_p, v2_p): if p1.isdigit(): p1 = int(p1) else: continue if p2.isdigit(): p2 = int(p2) else: continue if p1 > p2: return True if p1 < p2: return False return len(v1_p) > len(v2_p) def check_update(self): """Check for updates.""" updater = core.Updater() has_pre_release = updater.has_enabled_pre_release() local_version = updater.get_local_version() latest_version = updater.get_latest_version(has_pre_release) if latest_version is None: color.ColoredText.localize("update_check_fail") return color.ColoredText.localize( "version_line", local_version=local_version, latest_version=latest_version, ) is_local_beta = "b" in local_version is_latest_beta = "b" in latest_version local_no_beta = local_version.split("b")[0] latest_no_beta = latest_version.split("b")[0] if self.version_check(latest_no_beta, local_no_beta): update_needed = True elif self.version_check(local_no_beta, latest_no_beta): update_needed = False else: if latest_version == local_version: update_needed = False else: if is_local_beta and is_latest_beta: update_needed = self.version_check( latest_version.replace("b", "."), local_version.replace("b", "."), ) elif is_local_beta: update_needed = True else: update_needed = False show_message = core.core_data.config.get(core.ConfigKey.SHOW_UPDATE_MESSAGE) if not show_message: update_needed = False if update_needed: update = dialog_creator.YesNoInput(True).get_input_once( "update_available", {"latest_version": latest_version} ) if update is None: return if update: if updater.update(latest_version): color.ColoredText.localize("update_success") else: color.ColoredText.localize("update_fail") sys.exit() else: disable_message = dialog_creator.YesNoInput(False).get_input_once( "disable_update_message" ) if disable_message is None: return core.core_data.config.set( core.ConfigKey.SHOW_UPDATE_MESSAGE, not disable_message ) def print_start_text(self): external_theme = core.ExternalThemeManager.get_external_theme_config() external_locale = core.ExternalLocaleManager.get_external_locale_config() if external_theme is None: theme_text = core.core_data.local_manager.get_key( "theme_text", theme_path=core.ThemeHandler.get_theme_path( core.core_data.theme_manager.theme_code ), theme_version=core.core_data.theme_manager.get_version(), theme_author=core.core_data.theme_manager.get_author(), theme_name=core.core_data.theme_manager.get_name(), escape=False, ) else: theme_text = core.core_data.local_manager.get_key( "theme_text", theme_name=external_theme.name, theme_version=external_theme.version, theme_author=external_theme.author, theme_path=core.ThemeHandler.get_theme_path( external_theme.get_full_name() ), escape=False, ) if external_locale is None: authors = core.core_data.local_manager.authors locale_text = core.core_data.local_manager.get_key( "default_locale_text_authors", path=core.core_data.local_manager.path, authors=", ".join(authors), name=core.core_data.local_manager.name, escape=False, ) else: locale_text = core.core_data.local_manager.get_key( "locale_text", locale_name=external_locale.name, locale_version=external_locale.version, locale_author=external_locale.author, locale_path=core.LocalManager.get_locale_folder( external_locale.get_full_name() ), escape=False, ) color.ColoredText.localize( "welcome", config_path=core.core_data.config.get_config_path(), locale_text=locale_text, theme_text=theme_text, escape=False, ) print() def load_save_options(self, input_path: str | None = None): """Load save options.""" save_file, stop = save_management.SaveManagement.select_save(True, input_path) if save_file is None: return stop self.save_file = save_file color.ColoredText.localize( "current_save", inquiry_code=save_file.inquiry_code[:4] + "***" + save_file.inquiry_code[-2:], gv=save_file.game_version, cc=save_file.cc, ) self.feature_handler() return False def feature_handler(self): """Run the feature handler.""" if self.save_file is None: return self.fh = feature_handler.FeatureHandler(self.save_file) self.fh.select_features_run() @staticmethod def save_save_dialog(save_file: core.SaveFile) -> core.Path | None: """Save save file dialog. Args: save_file (core.SaveFile): Save file to save. Returns: core.Path: Path to save file. """ path = file_dialog.FileDialog().save_file( "save_save_dialog", initialdir=core.SaveFile.get_saves_path().to_str(), initialfile="SAVE_DATA", ) if path is None: return None path = core.Path(path) path.parent().generate_dirs() save_file.save_path = path return path @staticmethod def save_json_dialog(json_data: dict[str, Any]) -> core.Path | None: """Save json file dialog. Args: json_data (dict): Json data to save. Returns: core.Path: Path to save file. """ path = file_dialog.FileDialog().save_file( "save_json_dialog", initialfile="SAVE_DATA.json", initialdir=core.SaveFile.get_saves_path().to_str(), ) if path is None: return None path = core.Path(path) path.parent().generate_dirs() core.JsonFile.from_object(json_data).to_data().to_file(path) return path @staticmethod def load_save_file() -> core.Path | None: """Load save file from file dialog. Returns: core.Path: Path to save file. """ path = file_dialog.FileDialog().get_file( "select_save_file", initialdir=core.SaveFile.get_saves_path().to_str(), initialfile="SAVE_DATA", ignore_json=True, ) if path is None: return None path = core.Path(path) return path @staticmethod def load_save_data_json() -> tuple[core.Path, core.CountryCode] | None: """Load save data from json file. Returns: core.Path: Path to save file. """ path = file_dialog.FileDialog().get_file( "load_save_data_json", initialfile="SAVE_DATA.json", initialdir=core.SaveFile.get_saves_path().to_str(), ) if path is None: return None path = core.Path(path) if not path.exists(): return None try: json_data = core.JsonFile.from_data(path.read()).to_object() except (core.JSONDecodeError, UnicodeDecodeError): color.ColoredText.localize("parse_json_fail") return None try: save_file = core.SaveFile.from_dict(json_data) except core.SaveError: color.ColoredText.localize( "load_json_fail", error=core.core_data.logger.get_traceback() ) return None path = Main.save_save_dialog(save_file) if path is None: return None save_file.to_file(path) return path, save_file.cc @staticmethod def exit_editor( save_file: core.SaveFile | None = None, check_temp: bool = True ) -> NoReturn: """Exit the editor.""" save_file_temp = None if check_temp: temp_path = core.SaveFile.get_temp_path() if temp_path.exists(): try: save_file_temp = core.SaveFile(temp_path.read()) except core.SaveError as e: tb = traceback.format_exc() color.ColoredText.localize( "save_temp_fail", error=str(e), traceback=tb ) Main.leave() if save_file is None: save_file = save_file_temp if save_file is None: if check_temp: color.ColoredText.localize("save_temp_not_found") Main.leave() if save_file_temp is None: save_file_temp = save_file try: print() color.ColoredText.localize("checking_for_changes") if save_file.save_path is None: same = False else: same = save_file.save_path.read() == save_file.to_data() except core.SaveError: same = False if not same: color.ColoredText.localize("changes_found") print() save = color.ColoredInput().localize("save_before_exit") == "y" if save: save_management.SaveManagement.save_save(save_file) else: color.ColoredText.localize("no_changes") Main.leave() @staticmethod def leave() -> NoReturn: """Leave the editor.""" color.ColoredText.localize("leave") sys.exit() ================================================ FILE: src/bcsfe/cli/recent_saves.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core import datetime import json from bcsfe.cli import color, dialog_creator class RecentSave: def __init__( self, path: core.Path, cc: core.CountryCode, gv: core.GameVersion, inquiry: str, time: datetime.datetime, name: core.Path, ): self.path = path self.cc = cc self.gv = gv self.inquiry = inquiry self.time = time self.name = name @staticmethod def from_dict(data: dict[str, Any]) -> RecentSave | None: path = data.get("path") cc = data.get("cc") gv = data.get("gv") inquiry = data.get("inquiry") time_stamp = data.get("timestamp") name = data.get("name") if ( path is None or cc is None or gv is None or inquiry is None or time_stamp is None or name is None ): return None return RecentSave( core.Path(path), core.CountryCode(cc), core.GameVersion.from_string(gv), inquiry, datetime.datetime.fromtimestamp(time_stamp), core.Path(name), ) def to_dict(self) -> dict[str, Any]: return { "path": self.path.to_str(), "cc": self.cc.get_code(), "gv": self.gv.to_string(), "inquiry": self.inquiry, "timestamp": self.time.timestamp(), "name": self.name.to_str(), } class RecentSaves: def __init__(self, saves: list[RecentSave]): self.saves = saves @staticmethod def from_json(data: list[dict[str, Any]]) -> RecentSaves: res: list[RecentSave] = [] for item in data: save = RecentSave.from_dict(item) if save is not None: res.append(save) return RecentSaves(res) def to_json(self) -> list[dict[str, Any]]: return [save.to_dict() for save in self.saves][-10:] # only store 10 @staticmethod def from_path(path: core.Path) -> RecentSaves | None: json_data = json.loads(path.read().to_str()) return RecentSaves.from_json(json_data) def to_path(self, path: core.Path): data = json.dumps(self.to_json(), indent=4) try: path.write(core.Data(data)) except Exception as e: print(e) @staticmethod def read_default() -> RecentSaves: path = RecentSaves.get_path() if path.exists(): return RecentSaves.from_path(path) or RecentSaves([]) return RecentSaves([]) @staticmethod def get_path() -> core.Path: return core.Path.get_data_folder().add("recent_saves.json") def save_default(self): path = RecentSaves.get_path() self.to_path(path) def select(self) -> RecentSave | None: if not self.saves: color.ColoredText.localize("no_recent_saves") return None items: list[str] = [] for save in self.saves: items.append( core.localize( "recent_save", path=save.path, cc=save.cc, gv=save.gv, inquiry_code=save.inquiry, year=save.time.year, month=str(save.time.month).zfill(2), day=str(save.time.day).zfill(2), hour=str(save.time.hour).zfill(2), minute=str(save.time.minute).zfill(2), second=str(save.time.second).zfill(2), name=save.name, ) ) items.reverse() resp = dialog_creator.ChoiceInput.from_reduced( items, localize_options=False, dialog="select_recent" ).single_choice() if resp is None: return None resp = len(self.saves) - resp return self.saves[resp] ================================================ FILE: src/bcsfe/cli/save_management.py ================================================ from __future__ import annotations import datetime from bcsfe import core import bcsfe from bcsfe.core import io from bcsfe.cli import main, color, dialog_creator, server_cli, recent_saves from bcsfe.core.country_code import CountryCode from bcsfe.core.io.config import ConfigKey class SaveManagement: def __init__(self): pass @staticmethod def save_save(save_file: core.SaveFile, check_strict: bool = True): """Save the save file without a dialog. Args: save_file (core.SaveFile): The save file to save. """ SaveManagement.upload_items_checker(save_file, check_strict) if save_file.save_path is None: save_file.save_path = main.Main.save_save_dialog(save_file) if save_file.save_path is None: return try: save_file.to_file(save_file.save_path) except OSError as e: print(e) return color.ColoredText.localize("save_success", path=save_file.save_path) @staticmethod def save_save_dialog(save_file: core.SaveFile): """Save the save file with a dialog. Args: save_file (core.SaveFile): The save file to save. """ SaveManagement.upload_items_checker(save_file) save_file.save_path = main.Main.save_save_dialog(save_file) if save_file.save_path is None: return save_file.to_file(save_file.save_path) color.ColoredText.localize("save_success", path=save_file.save_path) @staticmethod def save_save_data_dir(save_file: core.SaveFile): """Save the save file to the data folder. Args: save_file (core.SaveFile): The save file to save. """ SaveManagement.upload_items_checker(save_file) save_file.save_path = core.SaveFile.get_save_path() save_file.to_file(save_file.save_path) color.ColoredText.localize("save_success", path=save_file.save_path) @staticmethod def save_upload(save_file: core.SaveFile): """Save the save file and upload it to the server. Args: save_file (core.SaveFile): The save file to save. """ if core.core_data.config.get_bool(core.ConfigKey.STRICT_BAN_PREVENTION): color.ColoredText.localize("strict_ban_prevention_enabled") SaveManagement.create_new_account(save_file) result = core.ServerHandler(save_file).get_codes() if result is not None: SaveManagement.save_save(save_file, check_strict=False) transfer_code, confirmation_code = result color.ColoredText.localize( "upload_result", transfer_code=transfer_code, confirmation_code=confirmation_code, ) else: color.ColoredText.localize("upload_fail") SaveManagement.save_save(save_file, check_strict=False) @staticmethod def unban_account(save_file: core.SaveFile): """Unban the account. Args: save_file (core.SaveFile): The save file to unban. """ server_handler = core.ServerHandler(save_file) success = server_handler.create_new_account() if success: color.ColoredText.localize("unban_success") else: color.ColoredText.localize("unban_fail") @staticmethod def create_new_account(save_file: core.SaveFile): """Create a new account. Args: save_file (core.SaveFile): The save file to create a new account. """ server_handler = core.ServerHandler(save_file) success = server_handler.create_new_account() if success: color.ColoredText.localize("create_new_account_success") else: color.ColoredText.localize("create_new_account_fail") @staticmethod def waydroid_push(save_file: core.SaveFile) -> core.WayDroidHandler | None: SaveManagement.save_save(save_file) try: waydroid_handler = core.WayDroidHandler() except core.AdbNotInstalled as e: core.AdbHandler.display_no_adb_error(e) return None except core.io.waydroid.WayDroidNotInstalledError as e: core.WayDroidHandler.display_waydroid_not_installed(e) return None if not waydroid_handler.adb_handler.select_device(): return None if save_file.used_storage and save_file.package_name is not None: waydroid_handler.set_package_name(save_file.package_name) else: packages = waydroid_handler.get_battlecats_packages() package_name = SaveManagement.select_package_name(packages) if package_name is None: color.ColoredText.localize("no_package_name_error") return waydroid_handler waydroid_handler.set_package_name(package_name) if save_file.save_path is None: return waydroid_handler result = waydroid_handler.load_battlecats_save(save_file.save_path) if result.success: color.ColoredText.localize("waydroid_push_success") else: color.ColoredText.localize("waydroid_push_fail", error=result.result) return waydroid_handler @staticmethod def waydroid_push_rerun(save_file: core.SaveFile) -> core.AdbHandler | None: waydroid_handler = SaveManagement.waydroid_push(save_file) if not waydroid_handler: return if waydroid_handler.package_name is None: return result = waydroid_handler.rerun_game() if result.success: color.ColoredText.localize("waydroid_rerun_success") else: color.ColoredText.localize("waydroid_rerun_fail", error=result.result) @staticmethod def adb_push(save_file: core.SaveFile) -> core.AdbHandler | None: """Push the save file to the device. Args: save_file (core.SaveFile): The save file to push. Returns: core.AdbHandler: The AdbHandler instance. """ SaveManagement.save_save(save_file) try: adb_handler = core.AdbHandler() except core.AdbNotInstalled as e: core.AdbHandler.display_no_adb_error(e) return None success = adb_handler.select_device() if not success: return adb_handler if save_file.used_storage and save_file.package_name is not None: adb_handler.set_package_name(save_file.package_name) else: packages = adb_handler.get_battlecats_packages() package_name = SaveManagement.select_package_name(packages) if package_name is None: color.ColoredText.localize("no_package_name_error") return adb_handler adb_handler.set_package_name(package_name) if save_file.save_path is None: return adb_handler result = adb_handler.load_battlecats_save(save_file.save_path) if result.success: color.ColoredText.localize("adb_push_success") else: color.ColoredText.localize("adb_push_fail", error=result.result) return adb_handler @staticmethod def root_push(save_file: core.SaveFile) -> core.RootHandler | None: """Push the save file to the device. Args: save_file (core.SaveFile): The save file to push. Returns: core.AdbHandler: The AdbHandler instance. """ SaveManagement.save_save(save_file) root_handler = core.RootHandler() if not root_handler.is_android(): color.ColoredText.localize("root_push_not_android_error") return None if not root_handler.is_rooted(): color.ColoredText.localize("not_rooted_error") return None if save_file.used_storage and save_file.package_name is not None: root_handler.set_package_name(save_file.package_name) else: packages = root_handler.get_battlecats_packages() package_name = SaveManagement.select_package_name(packages) if package_name is None: color.ColoredText.localize("no_package_name_error") return root_handler root_handler.set_package_name(package_name) if save_file.save_path is None: return root_handler result = root_handler.load_battlecats_save(save_file.save_path) if result.success: color.ColoredText.localize("root_push_success") else: color.ColoredText.localize("root_push_fail", error=result.result) return root_handler @staticmethod def adb_push_rerun(save_file: core.SaveFile): """Push the save file to the device and rerun the game. Args: save_file (core.SaveFile): The save file to push. """ adb_handler = SaveManagement.adb_push(save_file) if not adb_handler: return if adb_handler.package_name is None: return result = adb_handler.rerun_game() if result.success: color.ColoredText.localize("adb_rerun_success") else: color.ColoredText.localize("adb_rerun_fail", error=result.result) @staticmethod def root_push_rerun(save_file: core.SaveFile): """Push the save file to the device and rerun the game. Args: save_file (core.SaveFile): The save file to push. """ root_handler = SaveManagement.root_push(save_file) if not root_handler: return if root_handler.package_name is None: return result = root_handler.rerun_game() if result.success: color.ColoredText.localize("root_rerun_success") else: color.ColoredText.localize("root_rerun_fail", error=result.result) @staticmethod def export_save(save_file: core.SaveFile): """Export the save file to a json file. Args: save_file (core.SaveFile): The save file to export. """ data = save_file.to_dict() path = main.Main.save_json_dialog(data) if path is None: return data = core.JsonFile.from_object(data).to_data() data.to_file(path) color.ColoredText.localize("export_success", path=path) @staticmethod def init_save(save_file: core.SaveFile): """Initialize the save file to a new save file. Args: save_file (core.SaveFile): The save file to initialize. """ confirm = dialog_creator.YesNoInput().get_input_once("init_save_confirm") if not confirm: return save_file.init_save(save_file.game_version) color.ColoredText.localize("init_save_success") @staticmethod def upload_items(save_file: core.SaveFile, check_strict: bool = True): """Upload the items to the server. Args: save_file (core.SaveFile): The save file to upload. """ if ( core.core_data.config.get_bool(core.ConfigKey.STRICT_BAN_PREVENTION) and check_strict ): color.ColoredText.localize("strict_ban_prevention_enabled") SaveManagement.create_new_account(save_file) server_handler = core.ServerHandler(save_file) success = server_handler.upload_meta_data() if success: color.ColoredText.localize("upload_items_success") else: color.ColoredText.localize("upload_items_fail") @staticmethod def upload_items_checker(save_file: core.SaveFile, check_strict: bool = True): managed_items = core.BackupMetaData(save_file).get_managed_items() if not managed_items: return should_upload = dialog_creator.YesNoInput().get_input_once( "upload_items_checker_confirm" ) if not should_upload: return SaveManagement.upload_items(save_file, check_strict) @staticmethod def select_save( starting_options: bool = False, input_file: str | None = None ) -> tuple[core.SaveFile | None, bool]: """Select a new save file. Args: starting_options (bool, optional): Whether to add the starting specific options. Defaults to False. Returns: core.SaveFile | None: The save file. """ if input_file is not None: file = SaveManagement.load_save_file_path( core.Path(input_file), None, False, None ) if file is None: return (None, True) return (file[0], False) options = [ "download_save", "select_save_file", core.localize("load_from_documents", path=core.SaveFile.get_save_path()), "adb_pull_save", "load_save_data_json", ] if starting_options: options.append("edit_config") options.append("update_external") options.append("exit") use_waydroid = core.core_data.config.get_bool(ConfigKey.USE_WAYDROID) if use_waydroid: options[3] = "waydroid_pull_save" root_handler = io.root_handler.RootHandler() if root_handler.is_android(): options[3] = "root_storage_pull_save" choice = dialog_creator.ChoiceInput( options, options, [], {}, "save_load_option", True ).get_input_locale_while() if choice is None: return None, False choice = choice[0] - 1 save_path = None cc: core.CountryCode | None = None used_storage = False package_name = None if choice == 0: data = server_cli.ServerCLI().download_save() if data is not None: save_path, cc = data else: save_path = None elif choice == 1: save_path = main.Main.load_save_file() elif choice == 2: save_path = core.SaveFile.get_saves_path().add("SAVE_DATA") if not save_path.exists(): color.ColoredText.localize("save_file_not_found") return None, False elif choice == 3: handler = root_handler if not root_handler.is_android(): if use_waydroid: try: handler = core.WayDroidHandler() except core.AdbNotInstalled as e: core.AdbHandler.display_no_adb_error(e) return None, False except core.io.waydroid.WayDroidNotInstalledError as e: core.WayDroidHandler.display_waydroid_not_installed(e) return None, False if not handler.adb_handler.select_device(): return None, False else: try: handler = core.AdbHandler() except core.AdbNotInstalled as e: core.AdbHandler.display_no_adb_error(e) return None, False if not handler.select_device(): return None, False elif not root_handler.is_rooted(): color.ColoredText.localize("not_rooted_error") return None, False package_names = handler.get_battlecats_packages() package_name = SaveManagement.select_package_name(package_names) if package_name is None: color.ColoredText.localize("no_package_name_error") return None, False handler.set_package_name(package_name) if root_handler.is_android(): key = "storage_pulling" else: if use_waydroid: key = "waydroid_pulling" else: key = "adb_pulling" color.ColoredText.localize(key, package_name=package_name) save_path, result = handler.save_locally() if save_path is None: if root_handler.is_android(): key = "storage_pull_fail" else: if use_waydroid: key = "waydroid_pull_fail" else: key = "adb_pull_fail" color.ColoredText.localize( key, package_name=package_name, error=result.result, ) else: used_storage = True elif choice == 4: data = main.Main.load_save_data_json() if data is not None: save_path, cc = data else: save_path = None # elif choice == 5: # recent_save = recent_saves.RecentSaves.read_default().select() # if recent_save is None: # save_path = None # else: # save_path = recent_save.path # cc = recent_save.cc # elif choice == 5: # color.ColoredText.localize("create_new_save_warning") # cc = core.CountryCode.select() # if cc is None: # return None, False # try: # gv = core.GameVersion.from_string( # color.ColoredInput().localize( # "game_version_dialog", # ) # ) # except ValueError: # color.ColoredText.localize("invalid_game_version") # return None, False # save = core.SaveFile(cc=cc, gv=gv, load=False) # save_path = main.Main.save_save_dialog(save) # if save_path is None: # return None, False # save.to_file(save_path) # color.ColoredText.localize("create_new_save_success") elif choice == 5 and starting_options: core.core_data.config.edit_config() elif choice == 6 and starting_options: core.update_external_content() elif choice == 7 and starting_options: main.Main.exit_editor(check_temp=False) if save_path is None or not save_path.exists(): return None, False save = SaveManagement.load_save_file_path( save_path, cc, used_storage, package_name ) if save is None: return (None, False) save, backup_path = save if choice != 5: recent_s = recent_saves.RecentSaves.read_default() recent_s.saves.append( recent_saves.RecentSave( backup_path, save.cc, save.game_version, save.inquiry_code, datetime.datetime.now(), save_path, ) ) recent_s.save_default() return ( save, False, ) @staticmethod def load_save_file_path( save_path: core.Path, cc: CountryCode | None, used_storage: bool, package_name: str | None = None, ) -> tuple[core.SaveFile, core.Path] | None: color.ColoredText.localize("save_file_found", path=save_path) data = save_path.read() try: save_file = core.SaveFile(data, cc, package_name=package_name) except core.CantDetectSaveCCError: color.ColoredText.localize("cant_detect_cc") cc = core.CountryCode.select() if cc is None: return None try: save_file = core.SaveFile(data, cc) except Exception: tb = core.core_data.logger.get_traceback() data.reset_pos() color.ColoredText.localize( "parse_save_error", error=tb, version=bcsfe.__version__, game_version=data.read_int(), country_code=cc.get_code(), ) return None except Exception: tb = core.core_data.logger.get_traceback() save_file2 = core.SaveFile(data, cc, load=False) data.reset_pos() color.ColoredText.localize( "parse_save_error", error=tb, version=bcsfe.__version__, game_version=data.read_int(), country_code=save_file2.cc, ) return None save_file.save_path = save_path backup_path = save_file.get_default_path() try: save_file.save_path.copy_thread(backup_path) except Exception as e: print(e) save_file.used_storage = used_storage return save_file, backup_path @staticmethod def select_package_name(package_names: list[str]) -> str | None: choice = dialog_creator.ChoiceInput.from_reduced( package_names, dialog="select_package_name", single_choice=True, localize_options=False, ).single_choice() if choice is None: return None return package_names[choice - 1] @staticmethod def load_save(save_file: core.SaveFile): """Load a new save file. Args: save_file (core.SaveFile): The current save file. """ SaveManagement.upload_items_checker(save_file) new_save_file, stop = SaveManagement.select_save() if new_save_file is None: return stop save_file.load_save_file(new_save_file) core.core_data.init_data() color.ColoredText.localize("load_save_success") return False @staticmethod def convert_save_cc(save_file: core.SaveFile): color.ColoredText.localize("cc_warning", current=save_file.cc) ccs_to_select = core.CountryCode.get_all() cc = core.CountryCode.select_from_ccs(ccs_to_select) if cc is None: return save_file.set_cc(cc) core.ServerHandler(save_file).create_new_account() core.core_data.init_data() color.ColoredText.localize("country_code_set", cc=cc) @staticmethod def convert_save_gv(save_file: core.SaveFile): color.ColoredText.localize( "gv_warning", current=save_file.game_version.to_string() ) try: gv = core.GameVersion.from_string( color.ColoredInput().localize("game_version_dialog").strip() ) except ValueError: color.ColoredText.localize("invalid_game_version") return save_file.set_gv(gv) core.core_data.init_data() color.ColoredText.localize("game_version_set", version=gv.to_string()) ================================================ FILE: src/bcsfe/cli/server_cli.py ================================================ from __future__ import annotations from bcsfe.cli import dialog_creator, main, color, file_dialog from bcsfe import core class ServerCLI: def __init__(self): pass def download_save( self, ) -> tuple[core.Path, core.CountryCode] | None: transfer_code = dialog_creator.StringInput().get_input_locale_while( "enter_transfer_code", {} ) if transfer_code is None: return None confirmation_code = dialog_creator.StringInput().get_input_locale_while( "enter_confirmation_code", {} ) if confirmation_code is None: return None cc = core.CountryCode.select() if cc is None: return None gv = core.GameVersion(120200) # not important color.ColoredText.localize( "downloading_save_file", transfer_code=transfer_code, confirmation_code=confirmation_code, country_code=cc, ) server_handler, result = core.ServerHandler.from_codes( transfer_code, confirmation_code, cc, gv, ) if server_handler is None and result is not None: color.ColoredText.localize("invalid_codes_error") if dialog_creator.YesNoInput().get_input_once( "display_response_debug_info_q" ): if result.response is not None: color.ColoredText.localize( "response_text_display", url=result.url, request_headers=result.headers, request_body=result.data, response_headers=result.response.headers, response_body=result.response.text, ) return if server_handler is None: return save_file = server_handler.save_file if file_dialog.FileDialog().filedialog is None: path = core.SaveFile.get_saves_path().add("SAVE_DATA") else: path = main.Main().save_save_dialog(save_file) if path is None: return None try: save_file.to_file(path) except OSError as e: print( f"failed to write save file to: {path} due to: {e}. Skipping writing the save file to disk" ) input("press enter to continue anyway") color.ColoredText.localize("save_downloaded", path=path.to_str()) return path, cc ================================================ FILE: src/bcsfe/core/__init__.py ================================================ from __future__ import annotations from typing import Any from requests.exceptions import ConnectionError from requests import Response from json.decoder import JSONDecodeError from bcsfe.cli import color, dialog_creator from bcsfe.core import ( country_code, crypto, game, game_version, io, locale_handler, log, server, theme_handler, max_value_helper, ) from bcsfe.core.country_code import CountryCode, CountryCodeType from bcsfe.core.crypto import Hash, HashAlgorithm, Hmac, NyankoSignature, Random from bcsfe.core.game.battle.battle_items import BattleItems from bcsfe.core.game.battle.cleared_slots import ClearedSlots from bcsfe.core.game.battle.slots import LineUps from bcsfe.core.game.battle.enemy import ( Enemy, EnemyNames, EnemyDescriptions, EnemyDictionary, ) from bcsfe.core.game.catbase.beacon_base import BeaconEventListScene from bcsfe.core.game.catbase.cat import ( Cat, Cats, UnitBuy, TalentData, NyankoPictureBook, StorageItem, ) from bcsfe.core.game.catbase.gambling import GamblingEvent from bcsfe.core.game.catbase.gatya import ( Gatya, GatyaInfos, GatyaDataSet, GatyaDataOptionSet, GatyaDataOption, ) from bcsfe.core.game.catbase.gatya_item import ( GatyaItemBuy, GatyaItemNames, GatyaItemCategory, GatyaItemBuyItem, ) from bcsfe.core.game.catbase.item_pack import ( ItemPack, Purchases, PurchaseSet, PurchasedPack, ) from bcsfe.core.game.catbase.login_bonuses import LoginBonus from bcsfe.core.game.catbase.matatabi import Matatabi from bcsfe.core.game.catbase.drop_chara import CharaDrop from bcsfe.core.game.catbase.medals import Medals, MedalNames from bcsfe.core.game.catbase.mission import ( Missions, MissionNames, MissionConditions, ) from bcsfe.core.game.catbase.my_sale import MySale from bcsfe.core.game.catbase.nyanko_club import NyankoClub from bcsfe.core.game.catbase.officer_pass import OfficerPass from bcsfe.core.game.catbase.powerup import PowerUpHelper from bcsfe.core.game.catbase.scheme_items import SchemeItems from bcsfe.core.game.catbase.special_skill import ( SpecialSkills, SpecialSkill, AbilityData, AbilityDataItem, ) from bcsfe.core.game.catbase.stamp import StampData from bcsfe.core.game.catbase.talent_orbs import ( TalentOrb, TalentOrbs, OrbInfo, OrbInfoList, RawOrbInfo, SaveOrb, SaveOrbs, ) from bcsfe.core.game.catbase.unlock_popups import ( UnlockPopups, UnlockPopupData, UnlockPopupLine, ) from bcsfe.core.game.catbase.upgrade import Upgrade from bcsfe.core.game.catbase.user_rank_rewards import ( UserRankRewards, RankGifts, RankGiftDescriptions, ) from bcsfe.core.game.catbase.playtime import PlayTime from bcsfe.core.game.gamoto.base_materials import BaseMaterials from bcsfe.core.game.gamoto.cat_shrine import CatShrine, CatShrineLevels from bcsfe.core.game.gamoto.gamatoto import ( Gamatoto, GamatotoLevels, GamatotoMembersName, ) from bcsfe.core.game.gamoto.ototo import Ototo from bcsfe.core.game.localizable import Localizable from bcsfe.core.game.map.aku import AkuChapters from bcsfe.core.game.map.challenge import ChallengeChapters from bcsfe.core.game.map.chapters import Chapters from bcsfe.core.game.map.dojo import Dojo from bcsfe.core.game.map.enigma import Enigma from bcsfe.core.game.map.event import EventChapters from bcsfe.core.game.map.ex_stage import ExChapters from bcsfe.core.game.map.gauntlets import GauntletChapters from bcsfe.core.game.map.item_reward_stage import ItemRewardChapters from bcsfe.core.game.map.legend_quest import LegendQuestChapters from bcsfe.core.game.map.map_reset import MapResets from bcsfe.core.game.map.outbreaks import Outbreaks from bcsfe.core.game.map.story import StoryChapters, TreasureText, StageNames from bcsfe.core.game.map.timed_score import TimedScoreChapters from bcsfe.core.game.map.tower import TowerChapters from bcsfe.core.game.map.uncanny import UncannyChapters from bcsfe.core.game.map.zero_legends import ZeroLegendsChapters from bcsfe.core.game.map.map_names import MapNames from bcsfe.core.game.map.map_option import MapOption from bcsfe.core.game_version import GameVersion from bcsfe.core.io.adb_handler import AdbHandler, AdbNotInstalled from bcsfe.core.io.waydroid import WayDroidHandler from bcsfe.core.io.bc_csv import CSV, Delimeter, Row from bcsfe.core.io.command import Command, CommandResult from bcsfe.core.io.config import Config, ConfigKey from bcsfe.core.io.data import Data from bcsfe.core.io.json_file import JsonFile from bcsfe.core.io.path import Path from bcsfe.core.io.save import SaveError, SaveFile, CantDetectSaveCCError from bcsfe.core.io.thread_helper import thread_run_many, Thread from bcsfe.core.io.yaml import YamlFile from bcsfe.core.io.git_handler import GitHandler, Repo from bcsfe.core.io.root_handler import RootHandler from bcsfe.core.locale_handler import ( LocalManager, ExternalLocaleManager, ExternalLocale, ) from bcsfe.core.log import Logger from bcsfe.core.server.event_data import ( ServerItemData, ServerItemDataItem, ServerGatyaData, ServerGatyaDataSet, ServerGatyaDataItem, ) from bcsfe.core.server.client_info import ClientInfo from bcsfe.core.server.game_data_getter import GameDataGetter from bcsfe.core.server.headers import AccountHeaders from bcsfe.core.server.managed_item import ( BackupMetaData, ManagedItem, ManagedItemType, ) from bcsfe.core.server.request import RequestHandler, MultiPartFile, MultipartForm from bcsfe.core.server.server_handler import ServerHandler from bcsfe.core.server.updater import Updater from bcsfe.core.theme_handler import ( ThemeHandler, ExternalTheme, ExternalThemeManager, ) from bcsfe.core.max_value_helper import MaxValueHelper, MaxValueType class CoreData: def init_data(self): self.config = Config(config_path, print_config_err) self.logger = Logger(log_path) self.local_manager = LocalManager() self.theme_manager = ThemeHandler() self.max_value_manager = MaxValueHelper() self.game_data_getter: GameDataGetter | None = None self.gatya_item_names: GatyaItemNames | None = None self.gatya_item_buy: GatyaItemBuy | None = None self.chara_drop: CharaDrop | None = None self.gamatoto_levels: GamatotoLevels | None = None self.gamatoto_members_name: GamatotoMembersName | None = None self.localizable: Localizable | None = None self.abilty_data: AbilityData | None = None self.enemy_names: EnemyNames | None = None self.rank_gift_descriptions: RankGiftDescriptions | None = None self.rank_gifts: RankGifts | None = None self.treasure_text: TreasureText | None = None self.cat_shrine_levels: CatShrineLevels | None = None self.medal_names: MedalNames | None = None self.mission_names: MissionNames | None = None self.mission_conditions: MissionConditions | None = None def get_game_data_getter( self, save: SaveFile | None = None, cc: CountryCode | None = None, gv: GameVersion | None = None, ) -> GameDataGetter: if self.game_data_getter is None: if cc is None and save is not None: cc = save.cc if cc is None: raise ValueError("cc must be provided if save is not provided") if gv is None and save is not None: gv = save.game_version if gv is None: raise ValueError("gv must be provided if save is not provided") self.game_data_getter = GameDataGetter(cc, gv) return self.game_data_getter def get_gatya_item_names(self, save: SaveFile) -> GatyaItemNames: if self.gatya_item_names is None: self.gatya_item_names = GatyaItemNames(save) return self.gatya_item_names def get_gatya_item_buy(self, save: SaveFile) -> GatyaItemBuy: if self.gatya_item_buy is None: self.gatya_item_buy = GatyaItemBuy(save) return self.gatya_item_buy def get_chara_drop(self, save: SaveFile) -> CharaDrop: if self.chara_drop is None: self.chara_drop = CharaDrop(save) return self.chara_drop def get_gamatoto_levels(self, save: SaveFile) -> GamatotoLevels: if self.gamatoto_levels is None: self.gamatoto_levels = GamatotoLevels(save) return self.gamatoto_levels def get_gamatoto_members_name(self, save: SaveFile) -> GamatotoMembersName: if self.gamatoto_members_name is None: self.gamatoto_members_name = GamatotoMembersName(save) return self.gamatoto_members_name def get_localizable(self, save: SaveFile) -> Localizable: if self.localizable is None: self.localizable = Localizable(save) return self.localizable def get_ability_data(self, save: SaveFile) -> AbilityData: if self.abilty_data is None: self.abilty_data = AbilityData(save) return self.abilty_data def get_enemy_names(self, save: SaveFile) -> EnemyNames: if self.enemy_names is None: self.enemy_names = EnemyNames(save) return self.enemy_names def get_rank_gift_descriptions(self, save: SaveFile) -> RankGiftDescriptions: if self.rank_gift_descriptions is None: self.rank_gift_descriptions = RankGiftDescriptions(save) return self.rank_gift_descriptions def get_rank_gifts(self, save: SaveFile) -> RankGifts: if self.rank_gifts is None: self.rank_gifts = RankGifts(save) return self.rank_gifts def get_treasure_text(self, save: SaveFile) -> TreasureText: if self.treasure_text is None: self.treasure_text = TreasureText(save) return self.treasure_text def get_cat_shrine_levels(self, save: SaveFile) -> CatShrineLevels: if self.cat_shrine_levels is None: self.cat_shrine_levels = CatShrineLevels(save) return self.cat_shrine_levels def get_medal_names(self, save: SaveFile) -> MedalNames: if self.medal_names is None: self.medal_names = MedalNames(save) return self.medal_names def get_mission_names(self, save: SaveFile) -> MissionNames: if self.mission_names is None: self.mission_names = MissionNames(save) return self.mission_names def get_mission_conditions(self, save: SaveFile) -> MissionConditions: if self.mission_conditions is None: self.mission_conditions = MissionConditions(save) return self.mission_conditions def get_lang(self, save: SaveFile) -> str: return self.get_localizable(save).get_lang() or "en" config_path = None print_config_err = True log_path = None transfer_backup_path = None game_data_path = None def set_config_path(path: Path): global config_path config_path = path def set_game_data_path(path: Path): global game_data_path game_data_path = path def set_log_path(path: Path): global log_path log_path = path def set_transfer_backup_path(path: Path): global transfer_backup_path transfer_backup_path = path def get_transfer_backup_path() -> Path | None: return transfer_backup_path def get_game_data_path() -> Path | None: return game_data_path def update_external_content(_: Any = None): """Updates external content.""" color.ColoredText.localize("updating_external_content") print() ExternalThemeManager.update_all_external_themes() ExternalLocaleManager.update_all_external_locales() core_data.init_data() clear_game_data = dialog_creator.YesNoInput().get_input_once("clear_game_data_q") if clear_game_data is None: return if clear_game_data: GameDataGetter.delete_old_versions(0) color.ColoredText.localize("cleared_game_data") def print_no_internet(): color.ColoredText.localize("no_internet") core_data = CoreData() def localize(key: str, escape: bool = True, **kwargs: Any) -> str: return core_data.local_manager.get_key(key, escape=escape, **kwargs) __all__ = [ "server", "io", "locale_handler", "country_code", "log", "game_version", "crypto", "game", "theme_handler", "max_value_helper", "AdbHandler", "AdbNotInstalled", "CountryCode", "Path", "Data", "CSV", "ServerHandler", "GameVersion", "SaveFile", "JsonFile", "ManagedItem", "ManagedItemType", "BackupMetaData", "Cat", "Upgrade", "PowerUpHelper", "TalentOrb", "TalentOrbs", "OrbInfo", "OrbInfoList", "RawOrbInfo", "SaveOrb", "SaveOrbs", "ConfigKey", "SpecialSkill", "WayDroidHandler", "EnemyDescriptions", "EnemyDictionary", "GatyaItemCategory", "ServerItemData", "GatyaItemBuyItem", "ServerItemDataItem", "ServerGatyaData", "ServerGatyaDataSet", "ServerGatyaDataItem", "GatyaDataOptionSet", "GatyaDataOption", "MaxValueType", "GamblingEvent", "UnitBuy", "NyankoPictureBook", "StorageItem", "OfficerPass", "LocalManager", "MapOption", "CantDetectSaveCCError", "UnlockPopupData", "UnlockPopupLine", ] ================================================ FILE: src/bcsfe/core/country_code.py ================================================ from __future__ import annotations import enum from bcsfe.cli import dialog_creator from bcsfe import core class CountryCodeType(enum.Enum): EN = "en" JP = "jp" KR = "kr" TW = "tw" class CountryCode: def __init__(self, cc: str | CountryCodeType): self.value = cc.value if isinstance(cc, CountryCodeType) else cc self.value = self.value.lower() def get_code(self) -> str: return self.value def get_client_info_code(self) -> str: code = self.get_code() if code == "jp": return "ja" return code def get_patching_code(self) -> str: code = self.get_code() if code == "jp": return "" return code @staticmethod def from_patching_code(code: str) -> CountryCode: if code == "": return CountryCode(CountryCodeType.JP) return CountryCode(code) @staticmethod def from_code(code: str) -> CountryCode: return CountryCode(code) @staticmethod def get_all() -> list["CountryCode"]: return [CountryCode(cc) for cc in CountryCodeType] @staticmethod def get_all_str() -> list[str]: ccts = CountryCode.get_all() return [cc.get_code() for cc in ccts] def __str__(self) -> str: return self.get_code() def __repr__(self) -> str: return self.get_code() def copy(self) -> CountryCode: return self @staticmethod def select() -> CountryCode | None: index = dialog_creator.ChoiceInput.from_reduced( CountryCode.get_all_str(), dialog="country_code_select", single_choice=True, ).single_choice() if index is None: return None return CountryCode.get_all()[index - 1] @staticmethod def select_from_ccs(ccs: list[CountryCode]) -> CountryCode | None: index = dialog_creator.ChoiceInput.from_reduced( [cc.get_code() for cc in ccs], dialog="country_code_select", single_choice=True, ).single_choice() if index is None: return None return ccs[index - 1] def __eq__(self, o: object) -> bool: if isinstance(o, CountryCode): return self.get_code() == o.get_code() elif isinstance(o, str): return self.get_code() == o elif isinstance(o, CountryCodeType): return self.get_code() == o.value return False def get_cc_lang(self) -> core.CountryCode: if core.core_data.config.get_bool(core.ConfigKey.FORCE_LANG_GAME_DATA): locale = core.core_data.config.get_str(core.ConfigKey.LOCALE) return core.CountryCode.from_code(locale) return self @staticmethod def get_langs() -> list[str]: return ["de", "it", "es", "fr", "th"] def is_lang(self) -> bool: return self.get_code() in CountryCode.get_langs() ================================================ FILE: src/bcsfe/core/crypto.py ================================================ from __future__ import annotations import enum import hashlib import hmac import random from bcsfe import core class HashAlgorithm(enum.Enum): """An enum representing a hash algorithm.""" MD5 = enum.auto() SHA1 = enum.auto() SHA256 = enum.auto() class Hash: """A class to hash data.""" def __init__(self, algorithm: HashAlgorithm): """Initializes a new instance of the Hash class. Args: algorithm (HashAlgorithm): The hash algorithm to use. """ self.algorithm = algorithm def get_hash( self, data: core.Data, length: int | None = None, ) -> core.Data: """Gets the hash of the given data. Args: data (core.Data): The data to hash. length (int | None, optional): The length of the hash. Defaults to None. Raises: ValueError: Invalid hash algorithm. Returns: core.Data: The hash of the data. """ if self.algorithm == HashAlgorithm.MD5: hash = hashlib.md5() elif self.algorithm == HashAlgorithm.SHA1: hash = hashlib.sha1() elif self.algorithm == HashAlgorithm.SHA256: hash = hashlib.sha256() else: raise ValueError("Invalid hash algorithm") hash.update(data.get_bytes()) if length is None: return core.Data(hash.digest()) return core.Data(hash.digest()[:length]) class Random: """A class to get random data""" @staticmethod def get_bytes(length: int) -> bytes: """Gets random bytes. Args: length (int): The length of the bytes. Returns: bytes: The random bytes. """ return bytes(random.getrandbits(8) for _ in range(length)) @staticmethod def get_alpha_string(length: int) -> str: """Gets a random string of the given length. Args: length (int): The length of the string. Returns: str: The random string. """ characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" return "".join(random.choice(characters) for _ in range(length)) @staticmethod def get_hex_string(length: int) -> str: """Gets a random hex string of the given length. Args: length (int): The length of the string. Returns: str: The random string. """ characters = "0123456789abcdef" return "".join(random.choice(characters) for _ in range(length)) @staticmethod def get_digits_string(length: int) -> str: """Gets a random digits string of the given length. Args: length (int): The length of the string. Returns: str: The random string. """ characters = "0123456789" return "".join(random.choice(characters) for _ in range(length)) class Hmac: def __init__(self, algorithm: HashAlgorithm): self.algorithm = algorithm def get_hmac(self, key: core.Data, data: core.Data) -> core.Data: if self.algorithm == HashAlgorithm.MD5: alg = hashlib.md5 elif self.algorithm == HashAlgorithm.SHA1: alg = hashlib.sha1 elif self.algorithm == HashAlgorithm.SHA256: alg = hashlib.sha256 else: raise ValueError("Invalid hash algorithm") hmac_data = hmac.new( key.get_bytes(), data.get_bytes(), digestmod=alg ).digest() return core.Data(hmac_data) class NyankoSignature: def __init__(self, inquiry_code: str, data: str): self.inquiry_code = inquiry_code self.data = data def generate_signature(self) -> str: """Generates a signature from the inquiry code and data. Returns: str: The signature. """ random_data = Random.get_hex_string(64) key = self.inquiry_code + random_data hmac_ = Hmac(HashAlgorithm.SHA256) signature = hmac_.get_hmac(core.Data(key), core.Data(self.data)) return random_data + signature.to_hex() def generate_signature_v1(self) -> str: """Generates a signature from the inquiry code and data. Returns: str: The signature. """ data = self.data + self.data # repeat data for some reason random_data = Random.get_hex_string(40) key = self.inquiry_code + random_data hmac_ = Hmac(HashAlgorithm.SHA1) signature = hmac_.get_hmac(core.Data(key), core.Data(data)) return random_data + signature.to_hex() ================================================ FILE: src/bcsfe/core/game/__init__.py ================================================ from bcsfe.core.game import catbase, battle, map, gamoto, localizable __all__ = ["catbase", "battle", "map", "gamoto", "localizable"] ================================================ FILE: src/bcsfe/core/game/battle/__init__.py ================================================ from bcsfe.core.game.battle import slots, battle_items, cleared_slots __all__ = ["slots", "battle_items", "cleared_slots"] ================================================ FILE: src/bcsfe/core/game/battle/battle_items.py ================================================ from __future__ import annotations import datetime from math import inf, isnan import math from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator, color class EndlessItem: def __init__( self, active: bool, unknown: bool, amount: int, start: float, end: float ): self.active = active self.unknown = unknown self.amount = amount self.start = start self.end = end @staticmethod def init() -> EndlessItem: return EndlessItem(False, False, 0, 0, 0) @staticmethod def read(stream: core.Data) -> EndlessItem: return EndlessItem( stream.read_bool(), stream.read_bool(), stream.read_byte(), stream.read_double(), stream.read_double(), ) def write(self, stream: core.Data): stream.write_bool(self.active) stream.write_bool(self.unknown) stream.write_byte(self.amount) stream.write_double(self.start) stream.write_double(self.end) def serialize(self) -> dict[str, Any]: return { "active": self.active, "unknown": self.unknown, "amount": self.amount, "start": self.start, "end": self.end, } @staticmethod def deserialize(data: dict[str, Any]) -> EndlessItem: return EndlessItem( data.get("active", False), data.get("unknown", False), data.get("amount", 0), data.get("start", 0.0), data.get("end", 0.0), ) def get_endless_duration(self) -> datetime.timedelta | None: if not self.active: return datetime.timedelta() if self.end == inf: return None if math.isnan(self.end) or math.isnan(self.start): return None return datetime.timedelta( seconds=self.end - self.start + (self.amount * 3 * 60 * 60) ) def get_endless_duration_formatted(self) -> str: duration = self.get_endless_duration() if duration is None: return core.localize("infinity_duration") days = duration.days hours, rem = divmod(duration.seconds, 3600) minutes, seconds = divmod(rem, 60) return core.localize( "duration", days=days, hours=hours, minutes=minutes, seconds=seconds ) def set_duration_mins(self, mins: float, amount: int): self.active = True self.unknown = True self.amount = amount self.start = datetime.datetime.now(datetime.timezone.utc).timestamp() self.end = self.start + mins * 60 class BattleItem: def __init__(self, amount: int): self.amount = amount self.locked = False self.endless_item = EndlessItem.init() @staticmethod def init() -> BattleItem: return BattleItem(0) @staticmethod def read_amount(stream: core.Data) -> BattleItem: return BattleItem(stream.read_int()) def write_amount(self, stream: core.Data): stream.write_int(self.amount) def read_locked(self, stream: core.Data): self.locked = stream.read_bool() def write_locked(self, stream: core.Data): stream.write_bool(self.locked) def read_endless_items(self, stream: core.Data): self.endless_item = EndlessItem.read(stream) def write_endless_items(self, stream: core.Data): self.endless_item.write(stream) def serialize(self) -> dict[str, Any]: return { "amount": self.amount, "locked": self.locked, "endless": self.endless_item.serialize(), } @staticmethod def deserialize(data: dict[str, Any]) -> BattleItem: battle_item = BattleItem(data.get("amount", 0)) battle_item.locked = data.get("locked", False) battle_item.endless_item = EndlessItem.deserialize(data.get("endless", {})) return battle_item def __repr__(self): try: return f"BattleItem({self.amount}, {self.locked}, {self.endless_item})" except AttributeError: return f"BattleItem({self.amount}, {self.endless_item})" def __str__(self): return self.__repr__() class BattleItems: def __init__(self, items: list[BattleItem]): self.items = items self.lock_item = False @staticmethod def init() -> BattleItems: return BattleItems([BattleItem.init() for _ in range(6)]) @staticmethod def read_items(stream: core.Data) -> BattleItems: total_items = 6 items = [BattleItem.read_amount(stream) for _ in range(total_items)] return BattleItems(items) def write_items(self, stream: core.Data): for item in self.items: item.write_amount(stream) def read_locked_items(self, stream: core.Data): self.lock_item = stream.read_bool() for item in self.items: item.read_locked(stream) def write_locked_items(self, stream: core.Data): stream.write_bool(self.lock_item) for item in self.items: item.write_locked(stream) def read_endless_items(self, stream: core.Data): for i in range(6): if i >= len(self.items): _ = EndlessItem.read(stream) # ensure we still read 6 items else: item = self.items[i] item.read_endless_items(stream) def write_endless_items(self, stream: core.Data): for i in range(6): if i >= len(self.items): EndlessItem.init().write(stream) # ensure we still write 6 items else: item = self.items[i] item.write_endless_items(stream) def serialize(self) -> dict[str, Any]: return { "items": [item.serialize() for item in self.items], "lock_item": self.lock_item, } @staticmethod def deserialize(data: dict[str, Any]) -> BattleItems: battle_items = BattleItems( [BattleItem.deserialize(item) for item in data.get("items", [])] ) battle_items.lock_item = data.get("lock_item", False) return battle_items def __repr__(self): return f"BattleItems({self.items})" def __str__(self): return f"BattleItems({self.items})" def get_names(self, save_file: core.SaveFile) -> list[str] | None: names = core.core_data.get_gatya_item_names(save_file).names if names is None: return None items = core.core_data.get_gatya_item_buy(save_file).get_by_category(3) if items is None: return None names = [names[item.id] for item in items] return names def edit(self, save_file: core.SaveFile): group_name = save_file.get_localizable().get("shop_category1") if group_name is None: group_name = core.core_data.local_manager.get_key("battle_items") item_names = self.get_names(save_file) if item_names is None: return current_values = [item.amount for item in self.items] values = dialog_creator.MultiEditor.from_reduced( group_name, item_names, current_values, core.core_data.max_value_manager.get("battle_items"), ).edit() for i, value in enumerate(values): self.items[i].amount = value def edit_endless_items(self, save_file: core.SaveFile): item_names = self.get_names(save_file) if item_names is None: return current_values = [ item.endless_item.get_endless_duration_formatted() for item in self.items ] (options, all_at_once) = dialog_creator.ChoiceInput.from_reduced( [core.localize("endless_item_item", item=item) for item in item_names], current_values, localize_options=False, dialog="select_option", ).multiple_choice(False) if options is None: return infinity_str = core.localize("infinity") if all_at_once: val = dialog_creator.StringInput().get_input_locale_while( "enter_duration_minutes", {} ) if val is None: return if val.lower() == infinity_str.lower(): val = inf else: try: val = float(val) except ValueError: return for item in self.items: item.endless_item.set_duration_mins(val, 0) else: for opt in options: val = dialog_creator.StringInput().get_input_locale_while( "enter_duration_minutes_item", {"item": item_names[opt]} ) if val is None: return if val.lower() == infinity_str.lower(): val = inf else: try: val = float(val) except ValueError: color.ColoredText.localize("invalid_minute_count") continue self.items[opt].endless_item.set_duration_mins(val, 0) color.ColoredText.localize("endless_items_success") ================================================ FILE: src/bcsfe/core/game/battle/cleared_slots.py ================================================ from __future__ import annotations from bcsfe import core from typing import Any class CatSlot: def __init__(self, cat_id: int, form: int): self.cat_id = cat_id self.form = form @staticmethod def init() -> CatSlot: return CatSlot(0, 0) @staticmethod def read(stream: core.Data) -> CatSlot: cat_id = stream.read_short() form = stream.read_byte() return CatSlot(cat_id, form) def write(self, stream: core.Data): stream.write_short(self.cat_id) stream.write_byte(self.form) def serialize(self) -> dict[str, Any]: return { "cat_id": self.cat_id, "form": self.form, } @staticmethod def deserialize(data: dict[str, Any]) -> CatSlot: return CatSlot(data.get("cat_id", 0), data.get("form", 0)) def __repr__(self): return f"CatSlot({self.cat_id}, {self.form})" def __str__(self): return self.__repr__() class LineupCat: def __init__( self, index: int, cats: list[CatSlot], u1: int, u2: int, u3: int, ): self.index = index self.cats = cats self.u1 = u1 self.u2 = u2 self.u3 = u3 @staticmethod def init() -> LineupCat: cats = [CatSlot.init() for _ in range(10)] return LineupCat(0, cats, 0, 0, 0) @staticmethod def read(stream: core.Data) -> LineupCat: index = stream.read_short() length = 10 cats = [CatSlot.read(stream) for _ in range(length)] u1 = stream.read_byte() u2 = stream.read_byte() u3 = stream.read_byte() return LineupCat(index, cats, u1, u2, u3) def write(self, stream: core.Data): stream.write_short(self.index) for cat in self.cats: cat.write(stream) stream.write_byte(self.u1) stream.write_byte(self.u2) stream.write_byte(self.u3) def serialize(self) -> dict[str, Any]: return { "index": self.index, "cats": [cat.serialize() for cat in self.cats], "u1": self.u1, "u2": self.u2, "u3": self.u3, } @staticmethod def deserialize(data: dict[str, Any]) -> LineupCat: return LineupCat( data.get("index", 0), [CatSlot.deserialize(cat) for cat in data.get("cats", [])], data.get("u1", 0), data.get("u2", 0), data.get("u3", 0), ) def __repr__(self): return f"LineupCat({self.index}, {self.cats}, {self.u1}, {self.u2}, {self.u3})" def __str__(self): return self.__repr__() class ClearedSlotsCat: def __init__(self, lineups: list[LineupCat]): self.lineups = lineups @staticmethod def init() -> ClearedSlotsCat: return ClearedSlotsCat([]) @staticmethod def read(stream: core.Data) -> ClearedSlotsCat: total = stream.read_short() lineups = [LineupCat.read(stream) for _ in range(total)] return ClearedSlotsCat(lineups) def write(self, stream: core.Data): stream.write_short(len(self.lineups)) for lineup in self.lineups: lineup.write(stream) def serialize(self) -> list[dict[str, Any]]: return [lineup.serialize() for lineup in self.lineups] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ClearedSlotsCat: return ClearedSlotsCat( [LineupCat.deserialize(lineup) for lineup in data], ) def __repr__(self): return f"ClearedSlotsCat({self.lineups})" def __str__(self): return self.__repr__() class StageSlot: def __init__(self, stage_id: int): self.stage_id = stage_id @staticmethod def init() -> StageSlot: return StageSlot(0) @staticmethod def read(stream: core.Data) -> StageSlot: stage_id = stream.read_int() return StageSlot(stage_id) def write(self, stream: core.Data): stream.write_int(self.stage_id) def serialize(self) -> int: return self.stage_id @staticmethod def deserialize(data: int) -> StageSlot: return StageSlot(data) def __repr__(self): return f"StageSlot({self.stage_id})" def __str__(self): return self.__repr__() class StageLineups: def __init__(self, index: int, slots: list[StageSlot]): self.index = index self.slots = slots @staticmethod def init() -> StageLineups: return StageLineups(0, []) @staticmethod def read(stream: core.Data) -> StageLineups: index = stream.read_short() total = stream.read_short() slots = [StageSlot.read(stream) for _ in range(total)] return StageLineups(index, slots) def write(self, stream: core.Data): stream.write_short(self.index) stream.write_short(len(self.slots)) for slot in self.slots: slot.write(stream) def serialize(self) -> dict[str, Any]: return { "index": self.index, "slots": [slot.serialize() for slot in self.slots], } @staticmethod def deserialize(data: dict[str, Any]) -> StageLineups: return StageLineups( data.get("index", 0), [StageSlot.deserialize(slot) for slot in data.get("slots", [])], ) def __repr__(self): return f"StageLineups({self.index}, {self.slots})" def __str__(self): return self.__repr__() class ClearedStageSlots: def __init__(self, lineups: list[StageLineups]): self.lineups = lineups @staticmethod def init() -> ClearedStageSlots: return ClearedStageSlots([]) @staticmethod def read(stream: core.Data) -> ClearedStageSlots: total = stream.read_short() lineups = [StageLineups.read(stream) for _ in range(total)] return ClearedStageSlots(lineups) def write(self, stream: core.Data): stream.write_short(len(self.lineups)) for lineup in self.lineups: lineup.write(stream) def serialize(self) -> dict[str, Any]: return { "lineups": [lineup.serialize() for lineup in self.lineups], } @staticmethod def deserialize(data: dict[str, Any]) -> ClearedStageSlots: return ClearedStageSlots( [ StageLineups.deserialize(lineup) for lineup in data.get("lineups", []) ], ) def __repr__(self): return f"ClearedStageSlots({self.lineups})" def __str__(self): return self.__repr__() class ClearedSlots: def __init__( self, cleared_slots: ClearedSlotsCat, cleared_stage_slots: ClearedStageSlots, unknown: dict[int, bool], ): self.cleared_slots = cleared_slots self.cleared_stage_slots = cleared_stage_slots self.unknown = unknown @staticmethod def init() -> ClearedSlots: return ClearedSlots( ClearedSlotsCat.init(), ClearedStageSlots.init(), {}, ) @staticmethod def read(stream: core.Data) -> ClearedSlots: cleared_slots = ClearedSlotsCat.read(stream) cleared_stage_slots = ClearedStageSlots.read(stream) length = stream.read_short() unknown = stream.read_short_bool_dict(length) return ClearedSlots(cleared_slots, cleared_stage_slots, unknown) def write(self, stream: core.Data): self.cleared_slots.write(stream) self.cleared_stage_slots.write(stream) stream.write_short(len(self.unknown)) stream.write_short_bool_dict(self.unknown, write_length=False) def serialize(self) -> dict[str, Any]: return { "cleared_slots": self.cleared_slots.serialize(), "cleared_stage_slots": self.cleared_stage_slots.serialize(), "unknown": self.unknown, } @staticmethod def deserialize(data: dict[str, Any]) -> ClearedSlots: return ClearedSlots( ClearedSlotsCat.deserialize(data.get("cleared_slots", [])), ClearedStageSlots.deserialize(data.get("cleared_stage_slots", {})), data.get("unknown", {}), ) def __repr__(self): return f"ClearedSlots({self.cleared_slots}, {self.cleared_stage_slots}, {self.unknown})" def __str__(self): return self.__repr__() ================================================ FILE: src/bcsfe/core/game/battle/enemy.py ================================================ from __future__ import annotations from bcsfe import core class Enemy: def __init__(self, id: int): self.id = id def unlock_enemy_guide(self, save_file: core.SaveFile): save_file.enemy_guide[self.id] = 1 def reset_enemy_guide(self, save_file: core.SaveFile): save_file.enemy_guide[self.id] = 0 def get_name(self, save_file: core.SaveFile) -> str | None: return core.core_data.get_enemy_names(save_file).get_name(self.id) class EnemyDictionaryItem: def __init__(self, enemy_id: int, scale: int, first_seen: int | None): self.enemy_id = enemy_id self.scale = scale self.first_seen = first_seen class EnemyDictionary: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.dictionary = self.__get_dictionary() def __get_dictionary(self) -> list[EnemyDictionaryItem] | None: gdg = core.core_data.get_game_data_getter(self.save_file) csv_data = gdg.download("DataLocal", "enemy_dictionary_list.csv") if csv_data is None: return None csv = core.CSV(csv_data) data: list[EnemyDictionaryItem] = [] for row in csv: first_seen = None if len(row) >= 3: first_seen = row[2].to_int() data.append( EnemyDictionaryItem(row[0].to_int(), row[1].to_int(), first_seen) ) return data def get_valid_enemies(self) -> list[int] | None: if self.dictionary is None: return None return [enemy.enemy_id for enemy in self.dictionary] def get_invalid_enemies(self, total_enemies: int) -> list[int] | None: valid_enemies = self.get_valid_enemies() if valid_enemies is None: return None valid_enemies = set(valid_enemies) return list(filter(lambda i: i not in valid_enemies, range(total_enemies))) class EnemyDescription: def __init__(self, trait_str: str, description: list[str] | None): self.trait_str = trait_str self.description = description class EnemyDescriptions: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.descriptions = self.__get_descriptions() def __get_descriptions(self) -> list[EnemyDescription] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download( "resLocal", f"EnemyPictureBook_{core.core_data.get_lang(self.save_file)}.csv", ) if data is None: return None csv = core.CSV(data, core.Delimeter.from_country_code_res(self.save_file.cc)) descriptions: list[EnemyDescription] = [] for i, row in enumerate(csv): if len(row) == 1: descriptions.append(EnemyDescription(row[0].to_str(), None)) else: descriptions.append( EnemyDescription(row[0].to_str(), row[1:].to_str_list()) ) return descriptions class EnemyNames: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.names = self.get_names() def get_names(self) -> list[str] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", "Enemyname.tsv") if data is None: return None csv = core.CSV( data, "\t", remove_empty=False, ) names: list[str] = [] for row in csv: names.append(row[0].to_str()) return names def get_name(self, id: int) -> str | None: if self.names is None: return None try: name = self.names[id] if not name: return core.core_data.local_manager.get_key( "enemy_not_in_name_list", id=id ) except IndexError: return core.core_data.local_manager.get_key("enemy_unknown_name", id=id) return name ================================================ FILE: src/bcsfe/core/game/battle/slots.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator class EquipSlot: def __init__(self, cat_id: int): self.cat_id = cat_id @staticmethod def read(stream: core.Data) -> EquipSlot: return EquipSlot(stream.read_int()) def write(self, stream: core.Data): stream.write_int(self.cat_id) def serialize(self) -> int: return self.cat_id @staticmethod def deserialize(data: int) -> EquipSlot: return EquipSlot(data) def __repr__(self): return f"EquipSlot({self.cat_id})" def __str__(self): return f"EquipSlot({self.cat_id})" class EquipSlots: def __init__(self, slots: list[EquipSlot]): self.slots = slots self.name = "" @staticmethod def read(stream: core.Data) -> EquipSlots: length = 10 slots = [EquipSlot.read(stream) for _ in range(length)] return EquipSlots(slots) @staticmethod def init() -> EquipSlots: length = 10 slots = [EquipSlot(-1) for _ in range(length)] return EquipSlots(slots) def write(self, stream: core.Data): for slot in self.slots: slot.write(stream) def read_name(self, stream: core.Data): length = stream.read_int() try: self.name = stream.read_string(length) except UnicodeDecodeError: stream.pos -= length self.name = stream.read_utf8_string_by_char_length(length) def write_name(self, stream: core.Data): stream.write_string(self.name) def serialize(self) -> dict[str, Any]: return { "slots": [slot.serialize() for slot in self.slots], "name": self.name, } @staticmethod def deserialize(data: dict[str, Any]) -> EquipSlots: slots = EquipSlots( [EquipSlot.deserialize(slot) for slot in data.get("slots", [])] ) slots.name = data.get("name") return slots def __repr__(self): return f"EquipSlots({self.slots}, {self.name})" def __str__(self): return self.__repr__() class LineUps: def __init__(self, slots: list[EquipSlots], total_slots: int = 15): self.slots = slots self.selected_slot = 0 self.unlocked_slots = 0 self.slot_names_length = total_slots @staticmethod def init(gv: core.GameVersion) -> LineUps: if gv < 90700: length = 10 else: length = 15 slots = [EquipSlots.init() for _ in range(length)] return LineUps(slots, length) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> LineUps: if gv < 90700: length = 10 else: length = stream.read_byte() slots = [EquipSlots.read(stream) for _ in range(length)] return LineUps(slots) def write(self, stream: core.Data, gv: core.GameVersion): if gv >= 90700: stream.write_byte(len(self.slots)) length = len(self.slots) else: length = 10 if length > len(self.slots): self.slots += [EquipSlots.init() for _ in range(length)] else: self.slots = self.slots[:length] for slot in self.slots: slot.write(stream) def read_2(self, stream: core.Data, gv: core.GameVersion): self.selected_slot = stream.read_int() if gv < 90700: unlocked_slots_l = stream.read_bool_list(10) unlocked_slots = sum(unlocked_slots_l) else: unlocked_slots = stream.read_byte() self.unlocked_slots = unlocked_slots def write_2(self, stream: core.Data, gv: core.GameVersion): stream.write_int(self.selected_slot) if gv < 90700: unlocked_slots_l = [False] * 10 unlocked_slots = min(self.unlocked_slots, 10) for i in range(unlocked_slots): unlocked_slots_l[i] = True stream.write_bool_list(unlocked_slots_l, write_length=False) else: stream.write_byte(self.unlocked_slots) def read_slot_names(self, stream: core.Data, gv: core.GameVersion): if gv >= 110600: total_slots = stream.read_byte() else: total_slots = 15 for i in range(total_slots): try: self.slots[i].read_name(stream) except IndexError: slot = EquipSlots.init() slot.read_name(stream) self.slots.append(slot) self.slot_names_length = total_slots def write_slot_names(self, stream: core.Data, gv: core.GameVersion): if gv >= 110600: stream.write_byte(self.slot_names_length) for i in range(self.slot_names_length): try: self.slots[i].write_name(stream) except IndexError: slot = EquipSlots.init() slot.write_name(stream) self.slots.append(slot) def serialize(self) -> dict[str, Any]: return { "slots": [slot.serialize() for slot in self.slots], "selected_slot": self.selected_slot, "unlocked_slots": self.unlocked_slots, "slot_names_length": self.slot_names_length, } @staticmethod def deserialize(data: dict[str, Any]) -> LineUps: line_ups = LineUps( [EquipSlots.deserialize(slot) for slot in data.get("slots", [])] ) line_ups.selected_slot = data.get("selected_slot", 0) line_ups.unlocked_slots = data.get("unlocked_slots", 0) line_ups.slot_names_length = data.get("slot_names_length", 0) return line_ups def __repr__(self): return f"LineUps({self.slots}, {self.selected_slot}, {self.unlocked_slots})" def __str__(self): return self.__repr__() def edit_unlocked_slots(self): self.unlocked_slots = dialog_creator.SingleEditor( "unlocked_slots", self.unlocked_slots, self.slot_names_length, localized_item=True, remove_alias=True, ).edit() ================================================ FILE: src/bcsfe/core/game/catbase/__init__.py ================================================ from bcsfe.core.game.catbase import ( gatya_item, stamp, cat, upgrade, special_skill, my_sale, gatya, user_rank_rewards, item_pack, login_bonuses, scheme_items, unlock_popups, beacon_base, mission, nyanko_club, officer_pass, medals, talent_orbs, matatabi, powerup, drop_chara, playtime, gambling, ) __all__ = [ "stamp", "cat", "upgrade", "special_skill", "my_sale", "gatya", "user_rank_rewards", "item_pack", "login_bonuses", "scheme_items", "unlock_popups", "beacon_base", "mission", "nyanko_club", "officer_pass", "medals", "talent_orbs", "gatya_item", "matatabi", "powerup", "drop_chara", "playtime", "gambling", ] ================================================ FILE: src/bcsfe/core/game/catbase/beacon_base.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class BeaconEventListScene: def __init__( self, int_dict: dict[int, int], str_dict: dict[int, list[str]], bool_dict: dict[int, bool], ): self.int_array = int_dict self.str_array = str_dict self.bool_array = bool_dict @staticmethod def init() -> BeaconEventListScene: return BeaconEventListScene({}, {}, {}) @staticmethod def read(stream: core.Data) -> BeaconEventListScene: int_dict = {} str_dict = {} bool_dict = {} for _ in range(stream.read_int()): int_dict[stream.read_int()] = stream.read_int() for _ in range(stream.read_int()): str_dict[stream.read_int()] = stream.read_string_list() for _ in range(stream.read_int()): bool_dict[stream.read_int()] = stream.read_bool() return BeaconEventListScene(int_dict, str_dict, bool_dict) def write(self, stream: core.Data): stream.write_int(len(self.int_array)) for key, value in self.int_array.items(): stream.write_int(key) stream.write_int(value) stream.write_int(len(self.str_array)) for key, value in self.str_array.items(): stream.write_int(key) stream.write_string_list(value) stream.write_int(len(self.bool_array)) for key, value in self.bool_array.items(): stream.write_int(key) stream.write_bool(value) def serialize(self) -> dict[str, Any]: return { "int_array": self.int_array, "str_array": self.str_array, "bool_array": self.bool_array, } @staticmethod def deserialize(data: dict[str, Any]) -> BeaconEventListScene: return BeaconEventListScene( data.get("int_array", []), data.get("str_array", []), data.get("bool_array", []), ) def __repr__(self): return f"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})" def __str__(self): return f"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})" ================================================ FILE: src/bcsfe/core/game/catbase/cat.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class SkillLevel: def __init__( self, id: int, levels: list[int], ): self.id = id self.levels = levels def get_total_levels(self) -> int: return len(self.levels) @staticmethod def from_row(row: core.Row): id = row[0].to_int() levels = row[1:].to_int_list() return SkillLevel(id, levels) class SkillLevelData: def __init__(self, levels: list[SkillLevel] | None): self.levels = levels @staticmethod def from_game_data(save_file: core.SaveFile) -> SkillLevelData | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("DataLocal", "SkillLevel.csv") if data is None: return None csv = core.CSV(data) levels: list[SkillLevel] = [] for line in csv.lines[1:]: levels.append(SkillLevel.from_row(line)) return SkillLevelData(levels) def get_skill_level(self, id: int) -> SkillLevel | None: if self.levels is None: return None for level in self.levels: if level.id == id: return level return None class Skill: def __init__( self, ability_id: int, max_lv: int, min1: int, max1: int, min2: int, max2: int, min3: int, max3: int, min4: int, max4: int, text_id: int, lvid: int, name_id: int, limit: int, ): self.ability_id = ability_id self.max_lv = max_lv self.min1 = min1 self.max1 = max1 self.min2 = min2 self.max2 = max2 self.min3 = min3 self.max3 = max3 self.min4 = min4 self.max4 = max4 self.text_id = text_id self.lvid = lvid self.name_id = name_id self.limit = limit class CatSkill: def __init__( self, cat_id: int, type_id: int, skills: list[Skill], ): self.cat_id = cat_id self.type_id = type_id self.skills = skills @staticmethod def from_row(row: core.Row): cat_id = row[0].to_int() type_id = row[1].to_int() skills: list[Skill] = [] for i in range(2, len(row), 14): skill = Skill( row[i].to_int(), row[i + 1].to_int(), row[i + 2].to_int(), row[i + 3].to_int(), row[i + 4].to_int(), row[i + 5].to_int(), row[i + 6].to_int(), row[i + 7].to_int(), row[i + 8].to_int(), row[i + 9].to_int(), row[i + 10].to_int(), row[i + 11].to_int(), row[i + 12].to_int(), row[i + 13].to_int(), ) skills.append(skill) return CatSkill(cat_id, type_id, skills) class CatSkills: def __init__(self, skills: dict[int, CatSkill]): self.skills = skills @staticmethod def from_game_data(save_file: core.SaveFile) -> CatSkills | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("DataLocal", "SkillAcquisition.csv") if data is None: return None csv = core.CSV(data) skills: dict[int, CatSkill] = {} for line in csv.lines[1:]: skill = CatSkill.from_row(line) skills[skill.cat_id] = skill return CatSkills(skills) def get_cat_skill(self, cat_id: int) -> CatSkill | None: return self.skills.get(cat_id) class SkillNames: def __init__(self, names: dict[int, str]): self.names = names @staticmethod def from_game_data(save_file: core.SaveFile) -> SkillNames | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("resLocal", "SkillDescriptions.csv") if data is None: return None csv = core.CSV( data, delimiter=core.Delimeter.from_country_code_res(save_file.cc) ) names: dict[int, str] = {} for line in csv.lines[1:]: names[line[0].to_int()] = line[1].to_str() return SkillNames(names) def get_skill_name(self, skill_id: int) -> str | None: return self.names.get(skill_id) class TalentData: def __init__( self, skill_names: SkillNames, skill_levels: SkillLevelData, cats: CatSkills, ): self.skill_names = skill_names self.skill_levels = skill_levels self.cats = cats @staticmethod def from_game_data(save_file: core.SaveFile) -> TalentData | None: skill_names = SkillNames.from_game_data(save_file) skill_levels = SkillLevelData.from_game_data(save_file) cats = CatSkills.from_game_data(save_file) if skill_names is None or skill_levels is None or cats is None: return None return TalentData(skill_names, skill_levels, cats) def get_skill_name(self, skill_id: int) -> str | None: return self.skill_names.get_skill_name(skill_id) def get_skill_level(self, skill_id: int) -> SkillLevel | None: return self.skill_levels.get_skill_level(skill_id) def get_cat_skill(self, cat_id: int) -> CatSkill | None: return self.cats.get_cat_skill(cat_id) def get_skill_from_cat(self, cat_id: int, skill_id: int) -> Skill | None: cat_skill = self.get_cat_skill(cat_id) if cat_skill is None: return None for skill in cat_skill.skills: if skill.ability_id == skill_id: return skill return None def get_talent_from_cat_skill(self, cat: core.Cat, skill_id: int) -> Talent | None: talents = cat.talents if talents is None: return None for talent in talents: if talent.id == skill_id: return talent return None def get_cat_skill_name(self, cat_id: int, skill_id: int) -> str | None: skill = self.get_skill_from_cat(cat_id, skill_id) if skill is None: return None return self.get_skill_name(skill.text_id) def get_cat_skill_level(self, cat_id: int, skill_id: int) -> SkillLevel | None: skill = self.get_skill_from_cat(cat_id, skill_id) if skill is None: return None return self.get_skill_level(skill.lvid) def get_cat_talents( self, cat: core.Cat ) -> tuple[list[str], list[int], list[int], list[int]] | None: talent_data_cat = self.get_cat_skill(cat.id) if talent_data_cat is None or cat.talents is None: return None # save_talent_data = cat.talents talent_names: list[str] = [] max_levels: list[int] = [] current_levels: list[int] = [] ids: list[int] = [] for skill in talent_data_cat.skills: name = self.get_skill_name(skill.text_id) talent = self.get_talent_from_cat_skill(cat, skill.ability_id) if name is None or talent is None: continue max_level = skill.max_lv if max_level == 0: max_level = 1 max_levels.append(max_level) talent_names.append(name.split("
")[0]) current_levels.append(talent.level) ids.append(skill.ability_id) return talent_names, max_levels, current_levels, ids class Talent: def __init__(self, id: int, level: int): self.id = id self.level = level @staticmethod def init() -> Talent: return Talent(0, 0) def reset(self): self.level = 0 @staticmethod def read(stream: core.Data): return Talent(stream.read_int(), stream.read_int()) def write(self, stream: core.Data): stream.write_int(self.id) stream.write_int(self.level) def serialize(self) -> dict[str, Any]: return { "id": self.id, "level": self.level, } @staticmethod def deserialize(data: dict[str, Any]) -> Talent: return Talent( data["id"], data["level"], ) def __repr__(self): return f"Talent({self.id}, {self.level})" def __str__(self): return self.__repr__() class NyankoPictureBookCatData: def __init__( self, cat_id: int, is_displayed_in_catguide: bool, limited: bool, total_forms: int, hint_display_type: int, scale_0: int, scale_1: int, scale_2: int, scale_3: int, ): self.cat_id = cat_id self.is_displayed_in_catguide = is_displayed_in_catguide self.limited = limited self.total_forms = total_forms self.hint_display_type = hint_display_type self.scale_0 = scale_0 self.scale_1 = scale_1 self.scale_2 = scale_2 self.scale_3 = scale_3 class NyankoPictureBook: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.cats = self.get_cats() def get_cats(self) -> list[NyankoPictureBookCatData] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "nyankoPictureBookData.csv") if data is None: return None csv = core.CSV(data) cats: list[NyankoPictureBookCatData] = [] for i, line in enumerate(csv): cat = NyankoPictureBookCatData( i, line[0].to_bool(), line[1].to_bool(), line[2].to_int(), line[3].to_int(), line[4].to_int(), line[5].to_int(), line[6].to_int(), line[7].to_int(), ) cats.append(cat) return cats def get_cat(self, cat_id: int) -> NyankoPictureBookCatData | None: if self.cats is None: return None for cat in self.cats: if cat.cat_id == cat_id: return cat return None def get_obtainable_cats(self) -> list[NyankoPictureBookCatData] | None: if self.cats is None: return None return [cat for cat in self.cats if cat.is_displayed_in_catguide] class EvolveItem: """Represents an item used to evolve a unit.""" def __init__( self, item_id: int, amount: int, ): """Initializes a new EvolveItem object. Args: item_id (int): The ID of the item. amount (int): The amount of the item. """ self.item_id = item_id self.amount = amount def __str__(self) -> str: """Gets a string representation of the EvolveItem object. Returns: str: The string representation of the EvolveItem object. """ return f"{self.item_id}:{self.amount}" def __repr__(self) -> str: """Gets a string representation of the EvolveItem object. Returns: str: The string representation of the EvolveItem object. """ return str(self) class EvolveItems: """Represents the items used to evolve a unit.""" def __init__(self, evolve_items: list[EvolveItem]): """Initializes a new EvolveItems object. Args: evolve_items (list[EvolveItem]): The items used to evolve a unit. """ self.evolve_items = evolve_items @staticmethod def from_unit_buy_list(raw_data: core.Row, start_index: int) -> EvolveItems: """Creates a new EvolveItems object from a row from unitbuy.csv. Args: raw_data (core.Row): The row from unitbuy.csv. Returns: EvolveItems: The EvolveItems object. """ items: list[EvolveItem] = [] for i in range(5): item_id = raw_data[start_index + i * 2].to_int() amount = raw_data[start_index + 1 + i * 2].to_int() items.append(EvolveItem(item_id, amount)) return EvolveItems(items) class UnitBuyCatData: def __init__(self, id: int, raw_data: core.Row): self.id = id self.assign(raw_data) def assign(self, raw_data: core.Row): self.stage_unlock = raw_data[0].to_int() self.purchase_cost = raw_data[1].to_int() self.upgrade_costs = [cost.to_int() for cost in raw_data[2:12]] self.unlock_source = raw_data[12].to_int() self.rarity = raw_data[13].to_int() self.position_order = raw_data[14].to_int() self.chapter_unlock = raw_data[15].to_int() self.sell_price = raw_data[16].to_int() self.gatya_rarity = raw_data[17].to_int() self.original_max_levels = raw_data[18].to_int(), raw_data[19].to_int() self.force_true_form_level = raw_data[20].to_int() self.second_form_unlock_level = raw_data[21].to_int() self.unknown_22 = raw_data[22].to_int() self.tf_id = raw_data[23].to_int() self.ff_id = raw_data[24].to_int() self.evolve_level_tf = raw_data[25].to_int() self.evolve_level_ff = raw_data[26].to_int() self.evolve_cost_tf = raw_data[27].to_int() self.evolve_items_tf = EvolveItems.from_unit_buy_list(raw_data, 28) self.evolve_cost_ff = raw_data[38].to_int() self.evolve_items_ff = EvolveItems.from_unit_buy_list(raw_data, 39) self.max_upgrade_level_no_catseye = raw_data[49].to_int() self.max_upgrade_level_catseye = raw_data[50].to_int() self.max_plus_upgrade_level = raw_data[51].to_int() self.unknown_52 = raw_data[52].to_int() self.unknown_53 = raw_data[53].to_int() self.unknown_54 = raw_data[54].to_int() self.unknown_55 = raw_data[55].to_int() self.catseye_usage_pattern = raw_data[56].to_int() self.game_version = raw_data[57].to_int() self.np_sell_price = raw_data[58].to_int() self.unknwon_59 = raw_data[59].to_int() self.unknown_60 = raw_data[60].to_int() self.egg_value = raw_data[61].to_int() self.egg_id = raw_data[62].to_int() class UnitBuy: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.unit_buy = self.read_unit_buy() def read_unit_buy(self) -> list[UnitBuyCatData] | None: unit_buy: list[UnitBuyCatData] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "unitbuy.csv") if data is None: return None csv = core.CSV(data) for i, line in enumerate(csv): unit_buy.append(UnitBuyCatData(i, line)) return unit_buy def get_unit_buy(self, id: int) -> UnitBuyCatData | None: if self.unit_buy is None: return None try: return self.unit_buy[id] except IndexError: return None def get_cat_rarity(self, id: int) -> int: unit_buy = self.get_unit_buy(id) if unit_buy is None: return -1 return unit_buy.rarity class UnitLimitCatData: def __init__(self, cat_id: int, values: list[int]): self.cat_id = cat_id self.values = values class UnitLimit: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.unit_limit = self.read_unit_limit() def read_unit_limit(self) -> list[UnitLimitCatData] | None: unit_limit: list[UnitLimitCatData] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "unitlimit.csv") if data is None: return None csv = core.CSV(data) for i, line in enumerate(csv): unit_limit.append(UnitLimitCatData(i, line.to_int_list())) return unit_limit def get_unit_limit(self, id: int) -> UnitLimitCatData | None: if self.unit_limit is None: return None try: return self.unit_limit[id] except IndexError: return None class Cat: def __init__(self, id: int, unlocked: int): self.id = id self.unlocked = unlocked self.talents: list[Talent] | None = None self.upgrade: core.Upgrade = core.Upgrade.init() self.current_form: int = 0 self.unlocked_forms: int = 0 self.gatya_seen: int = 0 self.max_upgrade_level: core.Upgrade = core.Upgrade.init() self.catguide_collected: bool = False self.fourth_form: int = 0 self.catseyes_used: int = 0 self.names: list[str] | None = None def get_talent_from_id(self, id: int) -> Talent | None: for talent in self.talents or []: if talent.id == id: return talent return None def unlock(self, save_file: core.SaveFile): self.unlocked = 1 self.gatya_seen = 1 core.core_data.get_chara_drop(save_file).unlock_drops_from_cat_id(self.id) save_file.unlock_equip_menu() def remove(self, reset: bool = False, save_file: core.SaveFile | None = None): self.unlocked = 0 if reset: self.reset() if save_file is not None: save_file.cats.chara_new_flags[self.id] = 0 core.core_data.get_chara_drop(save_file).remove_drops_from_cat_id( self.id ) def true_form(self, save_file: core.SaveFile, set_current_form: bool = True): self.set_form(2, save_file, set_current_form) def set_form( self, form: int, save_file: core.SaveFile, set_current_form: bool = True ): if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): self.unlock(save_file) self.unlocked_forms = form + 1 if set_current_form: self.current_form = form def set_form_true( self, save_file: core.SaveFile, total_forms: int, set_current_form: bool = True, fourth_form: bool = False, ): if total_forms == 4 and self.unlocked_forms == 3 and fourth_form: self.unlock_fourth_form(save_file, set_current_form) elif total_forms >= 3: self.true_form(save_file, set_current_form) elif total_forms == 2: self.unlocked_forms = 0 self.current_form = 1 else: self.unlocked_forms = 0 self.current_form = 0 def remove_true_form(self): self.unlocked_forms = 0 self.current_form = min(self.current_form, 1) self.fourth_form = 0 def unlock_fourth_form( self, save_file: core.SaveFile, set_current_form: bool = True ): if set_current_form: self.current_form = 3 if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): self.unlock(save_file) self.fourth_form = 2 def remove_fourth_form(self): self.current_form = min(self.current_form, 2) self.fourth_form = 0 def set_upgrade( self, save_file: core.SaveFile, upgrade: core.Upgrade, only_plus: bool = False, ): if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): self.unlock(save_file) base = upgrade.base plus = upgrade.plus if base != -1 and not only_plus: self.upgrade.base = upgrade.get_random_base() if plus != -1: self.upgrade.plus = upgrade.get_random_plus() def upgrade_base(self, save_file: core.SaveFile): if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT): self.unlock(save_file) self.upgrade.upgrade() def reset(self): self.unlocked = 0 self.current_form = 0 self.unlocked_forms = 0 self.gatya_seen = 0 self.catguide_collected = False self.fourth_form = 0 self.catseyes_used = 0 self.upgrade.reset() for talent in self.talents or []: talent.reset() @staticmethod def init(id: int) -> Cat: return Cat(id, 0) @staticmethod def read_unlocked(id: int, stream: core.Data): return Cat(id, stream.read_int()) def write_unlocked(self, stream: core.Data): stream.write_int(self.unlocked) def read_upgrade(self, stream: core.Data): self.upgrade = core.Upgrade.read(stream) def write_upgrade(self, stream: core.Data): self.upgrade.write(stream) def read_current_form(self, stream: core.Data): self.current_form = stream.read_int() def write_current_form(self, stream: core.Data): stream.write_int(self.current_form) def read_unlocked_forms(self, stream: core.Data): self.unlocked_forms = stream.read_int() def write_unlocked_forms(self, stream: core.Data): stream.write_int(self.unlocked_forms) def read_gatya_seen(self, stream: core.Data): self.gatya_seen = stream.read_int() def write_gatya_seen(self, stream: core.Data): stream.write_int(self.gatya_seen) def read_max_upgrade_level(self, stream: core.Data): level = core.Upgrade.read(stream) self.max_upgrade_level = level def write_max_upgrade_level(self, stream: core.Data): self.max_upgrade_level.write(stream) def read_catguide_collected(self, stream: core.Data): self.catguide_collected = stream.read_bool() def write_catguide_collected(self, stream: core.Data): stream.write_bool(self.catguide_collected) def read_fourth_form(self, stream: core.Data): self.fourth_form = stream.read_int() def write_fourth_form(self, stream: core.Data): stream.write_int(self.fourth_form) def read_catseyes_used(self, stream: core.Data): self.catseyes_used = stream.read_int() def write_catseyes_used(self, stream: core.Data): stream.write_int(self.catseyes_used) def serialize(self) -> dict[str, Any]: return { "id": self.id, "unlocked": self.unlocked, "upgrade": self.upgrade.serialize(), "current_form": self.current_form, "unlocked_forms": self.unlocked_forms, "gatya_seen": self.gatya_seen, "max_upgrade_level": self.max_upgrade_level.serialize(), "catguide_collected": self.catguide_collected, "fourth_form": self.fourth_form, "catseyes_used": self.catseyes_used, "talents": ( [talent.serialize() for talent in self.talents] if self.talents is not None else None ), } @staticmethod def deserialize(data: dict[str, Any]) -> Cat: cat = Cat(data["id"], data["unlocked"]) cat.upgrade = core.Upgrade.deserialize(data["upgrade"]) cat.current_form = data["current_form"] cat.unlocked_forms = data["unlocked_forms"] cat.gatya_seen = data["gatya_seen"] cat.max_upgrade_level = core.Upgrade.deserialize(data["max_upgrade_level"]) cat.catguide_collected = data["catguide_collected"] cat.fourth_form = data["fourth_form"] cat.catseyes_used = data["catseyes_used"] cat.talents = ( [Talent.deserialize(talent) for talent in data["talents"]] if data["talents"] is not None else None ) return cat def __repr__(self) -> str: return f"Cat(id={self.id}, unlocked={self.unlocked}, upgrade={self.upgrade}, current_form={self.current_form}, unlocked_forms={self.unlocked_forms}, gatya_seen={self.gatya_seen}, max_upgrade_level={self.max_upgrade_level}, catguide_collected={self.catguide_collected}, fourth_form={self.fourth_form}, catseyes_used={self.catseyes_used}, talents={self.talents})" def __str__(self) -> str: return self.__repr__() def read_talents(self, stream: core.Data): self.talents = [] for _ in range(stream.read_int()): self.talents.append(Talent.read(stream)) def write_talents(self, stream: core.Data): if self.talents is None: return stream.write_int(len(self.talents)) for talent in self.talents: talent.write(stream) def get_names_cls(self, save_file: core.SaveFile) -> list[str] | None: if self.names is None: self.names = Cat.get_names(self.id, save_file) return self.names @staticmethod def get_names( id: int, save_file: core.SaveFile, ) -> list[str] | None: file_name = f"Unit_Explanation{id + 1}_{core.core_data.get_lang(save_file)}.csv" data = core.core_data.get_game_data_getter(save_file).download( "resLocal", file_name ) if data is None: return None csv = core.CSV( data, core.Delimeter.from_country_code_res(save_file.cc), remove_empty=False, ) names: list[str] = [] for line in csv.lines: names.append(line[0].to_str()) return names class StorageItem: def __init__(self, item_id: int): self.item_id = item_id self.item_type = 0 @staticmethod def from_cat(cat_id: int) -> StorageItem: item = StorageItem(cat_id) item.item_type = 1 return item @staticmethod def from_special_skill(special_skill_id: int) -> StorageItem: item = StorageItem(special_skill_id) item.item_type = 2 return item @staticmethod def init() -> StorageItem: return StorageItem(0) @staticmethod def read_item_id(stream: core.Data): return StorageItem(stream.read_int()) def write_item_id(self, stream: core.Data): stream.write_int(self.item_id) def read_item_type(self, stream: core.Data): self.item_type = stream.read_int() def write_item_type(self, stream: core.Data): stream.write_int(self.item_type) def serialize(self) -> dict[str, Any]: return { "item_id": self.item_id, "item_type": self.item_type, } @staticmethod def deserialize(data: dict[str, Any]) -> StorageItem: item = StorageItem(data.get("item_id", 0)) item.item_type = data.get("item_type", 0) return item def __repr__(self) -> str: return f"StorageItem(item_id={self.item_id}, item_type={self.item_type})" def __str__(self) -> str: return f"StorageItem(item_id={self.item_id}, item_type={self.item_type})" class Cats: def __init__(self, cats: list[Cat], total_storage_items: int = 0): self.cats = cats self.storage_items = [StorageItem.init() for _ in range(total_storage_items)] self.favourites: dict[int, bool] = {} self.chara_new_flags: dict[int, int] = {} self.unit_buy: UnitBuy | None = None self.unit_limit: UnitLimit | None = None self.nyanko_picture_book: NyankoPictureBook | None = None self.talent_data: TalentData | None = None def get_all_cats(self) -> list[Cat]: return self.cats @staticmethod def init(gv: core.GameVersion) -> Cats: total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = 0 cats_l: list[Cat] = [] for i in range(total_cats): cats_l.append(Cat.init(i)) if gv < 110100: total_storage_items = 100 else: total_storage_items = 0 return Cats(cats_l, total_storage_items) @staticmethod def get_gv_cats(gv: core.GameVersion) -> int | None: if gv == 20: total_cats = 203 elif gv == 21: total_cats = 214 elif gv == 22: total_cats = 231 elif gv == 23: total_cats = 241 elif gv == 24: total_cats = 249 elif gv == 25: total_cats = 260 else: total_cats = None return total_cats def get_unlocked_cats(self) -> list[Cat]: return [cat for cat in self.cats if cat.unlocked] def get_non_unlocked_cats(self) -> list[Cat]: return [cat for cat in self.cats if not cat.unlocked] def get_non_gacha_cats(self, save_file: core.SaveFile) -> list[Cat]: unitbuy = self.read_unitbuy(save_file) cats: list[Cat] = [] for cat in self.cats: unit_buy_data = unitbuy.get_unit_buy(cat.id) if unit_buy_data is None: continue if unit_buy_data.unlock_source != 2: cats.append(cat) return cats def read_unitbuy(self, save_file: core.SaveFile) -> UnitBuy: if self.unit_buy is None: self.unit_buy = UnitBuy(save_file) return self.unit_buy def read_unitlimit(self, save_file: core.SaveFile) -> UnitLimit: if self.unit_limit is None: self.unit_limit = UnitLimit(save_file) return self.unit_limit def read_nyanko_picture_book(self, save_file: core.SaveFile) -> NyankoPictureBook: if self.nyanko_picture_book is None: self.nyanko_picture_book = NyankoPictureBook(save_file) return self.nyanko_picture_book def read_talent_data(self, save_file: core.SaveFile) -> TalentData | None: if self.talent_data is None: self.talent_data = TalentData.from_game_data(save_file) return self.talent_data def get_cats_rarity(self, save_file: core.SaveFile, rarity: int) -> list[Cat]: unit_buy = self.read_unitbuy(save_file) return [cat for cat in self.cats if unit_buy.get_cat_rarity(cat.id) == rarity] def get_cats_name( self, save_file: core.SaveFile, search_name: str, ) -> list[Cat]: cats: list[Cat] = [] for cat in self.cats: names = cat.get_names_cls(save_file) if names is None: continue for name in names: if search_name.lower() in name.lower(): cats.append(cat) break return cats def get_cats_obtainable(self, save_file: core.SaveFile) -> list[Cat] | None: nyanko_picture_book = self.read_nyanko_picture_book(save_file) obtainable_cats = nyanko_picture_book.get_obtainable_cats() if obtainable_cats is None: return None ny_cats = [cat.cat_id for cat in obtainable_cats] cats: list[Cat] = [] for cat in self.cats: if cat.id in ny_cats: cats.append(cat) return cats def get_cats_non_obtainable(self, save_file: core.SaveFile) -> list[Cat] | None: nyanko_picture_book = self.read_nyanko_picture_book(save_file) obtainable_cats = nyanko_picture_book.get_obtainable_cats() if obtainable_cats is None: return None ny_cats = [cat.cat_id for cat in obtainable_cats] cats: list[Cat] = [] for cat in self.cats: if cat.id not in ny_cats: cats.append(cat) return cats def get_cats_gatya_banner( self, save_file: core.SaveFile, gatya_id: int ) -> list[core.Cat] | None: cat_ids = save_file.gatya.read_gatya_data_set(save_file).get_cat_ids(gatya_id) if cat_ids is None: return None return self.get_cats_by_ids(cat_ids) def true_form_cats( self, save_file: core.SaveFile, cats: list[Cat], force: bool = False, set_current_forms: bool = True, ): pic_book = self.read_nyanko_picture_book(save_file) for cat in cats: pic_book_cat = pic_book.get_cat(cat.id) if force: cat.true_form(save_file, set_current_form=set_current_forms) elif pic_book_cat is not None: cat.set_form_true( save_file, pic_book_cat.total_forms, set_current_form=set_current_forms, ) def fourth_form_cats( self, save_file: core.SaveFile, cats: list[Cat], force: bool = False, set_current_forms: bool = True, ): pic_book = self.read_nyanko_picture_book(save_file) for cat in cats: pic_book_cat = pic_book.get_cat(cat.id) if force: cat.unlock_fourth_form(save_file, set_current_form=set_current_forms) elif pic_book_cat is not None: cat.set_form_true( save_file, pic_book_cat.total_forms, set_current_form=set_current_forms, fourth_form=True, ) def get_cats_by_ids(self, ids: list[int]) -> list[Cat]: cats: list[Cat] = [] for cat in self.cats: if cat.id in ids: cats.append(cat) return cats def get_cat_by_id(self, id: int) -> Cat | None: for cat in self.cats: if cat.id == id: return cat return None @staticmethod def get_rarity_names(save_file: core.SaveFile) -> list[str]: localizable = save_file.get_localizable() rarity_names: list[str] = [] rarity_index = 1 while True: rarity_name = localizable.get(f"rarity_name_{rarity_index}") if rarity_name is None: break rarity_names.append(rarity_name) rarity_index += 1 return rarity_names @staticmethod def read_unlocked(stream: core.Data, gv: core.GameVersion) -> Cats: total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() cats_l: list[Cat] = [] for i in range(total_cats): cats_l.append(Cat.read_unlocked(i, stream)) return Cats(cats_l) def write_unlocked(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_unlocked(stream) def read_upgrade(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() for cat in self.cats: cat.read_upgrade(stream) def write_upgrade(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_upgrade(stream) def read_current_form(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() for cat in self.cats: cat.read_current_form(stream) def write_current_form(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_current_form(stream) def read_unlocked_forms(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() for cat in self.cats: cat.read_unlocked_forms(stream) def write_unlocked_forms(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_unlocked_forms(stream) def read_gatya_seen(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() for cat in self.cats: cat.read_gatya_seen(stream) def write_gatya_seen(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_gatya_seen(stream) def read_max_upgrade_levels(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: total_cats = stream.read_int() for cat in self.cats: cat.read_max_upgrade_level(stream) def write_max_upgrade_levels(self, stream: core.Data, gv: core.GameVersion): total_cats = Cats.get_gv_cats(gv) if total_cats is None: stream.write_int(len(self.cats)) for cat in self.cats: cat.write_max_upgrade_level(stream) def read_storage(self, stream: core.Data, gv: core.GameVersion): if gv < 110100: total_storage = 100 else: total_storage = stream.read_short() self.storage_items: list[StorageItem] = [] for _ in range(total_storage): self.storage_items.append(StorageItem.read_item_id(stream)) for item in self.storage_items: item.read_item_type(stream) def write_storage(self, stream: core.Data, gv: core.GameVersion): if gv >= 110100: stream.write_short(len(self.storage_items)) for item in self.storage_items: item.write_item_id(stream) for item in self.storage_items: item.write_item_type(stream) def read_catguide_collected(self, stream: core.Data): total_cats = stream.read_int() for i in range(total_cats): self.cats[i].read_catguide_collected(stream) def write_catguide_collected(self, stream: core.Data): stream.write_int(len(self.cats)) for cat in self.cats: cat.write_catguide_collected(stream) def read_fourth_forms(self, stream: core.Data): total_cats = stream.read_int() for i in range(total_cats): self.cats[i].read_fourth_form(stream) def read_catseyes_used(self, stream: core.Data): total_cats = stream.read_int() for i in range(total_cats): self.cats[i].read_catseyes_used(stream) def write_catseyes_used(self, stream: core.Data): stream.write_int(len(self.cats)) for cat in self.cats: cat.write_catseyes_used(stream) def write_fourth_forms(self, stream: core.Data): stream.write_int(len(self.cats)) for cat in self.cats: cat.write_fourth_form(stream) def read_favorites(self, stream: core.Data): self.favourites: dict[int, bool] = {} total_cats = stream.read_int() for _ in range(total_cats): cat_id = stream.read_int() self.favourites[cat_id] = stream.read_bool() def write_favorites(self, stream: core.Data): stream.write_int(len(self.favourites)) for cat_id, is_favourite in self.favourites.items(): stream.write_int(cat_id) stream.write_bool(is_favourite) def read_chara_new_flags(self, stream: core.Data): self.chara_new_flags: dict[int, int] = {} total_cats = stream.read_int() for _ in range(total_cats): cat_id = stream.read_int() self.chara_new_flags[cat_id] = stream.read_int() def write_chara_new_flags(self, stream: core.Data): stream.write_int(len(self.chara_new_flags)) for cat_id, new_flag in self.chara_new_flags.items(): stream.write_int(cat_id) stream.write_int(new_flag) def read_talents(self, stream: core.Data): total_cats = stream.read_int() for _ in range(total_cats): cat_id = stream.read_int() if cat_id < 0 or cat_id >= len(self.cats): cat = Cat.init(cat_id) cat.read_talents(stream) continue self.cats[cat_id].read_talents(stream) def write_talents(self, stream: core.Data): total_talents = 0 for cat in self.cats: total_talents += 1 if cat.talents is not None else 0 stream.write_int(total_talents) for cat in self.cats: if cat.talents is None: continue stream.write_int(cat.id) cat.write_talents(stream) def serialize(self) -> dict[str, Any]: return { "cats": [cat.serialize() for cat in self.cats], "storage_items": [item.serialize() for item in self.storage_items], "favorites": self.favourites, "chara_new_flags": self.chara_new_flags, } @staticmethod def deserialize(data: dict[str, Any]) -> Cats: cats_l = [Cat.deserialize(cat) for cat in data.get("cats", [])] cats = Cats(cats_l) cats.storage_items = [ StorageItem.deserialize(item) for item in data.get("storage_items", []) ] cats.favourites = data.get("favorites", {}) cats.chara_new_flags = data.get("chara_new_flags", {}) return cats def __repr__(self) -> str: return f"Cats(cats={self.cats}, storage_items={self.storage_items}, favourites={self.favourites}, chara_new_flags={self.chara_new_flags})" def __str__(self) -> str: return self.__repr__() ================================================ FILE: src/bcsfe/core/game/catbase/drop_chara.py ================================================ from __future__ import annotations from dataclasses import dataclass from bcsfe import core @dataclass class Drop: stage_id: int save_id: int chara_id: int class CharaDrop: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.drops = self.get_drops() def get_drops(self) -> list[Drop] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "drop_chara.csv") if data is None: return None csv = core.CSV(data) drops: list[Drop] = [] for line in csv.lines[1:]: drops.append( Drop( stage_id=line[0].to_int(), save_id=line[1].to_int(), chara_id=line[2].to_int(), ) ) return drops def get_drop(self, stage_id: int) -> Drop | None: if self.drops is None: return None for drop in self.drops: if drop.stage_id == stage_id: return drop return None def get_drops_from_chara_id(self, chara_id: int) -> list[Drop] | None: if self.drops is None: return None drops: list[Drop] = [] for drop in self.drops: if drop.chara_id == chara_id: drops.append(drop) return drops def unlock_drops_from_cat_id(self, cat_id: int) -> None: drops = self.get_drops_from_chara_id(cat_id) if drops is None: return for drop in drops: try: self.save_file.unit_drops[drop.save_id] = 1 except IndexError: pass def remove_drops_from_cat_id(self, cat_id: int) -> None: drops = self.get_drops_from_chara_id(cat_id) if drops is None: return for drop in drops: try: self.save_file.unit_drops[drop.save_id] = 0 except IndexError: pass ================================================ FILE: src/bcsfe/core/game/catbase/gambling.py ================================================ from __future__ import annotations from bcsfe import core from typing import Any from bcsfe.cli import color class GamblingEvent: def __init__( self, completed: dict[int, bool], values: dict[int, dict[int, int]], start_times: dict[int, int | float], ): self.completed = completed self.values = values self.start_times = start_times @staticmethod def init() -> GamblingEvent: return GamblingEvent({}, {}, {}) @staticmethod def read(data: core.Data, game_version: core.GameVersion) -> GamblingEvent: total = data.read_short() completed: dict[int, bool] = {} for _ in range(total): key = data.read_short() completed[key] = data.read_bool() total = data.read_short() values: dict[int, dict[int, int]] = {} for _ in range(total): key = data.read_short() if key not in values: values[key] = {} total2 = data.read_short() for _ in range(total2): key2 = data.read_short() values[key][key2] = data.read_short() total = data.read_short() start_times: dict[int, int | float] = {} for _ in range(total): key = data.read_short() if game_version < 90100: value = data.read_double() else: value = data.read_int() start_times[key] = value return GamblingEvent(completed, values, start_times) def write(self, data: core.Data, game_version: core.GameVersion): data.write_short(len(self.completed)) data.write_short_bool_dict(self.completed, write_length=False) data.write_short(len(self.values)) for key, value in self.values.items(): data.write_short(key) data.write_short(len(value)) for key2, value2 in value.items(): data.write_short(key2) data.write_short(value2) data.write_short(len(self.start_times)) for key, value in self.start_times.items(): data.write_short(key) # this is a bad conversion, since float is timestamp i assume and int as the date as YYYMMDD. FIXME if game_version < 90100: data.write_double(float(value)) else: data.write_int(int(value)) def serialize(self) -> dict[str, Any]: return { "completed": self.completed, "values": self.values, "start_times": self.start_times, } @staticmethod def deserialize(data: dict[str, Any]) -> GamblingEvent: return GamblingEvent( data.get("completed", {}), data.get("values", {}), data.get("start_times", {}), ) def reset(self): self.completed = {} self.values = {} # TODO: check start times self.start_times = {} @staticmethod def reset_events(save_file: core.SaveFile): save_file.wildcat_slots.reset() color.ColoredText.localize("reset_wildcat_slots") save_file.cat_scratcher.reset() color.ColoredText.localize("reset_cat_scratcher") ================================================ FILE: src/bcsfe/core/game/catbase/gatya.py ================================================ from __future__ import annotations import enum from typing import Any, Callable from bcsfe import core from bcsfe.cli import dialog_creator, color class Gatya: def __init__(self, rare_seed: int, normal_seed: int): self.rare_seed = rare_seed self.normal_seed = normal_seed self.event_seed = 0 self.stepup_stage_3_cooldown = 0 self.previous_normal_roll = 0 self.previous_normal_roll_type = 0 self.previous_rare_roll = 0 self.previous_rare_roll_type = 0 self.unknown1 = False self.roll_single = False self.roll_multi = False self.trade_progress = 0 self.step_up_stages: dict[int, int] = {} self.stepup_durations: dict[int, float] = {} self.gatya_data_set: GatyaDataSet | None = None @staticmethod def init() -> Gatya: return Gatya(0, 0) @staticmethod def read_rare_normal_seed(data: core.Data, gv: core.GameVersion) -> Gatya: if gv < 33: return Gatya(data.read_ulong(), data.read_ulong()) return Gatya(data.read_uint(), data.read_uint()) def read_event_seed(self, data: core.Data, gv: core.GameVersion): if gv < 33: self.event_seed = data.read_ulong() else: self.event_seed = data.read_uint() def write_rare_normal_seed(self, data: core.Data): data.write_uint(self.rare_seed) data.write_uint(self.normal_seed) def write_event_seed(self, data: core.Data): data.write_uint(self.event_seed) def read2(self, data: core.Data): self.stepup_stage_3_cooldown = data.read_int() self.previous_normal_roll = data.read_int() self.previous_normal_roll_type = data.read_int() self.previous_rare_roll = data.read_int() self.previous_rare_roll_type = data.read_int() self.unknown1 = data.read_bool() self.roll_single = data.read_bool() self.roll_multi = data.read_bool() def write2(self, data: core.Data): data.write_int(self.stepup_stage_3_cooldown) data.write_int(self.previous_normal_roll) data.write_int(self.previous_normal_roll_type) data.write_int(self.previous_rare_roll) data.write_int(self.previous_rare_roll_type) data.write_bool(self.unknown1) data.write_bool(self.roll_single) data.write_bool(self.roll_multi) def read_trade_progress(self, data: core.Data): self.trade_progress = data.read_int() def write_trade_progress(self, data: core.Data): data.write_int(self.trade_progress) def read_stepup(self, data: core.Data): self.step_up_stages: dict[int, int] = {} total = data.read_int() for _ in range(total): key = data.read_int() self.step_up_stages[key] = data.read_int() self.stepup_durations: dict[int, float] = {} total = data.read_int() for _ in range(total): key = data.read_int() self.stepup_durations[key] = data.read_double() def write_stepup(self, data: core.Data): data.write_int(len(self.step_up_stages)) for id, stage in self.step_up_stages.items(): data.write_int(id) data.write_int(stage) data.write_int(len(self.stepup_durations)) for id, duration in self.stepup_durations.items(): data.write_int(id) data.write_double(duration) def serialize(self) -> dict[str, Any]: return { "rare_seed": self.rare_seed, "normal_seed": self.normal_seed, "stepup_stage_3_cooldown": self.stepup_stage_3_cooldown, "previous_normal_roll": self.previous_normal_roll, "previous_normal_roll_type": self.previous_normal_roll_type, "previous_rare_roll": self.previous_rare_roll, "previous_rare_roll_type": self.previous_rare_roll_type, "unknown1": self.unknown1, "roll_single": self.roll_single, "roll_multi": self.roll_multi, "trade_progress": self.trade_progress, "event_seed": self.event_seed, "step_up_stages": self.step_up_stages, "stepup_durations": self.stepup_durations, } @staticmethod def deserialize(data: dict[str, Any]) -> Gatya: gatya = Gatya(data.get("rare_seed", 0), data.get("normal_seed", 0)) gatya.stepup_stage_3_cooldown = data.get("stepup_stage_3_cooldown", 0) gatya.previous_normal_roll = data.get("previous_normal_roll", 0) gatya.previous_normal_roll_type = data.get("previous_normal_roll_type", 0) gatya.previous_rare_roll = data.get("previous_rare_roll", 0) gatya.previous_rare_roll_type = data.get("previous_rare_roll_type", 0) gatya.unknown1 = data.get("unknown1", False) gatya.roll_single = data.get("roll_single", False) gatya.roll_multi = data.get("roll_multi", False) gatya.trade_progress = data.get("trade_progress", 0) gatya.event_seed = data.get("event_seed", 0) gatya.step_up_stages = data.get("step_up_stages", {}) gatya.stepup_durations = data.get("stepup_durations", {}) return gatya def __repr__(self) -> str: return f"Gatya({self.serialize()})" def __str__(self) -> str: return f"Gatya({self.serialize()})" def edit_rare_gatya_seed(self): self.rare_seed = dialog_creator.SingleEditor( "rare_gatya_seed", self.rare_seed, None, localized_item=True, signed=False, ).edit() def edit_normal_gatya_seed(self): self.normal_seed = dialog_creator.SingleEditor( "normal_gatya_seed", self.normal_seed, None, localized_item=True, signed=False, ).edit() def edit_event_gatya_seed(self): self.event_seed = dialog_creator.SingleEditor( "event_gatya_seed", self.event_seed, None, localized_item=True, signed=False, ).edit() def read_gatya_data_set(self, save_file: core.SaveFile) -> GatyaDataSet: if self.gatya_data_set is not None: return self.gatya_data_set self.gatya_data_set = GatyaDataSet(save_file) return self.gatya_data_set class GatyaDataSet: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.gatya_data_set = self.load_gatya_data_set("R", 1) def load_gatya_data_set(self, rarity: str, id: int) -> list[list[int]] | None: file_name = f"GatyaDataSet{rarity.upper()[0]}{id}.csv" gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", file_name) if data is None: return None csv = core.CSV(data) dt: list[list[int]] = [] for line in csv: cat_ids: list[int] = [] for cat_id in line: cat_id = cat_id.to_int() if cat_id != -1: cat_ids.append(cat_id) dt.append(cat_ids) return dt def get_cat_ids(self, gatya_id: int) -> list[int] | None: if self.gatya_data_set is None: return None try: return self.gatya_data_set[gatya_id] except IndexError: return None class GatyaInfo: def __init__(self, gatya_id: int, cc: core.CountryCode, type_str: str = "R"): self.gatya_id = gatya_id self.cc = cc self.gatya_data_set: GatyaDataSet | None = None self.type = type_str self.data: core.Data | None = None def get_id_str(self) -> str: return f"{self.gatya_id:03}" def get_cc_str(self) -> str: if self.cc == core.CountryCode("jp"): return "" return self.cc.get_patching_code() + "/" def get_url(self) -> str: return f"https://ponosgames.com/information/appli/battlecats/gacha/rare/{self.get_cc_str()}{self.type}{self.get_id_str()}.html" def download_data(self) -> core.Data | None: url = self.get_url() response = core.RequestHandler(url).get() if response is None: return data = core.Data(response.content) self.save_data(data) return data def get_file_path(self) -> core.Path: return ( core.Path.get_data_folder() .add("other_game_data") .add(self.cc.get_code()) .add("gatya_info") .generate_dirs() .add(f"{self.type}{self.get_id_str()}.html") ) def save_data(self, data: core.Data): try: data.to_file(self.get_file_path()) except Exception as e: color.ColoredText.localize("save_gatya_error", error=e) self.data = data def load_data_from_file(self) -> core.Data | None: if not self.get_file_path().exists(): return None return core.Data.from_file(self.get_file_path()) def get_data(self) -> core.Data | None: if self.data is not None: return self.data data = self.load_data_from_file() if data is None: data = self.download_data() return data def get_name(self) -> str | None: data = self.get_data() if data is None: return None # find

...

data = data.get_bytes() h2 = data.find(b"

") if h2 == -1: return None h2_end = data.find(b"

", h2) if h2_end == -1: return None text = data[h2 + 4 : h2_end].decode("utf-8") # remove span = text.find("", span) if span_end == -1: return text return text[:span] + text[span_end + 7 :] class GatyaInfos: def __init__(self, save_file: core.SaveFile, type_str: str = "R", set_id: int = 1): self.save_file = save_file self.type = type_str self.set_id = set_id self.gatya_data_set = GatyaDataSet(save_file).load_gatya_data_set( type_str, set_id ) self.infos: list[GatyaInfo] = [] self.got_all = False def get_all( self, threaded: bool = True, print_progress: bool = True, max_threads: int = 16, ): if self.gatya_data_set is None: return all_ids = len(self.gatya_data_set) if threaded: funcs: list[Callable[..., Any]] = [] args: list[list[Any]] = [] for id in range(all_ids): funcs.append(self.get) args.append([id, print_progress]) core.thread_run_many(funcs, args, max_threads=max_threads) else: for id in range(all_ids): self.infos.append(self.get(id, print_progress=print_progress)) self.got_all = True def get(self, gatya_id: int, print_progress: bool): if print_progress: color.ColoredText.localize( "gatya_info_progress", current=len(self.infos or []) + 1, total=len(self.gatya_data_set or []), ) info = GatyaInfo(gatya_id, self.save_file.cc, self.type) info.get_data() self.infos.append(info) return info def get_info(self, gatya_id: int) -> GatyaInfo | None: if self.infos: return self.infos[gatya_id] return None def get_all_names(self) -> dict[int, str]: if not self.got_all: self.get_all(True, max_threads=64) names: dict[int, str] = {} for info in self.infos: names[ info.gatya_id ] = info.get_name() or core.core_data.local_manager.get_key( "unknown_banner" ) return names class GatyaDataOptionSet: def __init__( self, id: int, banner_on: bool, ticket_item_id: int, anim_id: int, button_cut_id: int, series_id: int, menu_cut_id: int, char_id: int | None, wait_maanim: bool | None, ): self.id = id self.banner_on = banner_on self.ticket_item_id = ticket_item_id self.anim_id = anim_id self.button_cut_id = button_cut_id self.series_id = series_id self.menu_cut_id = menu_cut_id self.char_id = char_id self.wait_maanim = wait_maanim @staticmethod def from_csv_row(row: core.Row) -> GatyaDataOptionSet: return GatyaDataOptionSet( row.next_int(), row.next_bool(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int_opt(), row.next_bool_opt(), ) class GatyaEventType(enum.Enum): NORMAL = "N" RARE = "R" EVENT = "E" class GatyaDataOption: def __init__(self, sets: list[GatyaDataOptionSet]): self.sets = sets def get(self, set_id: int) -> GatyaDataOptionSet | None: for gset in self.sets: if gset.id == set_id: return gset return None @staticmethod def from_csv(csv: core.CSV) -> GatyaDataOption: sets: list[GatyaDataOptionSet] = [] csv.read_line() # skip headers for row in csv: sets.append(GatyaDataOptionSet.from_csv_row(row)) return GatyaDataOption(sets) @staticmethod def from_data(data: core.Data) -> GatyaDataOption: return GatyaDataOption.from_csv(core.CSV(data, "\t")) @staticmethod def get_filename(event_type: GatyaEventType) -> str: return f"GatyaData_Option_Set{event_type.value}.tsv" @staticmethod def read( save_file: core.SaveFile, e_type: GatyaEventType ) -> GatyaDataOption | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("DataLocal", GatyaDataOption.get_filename(e_type)) if data is None: return None return GatyaDataOption.from_data(data) ================================================ FILE: src/bcsfe/core/game/catbase/gatya_item.py ================================================ from __future__ import annotations import enum from bcsfe import core class GatyaItemNames: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.names = self.__get_names() def __get_names(self) -> list[str] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", "GatyaitemName.csv") if data is None: return None csv = core.CSV( data, core.Delimeter.from_country_code_res(self.save_file.cc) ) names: list[str] = [] for line in csv: names.append(line[0].to_str()) return names def get_name(self, index: int) -> str | None: if self.names is None: return None try: return self.names[index] except IndexError: return core.core_data.local_manager.get_key( "gatya_item_unknown_name", index=index ) class GatyaItemBuyItem: def __init__( self, id: int, rarity: int, reflect_or_storage: bool, price: int, stage_drop_id: int, quantity: int, server_id: int, category: int, index: int, src_item_id: int, main_menu_type: int, gatya_ticket_id: int, comment: str, ): self.id = id self.rarity = rarity self.reflect_or_storage = reflect_or_storage self.price = price self.stage_drop_id = stage_drop_id self.quantity = quantity self.server_id = server_id self.category = category self.index = index self.src_item_id = src_item_id self.main_menu_type = main_menu_type self.gatya_ticket_id = gatya_ticket_id self.comment = comment class GatyaItemCategory(enum.Enum): MISC = 0 EVENT_TICKETS = 1 SPECIAL_SKILLS = 2 BATTLE_ITEMS = 3 EVOLVE_ITEMS = 4 CATSEYES = 5 CATAMINS = 6 BASE_MATERIALS = 7 LUCKY_TICKETS_1 = 8 ENDLESS_ITEMS = 9 LUCKY_TICKETS_2 = 10 LABYRINTH_MEDALS = 11 TREASURE_CHESTS = 12 class GatyaItemBuy: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.buy = self.get_buy() def get_buy(self) -> list[GatyaItemBuyItem] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "Gatyaitembuy.csv") if data is None: return None csv = core.CSV(data) buy: list[GatyaItemBuyItem] = [] for i, line in enumerate(csv.lines[1:]): try: buy.append( GatyaItemBuyItem( i, line[0].to_int(), line[1].to_bool(), line[2].to_int(), line[3].to_int(), line[4].to_int(), line[5].to_int(), line[6].to_int(), line[7].to_int(), line[8].to_int(), line[9].to_int(), line[10].to_int(), line[11].to_str(), ) ) except IndexError: pass return buy def sort_by_index(self, items: list[GatyaItemBuyItem]): items.sort(key=lambda x: x.index) return items def get_by_category(self, category: int | GatyaItemCategory) -> list[GatyaItemBuyItem] | None: if self.buy is None: return None if isinstance(category, GatyaItemCategory): category = category.value return self.sort_by_index( [item for item in self.buy if item.category == category] ) def get_names_by_category(self, category: int | GatyaItemCategory) -> list[tuple[GatyaItemBuyItem, str | None]] | None: items = self.get_by_category(category) if items is None: return None names = GatyaItemNames(self.save_file) return [(item, names.get_name(item.id)) for item in items] def get(self, item_id: int) -> GatyaItemBuyItem | None: if self.buy is None: return None if item_id < 0 or item_id >= len(self.buy): return None return self.buy[item_id] def get_by_server_id(self, server_id: int) -> GatyaItemBuyItem | None: if self.buy is None: return None for item in self.buy: if item.server_id == server_id: return item return None ================================================ FILE: src/bcsfe/core/game/catbase/item_pack.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class PurchasedPack: def __init__(self, purchased: bool): self.purchased = purchased @staticmethod def init() -> PurchasedPack: return PurchasedPack(False) @staticmethod def read(stream: core.Data) -> PurchasedPack: purchased = stream.read_bool() return PurchasedPack(purchased) def write(self, stream: core.Data): stream.write_bool(self.purchased) def serialize(self) -> bool: return self.purchased @staticmethod def deserialize(data: bool) -> PurchasedPack: return PurchasedPack(data) def __repr__(self) -> str: return f"PurchasedPack(purchased={self.purchased!r})" def __str__(self) -> str: return self.__repr__() class PurchaseSet: def __init__(self, purchases: dict[str, PurchasedPack]): self.purchases = purchases @staticmethod def init() -> PurchaseSet: return PurchaseSet({}) @staticmethod def read(stream: core.Data) -> PurchaseSet: total = stream.read_int() purchases: dict[str, PurchasedPack] = {} for _ in range(total): key = stream.read_string() purchases[key] = PurchasedPack.read(stream) return PurchaseSet(purchases) def write(self, stream: core.Data): stream.write_int(len(self.purchases)) for key, purchase in self.purchases.items(): stream.write_string(key) purchase.write(stream) def serialize(self) -> dict[str, Any]: return { key: purchase.serialize() for key, purchase in self.purchases.items() } @staticmethod def deserialize(data: dict[str, Any]) -> PurchaseSet: return PurchaseSet( { key: PurchasedPack.deserialize(purchase) for key, purchase in data.items() }, ) def __repr__(self) -> str: return f"PurchaseSet(purchases={self.purchases!r})" def __str__(self) -> str: return self.__repr__() class Purchases: def __init__(self, purchases: dict[int, PurchaseSet]): self.purchases = purchases @staticmethod def init() -> Purchases: return Purchases({}) @staticmethod def read(stream: core.Data) -> Purchases: total = stream.read_int() purchases: dict[int, PurchaseSet] = {} for _ in range(total): key = stream.read_int() purchases[key] = PurchaseSet.read(stream) return Purchases(purchases) def write(self, stream: core.Data): stream.write_int(len(self.purchases)) for key, purchase in self.purchases.items(): stream.write_int(key) purchase.write(stream) def serialize(self) -> dict[int, Any]: return { key: purchase.serialize() for key, purchase in self.purchases.items() } @staticmethod def deserialize(data: dict[int, Any]) -> Purchases: return Purchases( { key: PurchaseSet.deserialize(purchase) for key, purchase in data.items() }, ) def __repr__(self) -> str: return f"Purchases(purchases={self.purchases!r})" def __str__(self) -> str: return self.__repr__() class ItemPack: def __init__(self, purchases: Purchases): self.purchases = purchases self.displayed_packs: dict[int, bool] = {} self.three_days_started: bool = False self.three_days_end_timestamp: float = 0.0 @staticmethod def init() -> ItemPack: return ItemPack(Purchases.init()) @staticmethod def read(stream: core.Data) -> ItemPack: return ItemPack(Purchases.read(stream)) def write(self, stream: core.Data): self.purchases.write(stream) def read_displayed_packs(self, stream: core.Data) -> None: total = stream.read_int() displayed_packs: dict[int, bool] = {} for _ in range(total): key = stream.read_int() displayed_packs[key] = stream.read_bool() self.displayed_packs = displayed_packs def write_displayed_packs(self, stream: core.Data) -> None: stream.write_int(len(self.displayed_packs)) for key, displayed in self.displayed_packs.items(): stream.write_int(key) stream.write_bool(displayed) def read_three_days(self, stream: core.Data) -> None: self.three_days_started = stream.read_bool() self.three_days_end_timestamp = stream.read_double() def write_three_days(self, stream: core.Data) -> None: stream.write_bool(self.three_days_started) stream.write_double(self.three_days_end_timestamp) def serialize(self) -> dict[str, Any]: return { "purchases": self.purchases.serialize(), "displayed_packs": self.displayed_packs, "three_days_started": self.three_days_started, "three_days_end_timestamp": self.three_days_end_timestamp, } @staticmethod def deserialize(data: dict[str, Any]) -> ItemPack: item_pack = ItemPack(Purchases.deserialize(data.get("purchases", {}))) item_pack.displayed_packs = data.get("displayed_packs", {}) item_pack.three_days_started = data.get("three_days_started", False) item_pack.three_days_end_timestamp = data.get( "three_days_end_timestamp", 0.0 ) return item_pack def __repr__(self) -> str: return f"ItemPack(purchases={self.purchases!r}, displayed_packs={self.displayed_packs!r}, three_days_started={self.three_days_started!r}, three_days_end_timestamp={self.three_days_end_timestamp!r})" def __str__(self) -> str: return self.__repr__() ================================================ FILE: src/bcsfe/core/game/catbase/login_bonuses.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class Login: def __init__(self, count: int): self.count = count @staticmethod def init() -> Login: return Login(0) @staticmethod def read(stream: core.Data) -> Login: count = stream.read_int() return Login(count) def write(self, stream: core.Data): stream.write_int(self.count) def serialize(self) -> int: return self.count @staticmethod def deserialize(data: int) -> Login: return Login(data) def __repr__(self): return f"Login({self.count})" def __str__(self): return f"Login({self.count})" class Logins: def __init__(self, logins: list[Login]): self.logins = logins @staticmethod def init() -> Logins: return Logins([]) @staticmethod def read(stream: core.Data) -> Logins: total = stream.read_int() logins: list[Login] = [] for _ in range(total): logins.append(Login.read(stream)) return Logins(logins) def write(self, stream: core.Data): stream.write_int(len(self.logins)) for login in self.logins: login.write(stream) def serialize(self) -> list[int]: return [login.serialize() for login in self.logins] @staticmethod def deserialize(data: list[int]) -> Logins: return Logins([Login.deserialize(login) for login in data]) def __repr__(self): return f"Logins({self.logins})" def __str__(self): return f"Logins({self.logins})" class LoginSets: def __init__(self, logins: list[Logins]): self.logins = logins @staticmethod def init() -> LoginSets: return LoginSets([]) @staticmethod def read(stream: core.Data) -> LoginSets: total = stream.read_int() logins: list[Logins] = [] for _ in range(total): logins.append(Logins.read(stream)) return LoginSets(logins) def write(self, stream: core.Data): stream.write_int(len(self.logins)) for login in self.logins: login.write(stream) def serialize(self) -> list[list[int]]: return [login.serialize() for login in self.logins] @staticmethod def deserialize(data: list[list[int]]) -> LoginSets: return LoginSets([Logins.deserialize(login) for login in data]) def __repr__(self): return f"LoginSets({self.logins})" def __str__(self): return f"LoginSets({self.logins})" class LoginBonus: def __init__( self, old_logins: LoginSets | None = None, logins: dict[int, Login] | None = None, ): self.old_logins = old_logins self.logins = logins @staticmethod def init(gv: core.GameVersion) -> LoginBonus: if gv < 80000: return LoginBonus(old_logins=LoginSets.init()) else: return LoginBonus(logins={}) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> LoginBonus: if gv < 80000: logins_old = LoginSets.read(stream) return LoginBonus(logins_old) else: total = stream.read_int() logins: dict[int, Login] = {} for _ in range(total): id = stream.read_int() logins[id] = Login.read(stream) return LoginBonus(logins=logins) def write(self, stream: core.Data, gv: core.GameVersion): if gv < 80000: (self.old_logins or LoginSets([])).write(stream) elif gv >= 80000: logins = self.logins or {} stream.write_int(len(logins)) for id, login in logins.items(): stream.write_int(id) login.write(stream) def serialize( self, ) -> dict[str, Any]: if self.old_logins is not None: return {"old_logins": self.old_logins.serialize()} elif self.logins is not None: return { "logins": { id: login.serialize() for id, login in self.logins.items() } } else: return {} @staticmethod def deserialize(data: dict[str, Any]) -> LoginBonus: if "old_logins" in data: return LoginBonus( old_logins=LoginSets.deserialize(data["old_logins"]) ) elif "logins" in data: return LoginBonus( logins={ int(id): Login.deserialize(login) for id, login in data["logins"].items() } ) else: return LoginBonus() def __repr__(self): return f"LoginBonus({self.old_logins}, {self.logins})" def __str__(self): return f"LoginBonus({self.old_logins}, {self.logins})" def get_login(self, id: int) -> Login | None: if self.logins is not None: return self.logins.get(id) else: return None ================================================ FILE: src/bcsfe/core/game/catbase/matatabi.py ================================================ from __future__ import annotations from bcsfe import core class Fruit: def __init__( self, id: int, seed: bool, group: int, sort: int, require: int | None = None, text: str | None = None, grow_up: list[int] | None = None, ): self.id = id self.seed = seed self.group = group self.sort = sort self.require = require self.text = text self.grow_up = grow_up class Matatabi: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.matatabi = self.__get_matatabi() self.gatya_item_names = core.core_data.get_gatya_item_names( self.save_file ) def __get_matatabi(self) -> list[Fruit] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "Matatabi.tsv") if data is None: return None csv = core.CSV(data, "\t") matatabi: list[Fruit] = [] for line in csv.lines[1:]: id = line[0].to_int() seed = line[1].to_bool() group = line[2].to_int() sort = line[3].to_int() if len(line) > 4: require = line[4].to_int() else: require = None if len(line) > 5: text = line[5].to_str() else: text = None if len(line) > 6: grow_up = [item.to_int() for item in line[6:]] else: grow_up = None matatabi.append( Fruit(id, seed, group, sort, require, text, grow_up) ) return matatabi def get_names(self) -> list[str | None] | None: if self.matatabi is None: return None ids = [fruit.id for fruit in self.matatabi] names = [self.gatya_item_names.get_name(id) for id in ids] return names ================================================ FILE: src/bcsfe/core/game/catbase/medals.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class Medals: def __init__( self, u1: int, u2: int, u3: int, medal_data_1: list[int], medal_data_2: dict[int, int], ub: bool, ): self.u1 = u1 self.u2 = u2 self.u3 = u3 self.medal_data_1 = medal_data_1 self.medal_data_2 = medal_data_2 self.ub = ub @staticmethod def init() -> Medals: return Medals(0, 0, 0, [], {}, False) @staticmethod def read(data: core.Data) -> Medals: u1 = data.read_int() u2 = data.read_int() u3 = data.read_int() total_medals = data.read_short() medal_data_1 = data.read_short_list(total_medals) total_medals = data.read_short() medal_data_2: dict[int, int] = {} for _ in range(total_medals): key = data.read_short() value = data.read_byte() medal_data_2[key] = value ub = data.read_bool() return Medals(u1, u2, u3, medal_data_1, medal_data_2, ub) def write(self, data: core.Data) -> None: data.write_int(self.u1) data.write_int(self.u2) data.write_int(self.u3) data.write_short(len(self.medal_data_1)) data.write_short_list(self.medal_data_1, write_length=False) data.write_short(len(self.medal_data_2)) for key, value in self.medal_data_2.items(): data.write_short(key) data.write_byte(value) data.write_bool(self.ub) def serialize(self) -> dict[str, Any]: return { "u1": self.u1, "u2": self.u2, "u3": self.u3, "medal_data_1": self.medal_data_1, "medal_data_2": self.medal_data_2, "ub": self.ub, } @staticmethod def deserialize(data: dict[str, Any]) -> Medals: return Medals( data.get("u1", 0), data.get("u2", 0), data.get("u3", 0), data.get("medal_data_1", []), data.get("medal_data_2", {}), data.get("ub", False), ) def __repr__(self) -> str: return ( f"Medals(u1={self.u1}, u2={self.u2}, u3={self.u3}, " f"medal_data_1={self.medal_data_1}, medal_data_2={self.medal_data_2}, " f"ub={self.ub})" ) def __str__(self) -> str: return self.__repr__() def has_medal(self, medal_id: int) -> bool: return medal_id in self.medal_data_1 @staticmethod def edit_medals(save_file: core.SaveFile): medals = save_file.medals medal_names = core.core_data.get_medal_names(save_file) if medal_names.medal_names is None: return options = ["add_medals", "remove_medals"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="medal_add_remove_dialog", single_choice=True ).single_choice() if choice is None: return choice -= 1 add_medals = choice == 0 medals_to_choose_from: list[tuple[int, str]] = [] for i, medal in enumerate(medal_names.medal_names): if len(medal) == 0: continue if medals.has_medal(i) == add_medals: continue key = "medal_string" string = core.core_data.local_manager.get_key( key, medal_name=medal[0], medal_req=medal[1] ) medals_to_choose_from.append((i, string)) if len(medals_to_choose_from) == 0: return options = [medal[1] for medal in medals_to_choose_from] choices, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_medals" ).multiple_choice() if choices is None: return for choice in choices: medal_id = medals_to_choose_from[choice][0] if add_medals: medals.add_medal(medal_id) else: medals.remove_medal(medal_id) if add_medals: color.ColoredText.localize("medals_added") else: color.ColoredText.localize("medals_removed") def add_medal(self, medal_id: int) -> None: if self.has_medal(medal_id): return self.medal_data_1.append(medal_id) self.medal_data_2[medal_id] = 0 def remove_medal(self, medal_id: int) -> None: if medal_id in self.medal_data_2: del self.medal_data_2[medal_id] if medal_id in self.medal_data_1: self.medal_data_1.remove(medal_id) class MedalNames: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.medal_names = self.get_medal_names() def get_medal_names(self) -> list[list[str]] | None: file_name = "medalname.tsv" gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", file_name) if data is None: return None csv = core.CSV(data, delimiter="\t") names: list[list[str]] = [] for row in csv: names.append(row.to_str_list()) return names def get_medal_name(self, medal_id: int) -> list[str] | None: if self.medal_names is None: return None if medal_id < 0 or medal_id >= len(self.medal_names): return [] return self.medal_names[medal_id] ================================================ FILE: src/bcsfe/core/game/catbase/mission.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class Mission: def __init__( self, clear_state: int | None = None, requirement: int | None = None, progress_type: int | None = None, gamatoto_value: int | None = None, nyancombo_value: int | None = None, user_rank_value: int | None = None, expiry_value: int | None = None, preparing_value: int | bool | None = None, ): self.clear_state = clear_state self.requirement = requirement self.progress_type = progress_type self.gamatoto_value = gamatoto_value self.nyancombo_value = nyancombo_value self.user_rank_value = user_rank_value self.expiry_value = expiry_value self.preparing_value = preparing_value @staticmethod def init() -> Mission: return Mission( None, None, None, None, None, None, None, None, ) def serialize(self) -> dict[str, Any]: return { "clear_state": self.clear_state, "requirement": self.requirement, "progress_type": self.progress_type, "gamatoto_value": self.gamatoto_value, "nyancombo_value": self.nyancombo_value, "user_rank_value": self.user_rank_value, "expiry_value": self.expiry_value, "preparing_value": self.preparing_value, } @staticmethod def deserialize(data: dict[str, Any]) -> Mission: return Mission( data["clear_state"], data["requirement"], data["progress_type"], data["gamatoto_value"], data["nyancombo_value"], data["user_rank_value"], data["expiry_value"], data["preparing_value"], ) def __repr__(self): return f"Mission({self.clear_state}, {self.requirement}, {self.progress_type}, {self.gamatoto_value}, {self.nyancombo_value}, {self.user_rank_value}, {self.expiry_value}, {self.preparing_value})" def __str__(self): return self.__repr__() class Missions: def __init__( self, clear_states: dict[int, int], requirements: dict[int, int], progress_types: dict[int, int], gamatoto_values: dict[int, int], nyancombo_values: dict[int, int], user_rank_values: dict[int, int], expiry_values: dict[int, int], preparing_values: dict[int, int | bool], ): self.clear_states = clear_states self.requirements = requirements self.progress_types = progress_types self.gamatoto_values = gamatoto_values self.nyancombo_values = nyancombo_values self.user_rank_values = user_rank_values self.expiry_values = expiry_values self.preparing_values = preparing_values self.weekly_missions: dict[int, bool] = {} @staticmethod def init() -> Missions: return Missions( {}, {}, {}, {}, {}, {}, {}, {}, ) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> Missions: clear_states: dict[int, int] = stream.read_int_int_dict() requirements: dict[int, int] = stream.read_int_int_dict() progress_types: dict[int, int] = stream.read_int_int_dict() gamatoto_values: dict[int, int] = stream.read_int_int_dict() nyancombo_values: dict[int, int] = stream.read_int_int_dict() user_rank_values: dict[int, int] = stream.read_int_int_dict() expiry_values: dict[int, int] = stream.read_int_int_dict() preparing_values: dict[int, int | bool] = {} for _ in range(stream.read_int()): key = stream.read_int() if gv < 90300: preparing_values[key] = stream.read_bool() else: preparing_values[key] = stream.read_int() return Missions( clear_states, requirements, progress_types, gamatoto_values, nyancombo_values, user_rank_values, expiry_values, preparing_values, ) def write(self, stream: core.Data, gv: core.GameVersion): stream.write_int_int_dict(self.clear_states) stream.write_int_int_dict(self.requirements) stream.write_int_int_dict(self.progress_types) stream.write_int_int_dict(self.gamatoto_values) stream.write_int_int_dict(self.nyancombo_values) stream.write_int_int_dict(self.user_rank_values) stream.write_int_int_dict(self.expiry_values) stream.write_int(len(self.preparing_values)) for key, value in self.preparing_values.items(): stream.write_int(key) if gv < 90300: stream.write_bool(bool(value)) else: stream.write_int(int(value)) def read_weekly_missions(self, stream: core.Data): self.weekly_missions: dict[int, bool] = {} for _ in range(stream.read_int()): key = stream.read_int() self.weekly_missions[key] = stream.read_bool() def write_weekly_missions(self, stream: core.Data): stream.write_int(len(self.weekly_missions)) for key, value in self.weekly_missions.items(): stream.write_int(key) stream.write_bool(value) def serialize(self) -> dict[str, Any]: return { "clear_states": self.clear_states, "requirements": self.requirements, "progress_types": self.progress_types, "gamatoto_values": self.gamatoto_values, "nyancombo_values": self.nyancombo_values, "user_rank_values": self.user_rank_values, "expiry_values": self.expiry_values, "preparing_values": self.preparing_values, "weekly_missions": self.weekly_missions, } @staticmethod def deserialize(data: dict[str, Any]): missions = Missions( data.get("clear_states", {}), data.get("requirements", {}), data.get("progress_types", {}), data.get("gamatoto_values", {}), data.get("nyancombo_values", {}), data.get("user_rank_values", {}), data.get("expiry_values", {}), data.get("preparing_values", {}), ) missions.weekly_missions = data.get("weekly_missions", {}) return missions def __repr__(self): return f"" def __str__(self): return self.__repr__() @staticmethod def edit_missions(save_file: core.SaveFile): missions = save_file.missions names = core.core_data.get_mission_names(save_file) conditions = core.core_data.get_mission_conditions(save_file) if names.names is None or conditions.conditions is None: return options: list[str] = [] mssion_ids: list[int] = [] for mission_id, name in names.names.items(): if mission_id in missions.clear_states: name = name.split("
")[0] condition = conditions.conditions.get(mission_id) if not condition: continue name = name.replace("%d", str(condition.progress_count)) if "%@" in name and len(condition.conditions_value) > 2: name = name.replace( "%@", str(condition.conditions_value[2]) ) options.append(name) mssion_ids.append(mission_id) re_claim = dialog_creator.ChoiceInput.from_reduced( ["complete_reward", "complete_claim", "uncomplete"], dialog="select_mission_claim", single_choice=True, ).single_choice() if re_claim is None: return re_claim -= 1 choices, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_missions" ).multiple_choice(localized_options=False) if choices is None: return for choice in choices: mission_id = mssion_ids[choice] if re_claim == 0: missions.clear_states[mission_id] = 2 condition = conditions.get_condition(mission_id) if condition is not None: missions.requirements[mission_id] = condition.progress_count elif re_claim == 1: missions.clear_states[mission_id] = 4 condition = conditions.get_condition(mission_id) if condition is not None: missions.requirements[mission_id] = condition.progress_count elif re_claim == 2: missions.clear_states[mission_id] = 0 if mission_id in missions.requirements: missions.requirements[mission_id] = 0 color.ColoredText.localize("missions_edited") class MissionCondition: def __init__( self, mission_id: int, mission_type: int, conditions_type: int, progress_count: int, conditions_value: list[int], ): self.mission_id = mission_id self.mission_type = mission_type self.conditions_type = conditions_type self.progress_count = progress_count self.conditions_value = conditions_value class MissionConditions: def __init__(self, save: core.SaveFile): self.save = save self.conditions = self.get_conditions() def get_conditions(self) -> dict[int, MissionCondition] | None: file_name = "Mission_Condition.csv" gdg = core.core_data.get_game_data_getter(self.save) file = gdg.download("DataLocal", file_name) if file is None: return None csv = core.CSV(file) conditions: dict[int, MissionCondition] = {} for row in csv: conditions[row[0].to_int()] = MissionCondition( row[0].to_int(), row[1].to_int(), row[2].to_int(), row[3].to_int(), row[4:].to_int_list(), ) return conditions def get_condition(self, mission_id: int) -> MissionCondition | None: if self.conditions is None: return None return self.conditions.get(mission_id) class MissionNames: def __init__(self, save: core.SaveFile): self.save = save self.names = self.get_names() def get_names(self) -> dict[int, str] | None: file_name = "Mission_Name.csv" gdg = core.core_data.get_game_data_getter(self.save) file = gdg.download("resLocal", file_name) if file is None: return None csv = core.CSV( file, delimiter=core.Delimeter.from_country_code_res(self.save.cc) ) names: dict[int, str] = {} for row in csv: names[row[0].to_int()] = row[1].to_str() return names ================================================ FILE: src/bcsfe/core/game/catbase/my_sale.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class MySale: def __init__(self, dict_1: dict[int, int], dict_2: dict[int, bool]): self.dict_1 = dict_1 self.dict_2 = dict_2 @staticmethod def init() -> MySale: return MySale({}, {}) @staticmethod def read_bonus_hash(stream: core.Data): variable_length = stream.read_variable_length_int() dict_1 = {} for _ in range(variable_length): key = stream.read_variable_length_int() value = stream.read_variable_length_int() dict_1[key] = value variable_length = stream.read_variable_length_int() dict_2 = {} for _ in range(variable_length): key = stream.read_variable_length_int() value = stream.read_byte() dict_2[key] = value return MySale(dict_1, dict_2) def write_bonus_hash(self, stream: core.Data): stream.write_variable_length_int(len(self.dict_1)) for key, value in self.dict_1.items(): stream.write_variable_length_int(key) stream.write_variable_length_int(value) stream.write_variable_length_int(len(self.dict_2)) for key, value in self.dict_2.items(): stream.write_variable_length_int(key) stream.write_byte(value) def serialize(self) -> dict[str, Any]: return { "dict_1": self.dict_1, "dict_2": self.dict_2, } @staticmethod def deserialize(data: dict[str, Any]) -> MySale: return MySale(data.get("dict_1", {}), data.get("dict_2", {})) def __repr__(self) -> str: return f"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})" def __str__(self) -> str: return f"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})" ================================================ FILE: src/bcsfe/core/game/catbase/nyanko_club.py ================================================ from __future__ import annotations import datetime import random import time from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator, color class NyankoClub: def __init__( self, officer_id: int, total_renewal_times: int, start_date_now: float, end_date_now: float, start_date_next: float, end_date_next: float, start_date_total: float, end_date_total: float, time_error_end: float, total_state_updates: int, login_bonus_date: float, claimed_rewards: dict[int, int], remaing_days_popup: float, first_popup_flag: bool, badge_flag: bool | None = None, ): self.officer_id = officer_id self.total_renewal_times = total_renewal_times self.start_date_now = start_date_now self.end_date_now = end_date_now self.start_date_next = start_date_next self.end_date_next = end_date_next self.start_date_total = start_date_total self.end_date_total = end_date_total self.time_error_end = time_error_end self.total_state_updates = total_state_updates self.login_bonus_date = login_bonus_date self.claimed_rewards = claimed_rewards self.remaing_days_popup = remaing_days_popup self.first_popup_flag = first_popup_flag self.badge_flag = badge_flag @staticmethod def init() -> NyankoClub: return NyankoClub( 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, {}, 0.0, False, False, ) @staticmethod def read(data: core.Data, gv: core.GameVersion) -> NyankoClub: officer_id = data.read_int() total_renewal_times = data.read_int() start_date_now = data.read_double() end_date_now = data.read_double() start_date_next = data.read_double() end_date_next = data.read_double() start_date_total = data.read_double() end_date_total = data.read_double() time_error_end = data.read_double() total_state_updates = data.read_int() login_bonus_date = data.read_double() claimed_rewards = data.read_int_int_dict() remaing_days_popup = data.read_double() first_popup_flag = data.read_bool() if gv >= 80100: badge_flag = data.read_bool() else: badge_flag = None return NyankoClub( officer_id, total_renewal_times, start_date_now, end_date_now, start_date_next, end_date_next, start_date_total, end_date_total, time_error_end, total_state_updates, login_bonus_date, claimed_rewards, remaing_days_popup, first_popup_flag, badge_flag, ) def write(self, data: core.Data, gv: core.GameVersion): data.write_int(self.officer_id) data.write_int(self.total_renewal_times) data.write_double(self.start_date_now) data.write_double(self.end_date_now) data.write_double(self.start_date_next) data.write_double(self.end_date_next) data.write_double(self.start_date_total) data.write_double(self.end_date_total) data.write_double(self.time_error_end) data.write_int(self.total_state_updates) data.write_double(self.login_bonus_date) data.write_int_int_dict(self.claimed_rewards) data.write_double(self.remaing_days_popup) data.write_bool(self.first_popup_flag) if gv >= 80100: data.write_bool(self.badge_flag or False) def serialize(self) -> dict[str, Any]: return { "officer_id": self.officer_id, "total_renewal_times": self.total_renewal_times, "start_date_now": self.start_date_now, "end_date_now": self.end_date_now, "start_date_next": self.start_date_next, "end_date_next": self.end_date_next, "start_date_total": self.start_date_total, "end_date_total": self.end_date_total, "time_error_end": self.time_error_end, "total_state_updates": self.total_state_updates, "login_bonus_date": self.login_bonus_date, "claimed_rewards": self.claimed_rewards, "remaing_days_popup": self.remaing_days_popup, "first_popup_flag": self.first_popup_flag, "badge_flag": self.badge_flag, } @staticmethod def deserialize(data: dict[str, Any]) -> NyankoClub: return NyankoClub( data.get("officer_id", 0), data.get("total_renewal_times", 0), data.get("start_date_now", 0.0), data.get("end_date_now", 0.0), data.get("start_date_next", 0.0), data.get("end_date_next", 0.0), data.get("start_date_total", 0.0), data.get("end_date_total", 0.0), data.get("time_error_end", 0.0), data.get("total_state_updates", 0), data.get("login_bonus_date", 0.0), data.get("claimed_rewards", {}), data.get("remaing_days_popup", 0.0), data.get("first_popup_flag", False), data.get("badge_flag", False), ) def __repr__(self): return f"" def __str__(self): return f"NyankoClub {self.officer_id}" def get_gold_pass( self, officer_id: int, total_days: int, save_file: core.SaveFile ): self.officer_id = officer_id start_date_now = int(time.time()) end_date_now = ( start_date_now + datetime.timedelta(days=total_days).total_seconds() ) end_date_total = ( start_date_now + datetime.timedelta(days=total_days * 2).total_seconds() ) self.total_renewal_times = 2 self.start_date_now = start_date_now self.end_date_now = end_date_now self.start_date_next = end_date_now self.end_date_next = end_date_total self.start_date_total = start_date_now self.end_date_total = end_date_total self.time_error_end = start_date_now self.total_state_updates = 2 self.login_bonus_date = end_date_now self.remaing_days_popup = 0.0 self.first_popup_flag = True self.badge_flag = False login = save_file.logins.get_login(5100) if login is not None: login.count = 0 self.claimed_rewards = {} def remove_gold_pass(self, save_file: core.SaveFile): self.officer_id = -1 self.total_renewal_times = 0 self.start_date_now = 0.0 self.end_date_now = 0.0 self.start_date_next = 0.0 self.end_date_next = 0.0 self.start_date_total = 0.0 self.end_date_total = 0.0 self.time_error_end = 0.0 self.total_state_updates = 0 self.login_bonus_date = 0.0 self.remaing_days_popup = 0.0 self.first_popup_flag = False self.badge_flag = False login = save_file.logins.get_login(5100) if login is not None: login.count = 0 self.claimed_rewards = {} @staticmethod def get_random_officer_id() -> int: return random.randint(1, 2**16 - 1) @staticmethod def edit_gold_pass(save_file: core.SaveFile): club = save_file.officer_pass.gold_pass officer_id = color.ColoredInput().localize("gold_pass_dialog").strip() if not officer_id: officer_id = NyankoClub.get_random_officer_id() if officer_id == "-1": officer_id = -1 else: try: officer_id = int(officer_id) except ValueError: officer_id = NyankoClub.get_random_officer_id() officer_id = dialog_creator.IntInput().clamp_value(officer_id) if officer_id == -1: club.remove_gold_pass(save_file) color.ColoredText.localize("gold_pass_remove_success") else: club.get_gold_pass(officer_id, 30, save_file) color.ColoredText.localize("gold_pass_get_success", id=officer_id) ================================================ FILE: src/bcsfe/core/game/catbase/officer_pass.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color class OfficerPass: def __init__(self, play_time: int): self.play_time = play_time self.gold_pass = core.NyankoClub.init() self.cat_id = 0 self.cat_form = 0 @staticmethod def init() -> OfficerPass: return OfficerPass(0) @staticmethod def read(data: core.Data) -> OfficerPass: play_time = data.read_int() return OfficerPass(play_time) def write(self, data: core.Data): if self.play_time > 2**31 - 1: self.play_time = 2**31 - 1 data.write_int(self.play_time) def read_gold_pass(self, data: core.Data, gv: core.GameVersion): self.gold_pass = core.NyankoClub.read(data, gv) def write_gold_pass(self, data: core.Data, gv: core.GameVersion): self.gold_pass.write(data, gv) def read_cat_data(self, data: core.Data): self.cat_id = data.read_short() self.cat_form = data.read_short() def write_cat_data(self, data: core.Data): data.write_short(self.cat_id) data.write_short(self.cat_form) def serialize(self) -> dict[str, Any]: return { "play_time": self.play_time, "gold_pass": self.gold_pass.serialize(), "cat_id": self.cat_id, "cat_form": self.cat_form, } @staticmethod def deserialize(data: dict[str, Any]) -> OfficerPass: officer_pass = OfficerPass( data.get("play_time", 0), ) officer_pass.gold_pass = core.NyankoClub.deserialize( data.get("gold_pass", {}) ) officer_pass.cat_id = data.get("cat_id", 0) officer_pass.cat_form = data.get("cat_form", 0) return officer_pass def __repr__(self): return f"OfficerPass({self.play_time}, {self.gold_pass}, {self.cat_id}, {self.cat_form})" def __str__(self): return self.__repr__() def reset(self, save_file: core.SaveFile): self.cat_id = 0 self.cat_form = 0 self.play_time = 0 self.gold_pass.remove_gold_pass(save_file) @staticmethod def fix_crash(save_file: core.SaveFile): officer_pass = save_file.officer_pass officer_pass.reset(save_file) color.ColoredText.localize("officer_pass_fixed") ================================================ FILE: src/bcsfe/core/game/catbase/playtime.py ================================================ from __future__ import annotations from dataclasses import dataclass from bcsfe import core from bcsfe.cli import color, dialog_creator @dataclass class PlayTime: frames: int @staticmethod def get_fps() -> int: return 30 @property def seconds(self) -> int: return self.frames // self.get_fps() @property def minutes(self) -> int: return self.seconds // 60 @property def hours(self) -> int: return self.minutes // 60 @property def just_seconds(self) -> int: return self.seconds % 60 @property def just_minutes(self) -> int: return self.minutes % 60 @property def just_hours(self) -> int: return self.hours % 60 @staticmethod def from_hours(hours: int) -> PlayTime: return PlayTime(hours * 60 * 60 * PlayTime.get_fps()) @staticmethod def from_minutes(minutes: int) -> PlayTime: return PlayTime(minutes * 60 * PlayTime.get_fps()) @staticmethod def from_seconds(seconds: int) -> PlayTime: return PlayTime(seconds * PlayTime.get_fps()) @staticmethod def from_hours_mins_secs( hours: int, minutes: int, seconds: int ) -> PlayTime: return ( PlayTime.from_hours(hours) + PlayTime.from_minutes(minutes) + PlayTime.from_seconds(seconds) ) def __add__(self, other: PlayTime) -> PlayTime: return PlayTime(self.frames + other.frames) def edit(save_file: core.SaveFile): play_time = PlayTime(save_file.officer_pass.play_time) color.ColoredText.localize( "playtime_current", hours=play_time.hours, minutes=play_time.just_minutes, seconds=play_time.just_seconds, frames=play_time.frames, ) hours, _ = dialog_creator.IntInput().get_input("playtime_hours_prompt", {}) if hours is None: return minutes, _ = dialog_creator.IntInput().get_input( "playtime_minutes_prompt", {} ) if minutes is None: return seconds, _ = dialog_creator.IntInput().get_input( "playtime_seconds_prompt", {} ) if seconds is None: return play_time = PlayTime.from_hours_mins_secs(hours, minutes, seconds) save_file.officer_pass.play_time = play_time.frames color.ColoredText.localize( "playtime_edited", hours=play_time.hours, minutes=play_time.just_minutes, seconds=play_time.just_seconds, frames=play_time.frames, ) ================================================ FILE: src/bcsfe/core/game/catbase/powerup.py ================================================ from __future__ import annotations from bcsfe import core class PowerUpHelper: def __init__(self, cat: core.Cat, save_file: core.SaveFile): self.cat = cat self.save_file = save_file self.unit_limit = self.save_file.cats.read_unitlimit( self.save_file ).get_unit_limit(self.cat.id) self.all_unit_buy = self.save_file.cats.read_unitbuy(self.save_file) self.unit_buy = self.all_unit_buy.get_unit_buy(self.cat.id) self.rank_gifts = self.save_file.user_rank_rewards.read_rank_gifts( self.save_file ) self.max_upgrade_level = self.__get_max_upgrade_level_check() def get_current_max_level(self) -> int | None: if self.unit_buy is None: return None return min( self.unit_buy.original_max_levels[0] + self.max_upgrade_level, self.unit_buy.max_upgrade_level_catseye, ) def has_strict_upgrade(self) -> bool: return core.core_data.config.get_bool(core.ConfigKey.STRICT_UPGRADE) def get_upgrade_state_check(self) -> int: if not self.has_strict_upgrade(): return 100000 return self.save_file.upgrade_state def get_user_rank_check(self) -> int: if not self.has_strict_upgrade(): return 1000000 return self.save_file.calculate_user_rank() def __get_max_upgrade_level_check(self) -> int: if self.unit_limit is None: return self.cat.max_upgrade_level.base rewards = self.save_file.user_rank_rewards self.cat.max_upgrade_level.reset() strict_upgrade = self.has_strict_upgrade() for reward_id in range(len(rewards.rewards)): rank_gift = self.rank_gifts.get_by_id(reward_id) if rank_gift is None: continue user_rank_reward = rewards.rewards[reward_id] if not user_rank_reward.claimed and strict_upgrade: continue for present in rank_gift.rewards: if present[0] >= 1000 and present[0] <= 1599: for limit in self.unit_limit.values: if limit == present[0]: self.cat.max_upgrade_level.increment_base( present[1] ) elif present[0] >= 4000 and present[0] <= 4599: for limit in self.unit_limit.values: if limit == present[0]: self.cat.max_upgrade_level.increment_plus( present[1] ) return self.cat.max_upgrade_level.base def can_power_up(self) -> bool: if self.unit_buy is None: return False base_level = self.cat.upgrade.get_base() current_max_level = self.get_current_max_level() if current_max_level is None: return False if base_level >= current_max_level or ( ( self.get_upgrade_state_check() > 1 or base_level == self.unit_buy.unknown_22 ) and self.get_upgrade_state_check() < 2 ): return ( self.unit_buy.rarity != 0 and base_level >= self.unit_buy.max_upgrade_level_no_catseye and base_level < self.unit_buy.max_upgrade_level_catseye and base_level < current_max_level ) return True def can_use_catseye(self) -> bool: if self.unit_buy is None: return False base_level = self.cat.upgrade.get_base() return ( self.unit_buy.rarity != 0 and base_level >= self.unit_buy.max_upgrade_level_no_catseye and self.unit_buy.max_upgrade_level_no_catseye != -1 and self.get_user_rank_check() >= 1600 ) def upgrade_cat(self, force: bool = False) -> bool: if force: self.cat.upgrade_base(self.save_file) return True if self.unit_buy is None: return False current_max_level = self.get_current_max_level() if current_max_level is None: return False if self.can_power_up(): self.cat.upgrade_base(self.save_file) return True if ( self.can_use_catseye() and self.unit_buy.max_upgrade_level_no_catseye <= current_max_level ): if ( self.cat.upgrade.get_base() < self.unit_buy.max_upgrade_level_catseye ): self.cat.upgrade_base(self.save_file) self.cat.catseyes_used += 1 self.cat.max_upgrade_level.upgrade() return True return False return False def get_max_max_base_upgrade_level(self) -> int: max_level = 0 if self.all_unit_buy.unit_buy is None: return 90 for unit_buy in self.all_unit_buy.unit_buy: if unit_buy.max_upgrade_level_catseye > max_level: max_level = unit_buy.max_upgrade_level_catseye return max_level def get_max_max_plus_upgrade_level(self) -> int: max_level = 0 if self.all_unit_buy.unit_buy is None: return 90 for unit_buy in self.all_unit_buy.unit_buy: if unit_buy.max_plus_upgrade_level > max_level: max_level = unit_buy.max_plus_upgrade_level return max_level def get_max_possible_base(self) -> int: if self.unit_buy is None: return 90 return self.unit_buy.max_upgrade_level_catseye def get_max_possible_plus(self) -> int: if self.unit_buy is None: return 90 return self.unit_buy.max_plus_upgrade_level def reset_upgrade(self): self.cat.upgrade.base = 0 self.cat.catseyes_used = 0 def upgrade_by(self, amount: int): if amount == -1: return for _ in range(amount): did_upgrade = self.upgrade_cat() if not did_upgrade: break def max_upgrade(self): while self.upgrade_cat(): pass ================================================ FILE: src/bcsfe/core/game/catbase/scheme_items.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import dialog_creator, color class SchemeDataItem: def __init__( self, id: int, type: int, type_id: int, item_id: int, number: int, type_id2: int | None = None, item_id2: int | None = None, number2: int | None = None, type_id3: int | None = None, item_id3: int | None = None, number3: int | None = None, ): self.id = id self.type = type self.type_id = type_id self.item_id = item_id self.number = number self.type_id2 = type_id2 self.item_id2 = item_id2 self.number2 = number2 self.type_id3 = type_id3 self.item_id3 = item_id3 self.number3 = number3 def is_cat(self) -> bool: return self.type_id == 1 def get_name(self, localizable: core.Localizable) -> str | None: key = f"scheme_popup_{self.id}" name = localizable.get(key) if name is None: return None return name.replace(",", "").replace("", "") class SchemeItems: def __init__(self, to_obtain: list[int], received: list[int]): self.to_obtain = to_obtain self.received = received @staticmethod def init() -> SchemeItems: return SchemeItems([], []) @staticmethod def read(stream: core.Data) -> SchemeItems: total = stream.read_int() to_obtain: list[int] = [] for _ in range(total): to_obtain.append(stream.read_int()) total = stream.read_int() received: list[int] = [] for _ in range(total): received.append(stream.read_int()) return SchemeItems(to_obtain, received) def write(self, stream: core.Data): stream.write_int(len(self.to_obtain)) for item in self.to_obtain: stream.write_int(item) stream.write_int(len(self.received)) for item in self.received: stream.write_int(item) def serialize(self) -> dict[str, list[int]]: return {"to_obtain": self.to_obtain, "received": self.received} @staticmethod def deserialize(data: dict[str, list[int]]) -> SchemeItems: return SchemeItems(data.get("to_obtain", []), data.get("received", [])) def __repr__(self) -> str: return f"SchemeItems(to_obtain={self.to_obtain!r}, received={self.received!r})" def __str__(self) -> str: return self.__repr__() def edit(self, save_file: core.SaveFile): item_names = core.core_data.get_gatya_item_names(save_file) localizable = save_file.get_localizable() scheme_data = core.core_data.get_game_data_getter(save_file).download( "DataLocal", "schemeItemData.tsv" ) if scheme_data is None: return csv = core.CSV(scheme_data, "\t") scheme_items: dict[int, SchemeDataItem] = {} for line in csv.lines[1:]: scheme_items[line[0].to_int()] = SchemeDataItem( line[0].to_int(), line[1].to_int(), line[2].to_int(), line[3].to_int(), line[4].to_int(), line[5].to_int(), line[6].to_int(), line[7].to_int(), line[8].to_int(), line[9].to_int(), line[10].to_int(), ) options: list[str] = [] for item in scheme_items.values(): scheme_name = item.get_name(localizable) if scheme_name is None: return string = "\n\t" if item.is_cat(): cat_names = core.Cat.get_names(item.item_id, save_file) if cat_names: cat_name = cat_names[0] string += scheme_name.replace("%@", cat_name) else: item_name = item_names.get_name(item.item_id) if item_name: string += scheme_name first_index = string.find("%@") second_index = string.find("%@", first_index + 1) string = ( string[:first_index] + str(item.number) + " " + item_name + string[second_index + 2 :] ) string = string.replace("
", "\n\t") options.append(string) choice = dialog_creator.ChoiceInput.from_reduced( ["gain_scheme_items", "remove_scheme_items"], dialog="gain_remove_scheme_items", ).single_choice() if choice is None: return choice -= 1 if choice == 0: self.add_scheme_items(options, scheme_items) elif choice == 1: self.remove_scheme_items(options, scheme_items) def add_scheme_items( self, options: list[str], scheme_items: dict[int, SchemeDataItem], ): scheme_ids, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="scheme_items_select_gain", ).multiple_choice() if scheme_ids is None: return for option_id in scheme_ids: scheme_id = list(scheme_items.keys())[option_id] if scheme_id not in self.to_obtain: self.to_obtain.append(scheme_id) if scheme_id in self.received: self.received.remove(scheme_id) color.ColoredText.localize("scheme_items_edit_success") def remove_scheme_items( self, options: list[str], scheme_items: dict[int, SchemeDataItem], ): scheme_ids, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="scheme_items_select_remove", ).multiple_choice() if scheme_ids is None: return for option_id in scheme_ids: scheme_id = list(scheme_items.keys())[option_id] if scheme_id in self.to_obtain: self.to_obtain.remove(scheme_id) if scheme_id in self.received: self.received.remove(scheme_id) color.ColoredText.localize("scheme_items_edit_success") ================================================ FILE: src/bcsfe/core/game/catbase/special_skill.py ================================================ from __future__ import annotations, division from bcsfe import core from typing import Any from bcsfe.cli import dialog_creator, color class SpecialSkill: def __init__(self, upg: core.Upgrade): self.upgrade = upg self.seen = 0 self.max_upgrade_level = core.Upgrade(0, 0) @staticmethod def init() -> SpecialSkill: return SpecialSkill(core.Upgrade(0, 0)) @staticmethod def read_upgrade(stream: core.Data) -> SpecialSkill: up = core.Upgrade.read(stream) return SpecialSkill(up) def write_upgrade(self, stream: core.Data): self.upgrade.write(stream) def read_seen(self, stream: core.Data): self.seen = stream.read_int() def write_seen(self, stream: core.Data): stream.write_int(self.seen) def read_max_upgrade_level(self, stream: core.Data): level = core.Upgrade.read(stream) self.max_upgrade_level = level def write_max_upgrade_level(self, stream: core.Data): self.max_upgrade_level.write(stream) def serialize(self) -> dict[str, Any]: return { "upgrade": self.upgrade.serialize(), "seen": self.seen, "max_upgrade_level": self.max_upgrade_level.serialize(), } @staticmethod def deserialize(data: dict[str, Any]) -> SpecialSkill: skill = SpecialSkill(core.Upgrade.deserialize(data.get("upgrade", {}))) skill.seen = data.get("seen", 0) skill.max_upgrade_level = core.Upgrade.deserialize( data.get("max_upgrade_level", {}) ) return skill def __repr__(self) -> str: return f"Skill(upgrade={self.upgrade}, seen={self.seen}, max_upgrade_level={self.max_upgrade_level})" def __str__(self) -> str: return self.__repr__() def set_upgrade( self, upgrade: core.Upgrade, only_plus: bool = False, max_base: int | None = None, max_plus: int | None = None, ): if max_base is not None: upgrade.base = min(upgrade.base, max_base) if max_plus is not None: upgrade.plus = min(upgrade.plus, max_plus) base = upgrade.base plus = upgrade.plus if base != -1 and not only_plus: self.upgrade.base = upgrade.get_random_base(max_base) if plus != -1: self.upgrade.plus = upgrade.get_random_plus(max_plus) class SpecialSkills: def __init__(self, skills: list[SpecialSkill]): self.skills = skills def get_upgrade(self, valid_skill_id: int) -> SpecialSkill: if valid_skill_id >= 1: valid_skill_id += 1 return self.skills[valid_skill_id] def set_upgrade( self, valid_skill_id: int, upgrade: core.Upgrade, max_base: int | None = None, max_plus: int | None = None, ): u = upgrade.copy() valid_skills = self.get_valid_skills() valid_skills[valid_skill_id].set_upgrade( u, max_base=max_base, max_plus=max_plus ) if ( valid_skill_id == 0 ): # if it is a cat cannon power upgrade, mirror the upgrade to the hidden cat cannon power special skill self.skills[1].set_upgrade(u, max_base=max_base, max_plus=max_plus) @staticmethod def init() -> SpecialSkills: skills = [SpecialSkill.init() for _ in range(11)] return SpecialSkills(skills) def get_valid_skills(self) -> list[SpecialSkill]: new_skills: list[SpecialSkill] = [] for i, skill in enumerate(self.skills): if i == 1: continue new_skills.append(skill) return new_skills @staticmethod def read_upgrades(stream: core.Data) -> SpecialSkills: total_skills = 11 skills: list[SpecialSkill] = [] for _ in range(total_skills): skills.append(SpecialSkill.read_upgrade(stream)) return SpecialSkills(skills) def write_upgrades(self, stream: core.Data): for skill in self.skills: skill.write_upgrade(stream) def read_gatya_seen(self, stream: core.Data): for skill in self.get_valid_skills(): skill.read_seen(stream) def write_gatya_seen(self, stream: core.Data): for skill in self.get_valid_skills(): skill.write_seen(stream) def read_max_upgrade_levels(self, stream: core.Data): for skill in self.skills: skill.read_max_upgrade_level(stream) def write_max_upgrade_levels(self, stream: core.Data): for skill in self.skills: skill.write_max_upgrade_level(stream) def serialize(self) -> list[dict[str, Any]]: return [skill.serialize() for skill in self.skills] @staticmethod def deserialize(data: list[dict[str, Any]]) -> SpecialSkills: skills = SpecialSkills([]) for skill in data: skills.skills.append(SpecialSkill.deserialize(skill)) return skills def __repr__(self) -> str: return f"Skills(skills={self.skills})" def __str__(self) -> str: return f"Skills(skills={self.skills})" def edit(self, save_file: core.SaveFile): names_o = core.core_data.get_gatya_item_names(save_file) items = core.core_data.get_gatya_item_buy(save_file).get_by_category(2) if items is None: return names: list[str] = [] for item in items: name = names_o.get_name(item.id) if name is None: return names.append(name) ids, _ = dialog_creator.ChoiceInput.from_reduced( names, [], {}, "special_skills_dialog" ).multiple_choice() if not ids: return skills = self.get_valid_skills() if len(ids) == 1: option_id = 0 else: options: list[str] = [ "upgrade_individual_skill", "upgrade_all_skills", ] option_id = dialog_creator.ChoiceInput( options, options, [], {}, "upgrade_skills_select_mod", True ).single_choice() if option_id is None: return option_id -= 1 ability_data = core.core_data.get_ability_data(save_file) if ability_data.ability_data is None: return success = False if option_id == 0: for id in ids: color.ColoredText.localize( "selected_skill_upgrades", name=names[id], base_level=skills[id].upgrade.base + 1, plus_level=skills[id].upgrade.plus, ) ability = ability_data.get_ability_data_item(id) if ability is None: continue upgrade, should_exit = core.Upgrade.get_user_upgrade( ability.max_base_level - 1, ability.max_plus_level ) if should_exit: return if upgrade is not None: self.set_upgrade(id, upgrade) color.ColoredText.localize( "selected_skill_upgraded", name=names[id], base_level=skills[id].upgrade.base + 1, plus_level=skills[id].upgrade.plus, ) success = True elif option_id == 1: max_base_level = max( [ability.max_base_level for ability in ability_data.ability_data] ) max_plus_level = max( [ability.max_plus_level for ability in ability_data.ability_data] ) upgrade, should_exit = core.Upgrade.get_user_upgrade( max_base_level - 1, max_plus_level ) if should_exit or upgrade is None: return disable_maxes = core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES) for id in ids: max_base_level = ability_data.ability_data[id].max_base_level - 1 max_plus_level = ability_data.ability_data[id].max_plus_level if disable_maxes: max_base_level = None max_plus_level = None self.set_upgrade( id, upgrade.copy(), max_base=max_base_level, max_plus=max_plus_level, ) color.ColoredText.localize( "selected_skill_upgraded", name=names[id], base_level=skills[id].upgrade.base + 1, plus_level=skills[id].upgrade.plus, ) success = True if success: color.ColoredText.localize("skills_edited") def get_from_id(self, id: int, only_valid: bool = True) -> SpecialSkill | None: if only_valid: skills = self.get_valid_skills() else: skills = self.skills if id >= len(skills) or id < 0: return None return skills[id] class AbilityDataItem: def __init__( self, index: int, sell_price: int, gatya_rarity: int, max_base_level: int, max_plus_level: int, chapter_1_to_2_max_level: int, ): self.index = index self.sell_price = sell_price self.gatya_rarity = gatya_rarity self.max_base_level = max_base_level self.max_plus_level = max_plus_level self.chapter_1_to_2_max_level = chapter_1_to_2_max_level class AbilityData: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.ability_data = self.get_ability_data() def get_ability_data(self) -> list[AbilityDataItem] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "AbilityData.csv") if data is None: return None csv = core.CSV(data) ability_data: list[AbilityDataItem] = [] for i, row in enumerate(csv): ability_data.append( AbilityDataItem( i, row[0].to_int(), row[1].to_int(), row[2].to_int(), row[3].to_int(), row[4].to_int(), ) ) return ability_data def get_ability_data_item(self, item_id: int) -> AbilityDataItem | None: if self.ability_data is None: return None return self.ability_data[item_id] ================================================ FILE: src/bcsfe/core/game/catbase/stamp.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class StampData: def __init__( self, current_stamp: int, collected_stamp: list[int], unknown: int, daily_reward: int, ): self.current_stamp = current_stamp self.collected_stamp = collected_stamp self.unknown = unknown self.daily_reward = daily_reward @staticmethod def init() -> StampData: return StampData(0, [0] * 30, 0, 0) @staticmethod def read(stream: core.Data) -> StampData: current_stamp = stream.read_int() collected_stamp = stream.read_int_list(30) unknown = stream.read_int() daily_reward = stream.read_int() return StampData(current_stamp, collected_stamp, unknown, daily_reward) def write(self, stream: core.Data): stream.write_int(self.current_stamp) stream.write_int_list(self.collected_stamp, write_length=False) stream.write_int(self.unknown) stream.write_int(self.daily_reward) def serialize(self) -> dict[str, Any]: return { "current_stamp": self.current_stamp, "collected_stamp": self.collected_stamp, "unknown": self.unknown, "daily_reward": self.daily_reward, } @staticmethod def deserialize(data: dict[str, Any]) -> StampData: return StampData( data.get("current_stamp", 0), data.get("collected_stamp", []), data.get("unknown", 0), data.get("daily_reward", 0), ) def __repr__(self): return f"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})" def __str__(self): return f"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})" ================================================ FILE: src/bcsfe/core/game/catbase/talent_orbs.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class TalentOrb: def __init__(self, id: int, value: int): self.id = id self.value = value @staticmethod def init() -> TalentOrb: return TalentOrb( 0, 0, ) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> TalentOrb: id = stream.read_short() if gv < 110400: value = stream.read_byte() else: value = stream.read_short() return TalentOrb(id, value) def write(self, stream: core.Data, gv: core.GameVersion): stream.write_short(self.id) if gv < 110400: stream.write_byte(self.value) else: stream.write_short(self.value) def serialize(self) -> dict[str, Any]: return { "id": self.id, "value": self.value, } @staticmethod def deserialize(data: dict[str, Any]) -> TalentOrb: return TalentOrb(data.get("id", 0), data.get("value", 0)) def __repr__(self): return f"Orb({self.id}, {self.value})" def __str__(self): return self.__repr__() class TalentOrbs: def __init__(self, orbs: dict[int, TalentOrb]): self.orbs = orbs @staticmethod def init() -> TalentOrbs: return TalentOrbs({}) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> TalentOrbs: length = stream.read_short() orbs: dict[int, TalentOrb] = {} for _ in range(length): orb = TalentOrb.read(stream, gv) orbs[orb.id] = orb return TalentOrbs(orbs) def write(self, stream: core.Data, gv: core.GameVersion): stream.write_short(len(self.orbs)) for orb in self.orbs.values(): orb.write(stream, gv) def serialize(self) -> list[dict[str, Any]]: return [orb.serialize() for orb in self.orbs.values()] @staticmethod def deserialize(data: list[dict[str, Any]]) -> TalentOrbs: return TalentOrbs( {orb.get("id", 0): TalentOrb.deserialize(orb) for orb in data} ) def __repr__(self): return f"TalentOrbs({self.orbs})" def __str__(self): return self.__repr__() def set_orb(self, id: int, value: int): self.orbs[id] = TalentOrb(id, value) class RawOrbInfo: def __init__( self, orb_id: int, rank_id: int, effect_id: int, value: list[int], target_id: int | None, ): self.orb_id = orb_id self.rank_id = rank_id self.effect_id = effect_id self.value = value self.target_id = target_id class OrbInfo: def __init__( self, raw_orb_info: RawOrbInfo, rank: str, target: str | None, effect: str, ): self.raw_orb_info = raw_orb_info self.rank = rank self.target = target self.effect = effect def __str__(self) -> str: """Get the string representation of the OrbInfo Returns: str: The string representation of the OrbInfo """ target_color = color_from_enemy_type(self.raw_orb_info.target_id) rank_color = color_from_grade(self.raw_orb_info.rank_id) effect_color = color_from_effect(self.raw_orb_info.effect_id) effect_text = self.effect.replace("%@", "{}") effect_text = f"<{effect_color}>{effect_text}" target = self.target effect = effect_text.format( f"<{rank_color}>{self.rank}", f"<{target_color}>{target}" if target else "", ) return f"{effect}" def to_colortext(self) -> str: """Get the string representation of the OrbInfo with color Returns: str: The string representation of the OrbInfo with color """ return str(self) @staticmethod def create_unknown(orb_id: int) -> OrbInfo: """Create an unknown OrbInfo Args: orb_id (int): The id of the orb Returns: OrbInfo: The unknown OrbInfo """ return OrbInfo( RawOrbInfo(orb_id, 0, 0, [], 0), "???", "", "%@:%@", ) class OrbInfoList: equipment_data_file_name = "DataLocal/equipmentlist.json" grade_list_file_name = "DataLocal/equipmentgrade.csv" attribute_list_file_name = "resLocal/attribute_explonation.tsv" effect_list_file_name = "resLocal/equipment_explonation.tsv" def __init__(self, orb_info_list: list[OrbInfo]): """Initialize the OrbInfoList class Args: orb_info_list (list[OrbInfo]): The list of OrbInfo """ self.orb_info_list = orb_info_list @staticmethod def create(save_file: core.SaveFile) -> OrbInfoList | None: """Create an OrbInfoList Args: save_file (core.SaveFile): The save file Returns: OrbInfoList | None: The OrbInfoList """ gdg = core.core_data.get_game_data_getter(save_file) json_data_file = gdg.download_from_path(OrbInfoList.equipment_data_file_name) grade_list_file = gdg.download_from_path(OrbInfoList.grade_list_file_name) attribute_list_file = gdg.download_from_path( OrbInfoList.attribute_list_file_name ) equipment_list_file = gdg.download_from_path(OrbInfoList.effect_list_file_name) if ( json_data_file is None or grade_list_file is None or attribute_list_file is None or equipment_list_file is None ): return None raw_orbs = OrbInfoList.parse_json_data(json_data_file) if raw_orbs is None: return None orbs = OrbInfoList.load_names( raw_orbs, grade_list_file, attribute_list_file, equipment_list_file ) return OrbInfoList(orbs) @staticmethod def parse_json_data(json_data: core.Data) -> list[RawOrbInfo] | None: """Parse the json data of the equipment Args: json_data (core.Data): The json data Returns: list[RawOrbInfo]: The list of RawOrbInfo """ try: data: dict[str, Any] = core.JsonFile.from_data(json_data).to_object() except core.JSONDecodeError: return None orb_info_list: list[RawOrbInfo] = [] for id, orb in enumerate(data["ID"]): grade_id = orb["gradeID"] content = orb["content"] value = orb["value"] attribute = orb.get("attribute") orb_info_list.append(RawOrbInfo(id, grade_id, content, value, attribute)) return orb_info_list @staticmethod def load_names( raw_orb_info: list[RawOrbInfo], grade_data: core.Data, attribute_data: core.Data, effect_data: core.Data, ) -> list[OrbInfo]: """Load the names of the equipment Args: raw_orb_info (list[RawOrbInfo]): The list of RawOrbInfo grade_data (core.Data): Raw data of the grade list attribute_data (core.Data): Raw data of the attribute list effect_data (core.Data): Raw data of the effect list Returns: list[OrbInfo]: The list of OrbInfo """ grade_csv = core.CSV(grade_data) attribute_tsv = core.CSV(attribute_data, "\t") effect_csv = core.CSV(effect_data, "\t") orb_info_list: list[OrbInfo] = [] for orb in raw_orb_info: grade = grade_csv[orb.rank_id][3].to_str() effect = effect_csv[orb.effect_id][0].to_str() if orb.target_id is not None: attribute = attribute_tsv[orb.target_id][0].to_str() else: attribute = None orb_info_list.append(OrbInfo(orb, grade, attribute, effect)) return orb_info_list def get_orb_info(self, orb_id: int) -> OrbInfo | None: """Get the OrbInfo from the id Args: orb_id (int): The id of the orb Returns: OrbInfo | None: The OrbInfo """ try: return self.orb_info_list[orb_id] except IndexError: return None def get_orb_from_components( self, grade: str, attribute: str | None, effect: str, ) -> OrbInfo | None: """Get the OrbInfo from the components Args: grade (str): The grade of the orb attribute (str | None): The attribute of the orb. None if applies to all attributes effect (str): The effect of the orb Returns: OrbInfo | None: The OrbInfo """ for orb in self.orb_info_list: if orb.rank == grade and orb.target == attribute and orb.effect == effect: return orb return None def does_match_orb_str(self, str_1: str | None, str_2: str | None) -> bool: if str_2 == "*": return True if str_1 is None: return str_2 is None if str_2 is None: return False return str_1.lower() == str_2.lower() def get_orbs_from_component_fuzzy( self, grade: str, attribute: str | None, effect: str, ) -> list[OrbInfo]: """Get the OrbInfo from the components matching the first word of the effect and lowercased Args: grade (str): The grade of the orb attribute (str | None): The attribute of the orb. None if all effect (str): The effect of the orb Returns: list[OrbInfo]: The list of OrbInfo """ orbs: list[OrbInfo] = [] for orb in self.orb_info_list: if ( (orb.rank.lower() == grade.lower() or grade == "*") and (self.does_match_orb_str(orb.target, attribute)) and (orb.effect == effect or effect == "*") ): orbs.append(orb) return orbs def get_all_grades(self) -> list[str]: """Get all the grades Returns: list[str]: The list of grades """ data = list( set([(orb.rank, orb.raw_orb_info.rank_id) for orb in self.orb_info_list]) ) data.sort(key=lambda id: id[1]) return [orb[0] for orb in data] def get_all_attributes(self) -> list[str | None]: """Get all the attributes Returns: list[str]: The list of attributes """ data = list( set( [ (orb.target, orb.raw_orb_info.target_id) for orb in self.orb_info_list if orb.target is not None and orb.raw_orb_info.target_id is not None ] ) ) data.sort(key=lambda id: id[1]) return [orb[0] for orb in data] def get_all_effects(self) -> list[str]: """Get all the effects Returns: list[str]: The list of effects """ data = list( set( [(orb.effect, orb.raw_orb_info.effect_id) for orb in self.orb_info_list] ) ) data.sort(key=lambda id: id[1]) return [orb[0] for orb in data] class SaveOrb: """Represents a saved orb in the save file""" def __init__(self, orb: OrbInfo, count: int): """Initialize the SaveOrb class Args: orb (OrbInfo): The OrbInfo count (int): The amount of the orb """ self.count = count self.orb = orb def color_from_enemy_type(target_id: int | None) -> str: if target_id is None: return color.ColorHex.WHITE if target_id == 0: return color.ColorHex.RED elif target_id == 1: return color.ColorHex.GREEN elif target_id == 2: return color.ColorHex.DARK_GREY elif target_id == 3: return color.ColorHex.LIGHT_GREY elif target_id == 4: return color.ColorHex.YELLOW elif target_id == 5: return color.ColorHex.BLUE elif target_id == 6: return color.ColorHex.MAGENTA elif target_id == 7: return color.ColorHex.DARK_GREEN elif target_id == 8: return color.ColorHex.WHITE elif target_id == 9: return color.ColorHex.DARK_MAGENTA elif target_id == 10: return color.ColorHex.ORANGE elif target_id == 11: return color.ColorHex.CYAN return color.ColorHex.BLACK def color_from_grade(grade_id: int) -> str: if grade_id == 0: return color.ColorHex.RED elif grade_id == 1: return color.ColorHex.ORANGE elif grade_id == 2: return color.ColorHex.YELLOW elif grade_id == 3: return color.ColorHex.GREEN elif grade_id == 4: return color.ColorHex.BLUE return color.ColorHex.BLACK def color_from_effect(effect_id: int): # if effect_id == 0: # return color.ColorHex.RED # elif effect_id == 1: # return color.ColorHex.GREEN # elif effect_id == 2: # return color.ColorHex.DARK_GREY # elif effect_id == 3: # return color.ColorHex.LIGHT_GREY # elif effect_id == 4: # return color.ColorHex.YELLOW # elif effect_id == 5: # return color.ColorHex.BLUE # elif effect_id == 6: # return color.ColorHex.MAGENTA # elif effect_id == 7: # return color.ColorHex.DARK_GREEN # elif effect_id == 8: # return color.ColorHex.WHITE # elif effect_id == 9: # return color.ColorHex.DARK_MAGENTA # elif effect_id == 10: # return color.ColorHex.ORANGE return "@t" class SaveOrbs: def __init__( self, orbs: dict[int, SaveOrb], orb_info_list: OrbInfoList, ): """Initialize the SaveOrbs class Args: orbs (dict[int, SaveOrb]): The orbs orb_info_list (OrbInfoList): The orb info list """ self.orbs = orbs self.orb_info_list = orb_info_list @staticmethod def from_save_file(save_file: core.SaveFile) -> SaveOrbs | None: """Create a SaveOrbs from the save stats Args: save_file (core.SaveFile): The save file Returns: SaveOrbs | None: The SaveOrbs """ orb_info_list = OrbInfoList.create(save_file) if orb_info_list is None: return None orbs: dict[int, SaveOrb] = {} for orb_id, orb in save_file.talent_orbs.orbs.items(): try: orb_info = orb_info_list.orb_info_list[int(orb_id)] except IndexError: orb_info = OrbInfo.create_unknown(int(orb_id)) orbs[int(orb_id)] = SaveOrb(orb_info, orb.value) return SaveOrbs(orbs, orb_info_list) def print(self): """Print the orbs as a formatted list""" self.sort_orbs() total_orbs = sum([orb.count for orb in self.orbs.values()]) color.ColoredText.localize("total_current_orbs", total_orbs=total_orbs) color.ColoredText.localize( "total_current_orb_types", total_types=len(self.orbs) ) color.ColoredText.localize("current_orbs") for orb in self.orbs.values(): color.ColoredText(f"<@q>{orb.count} {orb.orb.to_colortext()}") def sort_orbs(self): """Sort the orbs by attribute, effect, grade and id in that order with attribute being the most important""" orbs = list(self.orbs.values()) orbs.sort(key=lambda orb: orb.orb.raw_orb_info.orb_id) orbs.sort(key=lambda orb: orb.orb.raw_orb_info.rank_id) orbs.sort(key=lambda orb: orb.orb.raw_orb_info.effect_id) orbs.sort(key=lambda orb: orb.orb.raw_orb_info.target_id or -1) def localize_attribute(self, attribute: str | None) -> str | None: if attribute is not None: return attribute def edit(self): """Edit the orbs""" # this code sucks quit a lot, but it works and i can't be bothered making it better atm self.print() all_grades = self.orb_info_list.get_all_grades() all_grades = [grade for grade in all_grades] all_grades.sort() all_attributes = self.orb_info_list.get_all_attributes() all_attributes = [ self.localize_attribute(attribute) or "" for attribute in all_attributes if attribute ] all_attributes.sort() all_effects = self.orb_info_list.get_all_effects() all_effects.sort() all_effects_str = [ effect.lower().replace("%@", "").replace(":", "").strip() + f" <@s>({i})" for (i, effect) in enumerate(all_effects) ] all_effect_ids = [i for i in range(len(all_effects))] all_grades_str = ",".join( f"<{color_from_grade(self.orb_info_list.get_all_grades().index(grade))}>{grade}" for grade in all_grades ) all_attributes_str = ",".join( f"<{color_from_enemy_type(self.orb_info_list.get_all_attributes().index(attribute))}>{attribute}" for attribute in all_attributes ) all_effects_str = ", ".join( f"<{color_from_effect(self.orb_info_list.get_all_effects().index(effect))}>{effect_str}" for effect_str, effect in zip(all_effects_str, all_effects) ) color.ColoredText.localize( "edit_orbs_help", escape=False, all_grades_str=all_grades_str, all_attributes_str=all_attributes_str, all_effects_str=all_effects_str, ) orb_input_selection = ( color.ColoredInput() .localize("orb_select") .lower() .replace("angle", "angel") .split(",") ) if orb_input_selection == [core.core_data.local_manager.get_key("quit_key")]: return orb_selection: list[OrbInfo] = [] for orb_input in orb_input_selection: grade = None attribute = None effect = None orb_input = orb_input.strip() parts = orb_input.split(" ") parts = [part.lower() for part in parts if part != ""] if len(parts) == 0: continue if parts[0] == "*": orb_selection = self.orb_info_list.orb_info_list break for available_grade in all_grades: if available_grade.lower() in parts: grade = available_grade break for available_attribute in all_attributes: if available_attribute.lower() in parts: attribute = available_attribute break for available_effect in all_effect_ids: if str(available_effect) in parts: effect = all_effects[available_effect] break if grade is None: grade = "*" if attribute is None: attribute = "*" if effect is None: effect = "*" orbs = self.orb_info_list.get_orbs_from_component_fuzzy( grade, attribute, effect ) orb_selection.extend(orbs) orb_selection = list(set(orb_selection)) orb_selection.sort(key=lambda orb: orb.raw_orb_info.orb_id) orb_selection.sort(key=lambda orb: orb.raw_orb_info.rank_id) orb_selection.sort(key=lambda orb: orb.raw_orb_info.effect_id) orb_selection.sort(key=lambda orb: orb.raw_orb_info.target_id or -1) color.ColoredText.localize("selected_orbs") for orb in orb_selection: color.ColoredText(orb.to_colortext()) max_orbs = core.core_data.max_value_manager.get("talent_orbs") if len(orb_selection) == 0: return if len(orb_selection) == 1: individual = True else: individual = dialog_creator.ChoiceInput.from_reduced( ["individual", "edit_all_at_once"], dialog="edit_orbs_individually", single_choice=True, ).single_choice() if individual is None: return individual = True if individual == 1 else False if individual: for orb in orb_selection: orb_id = orb.raw_orb_info.orb_id try: orb_count = self.orbs[orb_id].count except KeyError: orb_count = 0 orb_count = dialog_creator.SingleEditor( orb.to_colortext(), orb_count, max_orbs ).edit(escape_text=False) self.orbs[orb_id] = SaveOrb(orb, orb_count) else: int_input = dialog_creator.IntInput(max_orbs) orb_count = int_input.get_input_locale_while( "edit_orbs_all", {"max": max_orbs}, escape=False ) if orb_count is None: return orb_count = int_input.clamp_value(orb_count) for orb in orb_selection: orb_id = orb.raw_orb_info.orb_id self.orbs[orb_id] = SaveOrb(orb, orb_count) self.print() def save(self, save_file: core.SaveFile): """Save the orbs to the save_stats Args: save_file (core.SaveFile): The save_stats to save the orbs to """ for orb_id, orb in self.orbs.items(): save_file.talent_orbs.orbs[orb_id] = core.TalentOrb(orb_id, orb.count) @staticmethod def edit_talent_orbs(save_file: core.SaveFile): """Edit the talent orbs Args: save_file (core.SaveFile): The save_stats to edit the orbs of """ save_orbs = SaveOrbs.from_save_file(save_file) if save_orbs is None: color.ColoredText.localize("failed_to_load_orbs") return None save_orbs.edit() save_orbs.save(save_file) ================================================ FILE: src/bcsfe/core/game/catbase/unlock_popups.py ================================================ from __future__ import annotations from bcsfe import core class Popup: def __init__(self, seen: bool): self.seen = seen @staticmethod def init() -> Popup: return Popup(False) @staticmethod def read(stream: core.Data) -> Popup: seen = stream.read_bool() return Popup(seen) def write(self, stream: core.Data): stream.write_bool(self.seen) def serialize(self) -> bool: return self.seen @staticmethod def deserialize(data: bool) -> Popup: return Popup(data) def __repr__(self) -> str: return f"Popup(seen={self.seen!r})" def __str__(self) -> str: return self.__repr__() class UnlockPopups: def __init__(self, popups: dict[int, Popup]): self.popups = popups @staticmethod def init() -> UnlockPopups: return UnlockPopups({}) @staticmethod def read(stream: core.Data) -> UnlockPopups: total = stream.read_int() popups: dict[int, Popup] = {} for _ in range(total): key = stream.read_int() popups[key] = Popup.read(stream) return UnlockPopups(popups) def write(self, stream: core.Data): stream.write_int(len(self.popups)) for key, popup in self.popups.items(): stream.write_int(key) popup.write(stream) def serialize(self) -> dict[int, bool]: return {key: popup.serialize() for key, popup in self.popups.items()} @staticmethod def deserialize(data: dict[int, bool]) -> UnlockPopups: return UnlockPopups( {int(key): Popup.deserialize(popup) for key, popup in data.items()} ) def __repr__(self) -> str: return f"Popups(popups={self.popups!r})" def __str__(self) -> str: return self.__repr__() class UnlockPopupLine: def __init__( self, popup_id: int, enabled: bool, conditions: int, stage: int, map_conditions: int, user_rank: int, get_char_id1: int, get_char_id2: int, os_id: int, unlock_eye_1_id: int, add_level1: int, unlock_eye_2_id: int, add_level2: int, unlock_plus_id: int, add_level: int, skill_id: int, item_id: int, num: int, help_enabled: bool, ): self.popup_id = popup_id self.enabled = enabled self.conditions = conditions self.stage = stage self.map_conditions = map_conditions self.user_rank = user_rank self.get_char_id1 = get_char_id1 self.get_char_id2 = get_char_id2 self.os_id = os_id self.unlock_eye_1_id = unlock_eye_1_id self.add_level1 = add_level1 self.unlock_eye_2_id = unlock_eye_2_id self.add_level2 = add_level2 self.unlock_plus_id = unlock_plus_id self.add_level = add_level self.skill_id = skill_id self.item_id = item_id self.num = num self.help_enabled = help_enabled @staticmethod def from_csv_row(row: core.Row) -> UnlockPopupLine: return UnlockPopupLine( row.next_int(), row.next_bool(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_int(), row.next_bool(), ) class UnlockPopupData: def __init__(self, popups: list[UnlockPopupLine]): self.popups = popups @staticmethod def from_csv(csv: core.CSV) -> UnlockPopupData: popups: list[UnlockPopupLine] = [] for line in csv.lines[1:]: popups.append(UnlockPopupLine.from_csv_row(line)) return UnlockPopupData(popups) @staticmethod def from_save(save_file: core.SaveFile) -> UnlockPopupData | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("DataLocal", "unlockPopup.tsv") if data is None: return None csv = core.CSV(data, "\t") return UnlockPopupData.from_csv(csv) ================================================ FILE: src/bcsfe/core/game/catbase/upgrade.py ================================================ from __future__ import annotations import random from typing import Any from bcsfe import core from bcsfe.cli import color class Upgrade: def __init__(self, plus: int, base: int): self.plus = plus self.base = base self.base_range = None self.plus_range = None def get_base(self) -> int: return self.base + 1 def get_total(self) -> int: return self.get_base() + self.get_plus() def get_plus(self) -> int: return self.plus def upgrade(self): self.base += 1 def increment_base(self, amount: int): self.base += amount def increment_plus(self, amount: int): self.plus += amount def get_random_base(self, max_base: int | None = None) -> int: if self.base_range is None: return self.base base = random.randint(self.base_range[0], self.base_range[1]) if max_base is not None: base = min(base, max_base) return base def get_random_plus(self, max_plus: int | None = None) -> int: if self.plus_range is None: return self.plus plus = random.randint(self.plus_range[0], self.plus_range[1]) if max_plus is not None: plus = min(plus, max_plus) return plus @staticmethod def read(stream: core.Data) -> Upgrade: plus = stream.read_ushort() base = stream.read_ushort() return Upgrade(plus, base) def write(self, stream: core.Data): stream.write_ushort(self.plus) stream.write_ushort(self.base) def serialize(self) -> dict[str, Any]: return { "plus": self.plus, "base": self.base, } @staticmethod def init() -> Upgrade: return Upgrade(0, 0) def reset(self): self.plus = 0 self.base = 0 @staticmethod def deserialize(data: dict[str, Any]) -> Upgrade: return Upgrade(data.get("plus", 0), data.get("base", 0)) def __repr__(self) -> str: return f"Upgrade(plus={self.plus}, base={self.base})" def __str__(self) -> str: return f"Upgrade(plus={self.plus}, base={self.base})" @staticmethod def get_user_upgrade( max_pos_base: int, max_pos_plus: int, ) -> tuple[Upgrade | None, bool]: disable_maxes = core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES) if disable_maxes: max_pos_base = 50_000 max_pos_plus = 50_000 color.ColoredText.localize( "max_upgrade", max_base=max_pos_base + 1, max_plus=max_pos_plus ) usr_input = color.ColoredInput().localize("upgrade_input") if usr_input == core.core_data.local_manager.get_key("quit_key"): return None, True # example: # 10+20 = Upgrade(base=9, plus=20) # 10+ = Upgrade(base=9, plus=-1) # -1 means no change # +20 = Upgrade(base=-1, plus=20) # -1 means no change # 10 = Upgrade(base=9, plus=0) # 5-10+20-30 = Upgrade(base=random.randint(4, 9), plus=random.randint(20, 30)) # 5-10+ = Upgrade(base=random.randint(4, 9), plus=-1) # +20-30 = Upgrade(base=-1, plus=random.randint(20, 30)) # max+max = Upgrade(base=50000, plus=50000) parts = usr_input.split("+") if len(parts) == 1: base = parts[0] plus = "0" else: base = parts[0] plus = parts[1] min_base, max_base = None, None min_plus, max_plus = None, None max_text = core.core_data.local_manager.get_key("max") if not base: base_int = -1 else: range_parts = base.split("-") if len(range_parts) == 1: if range_parts[0].strip() == max_text: min_base = max_pos_base max_base = max_pos_base else: try: min_base = int(range_parts[0]) - 1 max_base = min_base except ValueError: color.ColoredText.localize("invalid_upgrade_base", base=base) return None, False else: try: min_base = int(range_parts[0]) - 1 max_base = int(range_parts[1]) - 1 except ValueError: color.ColoredText.localize( "invalid_upgrade_base_random", min=range_parts[0], max=range_parts[1], ) return None, False base_int = (min_base + max_base) // 2 if not plus: plus_int = -1 else: range_parts = plus.split("-") if len(range_parts) == 1: if range_parts[0].strip() == max_text: min_plus = max_pos_plus max_plus = max_pos_plus else: try: min_plus = int(range_parts[0]) max_plus = min_plus except ValueError: color.ColoredText.localize("invalid_upgrade_plus", plus=plus) return None, False else: try: min_plus = int(range_parts[0]) max_plus = int(range_parts[1]) except ValueError: color.ColoredText.localize( "invalid_upgrade_plus_random", min=range_parts[0], max=range_parts[1], ) return None, False plus_int = (min_plus + max_plus) // 2 upgrade = Upgrade(plus_int, base_int) upgrade.base_range = ( max(0, min(min_base or base_int, max_pos_base)), max(0, min(max_base or base_int, max_pos_base)), ) upgrade.plus_range = ( max(0, min(min_plus or plus_int, max_pos_plus)), max(0, min(max_plus or plus_int, max_pos_plus)), ) return upgrade, False def copy(self) -> Upgrade: upgrade = Upgrade(self.plus, self.base) upgrade.base_range = self.base_range upgrade.plus_range = self.plus_range return upgrade ================================================ FILE: src/bcsfe/core/game/catbase/user_rank_rewards.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import dialog_creator, color class RankGift: def __init__( self, index: int, threshold: int, rewards: list[tuple[int, int]] ): self.index = index self.threshold = threshold self.rewards = rewards def get_name( self, rank_gift_descriptions: RankGiftDescriptions ) -> str | None: return rank_gift_descriptions.get_name(self.threshold) class RankGifts: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.rank_gift = self.read_rank_gift() def read_rank_gift(self) -> list[RankGift] | None: rank_gift: list[RankGift] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "rankGift.csv") if data is None: return None csv = core.CSV(data) for i, line in enumerate(csv): rewards: list[tuple[int, int]] = [] for col in range(1, len(line), 2): value = line[col].to_int() if value == -1: break rewards.append((value, line[col + 1].to_int())) rank_gift.append(RankGift(i, line[0].to_int(), rewards)) return rank_gift def get_rank_gift(self, user_rank: int) -> RankGift | None: if self.rank_gift is None: return None for rank_gift in self.rank_gift: if rank_gift.threshold == user_rank: return rank_gift return None def get_all_rank_gifts(self, user_rank: int) -> list[RankGift] | None: if self.rank_gift is None: return None return [ rank_gift for rank_gift in self.rank_gift if rank_gift.threshold <= user_rank ] def get_by_id(self, id: int) -> RankGift | None: if self.rank_gift is None: return None if id >= len(self.rank_gift) or id < 0: return None return self.rank_gift[id] def get_all_unlocked(self, user_rank: int) -> list[RankGift] | None: if self.rank_gift is None: return None return [ rank_gift for rank_gift in self.rank_gift if rank_gift.threshold <= user_rank ] class RankGiftDescription: def __init__(self, index: int, threshold: int, description: str): self.index = index self.threshold = threshold self.description = description class RankGiftDescriptions: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.rank_gift_descriptions = self.read_rank_gift_descriptions() def read_rank_gift_descriptions(self) -> list[RankGiftDescription] | None: rank_gift_descriptions: list[RankGiftDescription] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", "user_info.tsv") if data is None: return None csv = core.CSV(data, delimiter="\t") for i, line in enumerate(csv): rank_gift_descriptions.append( RankGiftDescription(i, line[0].to_int(), line[1].to_str()) ) return rank_gift_descriptions def get_name(self, user_rank: int) -> str | None: if self.rank_gift_descriptions is None: return None for rank_gift_description in self.rank_gift_descriptions: if rank_gift_description.threshold == user_rank: return rank_gift_description.description return None class Reward: def __init__(self, claimed: bool): self.claimed = claimed @staticmethod def init() -> Reward: return Reward(False) @staticmethod def read(stream: core.Data) -> Reward: return Reward(stream.read_bool()) def write(self, stream: core.Data): stream.write_bool(self.claimed) def serialize(self) -> bool: return self.claimed @staticmethod def deserialize(data: bool) -> Reward: return Reward(data) def __repr__(self) -> str: return f"Reward(claimed={self.claimed})" def __str__(self) -> str: return self.__repr__() class UserRankRewards: def __init__(self, rewards: list[Reward]): self.rewards = rewards self.rank_gifts: RankGifts | None = None def read_rank_gifts(self, save_file: core.SaveFile) -> RankGifts: if self.rank_gifts is None: self.rank_gifts = RankGifts(save_file) return self.rank_gifts @staticmethod def init(gv: core.GameVersion) -> UserRankRewards: if gv >= 30: total = 0 else: total = 50 rewards = [Reward.init() for _ in range(total)] return UserRankRewards(rewards) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> UserRankRewards: if gv >= 30: total = stream.read_int() else: total = 50 rewards: list[Reward] = [] for _ in range(total): rewards.append(Reward.read(stream)) return UserRankRewards(rewards) def write(self, stream: core.Data, gv: core.GameVersion): if gv >= 30: stream.write_int(len(self.rewards)) for reward in self.rewards: reward.write(stream) def serialize(self) -> list[bool]: return [reward.serialize() for reward in self.rewards] @staticmethod def deserialize(data: list[bool]) -> UserRankRewards: return UserRankRewards([Reward.deserialize(reward) for reward in data]) def __repr__(self) -> str: return f"Rewards(rewards={self.rewards})" def __str__(self) -> str: return self.__repr__() def set_claimed(self, index: int, claimed: bool): self.rewards[index].claimed = claimed def edit(self, save_file: core.SaveFile): claim_choice = dialog_creator.ChoiceInput.from_reduced( ["claim", "unclaim", "fix_claimed"], dialog="claim_or_unclaim_ur", single_choice=True, ).single_choice() if claim_choice is None: return claim_choice -= 1 rank_gifts = core.core_data.get_rank_gifts(save_file) if rank_gifts.rank_gift is None: return user_rank = save_file.calculate_user_rank() if claim_choice == 2: for rank_gift in rank_gifts.rank_gift: reward = self.rewards[rank_gift.index] if rank_gift.threshold > user_rank: reward.claimed = False color.ColoredText.localize("ur_fix_claimed_success") return selected_rank_gifts: list[RankGift] = rank_gifts.rank_gift.copy() descriptions = core.core_data.get_rank_gift_descriptions(save_file) selected_rank_gifts.sort(key=lambda rank_gift: rank_gift.threshold) new_selected_rank_gifts: list[RankGift] = [] for rank_gift in selected_rank_gifts: reward = self.rewards[rank_gift.index] if reward.claimed and claim_choice == 0: continue if not reward.claimed and claim_choice == 1: continue if rank_gift.threshold > user_rank: continue new_selected_rank_gifts.append(rank_gift) selected_rank_gifts = new_selected_rank_gifts selected_descriptions: list[str] = [] for rank_gift in selected_rank_gifts: name = rank_gift.get_name(descriptions) if name is None: return description = name.replace("
", " ") # remove span tags start = description.find("<") while start != -1: end = description.find(">") description = description[:start] + description[end + 1 :] start = description.find("<") selected_descriptions.append( core.core_data.local_manager.get_key( "ur_string", description=description, rank=rank_gift.threshold, ) ) ids, _ = dialog_creator.ChoiceInput.from_reduced( selected_descriptions, dialog="select_ur" ).multiple_choice(localized_options=False) if ids is None: return for id in ids: index = selected_rank_gifts[id].index self.set_claimed(index, claim_choice == 0) if claim_choice == 0: color.ColoredText.localize("ur_claimed_success") else: color.ColoredText.localize("ur_unclaimed_success") def edit_user_rank_rewards(save_file: core.SaveFile): user_rank_rewards = save_file.user_rank_rewards user_rank_rewards.edit(save_file) ================================================ FILE: src/bcsfe/core/game/gamoto/__init__.py ================================================ from bcsfe.core.game.gamoto import ( catamins, gamatoto, base_materials, ototo, cat_shrine, ) __all__ = ["catamins", "gamatoto", "base_materials", "ototo", "cat_shrine"] ================================================ FILE: src/bcsfe/core/game/gamoto/base_materials.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import dialog_creator class Material: def __init__(self, amount: int): self.amount = amount @staticmethod def init() -> Material: return Material(0) @staticmethod def read(stream: core.Data) -> Material: amount = stream.read_int() return Material(amount) def write(self, stream: core.Data): stream.write_int(self.amount) def serialize(self) -> int: return self.amount @staticmethod def deserialize(data: int) -> Material: return Material(data) def __repr__(self) -> str: return f"Material(amount={self.amount!r})" def __str__(self) -> str: return self.__repr__() class BaseMaterials: def __init__(self, materials: list[Material]): self.materials = materials @staticmethod def init() -> BaseMaterials: return BaseMaterials([]) @staticmethod def read(stream: core.Data) -> BaseMaterials: total = stream.read_int() materials: list[Material] = [] for _ in range(total): materials.append(Material.read(stream)) return BaseMaterials(materials) def write(self, stream: core.Data): stream.write_int(len(self.materials)) for material in self.materials: material.write(stream) def serialize(self) -> list[int]: return [material.serialize() for material in self.materials] @staticmethod def deserialize(data: list[int]) -> BaseMaterials: return BaseMaterials( [Material.deserialize(material) for material in data] ) def __repr__(self) -> str: return f"Materials(materials={self.materials!r})" def __str__(self) -> str: return self.__repr__() def edit_base_materials(self, save_file: core.SaveFile): names = core.core_data.get_gatya_item_names(save_file).names items = core.core_data.get_gatya_item_buy(save_file).get_by_category(7) if items is None: return if names is None: return names = [names[item.id] for item in items] base_materials = [ base_material.amount for base_material in self.materials ] values = dialog_creator.MultiEditor.from_reduced( "base_materials", names, base_materials, core.core_data.max_value_manager.get("base_materials"), group_name_localized=True, ).edit() self.materials = [Material(value) for value in values] ================================================ FILE: src/bcsfe/core/game/gamoto/cat_shrine.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class CatShrine: def __init__( self, unknown: bool, stamp_1: float, stamp_2: float, shrine_gone: bool, flags: list[int], xp_offering: int, ): self.unknown = unknown self.stamp_1 = stamp_1 self.stamp_2 = stamp_2 self.shrine_gone = shrine_gone self.flags = flags self.xp_offering = xp_offering self.dialogs = 0 @staticmethod def init() -> CatShrine: return CatShrine(False, 0.0, 0.0, False, [], 0) @staticmethod def read(stream: core.Data) -> CatShrine: unknown = stream.read_bool() stamp_1 = stream.read_double() stamp_2 = stream.read_double() shrine_gone = stream.read_bool() flags = stream.read_byte_list(length=stream.read_byte()) xp_offering = stream.read_long() return CatShrine(unknown, stamp_1, stamp_2, shrine_gone, flags, xp_offering) def write(self, stream: core.Data): stream.write_bool(self.unknown) stream.write_double(self.stamp_1) stream.write_double(self.stamp_2) stream.write_bool(self.shrine_gone) stream.write_byte(len(self.flags)) stream.write_byte_list(self.flags, write_length=False) stream.write_long(self.xp_offering) def read_dialogs(self, stream: core.Data): self.dialogs = stream.read_int() def write_dialogs(self, stream: core.Data): stream.write_int(self.dialogs) def serialize(self) -> dict[str, Any]: return { "unknown": self.unknown, "stamp_1": self.stamp_1, "stamp_2": self.stamp_2, "shrine_gone": self.shrine_gone, "flags": self.flags, "xp_offering": self.xp_offering, "dialogs": self.dialogs, } @staticmethod def deserialize(data: dict[str, Any]) -> CatShrine: shrine = CatShrine( data.get("unknown", False), data.get("stamp_1", 0.0), data.get("stamp_2", 0.0), data.get("shrine_gone", False), data.get("flags", []), data.get("xp_offering", 0), ) shrine.dialogs = data.get("dialogs", 0) return shrine def __repr__(self): return ( f"CatShrine(" f"unknown={self.unknown}, " f"stamp_1={self.stamp_1}, " f"stamp_2={self.stamp_2}, " f"shrine_gone={self.shrine_gone}, " f"flags={self.flags}, " f"xp_offering={self.xp_offering}, " f"dialogs={self.dialogs}" f")" ) def __str__(self): return self.__repr__() @staticmethod def edit_catshrine(save_file: core.SaveFile): shrine = save_file.cat_shrine options = [ "shrine_level", "shrine_xp", "make_catshrine_appear", "make_catshrine_disappear", ] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="cat_shrine_choice_dialog", single_choice=True ).single_choice() if choice is None: return choice -= 1 if choice == 2: shrine.shrine_gone = False shrine.stamp_1 = 0.0 shrine.stamp_2 = 0.0 color.ColoredText.localize("cat_shrine_edited") return elif choice == 3: shrine.shrine_gone = True color.ColoredText.localize("cat_shrine_edited") return data = core.core_data.get_cat_shrine_levels(save_file) xp = shrine.xp_offering level = data.get_level_from_xp(xp) color.ColoredText.localize("current_shrine_xp_level", level=level, xp=xp) if choice == 0: max_level = data.get_max_level() if max_level is None: return level = dialog_creator.IntInput( min=1, max=max_level ).get_input_locale_while("shrine_level_dialog", {"max_level": max_level}) if level is None: return shrine.xp_offering = data.get_xp_from_level(level) elif choice == 1: max_xp = data.get_max_xp() if max_xp is None: return xp = dialog_creator.IntInput(min=0, max=max_xp).get_input_locale_while( "shrine_xp_dialog", {"max_xp": max_xp} ) if xp is None: return shrine.xp_offering = xp xp = shrine.xp_offering if xp is None: return level = data.get_level_from_xp(xp) if level is None: return shrine.dialogs = level - 1 shrine.shrine_gone = False shrine.stamp_1 = 0.0 shrine.stamp_2 = 0.0 color.ColoredText.localize("current_shrine_xp_level", level=level, xp=xp) color.ColoredText.localize("cat_shrine_edited") class CatShrineLevels: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.boundaries = self.get_boundaries() def get_boundaries(self) -> list[int] | None: file_name = "jinja_level.csv" gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", file_name) if data is None: return None csv = core.CSV( data, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), ) boundaries: list[int] = [] counter = 0 for row in csv: xp = row[0].to_int() counter += xp boundaries.append(counter) return boundaries def get_level_from_xp(self, xp: int) -> int | None: if self.boundaries is None: return None for i, boundary in enumerate(self.boundaries): if xp < boundary: return i + 1 return len(self.boundaries) def get_xp_from_level(self, level: int) -> int | None: if self.boundaries is None: return None if level < 1: return 0 if level > len(self.boundaries): return self.get_max_xp() return self.boundaries[level - 2] def get_max_level(self) -> int | None: if self.boundaries is None: return None return len(self.boundaries) def get_max_xp(self) -> int | None: if self.boundaries is None: return None return max(self.boundaries) ================================================ FILE: src/bcsfe/core/game/gamoto/catamins.py ================================================ from __future__ import annotations from bcsfe import core class Catamin: def __init__(self, amount: int): self.amount = amount @staticmethod def read(stream: core.Data) -> Catamin: amount = stream.read_int() return Catamin(amount) def write(self, stream: core.Data): stream.write_int(self.amount) def serialize(self) -> int: return self.amount @staticmethod def deserialize(data: int) -> Catamin: return Catamin(data) def __repr__(self): return f"Catamin({self.amount})" def __str__(self): return f"Catamin({self.amount})" class Catamins: def __init__(self, catamins: list[Catamin]): self.catamins = catamins @staticmethod def read(stream: core.Data) -> Catamins: total = stream.read_int() catamins: list[Catamin] = [] for _ in range(total): catamins.append(Catamin.read(stream)) return Catamins(catamins) def write(self, stream: core.Data): stream.write_int(len(self.catamins)) for catamin in self.catamins: catamin.write(stream) def serialize(self) -> list[int]: return [catamin.serialize() for catamin in self.catamins] @staticmethod def deserialize(data: list[int]) -> Catamins: return Catamins([Catamin.deserialize(catamin) for catamin in data]) def __repr__(self): return f"Catamins({self.catamins})" def __str__(self): return f"Catamins({self.catamins})" ================================================ FILE: src/bcsfe/core/game/gamoto/gamatoto.py ================================================ from __future__ import annotations from dataclasses import dataclass from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator @dataclass class MemberName: member_id: int rarity: int bonus: int name: str rarity_name: str description: list[str] class GamatotoMembersName: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.members = self.read_members() def read_members(self) -> list[MemberName] | None: members: list[MemberName] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download( "resLocal", f"GamatotoExpedition_Members_name_{core.core_data.get_lang(self.save_file)}.csv", ) if data is None: return None csv = core.CSV( data, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), remove_empty=False, ) for line in csv.lines[1:]: if line[0].to_int() == -1: continue members.append( MemberName( line[0].to_int(), line[1].to_int(), line[2].to_int(), line[3].to_str(), line[4].to_str(), line[5:].to_str_list(), ) ) return members def get_member(self, member_id: int) -> MemberName | None: if self.members is None: return None for member in self.members: if member.member_id == member_id: return member return None def get_members_from_ids(self, ids: list[int]) -> list[MemberName | None]: return [self.get_member(id) for id in ids] def get_all_rarity(self, rarity: int) -> list[MemberName] | None: if self.members is None: return None return [member for member in self.members if member.rarity == rarity] def get_members_from_helpers( self, helpers: Helpers ) -> list[MemberName | None]: return self.get_members_from_ids( [helper.id for helper in helpers.helpers if helper.is_valid()] ) def get_all_rarity_names(self) -> list[str] | None: if self.members is None: return None names: dict[int, str] = {} for member in self.members: names[member.rarity] = member.rarity_name return [names[i] for i in range(len(names))] @dataclass class GamatotoLevel: level: int xp_needed: int discovery_bonus: int skin: int @dataclass class GamatotoLimit: max_level: int total_stages: int total_helpers: int class GamatotoLevels: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.levels = self.read_levels() self.limit = self.read_max_level() def read_levels(self) -> list[GamatotoLevel] | None: levels: list[GamatotoLevel] = [] gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "GamatotoExpedition.csv") if data is None: return None csv = core.CSV(data) for i, line in enumerate(csv): levels.append( GamatotoLevel( i + 1, line[0].to_int(), line[1].to_int(), line[2].to_int() ) ) return levels def read_max_level(self) -> GamatotoLimit | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "GamatotoExpedition_Limit.csv") if data is None: return None csv = core.CSV(data) line = csv[0] return GamatotoLimit( line[0].to_int(), line[1].to_int(), line[2].to_int() ) def get_level(self, level: int) -> GamatotoLevel | None: if self.levels is None: return None if level < 1: return None return self.levels[level - 1] def get_all_levels(self) -> list[GamatotoLevel] | None: return self.levels def get_level_from_xp(self, xp: int) -> GamatotoLevel | None: if self.levels is None or self.limit is None: return None for level in self.levels: if level.level >= self.limit.max_level: break if level.xp_needed == -1: continue if xp < level.xp_needed: return level if self.limit.max_level >= len(self.levels): return self.levels[-1] return self.levels[self.limit.max_level - 1] def get_xp_from_level(self, level: int) -> int | None: if self.levels is None: return None level -= 1 if level < 1: return 0 return self.levels[level - 1].xp_needed def get_max_level(self) -> int | None: if self.limit is None: return None return self.limit.max_level def get_total_stages(self) -> int | None: if self.limit is None: return None return self.limit.total_stages def get_total_helpers(self) -> int | None: if self.limit is None: return None return self.limit.total_helpers class Helper: def __init__(self, id: int): self.id = id @staticmethod def init() -> Helper: return Helper(-1) @staticmethod def read(stream: core.Data) -> Helper: id = stream.read_int() return Helper(id) def write(self, stream: core.Data): stream.write_int(self.id) def serialize(self) -> int: return self.id @staticmethod def deserialize(data: int) -> Helper: return Helper(data) def __repr__(self) -> str: return f"Helper(id={self.id!r})" def __str__(self) -> str: return f"Helper(id={self.id!r})" def is_valid(self) -> bool: return self.id != -1 class Helpers: def __init__(self, helpers: list[Helper]): self.helpers = helpers @staticmethod def init() -> Helpers: return Helpers([]) @staticmethod def read(stream: core.Data) -> Helpers: total = stream.read_int() helpers: list[Helper] = [] for _ in range(total): helpers.append(Helper.read(stream)) return Helpers(helpers) def write(self, stream: core.Data): stream.write_int(len(self.helpers)) for helper in self.helpers: helper.write(stream) def serialize(self) -> list[int]: return [helper.serialize() for helper in self.helpers] @staticmethod def deserialize(data: list[int]) -> Helpers: return Helpers([Helper.deserialize(helper) for helper in data]) def __repr__(self) -> str: return f"Helpers(helpers={self.helpers!r})" def __str__(self) -> str: return f"Helpers(helpers={self.helpers!r})" class Gamatoto: def __init__( self, remaining_seconds: float, return_flag: bool, xp: int, dest_id: int, recon_length: int, unknown: int, notif_value: int, ): self.remaining_seconds = remaining_seconds self.return_flag = return_flag self.xp = xp self.dest_id = dest_id self.recon_length = recon_length self.unknown = unknown self.notif_value = notif_value self.helpers = Helpers.init() self.is_ad_present = False self.skin = 0 self.collab_flags: dict[int, bool] = {} self.collab_durations: dict[int, float] = {} @staticmethod def init() -> Gamatoto: return Gamatoto( 0.0, False, 0, 0, 0, 0, 0, ) @staticmethod def read(stream: core.Data) -> Gamatoto: remaining_seconds = stream.read_double() return_flag = stream.read_bool() xp = stream.read_int() dest_id = stream.read_int() recon_length = stream.read_int() unknown = stream.read_int() notif_value = stream.read_int() return Gamatoto( remaining_seconds, return_flag, xp, dest_id, recon_length, unknown, notif_value, ) def write(self, stream: core.Data): stream.write_double(self.remaining_seconds) stream.write_bool(self.return_flag) stream.write_int(self.xp) stream.write_int(self.dest_id) stream.write_int(self.recon_length) stream.write_int(self.unknown) stream.write_int(self.notif_value) def read_2(self, stream: core.Data): self.helpers = Helpers.read(stream) self.is_ad_present = stream.read_bool() def write_2(self, stream: core.Data): self.helpers.write(stream) stream.write_bool(self.is_ad_present) def read_skin(self, stream: core.Data): self.skin = stream.read_int() def write_skin(self, stream: core.Data): stream.write_int(self.skin) def read_collab_data(self, stream: core.Data): self.collab_flags: dict[int, bool] = stream.read_int_bool_dict() self.collab_durations: dict[int, float] = stream.read_int_double_dict() def write_collab_data(self, stream: core.Data): stream.write_int_bool_dict(self.collab_flags) stream.write_int_double_dict(self.collab_durations) def serialize(self) -> dict[str, Any]: return { "remaining_seconds": self.remaining_seconds, "return_flag": self.return_flag, "xp": self.xp, "dest_id": self.dest_id, "recon_length": self.recon_length, "unknown": self.unknown, "notif_value": self.notif_value, "helpers": self.helpers.serialize(), "is_ad_present": self.is_ad_present, "skin": self.skin, "collab_flags": self.collab_flags, "collab_durations": self.collab_durations, } @staticmethod def deserialize(data: dict[str, Any]) -> Gamatoto: gamatoto = Gamatoto( data.get("remaining_seconds", 0.0), data.get("return_flag", False), data.get("xp", 0), data.get("dest_id", 0), data.get("recon_length", 0), data.get("unknown", 0), data.get("notif_value", 0), ) gamatoto.helpers = Helpers.deserialize(data.get("helpers", [])) gamatoto.is_ad_present = data.get("is_ad_present", False) gamatoto.skin = data.get("skin", 0) gamatoto.collab_flags = data.get("collab_flags", {}) gamatoto.collab_durations = data.get("collab_durations", {}) return gamatoto def __repr__(self): return ( f"Gamatoto(remaining_seconds={self.remaining_seconds!r}, " f"return_flag={self.return_flag!r}, xp={self.xp!r}, " f"dest_id={self.dest_id!r}, recon_length={self.recon_length!r}, " f"unknown={self.unknown!r}, notif_value={self.notif_value!r}, " f"helpers={self.helpers!r}, is_ad_present={self.is_ad_present!r}, " f"skin={self.skin!r}, collab_flags={self.collab_flags!r}, " f"collab_durations={self.collab_durations!r})" ) def __str__(self): return self.__repr__() def edit_xp(self, save_file: core.SaveFile): gamatoto_levels = core.core_data.get_gamatoto_levels(save_file) current_level = gamatoto_levels.get_level_from_xp(self.xp) if current_level is None: return xp = self.xp color.ColoredText.localize( "gamatoto_level_current", level=current_level.level, xp=xp ) choice = dialog_creator.ChoiceInput( ["enter_raw_gamatoto_xp", "enter_gamatoto_level"], ["enter_raw_gamatoto_xp", "enter_gamatoto_level"], [], {}, "edit_gamatoto_level_q", single_choice=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: xp = dialog_creator.SingleEditor( "gamatoto_xp", self.xp, None, localized_item=True ).edit() current_level = gamatoto_levels.get_level_from_xp(xp) elif choice == 1: value = dialog_creator.SingleEditor( "gamatoto_level", current_level.level, gamatoto_levels.get_max_level(), localized_item=True, ).edit() xp = gamatoto_levels.get_xp_from_level(value) current_level = gamatoto_levels.get_level(value) if xp is None: return self.xp = xp if current_level is None: return color.ColoredText.localize( "gamatoto_level_success", level=current_level.level, xp=xp ) def edit_helpers(self, save_file: core.SaveFile): members_name = core.core_data.get_gamatoto_members_name(save_file) gamatoto_levels = core.core_data.get_gamatoto_levels(save_file) max_helpers = gamatoto_levels.get_total_helpers() members = members_name.get_members_from_helpers(self.helpers) color.ColoredText.localize("current_gamatoto_helpers") for member in members: if member is None: continue color.ColoredText.localize( "gamatoto_helper", name=member.name, rarity_name=member.rarity_name, ) rarity_names = members_name.get_all_rarity_names() if rarity_names is None: return total_rarity_amounts: list[int] = [0] * len(rarity_names) for helper in self.helpers.helpers: if not helper.is_valid(): continue member = members_name.get_member(helper.id) if member is None: continue total_rarity_amounts[member.rarity] += 1 rarity_amounts = dialog_creator.MultiEditor.from_reduced( "gamatoto_helpers", rarity_names, total_rarity_amounts, max_helpers, group_name_localized=True, cumulative_max=True, ).edit() helpers: list[Helper] = [] for i, rarity_amount in enumerate(rarity_amounts): rarity_members = members_name.get_all_rarity(i) if rarity_members is None: continue for _ in range(rarity_amount): member = rarity_members.pop(0) helpers.append(Helper(member.member_id)) self.helpers = Helpers(helpers) members = members_name.get_members_from_helpers(self.helpers) color.ColoredText.localize("new_gamatoto_helpers") for member in members: if member is None: continue color.ColoredText.localize( "gamatoto_helper", name=member.name, rarity_name=member.rarity_name, ) def edit_xp(save_file: core.SaveFile): save_file.gamatoto.edit_xp(save_file) def edit_helpers(save_file: core.SaveFile): save_file.gamatoto.edit_helpers(save_file) ================================================ FILE: src/bcsfe/core/game/gamoto/ototo.py ================================================ from __future__ import annotations from dataclasses import dataclass from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator, color @dataclass class LevelPartRecipeUnlock: index: int cannon_id: int part_id: int unknown: int unknown2: int level: int class CastleRecipeUnlock: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.level_part_recipe_unlocks = self.get_recipe_unlocks() def get_recipe_unlocks(self) -> list[LevelPartRecipeUnlock] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("DataLocal", "CastleRecipeUnlock.csv") if data is None: return None csv = core.CSV(data) level_part_recipe_unlocks: list[LevelPartRecipeUnlock] = [] for i, line in enumerate(csv): level_part_recipe_unlocks.append( LevelPartRecipeUnlock( index=i, cannon_id=line[0].to_int(), part_id=line[1].to_int(), unknown=line[2].to_int(), unknown2=line[3].to_int(), level=line[4].to_int(), ) ) return level_part_recipe_unlocks def get_recipe_unlock(self, index: int) -> LevelPartRecipeUnlock | None: if self.level_part_recipe_unlocks is None: return None for recipe_unlock in self.level_part_recipe_unlocks: if recipe_unlock.index == index: return recipe_unlock return None def get_max_level(self, cannon_id: int, part_id: int) -> int | None: if self.level_part_recipe_unlocks is None: return None max_level = 0 for recipe_unlock in self.level_part_recipe_unlocks: if ( recipe_unlock.cannon_id == cannon_id and recipe_unlock.part_id == part_id ): if recipe_unlock.level > max_level: max_level = recipe_unlock.level return max_level def get_max_part_level(self, part_id: int) -> int | None: if self.level_part_recipe_unlocks is None: return None max_level = 0 for recipe_unlock in self.level_part_recipe_unlocks: if recipe_unlock.part_id == part_id: if recipe_unlock.level > max_level: max_level = recipe_unlock.level return max_level @dataclass class CannonDescription: cannon_id: int build_name: str foundation_build_description: str style_build_description: str effect_build_description: str cannon_build_description: str cannon_name: str foundation_name: str style_name: str effect_description: str improve_foundation_description: str improve_style_description: str improved_foundation_name: str improved_style_name: str improved_effect1_description: str improved_effect2_description: str def get_part_names(self) -> list[str]: effect_name = self.effect_build_description.split("
")[0] if not effect_name: effect_name = self.build_name return [ effect_name, self.improve_foundation_description.split("
")[0], self.improve_style_description.split("
")[0], ] def get_part_name(self, index: int) -> str: return self.get_part_names()[index] def get_longest_part_name(self) -> str: return max(self.get_part_names(), key=len) def get_cannon_name(self) -> str: return self.cannon_name class CannonDescriptions: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.cannon_descriptions = self.get_cannon_descriptions() def get_cannon_descriptions(self) -> list[CannonDescription] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", "CastleRecipeDescriptions.csv") if data is None: return None csv = core.CSV( data, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), remove_empty=False, ) cannon_descriptions: list[CannonDescription] = [] for line in csv: cannon_descriptions.append( CannonDescription( cannon_id=line[0].to_int(), build_name=line[1].to_str(), foundation_build_description=line[2].to_str(), style_build_description=line[3].to_str(), effect_build_description=line[4].to_str(), cannon_build_description=line[5].to_str(), cannon_name=line[6].to_str(), foundation_name=line[7].to_str(), style_name=line[8].to_str(), effect_description=line[9].to_str(), improve_foundation_description=line[10].to_str(), improve_style_description=line[11].to_str(), improved_foundation_name=line[12].to_str(), improved_style_name=line[13].to_str(), improved_effect1_description=line[14].to_str(), improved_effect2_description=line[15].to_str(), ) ) return cannon_descriptions def get_cannon_description( self, cannon_id: int ) -> CannonDescription | None: if self.cannon_descriptions is None: return None for cannon_description in self.cannon_descriptions: if cannon_description.cannon_id == cannon_id: return cannon_description return None def get_longest_longest_part_name(self) -> str | None: if self.cannon_descriptions is None: return None longest_part_name = "" for cannon_description in self.cannon_descriptions: l_name = cannon_description.get_longest_part_name() if len(l_name) > len(longest_part_name): longest_part_name = l_name return longest_part_name class Cannon: def __init__(self, development: int, levels: list[int]): self.development = development self.levels = levels @staticmethod def init() -> Cannon: return Cannon(0, []) @staticmethod def read(stream: core.Data) -> Cannon: total = stream.read_int() levels: list[int] = [] development = stream.read_int() for _ in range(total - 1): levels.append(stream.read_int()) return Cannon(development, levels) def write(self, stream: core.Data): stream.write_int(len(self.levels) + 1) stream.write_int(self.development) for level in self.levels: stream.write_int(level) def serialize(self) -> list[int]: return [self.development] + self.levels @staticmethod def deserialize(data: list[int]) -> Cannon: return Cannon(data[0], data[1:]) def __repr__(self): return f"Cannon({self.development}, {self.levels})" def __str__(self): return f"Cannon({self.development}, {self.levels})" class Cannons: def __init__( self, cannons: dict[int, Cannon], selected_parts: list[list[int]] ): self.cannons = cannons self.selected_parts = selected_parts @staticmethod def init(gv: core.GameVersion) -> Cannons: cannnons = {} if gv < 80200: selected_parts = [[0, 0, 0]] else: if gv > 90699: total_selected_parts = 0 else: total_selected_parts = 10 selected_parts = [[0, 0, 0] for _ in range(total_selected_parts)] return Cannons(cannnons, selected_parts) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> Cannons: total = stream.read_int() cannons: dict[int, Cannon] = {} for _ in range(total): cannon_id = stream.read_int() cannon = Cannon.read(stream) cannons[cannon_id] = cannon if gv < 80200: selected_parts = [stream.read_int_list(length=3)] else: if gv > 90699: total_selected_parts = stream.read_byte() else: total_selected_parts = 10 selected_parts: list[list[int]] = [] for _ in range(total_selected_parts): selected_parts.append(stream.read_byte_list(length=3)) return Cannons(cannons, selected_parts) def write(self, stream: core.Data, gv: core.GameVersion): stream.write_int(len(self.cannons)) for cannon_id, cannon in self.cannons.items(): stream.write_int(cannon_id) cannon.write(stream) if gv < 80200: stream.write_int_list( self.selected_parts[0], write_length=False, length=3 ) else: if gv > 90699: stream.write_byte(len(self.selected_parts)) for part in self.selected_parts: stream.write_byte_list(part, write_length=False, length=3) def serialize(self) -> dict[str, Any]: return { "cannons": { cannon_id: cannon.serialize() for cannon_id, cannon in self.cannons.items() }, "selected_parts": self.selected_parts, } @staticmethod def deserialize(data: dict[str, Any]) -> Cannons: return Cannons( { cannon_id: Cannon.deserialize(cannon) for cannon_id, cannon in data.get("cannons", {}).items() }, data.get("selected_parts", []), ) def __repr__(self): return f"Cannons({self.cannons}, {self.selected_parts})" def __str__(self): return f"Cannons({self.cannons}, {self.selected_parts})" class Ototo: def __init__( self, base_materials: core.BaseMaterials, game_version: core.GameVersion | None = None, ): self.base_materials = base_materials self.remaining_seconds = 0.0 self.return_flag = False self.improve_id = 0 self.engineers = 0 self.cannons = Cannons.init(game_version) if game_version else None @staticmethod def init(game_version: core.GameVersion) -> Ototo: return Ototo(core.BaseMaterials.init(), game_version) @staticmethod def read(stream: core.Data) -> Ototo: bm = core.BaseMaterials.read(stream) return Ototo(bm) def write(self, stream: core.Data): self.base_materials.write(stream) def read_2(self, stream: core.Data, gv: core.GameVersion): self.remaining_seconds = stream.read_double() self.return_flag = stream.read_bool() self.improve_id = stream.read_int() self.engineers = stream.read_int() self.cannons = Cannons.read(stream, gv) def write_2(self, stream: core.Data, gv: core.GameVersion): stream.write_double(self.remaining_seconds) stream.write_bool(self.return_flag) stream.write_int(self.improve_id) stream.write_int(self.engineers) if self.cannons is None: Cannons.init(gv).write(stream, gv) else: self.cannons.write(stream, gv) def serialize(self) -> dict[str, Any]: return { "base_materials": self.base_materials.serialize(), "remaining_seconds": self.remaining_seconds, "return_flag": self.return_flag, "improve_id": self.improve_id, "engineers": self.engineers, "cannons": self.cannons.serialize() if self.cannons else None, } @staticmethod def deserialize(data: dict[str, Any]) -> Ototo: ototo = Ototo( core.BaseMaterials.deserialize(data.get("base_materials", [])) ) ototo.remaining_seconds = data.get("remaining_seconds", 0.0) ototo.return_flag = data.get("return_flag", False) ototo.improve_id = data.get("improve_id", 0) ototo.engineers = data.get("engineers", 0) ototo.cannons = Cannons.deserialize(data.get("cannons", {})) return ototo def __repr__(self): return f"Ototo({self.base_materials}, {self.remaining_seconds}, {self.return_flag}, {self.improve_id}, {self.engineers}, {self.cannons})" def __str__(self): return self.__repr__() @staticmethod def get_max_engineers(save_file: core.SaveFile) -> int: file = core.core_data.get_game_data_getter(save_file).download( "DataLocal", "CastleCustomLimit.csv" ) if file is None: return 5 csv = core.CSV(file) return csv.lines[0][0].to_int() def edit_engineers(self, save_file: core.SaveFile): name = core.core_data.get_gatya_item_names(save_file).get_name(92) if name is None: name = "engineers" localized_item = True else: localized_item = False self.engineers = dialog_creator.SingleEditor( name, self.engineers, Ototo.get_max_engineers(save_file), localized_item=localized_item, ).edit() def display_current_cannons( self, save_file: core.SaveFile ) -> list[str] | None: descriptions = CannonDescriptions(save_file) recipe_unlocks = CastleRecipeUnlock(save_file) color.ColoredText.localize("current_cannon_stats") if self.cannons is None: self.cannons = Cannons.init(save_file.game_version) names: list[str] = [] longest_part_name = descriptions.get_longest_longest_part_name() if longest_part_name is None: return None longest_part_name = len(longest_part_name) for cannon_id, cannon in self.cannons.cannons.items(): description = descriptions.get_cannon_description(cannon_id) if description is None: continue recipe_unlock = recipe_unlocks.get_recipe_unlock(cannon_id) if recipe_unlock is None: continue cannon_name = description.get_cannon_name() names.append(cannon_name) text = cannon_name if cannon_id != 0: cannon_name_length = len(cannon_name) - 10 buffer = " " * (longest_part_name - cannon_name_length) text += core.core_data.local_manager.get_key( "development", development=Ototo.get_stage_name(cannon.development), escape=False, buffer=buffer, ) for part_id, level in enumerate(cannon.levels): if part_id == 0: level += 1 text += "\n" text += " " buffer = " " * ( longest_part_name - len(description.get_part_name(part_id)) + 2 ) name = description.get_part_name(part_id) text += core.core_data.local_manager.get_key( "cannon_part", name=name, level=level, buffer=buffer ) text += "\n" color.ColoredText.localize("cannon_stats", parts=text, escape=False) return names def edit_cannon(self, save_file: core.SaveFile): if self.cannons is None: self.cannons = Cannons.init(save_file.game_version) names = self.display_current_cannons(save_file) if names is None: return cannon_ids, all_at_once = dialog_creator.ChoiceInput.from_reduced( names, dialog="select_cannon" ).multiple_choice() if cannon_ids is None: return if len(cannon_ids) > 1 and not all_at_once: choice = dialog_creator.ChoiceInput.from_reduced( ["individual", "edit_all_at_once"], dialog="cannon_edit_type", single_choice=True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: all_at_once = False else: all_at_once = True if len(cannon_ids) > 1 or (len(cannon_ids) == 1 and cannon_ids[0] != 0): choice = dialog_creator.ChoiceInput.from_reduced( ["development_o", "level_o"], dialog="cannon_dev_level_q", single_choice=True, ).single_choice() if choice is None: return choice -= 1 else: choice = 1 if choice == 0: self.edit_cannon_development(save_file, all_at_once, cannon_ids) elif choice == 1: self.edit_cannon_level(save_file, all_at_once, cannon_ids) color.ColoredText.localize("cannon_success") self.display_current_cannons(save_file) def select_development(self) -> int | None: return dialog_creator.ChoiceInput.from_reduced( ["none", "foundation", "style", "effect"], dialog="select_development", single_choice=True, ).single_choice() def edit_cannon_development( self, save_file: core.SaveFile, all_at_once: bool, cannon_ids: list[int] ): if self.cannons is None: self.cannons = Cannons.init(save_file.game_version) if all_at_once: development = self.select_development() if development is None: return for cannon_id in cannon_ids: if cannon_id == 0: continue self.cannons.cannons[cannon_id].development = development - 1 else: for cannon_id in cannon_ids: if cannon_id == 0: continue cannon_description = CannonDescriptions( save_file ).get_cannon_description(cannon_id) if cannon_description is None: continue current_development = self.cannons.cannons[ cannon_id ].development color.ColoredText.localize( "selected_cannon_stage", name=cannon_description.get_cannon_name(), stage=Ototo.get_stage_name(current_development), escape=False, ) development = self.select_development() if development is None: return self.cannons.cannons[cannon_id].development = development - 1 def edit_cannon_level( self, save_file: core.SaveFile, all_at_once: bool, cannon_ids: list[int] ): if self.cannons is None: self.cannons = Cannons.init(save_file.game_version) cannon_descriptions = CannonDescriptions(save_file) cannon_recipe = CastleRecipeUnlock(save_file) if all_at_once: max_part_level_0 = cannon_recipe.get_max_part_level(0) max_part_level_1 = cannon_recipe.get_max_part_level(1) max_part_level_2 = cannon_recipe.get_max_part_level(2) if ( max_part_level_0 is None or max_part_level_1 is None or max_part_level_2 is None ): return levels = dialog_creator.MultiEditor.from_reduced( "cannon_level", ["effect", "improved_foundation", "improved_style"], None, max_values=[ max_part_level_0, max_part_level_1, max_part_level_2, ], group_name_localized=True, items_localized=True, ).edit() if not levels: return for cannon_id in cannon_ids: cannon = self.get_cannon(cannon_id) if cannon is None: continue cannon.development = max(cannon.development, 3) for part_id, level in enumerate(levels): if part_id == 0: level -= 1 max_level = cannon_recipe.get_max_level(cannon_id, part_id) if max_level is None: continue if part_id >= len(cannon.levels): break cannon.levels[part_id] = min(level, max_level) else: for cannon_id in cannon_ids: cannon = self.get_cannon(cannon_id) if cannon is None: continue cannon.development = max(cannon.development, 3) cannon_desc = cannon_descriptions.get_cannon_description( cannon_id ) if cannon_desc is None: continue levels = cannon.levels levels[0] += 1 names = ["effect", "improved_foundation", "improved_style"] if cannon_id == 0: names = ["effect"] max_part_level_0 = cannon_recipe.get_max_part_level(0) max_part_level_1 = cannon_recipe.get_max_part_level(1) max_part_level_2 = cannon_recipe.get_max_part_level(2) if ( max_part_level_0 is None or max_part_level_1 is None or max_part_level_2 is None ): return levels = dialog_creator.MultiEditor.from_reduced( cannon_desc.get_cannon_name(), names, levels, max_values=[ max_part_level_0, max_part_level_1, max_part_level_2, ], items_localized=True, ).edit() for part_id, level in enumerate(levels): if part_id == 0: level -= 1 cannon.levels[part_id] = level def get_cannon(self, cannon_id: int) -> Cannon | None: if self.cannons is None: return None return self.cannons.cannons.get(cannon_id, None) @staticmethod def get_stage_name(development: int) -> str: if development == 0: return core.core_data.local_manager.get_key("none") if development == 1: return core.core_data.local_manager.get_key("foundation") if development == 2: return core.core_data.local_manager.get_key("style") if development == 3: return core.core_data.local_manager.get_key("effect") return core.core_data.local_manager.get_key( "unknown_stage", stage=development ) def edit_cannon(save_file: core.SaveFile): save_file.ototo.edit_cannon(save_file) ================================================ FILE: src/bcsfe/core/game/localizable.py ================================================ from __future__ import annotations from bcsfe import core class Localizable: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.localizable = self.get_localizable() def get_localizable(self) -> dict[str, str] | None: gdg = core.core_data.get_game_data_getter(self.save_file) data = gdg.download("resLocal", "localizable.tsv") if data is None: return None csv = core.CSV(data, "\t") keys: dict[str, str] = {} for line in csv: try: keys[line[0].to_str()] = line[1].to_str() except IndexError: pass return keys def get(self, key: str) -> str | None: if self.localizable is None: return None return self.localizable.get(key) def get_lang(self) -> str | None: return self.get("lang") ================================================ FILE: src/bcsfe/core/game/map/__init__.py ================================================ from bcsfe.core.game.map import ( story, event, item_reward_stage, timed_score, ex_stage, dojo, outbreaks, tower, challenge, map_reset, uncanny, legend_quest, gauntlets, enigma, aku, zero_legends, chapters, map_names, map_option, ) __all__ = [ "story", "event", "item_reward_stage", "timed_score", "ex_stage", "dojo", "outbreaks", "tower", "challenge", "map_reset", "uncanny", "legend_quest", "gauntlets", "enigma", "aku", "zero_legends", "chapters", "map_names", "map_option", ] ================================================ FILE: src/bcsfe/core/game/map/aku.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(data: core.Data) -> Stage: clear_times = data.read_short() return Stage(clear_times) def write(self, data: core.Data): data.write_short(self.clear_times) def serialize(self) -> int: return self.clear_times @staticmethod def deserialize(data: int) -> Stage: return Stage( data, ) def __repr__(self): return f"Stage({self.clear_times})" def __str__(self): return self.__repr__() def clear_stage(self, clear_count: int = 1): self.clear_times = clear_count class Chapter: def __init__(self, current_stage: int, total_stages: int = 0): self.current_stage = current_stage self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)] @staticmethod def init(total_stages: int) -> Chapter: return Chapter(0, total_stages) @staticmethod def read_current_stage(data: core.Data): current_stage = data.read_byte() return Chapter(current_stage) def write_current_stage(self, data: core.Data): data.write_byte(self.current_stage) def read_stages(self, data: core.Data, total_stages: int): self.stages = [Stage.read(data) for _ in range(total_stages)] def write_stages(self, data: core.Data): for stage in self.stages: stage.write(data) def serialize(self) -> dict[str, Any]: return { "current_stage": self.current_stage, "stages": [stage.serialize() for stage in self.stages], } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: chapter = Chapter(data.get("current_stage", 0)) chapter.stages = [ Stage.deserialize(stage) for stage in data.get("stages", []) ] return chapter def __repr__(self): return f"Chapter({self.current_stage}, {self.stages})" def __str__(self): return self.__repr__() class ChaptersStars: def __init__(self, chapters: list[Chapter]): self.chapters = chapters @staticmethod def init(total_stages: int, total_stars: int) -> ChaptersStars: return ChaptersStars( [Chapter.init(total_stages) for _ in range(total_stars)] ) @staticmethod def read_current_stage(data: core.Data, total_stars: int): chapters = [ Chapter.read_current_stage(data) for _ in range(total_stars) ] return ChaptersStars(chapters) def write_current_stage(self, data: core.Data): for chapter in self.chapters: chapter.write_current_stage(data) def read_stages(self, data: core.Data, total_stages: int): for chapter in self.chapters: chapter.read_stages(data, total_stages) def write_stages(self, data: core.Data): for chapter in self.chapters: chapter.write_stages(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ChaptersStars: chapters = [Chapter.deserialize(chapter) for chapter in data] return ChaptersStars(chapters) def __repr__(self): return f"ChaptersStars({self.chapters})" def __str__(self): return self.__repr__() class AkuChapters: def __init__(self, chapters: list[ChaptersStars]): self.chapters = chapters @staticmethod def init() -> AkuChapters: return AkuChapters([]) @staticmethod def read(data: core.Data) -> AkuChapters: total_chapters = data.read_short() total_stages = data.read_byte() total_stars = data.read_byte() chapters = [ ChaptersStars.read_current_stage(data, total_stars) for _ in range(total_chapters) ] for chapter in chapters: chapter.read_stages(data, total_stages) return AkuChapters(chapters) def write(self, data: core.Data): data.write_short(len(self.chapters)) try: data.write_byte(len(self.chapters[0].chapters[0].stages)) except IndexError: data.write_byte(0) try: data.write_byte(len(self.chapters[0].chapters)) except IndexError: data.write_byte(0) for chapter in self.chapters: chapter.write_current_stage(data) for chapter in self.chapters: chapter.write_stages(data) def serialize(self) -> list[list[dict[str, Any]]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[list[dict[str, Any]]]) -> AkuChapters: chapters = [ChaptersStars.deserialize(chapter) for chapter in data] return AkuChapters(chapters) def __repr__(self): return f"Chapters({self.chapters})" def __str__(self): return self.__repr__() @staticmethod def edit_aku_chapters(save_file: core.SaveFile): aku = save_file.aku chapter = aku.chapters[0].chapters[0] clear_progress = core.StoryChapters.get_selected_chapter_progress( max_stages=len(chapter.stages) ) if clear_progress is None: return if clear_progress > 1: individual_clear_count = ( core.StoryChapters.ask_if_individual_clear_counts() ) if individual_clear_count is None: return else: individual_clear_count = True if individual_clear_count: stage_names = core.StageNames(save_file, "DM", 49).stage_names if stage_names is None: return for i, stage in enumerate(chapter.stages[:clear_progress]): stage_name = stage_names[i] color.ColoredText.localize( "aku_current_stage", name=stage_name, id=i ) clear_count = core.StoryChapters.ask_clear_count() if clear_count is None: return stage.clear_stage(clear_count) else: clear_count = core.StoryChapters.ask_clear_count() if clear_count is None: return for stage in chapter.stages[:clear_progress]: stage.clear_stage(clear_count) for i in range(clear_progress, len(chapter.stages)): chapter.stages[i].clear_stage(clear_count=0) color.ColoredText.localize("aku_clear_success") ================================================ FILE: src/bcsfe/core/game/map/challenge.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator class ChallengeChapters: def __init__(self, chapters: core.Chapters): self.chapters = chapters self.scores: list[int] = [] self.shown_popup: bool = False @staticmethod def init() -> ChallengeChapters: return ChallengeChapters(core.Chapters.init()) @staticmethod def read(data: core.Data) -> ChallengeChapters: ch = core.Chapters.read(data) return ChallengeChapters(ch) def write(self, data: core.Data): self.chapters.write(data) def read_scores(self, data: core.Data): total_scores = data.read_int() self.scores = [data.read_int() for _ in range(total_scores)] def write_scores(self, data: core.Data): data.write_int(len(self.scores)) for score in self.scores: data.write_int(score) def read_popup(self, data: core.Data): self.shown_popup = data.read_bool() def write_popup(self, data: core.Data): data.write_bool(self.shown_popup) def serialize(self) -> dict[str, Any]: return { "chapters": self.chapters.serialize(), "scores": self.scores, "shown_popup": self.shown_popup, } @staticmethod def deserialize(data: dict[str, Any]) -> ChallengeChapters: challenge = ChallengeChapters( core.Chapters.deserialize(data.get("chapters", {})), ) challenge.scores = data.get("scores", []) challenge.shown_popup = data.get("shown_popup", False) return challenge def __repr__(self): return f"Challenge({self.chapters})" def __str__(self): return self.__repr__() def edit_score(self): if not self.scores: self.scores = [0] self.scores[0] = dialog_creator.SingleEditor( "challenge_score", self.scores[0], None, localized_item=True ).edit() self.shown_popup = True self.chapters.clear_stage(0, 0, 0, False) def edit_challenge_score(save_file: core.SaveFile): save_file.challenge.edit_score() ================================================ FILE: src/bcsfe/core/game/map/chapters.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import edits class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(data: core.Data) -> Stage: clear_times = data.read_int() return Stage(clear_times) def write(self, data: core.Data): data.write_int(self.clear_times) def serialize(self) -> int: return self.clear_times @staticmethod def deserialize(data: int) -> Stage: return Stage( data, ) def __repr__(self): return f"Stage({self.clear_times})" def __str__(self): return self.__repr__() def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False): if ensure_cleared_only: self.clear_times = self.clear_times or clear_amount else: self.clear_times = clear_amount def unclear_stage(self): self.clear_times = 0 class Chapter: def __init__(self, selected_stage: int, total_stages: int = 0): self.selected_stage = selected_stage self.clear_progress = 0 self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)] self.chapter_unlock_state = 0 self.total_stages = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: if overwrite_clear_progress: self.clear_progress = index + 1 else: self.clear_progress = max(self.clear_progress, index + 1) self.chapter_unlock_state = 3 self.stages[index].clear_stage(clear_amount, ensure_cleared_only) if index == self.total_stages - 1: return True return False def unclear_stage(self, index: int): self.clear_progress = min(self.clear_progress, index) self.stages[index].unclear_stage() return True @staticmethod def init(total_stages: int) -> Chapter: return Chapter(0, total_stages) @staticmethod def read_selected_stage(data: core.Data) -> Chapter: selected_stage = data.read_int() return Chapter(selected_stage) def write_selected_stage(self, data: core.Data): data.write_int(self.selected_stage) def read_clear_progress(self, data: core.Data): self.clear_progress = data.read_int() def write_clear_progress(self, data: core.Data): data.write_int(self.clear_progress) def read_stages(self, data: core.Data, total_stages: int): self.stages = [Stage.read(data) for _ in range(total_stages)] def write_stages(self, data: core.Data): for stage in self.stages: stage.write(data) def read_chapter_unlock_state(self, data: core.Data): self.chapter_unlock_state = data.read_int() def write_chapter_unlock_state(self, data: core.Data): data.write_int(self.chapter_unlock_state) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "clear_progress": self.clear_progress, "stages": [stage.serialize() for stage in self.stages], "chapter_unlock_state": self.chapter_unlock_state, } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: chapter = Chapter(data.get("selected_stage", 0)) chapter.clear_progress = data.get("clear_progress", 0) chapter.stages = [Stage.deserialize(stage) for stage in data.get("stages", [])] chapter.chapter_unlock_state = data.get("chapter_unlock_state", 0) return chapter def __repr__(self): return f"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})" def __str__(self): return self.__repr__() class ChaptersStars: def __init__(self, chapters: list[Chapter]): self.chapters = chapters def clear_stage( self, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[star].clear_stage( stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def unclear_stage(self, star: int, stage: int): finished = self.chapters[star].unclear_stage(stage) if finished and star + 1 < len(self.chapters): for chapter in self.chapters[star + 1 :]: chapter.chapter_unlock_state = 0 return finished @staticmethod def init(total_stages: int, total_stars: int) -> ChaptersStars: chapters = [Chapter.init(total_stages) for _ in range(total_stars)] return ChaptersStars(chapters) @staticmethod def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars: chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)] return ChaptersStars(chapters) def write_selected_stage(self, data: core.Data): for chapter in self.chapters: chapter.write_selected_stage(data) def read_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.read_clear_progress(data) def write_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.write_clear_progress(data) def read_stages(self, data: core.Data, total_stages: int): for _ in range(total_stages): for chapter in self.chapters: chapter.stages.append(Stage.read(data)) def write_stages(self, data: core.Data): for i in range(len(self.chapters[0].stages)): for chapter in self.chapters: chapter.stages[i].write(data) def read_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.read_chapter_unlock_state(data) def write_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.write_chapter_unlock_state(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ChaptersStars: chapters = [Chapter.deserialize(chapter) for chapter in data] return ChaptersStars(chapters) def __repr__(self): return f"ChaptersStars({self.chapters})" def __str__(self): return self.__repr__() class Chapters: def __init__(self, chapters: list[ChaptersStars]): self.chapters = chapters def get_total_stars(self, map: int) -> int: return len(self.chapters[map].chapters) def get_total_stages(self, map: int, star: int) -> int: return len(self.chapters[map].chapters[star].stages) def clear_stage( self, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[map].clear_stage( star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 return finished def unclear_stage(self, map: int, star: int, stage: int) -> bool: finished = self.chapters[map].unclear_stage(star, stage) if finished and map + 1 < len(self.chapters) and star == 0: for chapter in self.chapters[map + 1].chapters: chapter.chapter_unlock_state = 0 return finished @staticmethod def init() -> Chapters: return Chapters([]) @staticmethod def read(data: core.Data, read_every_time: bool = True) -> Chapters: total_stages = 0 total_chapters = 0 total_stars = 0 if read_every_time: total_chapters = data.read_int() total_stars = data.read_int() else: total_chapters = data.read_int() total_stages = data.read_int() total_stars = data.read_int() chapters = [ ChaptersStars.read_selected_stage(data, total_stars) for _ in range(total_chapters) ] if read_every_time: total_chapters = data.read_int() total_stars = data.read_int() for chapter in chapters: chapter.read_clear_progress(data) if read_every_time: total_chapters = data.read_int() total_stages = data.read_int() total_stars = data.read_int() for chapter in chapters: chapter.read_stages(data, total_stages) if read_every_time: total_chapters = data.read_int() total_stars = data.read_int() for chapter in chapters: chapter.read_chapter_unlock_state(data) return Chapters(chapters) def get_lengths(self) -> tuple[int, int, int]: total_chapters = len(self.chapters) try: total_stages = len(self.chapters[0].chapters[0].stages) except IndexError: total_stages = 0 try: total_stars = len(self.chapters[0].chapters) except IndexError: total_stars = 0 return (total_chapters, total_stages, total_stars) def write(self, data: core.Data, write_every_time: bool = True): total_chapters, total_stages, total_stars = self.get_lengths() if write_every_time: data.write_int(total_chapters) data.write_int(total_stars) else: data.write_int(total_chapters) data.write_int(total_stages) data.write_int(total_stars) for chapter in self.chapters: chapter.write_selected_stage(data) if write_every_time: data.write_int(total_chapters) data.write_int(total_stars) for chapter in self.chapters: chapter.write_clear_progress(data) if write_every_time: data.write_int(total_chapters) data.write_int(total_stages) data.write_int(total_stars) for chapter in self.chapters: chapter.write_stages(data) if write_every_time: data.write_int(total_chapters) data.write_int(total_stars) for chapter in self.chapters: chapter.write_chapter_unlock_state(data) def serialize(self) -> list[list[dict[str, Any]]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[list[dict[str, Any]]]) -> Chapters: chapters = [ChaptersStars.deserialize(chapter) for chapter in data] tower_chapters = Chapters(chapters) return tower_chapters def __repr__(self): return f"Chapters({self.chapters})" def __str__(self): return self.__repr__() def unclear_rest(self, stages: list[int], stars: int, id: int): if not stages: return for star in range(stars, self.get_total_stars(id)): for stage in range(max(stages), self.get_total_stages(id, star)): self.chapters[id].chapters[star].stages[stage].clear_times = 0 self.chapters[id].chapters[star].clear_progress = 0 def edit_chapters( self, save_file: core.SaveFile, letter_code: str, base_index: int ) -> dict[int, bool] | None: return edits.map.edit_chapters( save_file, self, letter_code, base_index=base_index ) def set_total_stages(self, map: int, total_stages: int): for chapter in self.chapters[map].chapters: chapter.total_stages = total_stages ================================================ FILE: src/bcsfe/core/game/map/dojo.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator class Stage: def __init__(self, score: int): self.score = score @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(stream: core.Data) -> Stage: score = stream.read_int() return Stage(score) def write(self, stream: core.Data): stream.write_int(self.score) def serialize(self) -> int: return self.score @staticmethod def deserialize(data: int) -> Stage: return Stage(data) def __repr__(self) -> str: return f"Stage(score={self.score!r})" def __str__(self) -> str: return f"Stage(score={self.score!r})" class Chapter: def __init__(self, stages: dict[int, Stage]): self.stages = stages def get_stage(self, stage_id: int) -> Stage: if stage_id not in self.stages: self.stages[stage_id] = Stage.init() return self.stages[stage_id] @staticmethod def init() -> Chapter: return Chapter({}) @staticmethod def read(stream: core.Data) -> Chapter: total = stream.read_int() stages: dict[int, Stage] = {} for _ in range(total): stage_id = stream.read_int() stage = Stage.read(stream) stages[stage_id] = stage return Chapter(stages) def write(self, stream: core.Data): stream.write_int(len(self.stages)) for stage_id, stage in self.stages.items(): stream.write_int(stage_id) stage.write(stream) def serialize(self) -> dict[int, Any]: return {stage_id: stage.serialize() for stage_id, stage in self.stages.items()} @staticmethod def deserialize(data: dict[int, Any]) -> Chapter: return Chapter( {stage_id: Stage.deserialize(stage) for stage_id, stage in data.items()} ) def __repr__(self) -> str: return f"Chapter(stages={self.stages!r})" def __str__(self) -> str: return f"Chapter(stages={self.stages!r})" class Chapters: def __init__(self, chapters: dict[int, Chapter]): self.chapters = chapters def get_stage(self, chapter_id: int, stage_id: int) -> Stage: if chapter_id not in self.chapters: self.chapters[chapter_id] = Chapter.init() return self.chapters[chapter_id].get_stage(stage_id) @staticmethod def init() -> Chapters: return Chapters({}) @staticmethod def read(stream: core.Data) -> Chapters: total = stream.read_int() chapters: dict[int, Chapter] = {} for _ in range(total): chapter_id = stream.read_int() chapter = Chapter.read(stream) chapters[chapter_id] = chapter return Chapters(chapters) def write(self, stream: core.Data): stream.write_int(len(self.chapters)) for chapter_id, chapter in self.chapters.items(): stream.write_int(chapter_id) chapter.write(stream) def serialize(self) -> dict[int, Any]: return { chapter_id: chapter.serialize() for chapter_id, chapter in self.chapters.items() } @staticmethod def deserialize(data: dict[int, Any]) -> Chapters: return Chapters( { chapter_id: Chapter.deserialize(chapter) for chapter_id, chapter in data.items() } ) def __repr__(self) -> str: return f"Chapters(chapters={self.chapters!r})" def __str__(self) -> str: return f"Chapters(chapters={self.chapters!r})" class Ranking: def __init__( self, score: int, ranking: int, has_submitted: bool, has_completed: bool, has_seen_results: bool, start_date: int, end_date: int, event_number: int, should_show_rank_description: bool, should_show_start_message: bool, submit_error_flag: bool, other: str | None, ): self.score = score self.ranking = ranking self.has_submitted = has_submitted self.has_completed = has_completed self.has_seen_results = has_seen_results self.start_date = start_date self.end_date = end_date self.event_number = event_number self.should_show_rank_description = should_show_rank_description self.should_show_start_message = should_show_start_message self.submit_error_flag = submit_error_flag self.did_win_rewards = False self.other = other @staticmethod def init() -> Ranking: return Ranking( 0, 0, False, False, False, 0, 0, 0, False, False, False, None, ) @staticmethod def read(stream: core.Data, game_version: core.GameVersion) -> Ranking: score = stream.read_int() ranking = stream.read_int() has_submitted = stream.read_bool() has_completed = stream.read_bool() has_seen_results = stream.read_bool() start_date = stream.read_int() end_date = stream.read_int() event_number = stream.read_int() should_show_rank_description = stream.read_bool() should_show_start_message = stream.read_bool() submit_error_flag = stream.read_bool() if game_version >= 140500: # game seems to do more that just this, may break in the future other = stream.read_string() else: other = None return Ranking( score, ranking, has_submitted, has_completed, has_seen_results, start_date, end_date, event_number, should_show_rank_description, should_show_start_message, submit_error_flag, other, ) def write(self, stream: core.Data, game_version: core.GameVersion): stream.write_int(self.score) stream.write_int(self.ranking) stream.write_bool(self.has_submitted) stream.write_bool(self.has_completed) stream.write_bool(self.has_seen_results) stream.write_int(self.start_date) stream.write_int(self.end_date) stream.write_int(self.event_number) stream.write_bool(self.should_show_rank_description) stream.write_bool(self.should_show_start_message) stream.write_bool(self.submit_error_flag) if game_version >= 140500: # game seems to do more that just this, may break in the future stream.write_string(self.other or "") def read_did_win_rewards(self, stream: core.Data): self.did_win_rewards = stream.read_bool() def write_did_win_rewards(self, stream: core.Data): stream.write_bool(self.did_win_rewards) def serialize(self) -> dict[str, Any]: return { "score": self.score, "ranking": self.ranking, "has_submitted": self.has_submitted, "has_completed": self.has_completed, "has_seen_results": self.has_seen_results, "start_date": self.start_date, "end_date": self.end_date, "event_number": self.event_number, "should_show_rank_description": self.should_show_rank_description, "should_show_start_message": self.should_show_start_message, "submit_error_flag": self.submit_error_flag, "did_win_rewards": self.did_win_rewards, "other": self.other, } @staticmethod def deserialize(data: dict[str, Any]) -> Ranking: ranking = Ranking( data.get("score", 0), data.get("ranking", 0), data.get("has_submitted", False), data.get("has_completed", False), data.get("has_seen_results", False), data.get("start_date", 0), data.get("end_date", 0), data.get("event_number", 0), data.get("should_show_rank_description", False), data.get("should_show_start_message", False), data.get("submit_error_flag", False), data.get("other", None), ) ranking.did_win_rewards = data.get("did_win_rewards", False) return ranking def __repr__(self) -> str: return ( f"Ranking(score={self.score!r}, ranking={self.ranking!r}, " f"has_submitted={self.has_submitted!r}, has_completed={self.has_completed!r}, " f"has_seen_results={self.has_seen_results!r}, start_date={self.start_date!r}, " f"end_date={self.end_date!r}, event_number={self.event_number!r}, " f"should_show_rank_description={self.should_show_rank_description!r}, " f"should_show_start_message={self.should_show_start_message!r}, " f"submit_error_flag={self.submit_error_flag!r}," f"did_win_rewards={self.did_win_rewards!r})," f"other={self.other!r})" ) def __str__(self) -> str: return self.__repr__() class Dojo: def __init__(self, chapters: Chapters): self.chapters = chapters self.item_lock_flags = False self.item_locks = [False] * 6 self.ranking = Ranking.init() @staticmethod def init() -> Dojo: return Dojo(Chapters.init()) @staticmethod def read_chapters(stream: core.Data) -> Dojo: chapters = Chapters.read(stream) return Dojo(chapters) def write_chapters(self, stream: core.Data): self.chapters.write(stream) def read_item_locks(self, stream: core.Data): self.item_lock_flags = stream.read_bool() self.item_locks = stream.read_bool_list(6) def write_item_locks(self, stream: core.Data): stream.write_bool(self.item_lock_flags) stream.write_bool_list(self.item_locks, write_length=False, length=6) def read_ranking(self, stream: core.Data, game_version: core.GameVersion): self.ranking = Ranking.read(stream, game_version) def write_ranking(self, stream: core.Data, game_version: core.GameVersion): self.ranking.write(stream, game_version) def serialize(self) -> dict[str, Any]: return { "chapters": self.chapters.serialize(), "item_locks": self.item_locks, "item_lock_flags": self.item_lock_flags, "ranking": self.ranking.serialize(), } @staticmethod def deserialize(data: dict[str, Any]) -> Dojo: chapters = Chapters.deserialize(data.get("chapters", {})) item_locks = data.get("item_locks", []) item_lock_flags = data.get("item_lock_flags", False) dojo = Dojo(chapters) dojo.item_locks = item_locks dojo.item_lock_flags = item_lock_flags dojo.ranking = Ranking.deserialize(data.get("ranking", {})) return dojo def __repr__(self) -> str: return f"Dojo(chapters={self.chapters!r}, item_locks={self.item_locks!r}, item_lock_flags={self.item_lock_flags!r}, ranking={self.ranking!r})" def __str__(self) -> str: return self.__repr__() def edit_score(self): stage = self.chapters.get_stage(0, 0) stage.score = dialog_creator.SingleEditor( "dojo_score", stage.score, None, localized_item=True, ).edit() def edit_dojo_score(save_file: core.SaveFile): save_file.dojo.edit_score() ================================================ FILE: src/bcsfe/core/game/map/enigma.py ================================================ from __future__ import annotations import time from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator, color class Stage: def __init__( self, level: int, stage_id: int, decoding_satus: int, start_time: float, ): self.level = level self.stage_id = stage_id self.decoding_satus = decoding_satus self.start_time = start_time @staticmethod def init() -> Stage: return Stage(0, 0, 0, 0.0) @staticmethod def read(data: core.Data) -> Stage: level = data.read_int() stage_id = data.read_int() decoding_satus = data.read_byte() start_time = data.read_double() return Stage(level, stage_id, decoding_satus, start_time) def write(self, data: core.Data): data.write_int(self.level) data.write_int(self.stage_id) data.write_byte(self.decoding_satus) data.write_double(self.start_time) def serialize(self) -> dict[str, Any]: return { "level": self.level, "stage_id": self.stage_id, "decoding_satus": self.decoding_satus, "start_time": self.start_time, } @staticmethod def deserialize(data: dict[str, Any]) -> Stage: return Stage( data.get("level", 0), data.get("stage_id", 0), data.get("decoding_satus", 0), data.get("start_time", 0.0), ) def __repr__(self): return f"Stage({self.level}, {self.stage_id}, {self.decoding_satus}, {self.start_time})" def __str__(self): return self.__repr__() class Enigma: def __init__( self, energy_since_1: int, energy_since_2: int, enigma_level: int, unknown_1: int, unknown_2: bool, stages: list[Stage], extra: tuple[int, int, int, float] | None, ): self.energy_since_1 = energy_since_1 self.energy_since_2 = energy_since_2 self.enigma_level = enigma_level self.unknown_1 = unknown_1 self.unknown_2 = unknown_2 self.stages = stages self.extra = extra @staticmethod def init() -> Enigma: return Enigma(0, 0, 0, 0, False, [], None) @staticmethod def read(data: core.Data, game_version: core.GameVersion) -> Enigma: energy_since_1 = data.read_int() energy_since_2 = data.read_int() enigma_level = data.read_byte() unknown_1 = data.read_byte() unknown_2 = data.read_bool() total_stages = data.read_byte() stages = [Stage.read(data) for _ in range(total_stages)] extra_data = None if game_version >= 140500: has_extra = data.read_bool() if has_extra: extra_data = ( data.read_int(), data.read_int(), data.read_byte(), data.read_double(), ) return Enigma( energy_since_1, energy_since_2, enigma_level, unknown_1, unknown_2, stages, extra_data, ) def write(self, data: core.Data, game_version: core.GameVersion): data.write_int(self.energy_since_1) data.write_int(self.energy_since_2) data.write_byte(self.enigma_level) data.write_byte(self.unknown_1) data.write_bool(self.unknown_2) data.write_byte(len(self.stages)) for stage in self.stages: stage.write(data) if game_version >= 140500: data.write_bool(self.extra is not None) if self.extra is not None: data.write_int(self.extra[0]) data.write_int(self.extra[1]) data.write_byte(self.extra[2]) data.write_double(self.extra[3]) def serialize(self) -> dict[str, Any]: return { "energy_since_1": self.energy_since_1, "energy_since_2": self.energy_since_2, "enigma_level": self.enigma_level, "unknown_1": self.unknown_1, "unknown_2": self.unknown_2, "stages": [stage.serialize() for stage in self.stages], "extra": self.extra, } @staticmethod def deserialize(data: dict[str, Any]) -> Enigma: return Enigma( data.get("energy_since_1", 0), data.get("energy_since_2", 0), data.get("enigma_level", 0), data.get("unknown_1", 0), data.get("unknown_2", False), [Stage.deserialize(stage) for stage in data.get("stages", [])], data.get("extra", None), ) def __repr__(self): return f"Enigma({self.energy_since_1}, {self.energy_since_2}, {self.enigma_level}, {self.unknown_1}, {self.unknown_2}, {self.stages}, {self.extra})" def __str__(self): return self.__repr__() def edit_enigma(self, save_file: core.SaveFile): names = core.MapNames(save_file, "H", base_index=25000).map_names names_list: list[str] = [] keys = list(names.keys()) keys.sort() for id in keys: name = names[id] if name is None: name = core.core_data.local_manager.get_key( "unknown_enigma_name", id=id ) names_list.append(name) base_level = 25000 color.ColoredText.localize("current_enigma_stages") for stage in self.stages: name = names[stage.stage_id - base_level] if name is None: name = core.core_data.local_manager.get_key( "unknown_enigma_name", id=stage.stage_id ) color.ColoredText.localize( "enigma_stage", name=name, id=stage.stage_id - base_level ) if self.stages: wipe = dialog_creator.YesNoInput().get_input_once("wipe_enigma") if wipe is None: return if wipe: for stage in self.stages: id = stage.stage_id save_file.event_stages.chapter_completion_count[id] = 0 self.stages = [] ids, _ = dialog_creator.ChoiceInput( names_list, names_list, [], {}, "enigma_select", ).multiple_choice() if ids is None: return for enigma_id in ids: abs_id = enigma_id + base_level save_file.event_stages.chapter_completion_count[abs_id] = 0 # TODO: level? they can go much higher than 3... not sure it really matters though stage = Stage(3, abs_id, 2, int(time.time())) self.stages.append(stage) color.ColoredText.localize("enigma_success") def edit_enigma(save_file: core.SaveFile): save_file.enigma.edit_enigma(save_file) ================================================ FILE: src/bcsfe/core/game/map/event.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator, edits class EventStage: def __init__(self, clear_amount: int): self.clear_amount = clear_amount @staticmethod def init() -> EventStage: return EventStage(0) @staticmethod def read(data: core.Data, is_int: bool) -> EventStage: if is_int: clear_amount = data.read_int() else: clear_amount = data.read_short() return EventStage(clear_amount) def write(self, data: core.Data, is_int: bool): if is_int: data.write_int(self.clear_amount) else: data.write_short(self.clear_amount) def serialize(self) -> int: return self.clear_amount @staticmethod def deserialize(data: int) -> EventStage: return EventStage( clear_amount=data, ) def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.__repr__() def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False): if ensure_cleared_only: self.clear_amount = self.clear_amount or clear_amount else: self.clear_amount = clear_amount def unclear_stage(self): self.clear_amount = 0 class EventSubChapter: def __init__(self, selected_stage: int, total_stages: int = 0): self.selected_stage = selected_stage self.clear_progress = 0 self.stages = [EventStage.init() for _ in range(total_stages)] self.chapter_unlock_state = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: if overwrite_clear_progress: self.clear_progress = index + 1 else: self.clear_progress = max(self.clear_progress, index + 1) self.stages[index].clear_stage(clear_amount, ensure_cleared_only) self.chapter_unlock_state = 3 if index == len(self.stages) - 1: return True return False def unclear_stage(self, index: int) -> bool: self.clear_progress = min(self.clear_progress, index) self.stages[index].unclear_stage() return True def clear_map(self, increment: bool = True) -> bool: self.clear_progress = len(self.stages) self.chapter_unlock_state = 3 for stage in self.stages: if increment: clear_amount = stage.clear_amount + 1 else: clear_amount = stage.clear_amount or 1 stage.clear_stage(clear_amount) return True @staticmethod def init(total_stages: int) -> EventSubChapter: return EventSubChapter(0, total_stages) @staticmethod def read_selected_stage(data: core.Data, is_int: bool) -> EventSubChapter: if is_int: selected_stage = data.read_int() else: selected_stage = data.read_byte() return EventSubChapter(selected_stage) def write_selected_stage(self, data: core.Data, is_int: bool): if is_int: data.write_int(self.selected_stage) else: data.write_byte(self.selected_stage) def read_clear_progress(self, data: core.Data, is_int: bool): if is_int: self.clear_progress = data.read_int() else: self.clear_progress = data.read_byte() def write_clear_progress(self, data: core.Data, is_int: bool): if is_int: data.write_int(self.clear_progress) else: data.write_byte(self.clear_progress) def read_stages(self, data: core.Data, total_stages: int, is_int: bool): self.stages = [EventStage.read(data, is_int) for _ in range(total_stages)] def write_stages(self, data: core.Data, is_int: bool): for stage in self.stages: stage.write(data, is_int) def read_chapter_unlock_state(self, data: core.Data, is_int: bool): if is_int: self.chapter_unlock_state = data.read_int() else: self.chapter_unlock_state = data.read_byte() def write_chapter_unlock_state(self, data: core.Data, is_int: bool): if is_int: data.write_int(self.chapter_unlock_state) else: data.write_byte(self.chapter_unlock_state) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "clear_progress": self.clear_progress, "stages": [stage.serialize() for stage in self.stages], "chapter_unlock_state": self.chapter_unlock_state, } @staticmethod def deserialize(data: dict[str, Any]) -> EventSubChapter: sub_chapter = EventSubChapter( selected_stage=data.get("selected_stage", 0), ) sub_chapter.clear_progress = data.get("clear_progress", 0) sub_chapter.stages = [ EventStage.deserialize(stage) for stage in data.get("stages", []) ] sub_chapter.chapter_unlock_state = data.get("chapter_unlock_state", 0) return sub_chapter def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.__repr__() class EventSubChapterStars: def __init__(self, chapters: list[EventSubChapter]): self.chapters = chapters self.legend_restriction = 0 def clear_stage( self, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[star].clear_stage( stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def unclear_stage(self, star: int, stage: int): finished = self.chapters[star].unclear_stage(stage) if finished and star + 1 < len(self.chapters): for chapter in self.chapters[star + 1 :]: chapter.chapter_unlock_state = 0 return finished def clear_map(self, star: int, increment: bool = True) -> bool: finished = self.chapters[star].clear_map(increment) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def clear_chapter(self, increment: bool = True) -> bool: for chapter in self.chapters: chapter.clear_map(increment) return True @staticmethod def init(total_stars: int) -> EventSubChapterStars: return EventSubChapterStars( [EventSubChapter.init(0) for _ in range(total_stars)] ) @staticmethod def read_selected_stage( data: core.Data, total_stars: int, is_int: bool ) -> EventSubChapterStars: chapters = [ EventSubChapter.read_selected_stage(data, is_int) for _ in range(total_stars) ] return EventSubChapterStars(chapters) def write_selected_stage(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_selected_stage(data, is_int) def read_clear_progress(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.read_clear_progress(data, is_int) def write_clear_progress(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_clear_progress(data, is_int) def read_stages(self, data: core.Data, total_stages: int, is_int: bool): for _ in range(total_stages): for chapter in self.chapters: chapter.stages.append(EventStage.read(data, is_int)) # chapter.read_stages(data, total_stages, is_int) def write_stages(self, data: core.Data, is_int: bool): for i in range(len(self.chapters[0].stages)): for chapter in self.chapters: chapter.stages[i].write(data, is_int) # chapter.write_stages(data, is_int) def read_chapter_unlock_state(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.read_chapter_unlock_state(data, is_int) def write_chapter_unlock_state(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_chapter_unlock_state(data, is_int) def read_legend_restrictions(self, data: core.Data): self.legend_restriction = data.read_int() def write_legend_restrictions(self, data: core.Data): data.write_int(self.legend_restriction) def serialize(self) -> dict[str, Any]: return { "chapters": [chapter.serialize() for chapter in self.chapters], "legend_restriction": self.legend_restriction, } @staticmethod def deserialize(data: dict[str, Any]) -> EventSubChapterStars: chapters = [ EventSubChapter.deserialize(chapter) for chapter in data.get("chapters", []) ] chapter = EventSubChapterStars(chapters) chapter.legend_restriction = data.get("legend_restriction", 0) return chapter def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.__repr__() class EventChapterGroup: def __init__(self, chapters: list[EventSubChapterStars]): self.chapters = chapters def clear_stage( self, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[map].clear_stage( star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only, ) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 return finished def unclear_stage(self, map: int, star: int, stage: int) -> bool: finished = self.chapters[map].unclear_stage(star, stage) if finished and map + 1 < len(self.chapters) and star == 0: for chapter in self.chapters[map + 1].chapters: chapter.chapter_unlock_state = 0 return finished def clear_map(self, map: int, star: int, increment: bool = True): finished = self.chapters[map].clear_map(star, increment) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 def clear_chapter(self, map: int, increment: bool = True): finished = self.chapters[map].clear_chapter(increment) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 def clear_group(self, increment: bool = True): for chapter in self.chapters: chapter.clear_chapter(increment) @staticmethod def init(total_subchapters: int, total_stars: int) -> EventChapterGroup: return EventChapterGroup( [EventSubChapterStars.init(total_stars) for _ in range(total_subchapters)] ) @staticmethod def read_selected_stage( data: core.Data, total_subchapters: int, total_stars: int, is_int: bool ) -> EventChapterGroup: chapters = [ EventSubChapterStars.read_selected_stage(data, total_stars, is_int) for _ in range(total_subchapters) ] return EventChapterGroup(chapters) def write_selected_stage(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_selected_stage(data, is_int) def read_clear_progress(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.read_clear_progress(data, is_int) def write_clear_progress(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_clear_progress(data, is_int) def read_stages(self, data: core.Data, total_stages: int, is_int: bool): for chapter in self.chapters: chapter.read_stages(data, total_stages, is_int) def write_stages(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_stages(data, is_int) def read_chapter_unlock_state(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.read_chapter_unlock_state(data, is_int) def write_chapter_unlock_state(self, data: core.Data, is_int: bool): for chapter in self.chapters: chapter.write_chapter_unlock_state(data, is_int) def read_legend_restrictions(self, data: core.Data): for chapter in self.chapters: chapter.read_legend_restrictions(data) def write_legend_restrictions(self, data: core.Data): for chapter in self.chapters: chapter.write_legend_restrictions(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> EventChapterGroup: chapters = [EventSubChapterStars.deserialize(chapter) for chapter in data] return EventChapterGroup(chapters) def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.__repr__() class EventChapters: def __init__(self, chapters: list[EventChapterGroup]): self.chapters = chapters self.chapter_completion_count: dict[int, int] = {} self.displayed_cleared_limit_text: dict[int, bool] = {} self.event_start_dates: dict[int, int] = {} self.stages_reward_claimed: list[int] = [] def clear_stage( self, type: int, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: return self.chapters[type].clear_stage( map, star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only, ) def unclear_stage(self, type: int, map: int, star: int, stage: int) -> bool: return self.chapters[type].unclear_stage(map, star, stage) def clear_map(self, type: int, map: int, star: int, increment: bool = True): self.chapters[type].clear_map(map, star, increment) def clear_chapter(self, type: int, map: int, increment: bool = True): self.chapters[type].clear_chapter(map, increment) def clear_group(self, type: int, increment: bool = True): self.chapters[type].clear_group(increment) @staticmethod def init(gv: core.GameVersion) -> EventChapters: if gv < 20: return EventChapters([]) if gv <= 32: total_map_types = 3 total_subchapters = 150 stars_per_subchapter = 3 elif gv <= 34: total_map_types = 4 total_subchapters = 150 stars_per_subchapter = 3 else: total_map_types = 0 total_subchapters = 0 stars_per_subchapter = 0 return EventChapters( [ EventChapterGroup.init(total_subchapters, stars_per_subchapter) for _ in range(total_map_types) ] ) @staticmethod def read(data: core.Data, gv: core.GameVersion) -> EventChapters: if gv < 20: return EventChapters([]) stages_per_subchapter = 0 if 80099 < gv: total_map_types = data.read_byte() total_subchapters = data.read_short() stars_per_subchapter = data.read_byte() stages_per_subchapter = data.read_byte() is_int = False elif gv <= 32: total_map_types = 3 total_subchapters = 150 stars_per_subchapter = 3 is_int = True elif gv <= 34: total_map_types = 4 total_subchapters = 150 stars_per_subchapter = 3 is_int = True else: total_map_types = data.read_int() total_subchapters = data.read_int() stars_per_subchapter = data.read_int() is_int = True chapters = [ EventChapterGroup.read_selected_stage( data, total_subchapters, stars_per_subchapter, is_int ) for _ in range(total_map_types) ] if 80099 < gv: is_int = False elif gv <= 32: total_map_types = 3 total_subchapters = 150 stars_per_subchapter = 3 is_int = True elif gv <= 34: total_map_types = 4 total_subchapters = 150 stars_per_subchapter = 3 is_int = True else: total_map_types = data.read_int() total_subchapters = data.read_int() stars_per_subchapter = data.read_int() is_int = True for chapter in chapters: chapter.read_clear_progress(data, is_int) if 80099 < gv: is_int = False elif gv <= 32: total_map_types = 3 total_subchapters = 150 stars_per_subchapter = 3 stages_per_subchapter = 12 is_int = True elif gv <= 34: total_map_types = 4 total_subchapters = 150 stars_per_subchapter = 3 stages_per_subchapter = 12 is_int = True else: total_map_types = data.read_int() total_subchapters = data.read_int() stages_per_subchapter = data.read_int() stars_per_subchapter = data.read_int() is_int = True for chapter in chapters: chapter.read_stages(data, stages_per_subchapter, is_int) if 80099 < gv: is_int = False elif gv <= 32: total_map_types = 3 total_subchapters = 150 stars_per_subchapter = 3 is_int = True elif gv <= 34: total_map_types = 4 total_subchapters = 150 stars_per_subchapter = 3 is_int = True else: total_map_types = data.read_int() total_subchapters = data.read_int() stars_per_subchapter = data.read_int() is_int = True for chapter in chapters: chapter.read_chapter_unlock_state(data, is_int) return EventChapters(chapters) def get_lengths(self) -> tuple[int, int, int, int]: total_map_types = len(self.chapters) try: total_subchapters = len(self.chapters[0].chapters) except IndexError: total_subchapters = 0 try: stars_per_subchapter = len(self.chapters[0].chapters[0].chapters) except IndexError: stars_per_subchapter = 0 try: stages_per_subchapter = len(self.chapters[0].chapters[0].chapters[0].stages) except IndexError: stages_per_subchapter = 0 return ( total_map_types, total_subchapters, stars_per_subchapter, stages_per_subchapter, ) def write(self, data: core.Data, gv: core.GameVersion): ( total_map_types, total_subchapters, stars_per_subchapter, stages_per_subchapter, ) = self.get_lengths() if gv <= 34: is_int = True else: if 80099 < gv: data.write_byte(total_map_types) data.write_short(total_subchapters) data.write_byte(stars_per_subchapter) data.write_byte(stages_per_subchapter) is_int = False else: data.write_int(total_map_types) data.write_int(total_subchapters) data.write_int(stars_per_subchapter) is_int = True for chapter in self.chapters: chapter.write_selected_stage(data, is_int) if gv <= 34: is_int = True else: if 80099 < gv: is_int = False else: data.write_int(total_map_types) data.write_int(total_subchapters) data.write_int(stars_per_subchapter) is_int = True for chapter in self.chapters: chapter.write_clear_progress(data, is_int) if gv <= 34: is_int = True else: if 80099 < gv: is_int = False else: data.write_int(total_map_types) data.write_int(total_subchapters) data.write_int(stages_per_subchapter) data.write_int(stars_per_subchapter) is_int = True for chapter in self.chapters: chapter.write_stages(data, is_int) if gv <= 34: is_int = True else: if 80099 < gv: is_int = False else: data.write_int(total_map_types) data.write_int(total_subchapters) data.write_int(stars_per_subchapter) is_int = True for chapter in self.chapters: chapter.write_chapter_unlock_state(data, is_int) def read_legend_restrictions(self, data: core.Data, gv: core.GameVersion): if gv < 20: return if gv < 33: total_map_types = 3 # type: ignore total_subchapters = 150 # type: ignore elif gv < 41: total_map_types = 4 # type: ignore total_subchapters = 150 # type: ignore else: total_map_types = data.read_int() # type: ignore total_subchapters = data.read_int() # type: ignore for chapter in self.chapters: chapter.read_legend_restrictions(data) def write_legend_restrictions(self, data: core.Data, gv: core.GameVersion): if gv < 20: return if gv >= 41: data.write_int(len(self.chapters)) try: data.write_int(len(self.chapters[0].chapters)) except IndexError: data.write_int(0) for chapter in self.chapters: chapter.write_legend_restrictions(data) def read_dicts(self, data: core.Data): self.chapter_completion_count = data.read_int_int_dict() self.displayed_cleared_limit_text = data.read_int_bool_dict() self.event_start_dates = data.read_int_int_dict() self.stages_reward_claimed = data.read_int_list() def write_dicts(self, data: core.Data): data.write_int_int_dict(self.chapter_completion_count) data.write_int_bool_dict(self.displayed_cleared_limit_text) data.write_int_int_dict(self.event_start_dates) data.write_int_list(self.stages_reward_claimed) def serialize(self) -> dict[str, Any]: return { "chapters": [chapter.serialize() for chapter in self.chapters], "chapter_completion_count": self.chapter_completion_count, "displayed_cleared_limit_text": self.displayed_cleared_limit_text, "event_start_dates": self.event_start_dates, "stages_reward_claimed": self.stages_reward_claimed, } @staticmethod def deserialize(data: dict[str, Any]) -> EventChapters: chapters = [ EventChapterGroup.deserialize(chapter) for chapter in data.get("chapters", []) ] ch = EventChapters(chapters) ch.chapter_completion_count = data.get("chapter_completion_count", {}) ch.displayed_cleared_limit_text = data.get("displayed_cleared_limit_text", {}) ch.event_start_dates = data.get("event_start_dates", {}) ch.stages_reward_claimed = data.get("stages_reward_claimed", []) return ch def __repr__(self) -> str: return f"EventChapters({self.chapters}, {self.chapter_completion_count}, {self.displayed_cleared_limit_text}, {self.event_start_dates}, {self.stages_reward_claimed})" def __str__(self) -> str: return self.__repr__() def get_total_stars(self, type: int, map: int) -> int: try: return len(self.chapters[type].chapters[map].chapters) except IndexError: return len(self.chapters[0].chapters[0].chapters) def get_total_stages(self, type: int, map: int, star: int) -> int: try: return len(self.chapters[type].chapters[map].chapters[star].stages) except IndexError: return len(self.chapters[0].chapters[0].chapters[0].stages) @staticmethod def ask_stars( max_stars: int, prompt: str = "custom_star_count_per_chapter" ) -> int | None: if max_stars <= 1: return max_stars stars = dialog_creator.IntInput(min=1, max=max_stars).get_input_locale( prompt, {"max": max_stars} )[0] if stars is None: return None return stars @staticmethod def ask_stars_unclear( max_stars: int, prompt: str = "custom_star_count_per_chapter" ) -> int | None: stars = dialog_creator.IntInput(min=0, max=max_stars).get_input_locale( prompt, {"max": max_stars} )[0] if stars is None: return None return stars @staticmethod def get_stage_names(map_names: core.MapNames, chapter_id: int) -> list[str] | None: stage_names = map_names.stage_names.get(chapter_id) if stage_names is None: return None new_stage_names: list[str] = [] for stage in stage_names: if stage == "@": continue new_stage_names.append(stage) return new_stage_names @staticmethod def ask_stages(map_names: core.MapNames, chapter_id: int) -> list[int] | None: stage_names = EventChapters.get_stage_names(map_names, chapter_id) if stage_names is None: return None dialog_creator.ListOutput( stage_names, ints=[], dialog="select_stage", localize_elements=False ).display_locale() choices = dialog_creator.RangeInput(len(stage_names), 1).get_input_locale( "stages_select", {} ) if choices is None: return None return [c - 1 for c in choices] @staticmethod def ask_stages_stage_names(stage_names: list[str]) -> list[int] | None: val = EventChapters.ask_stages_stage_names_one(stage_names) if val is None: return None return list(range(val + 1)) @staticmethod def ask_stages_stage_names_one(stage_names: list[str]) -> int | None: new_stage_names: list[str] = [] for stage in stage_names: if stage == "@": continue new_stage_names.append(stage) stage_names = new_stage_names choice = dialog_creator.ChoiceInput.from_reduced( stage_names, dialog="select_stage_progress", single_choice=True ).single_choice() if choice is None: return None return choice - 1 @staticmethod def ask_clear_amount() -> int | None: val = dialog_creator.IntInput( max=core.core_data.max_value_manager.get("stage_clear_count"), bit_count=16 ).get_input_locale("clear_amount_enter", {})[0] return val @staticmethod def edit_sol_chapters(save_file: core.SaveFile): EventChapters.edit_chapters(save_file, 0, "N", 0) @staticmethod def edit_event_chapters(save_file: core.SaveFile): EventChapters.edit_chapters(save_file, 1, "S", 1000) @staticmethod def edit_collab_chapters(save_file: core.SaveFile): EventChapters.edit_chapters(save_file, 2, "C", 2000) @staticmethod def select_map_names(names_dict: dict[int, str | None]) -> list[int] | None: map_ids: list[int] = [] names_list: list[str] = [] names_dict = dict(sorted(names_dict.items())) ids = list(names_dict.keys()) for id, map_name in names_dict.items(): if map_name is None: map_name = core.core_data.local_manager.get_key( "unknown_map_name", id=id ) else: map_name = core.core_data.local_manager.get_key( "map_name", name=map_name, id=id, escape=False ) names_list.append(map_name) while True: dialog_creator.ListOutput( names_list, [], "select_map", localize_elements=False ).display_locale() if names_list: example_name = names_list[0] else: example_name = "" usr_input = ( color.ColoredInput() .localize("select_map_dialog", example=example_name, escape=False) .lower() .strip() ) if usr_input == "q": return None usr_ids = dialog_creator.RangeInput(max=len(names_list), min=1).parse( usr_input ) if not usr_ids: found_names: list[tuple[int, str]] = [] for i, name in enumerate(names_list): if usr_input.replace(" ", "_") in name.lower().strip().replace( " ", "_" ): true_id = ids[i] found_names.append((i, name)) if len(found_names) == 0: color.ColoredText.localize("no_map_found", name=usr_input) elif len(found_names) == 1: id = found_names[0][0] true_id = ids[id] if true_id not in map_ids: map_ids.append(true_id) else: selected_ids, _ = dialog_creator.ChoiceInput.from_reduced( [name for _, name in found_names], dialog="select_map_from_names", ).multiple_choice(False) if selected_ids is None: continue for i in selected_ids: id = found_names[i][0] true_id = ids[id] if true_id not in map_ids: map_ids.append(true_id) else: for id in usr_ids: id -= 1 true_id = ids[id] if true_id not in map_ids: map_ids.append(true_id) color.ColoredText.localize("current_maps", maps=map_ids) for id in map_ids: name = names_dict[id] EventChapters.print_current_chapter(name, id) option = dialog_creator.ChoiceInput.from_reduced( ["keep_selecting", "remove_selection", "finish_selection"], dialog="map_selection_q", ).single_choice() if option is None: return None option -= 1 if option == 0: continue if option == 1: map_ids.clear() else: break return map_ids @staticmethod def print_current_chapter(name: str | None, id: int): if name is None: name = core.core_data.local_manager.get_key("unknown_map_name", id=id) color.ColoredText.localize( "current_sol_chapter", escape=False, name=name, id=id ) @staticmethod def print_current_stage(name: str | None, index: int): if name is None: name = core.core_data.local_manager.get_key( "unknown_stage_name", index=index ) color.ColoredText.localize("current_stage_map", name=name, index=index) @staticmethod def edit_chapters( save_file: core.SaveFile, type: int, letter_code: str, base_index: int ): edits.map.edit_chapters( save_file, save_file.event_stages, letter_code, type=type, base_index=base_index, ) def unclear_rest( self, stages: list[int], stars: int, id: int, type: int, ): if not stages: return for star in range(stars, self.get_total_stars(type, id)): for stage in range(max(stages), self.get_total_stages(type, id, star)): self.chapters[type].chapters[id].chapters[star].stages[ stage ].clear_amount = 0 self.chapters[type].chapters[id].chapters[star].clear_progress = 0 ================================================ FILE: src/bcsfe/core/game/map/ex_stage.py ================================================ from __future__ import annotations from bcsfe import core class Stage: def __init__(self, clear_amount: int): self.clear_amount = clear_amount @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(stream: core.Data) -> Stage: clear_amount = stream.read_int() return Stage(clear_amount) def write(self, stream: core.Data): stream.write_int(self.clear_amount) def serialize(self) -> int: return self.clear_amount @staticmethod def deserialize(data: int) -> Stage: return Stage(data) def __repr__(self) -> str: return f"Stage(clear_amount={self.clear_amount!r})" def __str__(self) -> str: return f"Stage(clear_amount={self.clear_amount!r})" class Chapter: def __init__(self, stages: list[Stage]): self.stages = stages @staticmethod def init() -> Chapter: return Chapter([Stage.init() for _ in range(12)]) @staticmethod def read(stream: core.Data) -> Chapter: total = 12 stages: list[Stage] = [] for _ in range(total): stages.append(Stage.read(stream)) return Chapter(stages) def write(self, stream: core.Data): for stage in self.stages: stage.write(stream) def serialize(self) -> list[int]: return [stage.serialize() for stage in self.stages] @staticmethod def deserialize(data: list[int]) -> Chapter: return Chapter([Stage.deserialize(stage) for stage in data]) def __repr__(self) -> str: return f"Chapter(stages={self.stages!r})" def __str__(self) -> str: return f"Chapter(stages={self.stages!r})" class ExChapters: def __init__(self, chapters: list[Chapter]): self.chapters = chapters @staticmethod def init() -> ExChapters: return ExChapters([]) @staticmethod def read(stream: core.Data) -> ExChapters: total = stream.read_int() chapters: list[Chapter] = [] for _ in range(total): chapters.append(Chapter.read(stream)) return ExChapters(chapters) def write(self, stream: core.Data): stream.write_int(len(self.chapters)) for chapter in self.chapters: chapter.write(stream) def serialize(self) -> list[list[int]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[list[int]]) -> ExChapters: return ExChapters([Chapter.deserialize(chapter) for chapter in data]) def __repr__(self) -> str: return f"Chapters(chapters={self.chapters!r})" def __str__(self) -> str: return f"Chapters(chapters={self.chapters!r})" ================================================ FILE: src/bcsfe/core/game/map/gauntlets.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import edits class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(data: core.Data) -> Stage: clear_times = data.read_short() return Stage(clear_times) def write(self, data: core.Data): data.write_short(self.clear_times) def serialize(self) -> int: return self.clear_times @staticmethod def deserialize(data: int) -> Stage: return Stage( data, ) def __repr__(self): return f"Stage({self.clear_times})" def __str__(self): return self.__repr__() def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False): if ensure_cleared_only: self.clear_times = self.clear_times or clear_amount else: self.clear_times = clear_amount def unclear_stage(self): self.clear_times = 0 class Chapter: def __init__(self, selected_stage: int, total_stages: int = 0): self.selected_stage = selected_stage self.clear_progress = 0 self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)] self.chapter_unlock_state = 0 self.total_stages = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: if overwrite_clear_progress: self.clear_progress = index + 1 else: self.clear_progress = max(self.clear_progress, index + 1) self.stages[index].clear_stage(clear_amount, ensure_cleared_only) self.chapter_unlock_state = 3 if index == self.total_stages - 1: return True return False def unclear_stage(self, index: int): self.clear_progress = min(self.clear_progress, index) self.stages[index].unclear_stage() return True @staticmethod def init(total_stages: int) -> Chapter: return Chapter(0, total_stages) @staticmethod def read_selected_stage(data: core.Data) -> Chapter: selected_stage = data.read_byte() return Chapter(selected_stage) def write_selected_stage(self, data: core.Data): data.write_byte(self.selected_stage) def read_clear_progress(self, data: core.Data): self.clear_progress = data.read_byte() def write_clear_progress(self, data: core.Data): data.write_byte(self.clear_progress) def read_stages(self, data: core.Data, total_stages: int): self.stages = [Stage.read(data) for _ in range(total_stages)] def write_stages(self, data: core.Data): for stage in self.stages: stage.write(data) def read_chapter_unlock_state(self, data: core.Data): self.chapter_unlock_state = data.read_byte() def write_chapter_unlock_state(self, data: core.Data): data.write_byte(self.chapter_unlock_state) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "clear_progress": self.clear_progress, "stages": [stage.serialize() for stage in self.stages], "chapter_unlock_state": self.chapter_unlock_state, } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: chapter = Chapter(data.get("selected_stage", 0)) chapter.clear_progress = data.get("clear_progress", 0) chapter.stages = [Stage.deserialize(stage) for stage in data.get("stages", [])] chapter.chapter_unlock_state = data.get("chapter_unlock_state", 0) return chapter def __repr__(self): return f"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})" def __str__(self): return self.__repr__() class ChaptersStars: def __init__(self, chapters: list[Chapter]): self.chapters = chapters def clear_stage( self, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[star].clear_stage( stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def unclear_stage(self, star: int, stage: int): finished = self.chapters[star].unclear_stage(stage) if finished and star + 1 < len(self.chapters): for chapter in self.chapters[star + 1 :]: chapter.chapter_unlock_state = 0 return finished @staticmethod def init(total_stages: int, total_stars: int) -> ChaptersStars: chapters = [Chapter.init(total_stages) for _ in range(total_stars)] return ChaptersStars(chapters) @staticmethod def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars: chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)] return ChaptersStars(chapters) def write_selected_stage(self, data: core.Data): for chapter in self.chapters: chapter.write_selected_stage(data) def read_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.read_clear_progress(data) def write_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.write_clear_progress(data) def read_stages(self, data: core.Data, total_stages: int): for _ in range(total_stages): for chapter in self.chapters: chapter.stages.append(Stage.read(data)) def write_stages(self, data: core.Data): for i in range(len(self.chapters[0].stages)): for chapter in self.chapters: chapter.stages[i].write(data) def read_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.read_chapter_unlock_state(data) def write_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.write_chapter_unlock_state(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ChaptersStars: chapters = [Chapter.deserialize(chapter) for chapter in data] return ChaptersStars(chapters) def __repr__(self): return f"ChaptersStars({self.chapters})" def __str__(self): return self.__repr__() class GauntletChapters: def __init__(self, chapters: list[ChaptersStars], unknown: list[int]): self.chapters = chapters self.unknown = unknown def clear_stage( self, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[map].clear_stage( star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 return finished def unclear_stage(self, map: int, star: int, stage: int) -> bool: finished = self.chapters[map].unclear_stage(star, stage) if finished and map + 1 < len(self.chapters) and star == 0: for chapter in self.chapters[map + 1].chapters: chapter.chapter_unlock_state = 0 return finished @staticmethod def init() -> GauntletChapters: return GauntletChapters([], []) @staticmethod def read(data: core.Data) -> GauntletChapters: total_chapters = data.read_short() total_stages = data.read_byte() total_stars = data.read_byte() chapters = [ ChaptersStars.read_selected_stage(data, total_stars) for _ in range(total_chapters) ] for chapter in chapters: chapter.read_clear_progress(data) for chapter in chapters: chapter.read_stages(data, total_stages) for chapter in chapters: chapter.read_chapter_unlock_state(data) unknown = [data.read_byte() for _ in range(total_chapters)] return GauntletChapters(chapters, unknown) def write(self, data: core.Data): data.write_short(len(self.chapters)) try: data.write_byte(len(self.chapters[0].chapters[0].stages)) except IndexError: data.write_byte(0) try: data.write_byte(len(self.chapters[0].chapters)) except IndexError: data.write_byte(0) for chapter in self.chapters: chapter.write_selected_stage(data) for chapter in self.chapters: chapter.write_clear_progress(data) for chapter in self.chapters: chapter.write_stages(data) for chapter in self.chapters: chapter.write_chapter_unlock_state(data) for unknown in self.unknown: data.write_byte(unknown) def serialize(self) -> dict[str, Any]: return { "chapters": [chapter.serialize() for chapter in self.chapters], "unknown": self.unknown, } @staticmethod def deserialize(data: dict[str, Any]) -> GauntletChapters: chapters = [ ChaptersStars.deserialize(chapter) for chapter in data.get("chapters", []) ] return GauntletChapters(chapters, data.get("unknown", [])) def __repr__(self): return f"Chapters({self.chapters}, {self.unknown})" def __str__(self): return self.__repr__() def get_total_stars(self, map: int) -> int: try: return len(self.chapters[map].chapters) except IndexError: return 0 def get_total_stages(self, map: int, star: int) -> int: try: return len(self.chapters[map].chapters[star].stages) except IndexError: return 0 @staticmethod def edit_gauntlets(save_file: core.SaveFile): gauntlets = save_file.gauntlets gauntlets.edit_chapters(save_file, "A", 24000) @staticmethod def edit_collab_gauntlets(save_file: core.SaveFile): gauntlets = save_file.collab_gauntlets gauntlets.edit_chapters(save_file, "CA", 27000) @staticmethod def edit_behemoth_culling(save_file: core.SaveFile): gauntlets = save_file.behemoth_culling gauntlets.edit_chapters(save_file, "Q", 31000) @staticmethod def edit_enigma_stages(save_file: core.SaveFile): save_file.enigma_clears.edit_chapters(save_file, "H", 25000) def edit_chapters( self, save_file: core.SaveFile, letter_code: str, base_index: int ): edits.map.edit_chapters(save_file, self, letter_code, base_index=base_index) def unclear_rest(self, stages: list[int], stars: int, id: int): if not stages: return for star in range(stars, self.get_total_stars(id)): for stage in range(max(stages), self.get_total_stages(id, star)): self.chapters[id].chapters[star].stages[stage].clear_times = 0 self.chapters[id].chapters[star].clear_progress = 0 def set_total_stages(self, map: int, total_stages: int): for chapter in self.chapters[map].chapters: chapter.total_stages = total_stages ================================================ FILE: src/bcsfe/core/game/map/item_reward_stage.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class Stage: def __init__(self, claimed: bool): self.claimed = claimed @staticmethod def init() -> Stage: return Stage(False) @staticmethod def read(stream: core.Data) -> Stage: return Stage(stream.read_bool()) def write(self, stream: core.Data): stream.write_bool(self.claimed) def serialize(self) -> bool: return self.claimed @staticmethod def deserialize(data: bool) -> Stage: return Stage(data) def __repr__(self) -> str: return f"Stage(claimed={self.claimed})" def __str__(self) -> str: return self.__repr__() class SubChapter: def __init__(self, stages: list[Stage]): self.stages = stages @staticmethod def init(total_stages: int) -> SubChapter: stages = [Stage.init() for _ in range(total_stages)] return SubChapter(stages) @staticmethod def read(stream: core.Data, total_stages: int) -> SubChapter: stages: list[Stage] = [] for _ in range(total_stages): stages.append(Stage.read(stream)) return SubChapter(stages) def write(self, stream: core.Data): for stage in self.stages: stage.write(stream) def serialize(self) -> list[bool]: return [stage.serialize() for stage in self.stages] @staticmethod def deserialize(data: list[bool]) -> SubChapter: return SubChapter([Stage.deserialize(stage) for stage in data]) def __repr__(self) -> str: return f"SubChapter(stages={self.stages})" def __str__(self) -> str: return self.__repr__() class SubChapterStars: def __init__(self, sub_chapters: list[SubChapter]): self.sub_chapters = sub_chapters @staticmethod def init(total_stages: int, total_stars: int) -> SubChapterStars: sub_chapters = [ SubChapter.init(total_stages) for _ in range(total_stars) ] return SubChapterStars(sub_chapters) @staticmethod def read( stream: core.Data, total_stages: int, total_stars: int ) -> SubChapterStars: sub_chapters: list[SubChapter] = [] for _ in range(total_stars): sub_chapters.append(SubChapter.read(stream, total_stages)) return SubChapterStars(sub_chapters) def write(self, stream: core.Data): for sub_chapter in self.sub_chapters: sub_chapter.write(stream) def serialize(self) -> list[list[bool]]: return [sub_chapter.serialize() for sub_chapter in self.sub_chapters] @staticmethod def deserialize(data: list[list[bool]]) -> SubChapterStars: return SubChapterStars( [SubChapter.deserialize(sub_chapter) for sub_chapter in data] ) def __repr__(self) -> str: return f"SubChapterStars(sub_chapters={self.sub_chapters})" def __str__(self) -> str: return self.__repr__() class ItemObtain: def __init__(self, flag: bool): self.flag = flag @staticmethod def init() -> ItemObtain: return ItemObtain(False) @staticmethod def read(stream: core.Data) -> ItemObtain: return ItemObtain(stream.read_bool()) def write(self, stream: core.Data): stream.write_bool(self.flag) def serialize(self) -> bool: return self.flag @staticmethod def deserialize(data: bool) -> ItemObtain: return ItemObtain(data) def __repr__(self) -> str: return f"ItemObtain(flag={self.flag})" def __str__(self) -> str: return self.__repr__() class ItemObtainSet: def __init__(self, item_obtains: dict[int, ItemObtain]): self.item_obtains = item_obtains @staticmethod def init() -> ItemObtainSet: return ItemObtainSet({}) @staticmethod def read(stream: core.Data) -> ItemObtainSet: item_obtains: dict[int, ItemObtain] = {} for _ in range(stream.read_int()): key = stream.read_int() item_obtains[key] = ItemObtain.read(stream) return ItemObtainSet(item_obtains) def write(self, stream: core.Data): stream.write_int(len(self.item_obtains)) for item_id, item_obtain in self.item_obtains.items(): stream.write_int(item_id) item_obtain.write(stream) def serialize(self) -> dict[str, Any]: return { "item_obtains": { item_id: item_obtain.serialize() for item_id, item_obtain in self.item_obtains.items() } } @staticmethod def deserialize(data: dict[str, Any]) -> ItemObtainSet: return ItemObtainSet( { int(item_id): ItemObtain.deserialize(item_obtain) for item_id, item_obtain in data.get("item_obtains", {}).items() } ) def __repr__(self) -> str: return f"ItemObtainSet(item_obtains={self.item_obtains})" def __str__(self) -> str: return self.__repr__() class ItemObtainSets: def __init__(self, item_obtain_sets: dict[int, ItemObtainSet]): self.item_obtain_sets = item_obtain_sets @staticmethod def init() -> ItemObtainSets: return ItemObtainSets({}) @staticmethod def read(stream: core.Data) -> ItemObtainSets: item_obtain_sets: dict[int, ItemObtainSet] = {} for _ in range(stream.read_int()): key = stream.read_int() item_obtain_sets[key] = ItemObtainSet.read(stream) return ItemObtainSets(item_obtain_sets) def write(self, stream: core.Data): stream.write_int(len(self.item_obtain_sets)) for item_id, item_obtain_set in self.item_obtain_sets.items(): stream.write_int(item_id) item_obtain_set.write(stream) def serialize(self) -> dict[int, Any]: return { item_id: item_obtain_set.serialize() for item_id, item_obtain_set in self.item_obtain_sets.items() } @staticmethod def deserialize(data: dict[int, Any]) -> ItemObtainSets: return ItemObtainSets( { int(item_id): ItemObtainSet.deserialize(item_obtain_set) for item_id, item_obtain_set in data.items() } ) def __repr__(self) -> str: return f"ItemObtainSets(item_obtain_sets={self.item_obtain_sets})" def __str__(self) -> str: return self.__repr__() class UnobtainedItem: def __init__(self, unobtained: bool): self.unobtained = unobtained @staticmethod def init() -> UnobtainedItem: return UnobtainedItem(False) @staticmethod def read(stream: core.Data) -> UnobtainedItem: return UnobtainedItem(stream.read_bool()) def write(self, stream: core.Data): stream.write_bool(self.unobtained) def serialize(self) -> bool: return self.unobtained @staticmethod def deserialize(data: bool) -> UnobtainedItem: return UnobtainedItem(data) def __repr__(self) -> str: return f"UnobtainedItem(unobtained={self.unobtained})" def __str__(self) -> str: return self.__repr__() class UnobtainedItems: def __init__(self, unobtained_items: dict[int, UnobtainedItem]): self.unobtained_items = unobtained_items @staticmethod def init() -> UnobtainedItems: return UnobtainedItems({}) @staticmethod def read(stream: core.Data) -> UnobtainedItems: unobtained_items: dict[int, UnobtainedItem] = {} for _ in range(stream.read_int()): key = stream.read_int() unobtained_items[key] = UnobtainedItem.read(stream) return UnobtainedItems(unobtained_items) def write(self, stream: core.Data): stream.write_int(len(self.unobtained_items)) for item_id, unobtained_item in self.unobtained_items.items(): stream.write_int(item_id) unobtained_item.write(stream) def serialize(self) -> dict[str, Any]: return { "unobtained_items": { item_id: unobtained_item.serialize() for item_id, unobtained_item in self.unobtained_items.items() } } @staticmethod def deserialize(data: dict[str, Any]) -> UnobtainedItems: return UnobtainedItems( { int(item_id): UnobtainedItem.deserialize(unobtained_item) for item_id, unobtained_item in data.get( "unobtained_items", {} ).items() } ) def __repr__(self) -> str: return f"UnobtainedItems(unobtained_items={self.unobtained_items})" def __str__(self) -> str: return self.__repr__() class ItemRewardChapters: def __init__(self, sub_chapters: list[SubChapterStars]): self.sub_chapters = sub_chapters self.item_obtains = ItemObtainSets.init() self.unobtained_items = UnobtainedItems.init() @staticmethod def init(gv: core.GameVersion) -> ItemRewardChapters: if gv < 20: return ItemRewardChapters([]) if gv <= 33: total_subchapters = 50 total_stages = 12 total_stars = 3 elif gv <= 34: total_subchapters = 0 total_stages = 12 total_stars = 3 else: total_subchapters = 0 total_stages = 0 total_stars = 0 return ItemRewardChapters( [ SubChapterStars.init(total_stages, total_stars) for _ in range(total_subchapters) ] ) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> ItemRewardChapters: if gv < 20: return ItemRewardChapters([]) if gv <= 33: total_subchapters = 50 total_stages = 12 total_stars = 3 elif gv <= 34: total_subchapters = stream.read_int() total_stages = 12 total_stars = 3 else: total_subchapters = stream.read_int() total_stages = stream.read_int() total_stars = stream.read_int() sub_chapters: list[SubChapterStars] = [] for _ in range(total_subchapters): sub_chapters.append( SubChapterStars.read(stream, total_stages, total_stars) ) return ItemRewardChapters(sub_chapters) def write(self, stream: core.Data, gv: core.GameVersion): if gv < 20: return if gv <= 33: pass elif gv <= 34: stream.write_int(len(self.sub_chapters)) else: stream.write_int(len(self.sub_chapters)) try: stream.write_int( len(self.sub_chapters[0].sub_chapters[0].stages) ) except IndexError: stream.write_int(0) try: stream.write_int(len(self.sub_chapters[0].sub_chapters)) except IndexError: stream.write_int(0) for sub_chapter in self.sub_chapters: sub_chapter.write(stream) def read_item_obtains(self, stream: core.Data): self.item_obtains = ItemObtainSets.read(stream) self.unobtained_items = UnobtainedItems.read(stream) def write_item_obtains(self, stream: core.Data): self.item_obtains.write(stream) self.unobtained_items.write(stream) def serialize(self) -> dict[str, Any]: return { "sub_chapters": [ sub_chapter.serialize() for sub_chapter in self.sub_chapters ], "item_obtains": self.item_obtains.serialize(), "unobtained_items": self.unobtained_items.serialize(), } @staticmethod def deserialize(data: dict[str, Any]) -> ItemRewardChapters: chapters = ItemRewardChapters( [ SubChapterStars.deserialize(sub_chapter) for sub_chapter in data.get("sub_chapters", []) ] ) chapters.item_obtains = ItemObtainSets.deserialize( data.get("item_obtains", {}) ) chapters.unobtained_items = UnobtainedItems.deserialize( data.get("unobtained_items", {}) ) return chapters def __repr__(self) -> str: return f"Chapters(sub_chapters={self.sub_chapters}, item_obtains={self.item_obtains}, unobtained_items={self.unobtained_items})" def __str__(self) -> str: return self.__repr__() ================================================ FILE: src/bcsfe/core/game/map/legend_quest.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import edits class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(data: core.Data) -> Stage: clear_times = data.read_short() return Stage(clear_times) def write(self, data: core.Data): data.write_short(self.clear_times) def read_tries(self, data: core.Data): self.tries = data.read_short() def write_tries(self, data: core.Data): data.write_short(self.tries) def serialize(self) -> dict[str, Any]: return { "clear_times": self.clear_times, "tries": self.tries, } @staticmethod def deserialize(data: dict[str, Any]) -> Stage: stage = Stage( data.get("clear_times", 0), ) stage.tries = data.get("tries", 0) return stage def __repr__(self): return f"Stage({self.clear_times}, {self.tries})" def __str__(self): return self.__repr__() def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False): if ensure_cleared_only: self.clear_times = self.clear_times or clear_amount self.tries = self.tries or clear_amount else: self.clear_times = clear_amount self.tries = clear_amount def unclear_stage(self): self.clear_times = 0 self.tries = 0 class Chapter: def __init__(self, selected_stage: int, total_stages: int = 0): self.selected_stage = selected_stage self.clear_progress = 0 self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)] self.chapter_unlock_state = 0 self.total_stages = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: if overwrite_clear_progress: self.clear_progress = index + 1 else: self.clear_progress = max(self.clear_progress, index + 1) self.stages[index].clear_stage(clear_amount, ensure_cleared_only) self.chapter_unlock_state = 3 if index == self.total_stages - 1: return True return False def unclear_stage(self, index: int) -> bool: self.clear_progress = min(self.clear_progress, index) self.stages[index].unclear_stage() return True @staticmethod def init(total_stages: int) -> Chapter: return Chapter(0, total_stages) @staticmethod def read_selected_stage(data: core.Data) -> Chapter: selected_stage = data.read_byte() return Chapter(selected_stage) def write_selected_stage(self, data: core.Data): data.write_byte(self.selected_stage) def read_clear_progress(self, data: core.Data): self.clear_progress = data.read_byte() def write_clear_progress(self, data: core.Data): data.write_byte(self.clear_progress) def read_stages(self, data: core.Data, total_stages: int): self.stages = [Stage.read(data) for _ in range(total_stages)] for stage in self.stages: stage.read_tries(data) def write_stages(self, data: core.Data): for stage in self.stages: stage.write(data) for stage in self.stages: stage.write_tries(data) def read_chapter_unlock_state(self, data: core.Data): self.chapter_unlock_state = data.read_byte() def write_chapter_unlock_state(self, data: core.Data): data.write_byte(self.chapter_unlock_state) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "clear_progress": self.clear_progress, "stages": [stage.serialize() for stage in self.stages], "chapter_unlock_state": self.chapter_unlock_state, } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: chapter = Chapter( data.get("selected_stage", 0), ) chapter.clear_progress = data.get("clear_progress", 0) chapter.stages = [Stage.deserialize(stage) for stage in data.get("stages", [])] chapter.chapter_unlock_state = data.get("chapter_unlock_state", 0) return chapter def __repr__(self): return f"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})" def __str__(self): return self.__repr__() class ChaptersStars: def __init__(self, chapters: list[Chapter]): self.chapters = chapters def clear_stage( self, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[star].clear_stage( stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def unclear_stage(self, star: int, stage: int) -> bool: finished = self.chapters[star].unclear_stage(stage) if finished and star + 1 < len(self.chapters): for chapter in self.chapters[star + 1 :]: chapter.chapter_unlock_state = 0 return finished @staticmethod def init(total_stages: int, total_stars: int) -> ChaptersStars: chapters = [Chapter.init(total_stages) for _ in range(total_stars)] return ChaptersStars(chapters) @staticmethod def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars: chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)] return ChaptersStars(chapters) def write_selected_stage(self, data: core.Data): for chapter in self.chapters: chapter.write_selected_stage(data) def read_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.read_clear_progress(data) def write_clear_progress(self, data: core.Data): for chapter in self.chapters: chapter.write_clear_progress(data) def read_stages(self, data: core.Data, total_stages: int): for _ in range(total_stages): for chapter in self.chapters: chapter.stages.append(Stage.read(data)) for i in range(total_stages): for chapter in self.chapters: chapter.stages[i].read_tries(data) def write_stages(self, data: core.Data): for i in range(len(self.chapters[0].stages)): for chapter in self.chapters: chapter.stages[i].write(data) for i in range(len(self.chapters[0].stages)): for chapter in self.chapters: chapter.stages[i].write_tries(data) def read_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.read_chapter_unlock_state(data) def write_chapter_unlock_state(self, data: core.Data): for chapter in self.chapters: chapter.write_chapter_unlock_state(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ChaptersStars: chapters = [Chapter.deserialize(chapter) for chapter in data] return ChaptersStars(chapters) def __repr__(self): return f"ChaptersStars({self.chapters})" def __str__(self): return self.__repr__() class LegendQuestChapters: def __init__( self, chapters: list[ChaptersStars], unknown: list[int], ids: list[int] ): self.chapters = chapters self.unknown = unknown self.ids = ids def clear_stage( self, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[map].clear_stage( star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 return finished def unclear_stage(self, map: int, star: int, stage: int) -> bool: finished = self.chapters[map].unclear_stage(star, stage) if finished and map + 1 < len(self.chapters) and star == 0: for chapter in self.chapters[map + 1].chapters: chapter.chapter_unlock_state = 0 return finished @staticmethod def init() -> LegendQuestChapters: return LegendQuestChapters([], [], []) @staticmethod def read(data: core.Data) -> LegendQuestChapters: total_chapters = data.read_byte() total_stages = data.read_byte() total_stars = data.read_byte() chapters = [ ChaptersStars.read_selected_stage(data, total_stars) for _ in range(total_chapters) ] for chapter in chapters: chapter.read_clear_progress(data) for chapter in chapters: chapter.read_stages(data, total_stages) for chapter in chapters: chapter.read_chapter_unlock_state(data) unknown = [data.read_byte() for _ in range(total_chapters)] ids = [data.read_int() for _ in range(total_stages)] return LegendQuestChapters(chapters, unknown, ids) def write(self, data: core.Data): data.write_byte(len(self.chapters)) try: data.write_byte(len(self.chapters[0].chapters[0].stages)) except IndexError: data.write_byte(0) try: data.write_byte(len(self.chapters[0].chapters)) except IndexError: data.write_byte(0) for chapter in self.chapters: chapter.write_selected_stage(data) for chapter in self.chapters: chapter.write_clear_progress(data) for chapter in self.chapters: chapter.write_stages(data) for chapter in self.chapters: chapter.write_chapter_unlock_state(data) for unknown in self.unknown: data.write_byte(unknown) for id in self.ids: data.write_int(id) def serialize(self) -> dict[str, Any]: return { "chapters": [chapter.serialize() for chapter in self.chapters], "unknown": self.unknown, "ids": self.ids, } @staticmethod def deserialize(data: dict[str, Any]) -> LegendQuestChapters: chapters = [ ChaptersStars.deserialize(chapter) for chapter in data.get("chapters", []) ] unknown = data.get("unknown", []) ids = data.get("ids", []) return LegendQuestChapters(chapters, unknown, ids) def __repr__(self): return f"Chapters({self.chapters}, {self.unknown}, {self.ids})" def __str__(self): return self.__repr__() def get_total_stars(self, map: int) -> int: try: return len(self.chapters[map].chapters) except IndexError: return 0 def get_total_stages(self, map: int, star: int) -> int: try: return len(self.chapters[map].chapters[star].stages) except IndexError: return 0 @staticmethod def edit_legend_quest(save_file: core.SaveFile): legend_quest = save_file.legend_quest legend_quest.edit_chapters(save_file, "D", base_index=16000) def edit_chapters( self, save_file: core.SaveFile, letter_code: str, base_index: int ): edits.map.edit_chapters(save_file, self, letter_code, base_index=base_index) def unclear_rest(self, stages: list[int], stars: int, id: int): if not stages: return for star in range(stars, self.get_total_stars(id)): for stage in range(max(stages), self.get_total_stages(id, star)): self.chapters[id].chapters[star].stages[stage].clear_times = 0 self.chapters[id].chapters[star].clear_progress = 0 def set_total_stages(self, map: int, total_stages: int): for chapter in self.chapters[map].chapters: chapter.total_stages = total_stages ================================================ FILE: src/bcsfe/core/game/map/map_names.py ================================================ from __future__ import annotations from bcsfe import core class MapNames: def __init__( self, save_file: core.SaveFile, code: str, base_index: int, output: bool = True, no_r_prefix: bool = False, ): self.save_file = save_file self.out = output self.code = code self.base_index = base_index self.no_r_prefix = no_r_prefix self.gdg = core.core_data.get_game_data_getter(self.save_file) self.map_names: dict[int, str | None] = {} self.stage_names: dict[int, list[str]] = {} self.get_map_names() def get_map_names_in_game( self, base_index: int, total_stages: int ) -> dict[int, str | None] | None: gdg = core.core_data.get_game_data_getter(self.save_file) map_name_data = gdg.download("resLocal", "Map_Name.csv") if map_name_data is None: return None csv = core.CSV( map_name_data, core.Delimeter.from_country_code_res(self.save_file.cc) ) names: dict[int, str | None] = {} for row in csv: id = row[0].to_int() name = row[1].to_str().strip() for i in range(total_stages): index = i + base_index if id == index: if name: names[i] = name else: names[i] = None break return names def get_map_names(self) -> dict[int, str | None] | None: gdg = core.core_data.get_game_data_getter(self.save_file) r_prefix = "" if self.no_r_prefix else "R" stage_names = gdg.download( "resLocal", f"StageName_{r_prefix}{self.code}_{core.core_data.get_lang(self.save_file)}.csv", ) if stage_names is None: return None csv = core.CSV( stage_names, core.Delimeter.from_country_code_res(self.save_file.cc), ) for i, row in enumerate(csv): stage_names_row = row.to_str_list() if not stage_names_row: continue self.stage_names[i] = stage_names_row names = self.get_map_names_in_game(self.base_index, len(self.stage_names)) if names is None: return None self.map_names = names return self.map_names @staticmethod def get_code_from_id(id: int) -> str | None: base_id = id // 1000 ids = { 0: "RN", 1: "RS", 2: "RC", 4: "EX", 6: "RT", 7: "RV", 11: "RR", 12: "RM", 13: "RNA", 14: "RB", 16: "RD", 20: "Z", 21: "Z", 22: "Z", 24: "RA", 25: "RH", 27: "RCA", 30: "DM", 31: "RQ", 32: "L", 34: "RND", } return ids.get(base_id) @staticmethod def from_id(id: int, save_file: core.SaveFile) -> MapNames | None: code = MapNames.get_code_from_id(id) if code is None: return None return MapNames(save_file, code, id, no_r_prefix=True) ================================================ FILE: src/bcsfe/core/game/map/map_option.py ================================================ from __future__ import annotations from bcsfe import core class MapOptionLine: def __init__( self, map_id: int, crown_count: int, crown_mults: list[int], guerrilla_set: int, reset_type: int, one_time_display: bool, display_order: int, interval: int, challenge_flag: bool, difficulty_mask: int, hide_after_clear: bool, name: str, ): self.map_id = map_id self.crown_count = crown_count self.crown_mults = crown_mults self.guerrilla_set = guerrilla_set self.reset_type = reset_type self.one_time_display = one_time_display self.display_order = display_order self.interval = interval self.challenge_flag = challenge_flag self.difficulty_mask = difficulty_mask self.hide_after_clear = hide_after_clear self.name = name @staticmethod def from_line(line: core.Row) -> MapOptionLine: return MapOptionLine( line.next_int(), line.next_int(), [line.next_int() for _ in range(4)], line.next_int(), line.next_int(), line.next_bool(), line.next_int(), line.next_int(), line.next_bool(), line.next_int(), line.next_bool(), line.next_str(), ) class MapOption: def __init__(self, maps: dict[int, MapOptionLine]): self.maps = maps @staticmethod def from_csv(csv: core.CSV) -> MapOption: data: dict[int, MapOptionLine] = {} for line in csv.lines[1:]: # skip headers item = MapOptionLine.from_line(line) data[item.map_id] = item return MapOption(data) @staticmethod def from_save(save_file: core.SaveFile) -> MapOption | None: gdg = core.core_data.get_game_data_getter(save_file) data = gdg.download("DataLocal", "Map_option.csv") if data is None: return None csv = core.CSV(data) return MapOption.from_csv(csv) def get_map(self, map_id: int) -> MapOptionLine | None: return self.maps.get(map_id) ================================================ FILE: src/bcsfe/core/game/map/map_reset.py ================================================ from __future__ import annotations from bcsfe import core class MapResetData: def __init__( self, yearly_end_timestamp: float, monthly_end_timestamp: float, weekly_end_timestamp: float, daily_end_timestamp: float, ): self.yearly_end_timestamp = yearly_end_timestamp self.monthly_end_timestamp = monthly_end_timestamp self.weekly_end_timestamp = weekly_end_timestamp self.daily_end_timestamp = daily_end_timestamp @staticmethod def init() -> MapResetData: return MapResetData( 0.0, 0.0, 0.0, 0.0, ) @staticmethod def read(stream: core.Data) -> MapResetData: yearly_end_timestamp = stream.read_double() monthly_end_timestamp = stream.read_double() weekly_end_timestamp = stream.read_double() daily_end_timestamp = stream.read_double() return MapResetData( yearly_end_timestamp, monthly_end_timestamp, weekly_end_timestamp, daily_end_timestamp, ) def write(self, stream: core.Data): stream.write_double(self.yearly_end_timestamp) stream.write_double(self.monthly_end_timestamp) stream.write_double(self.weekly_end_timestamp) stream.write_double(self.daily_end_timestamp) def serialize(self) -> dict[str, float]: return { "yearly_end_timestamp": self.yearly_end_timestamp, "monthly_end_timestamp": self.monthly_end_timestamp, "weekly_end_timestamp": self.weekly_end_timestamp, "daily_end_timestamp": self.daily_end_timestamp, } @staticmethod def deserialize(data: dict[str, float]) -> MapResetData: return MapResetData( data.get("yearly_end_timestamp", 0.0), data.get("monthly_end_timestamp", 0.0), data.get("weekly_end_timestamp", 0.0), data.get("daily_end_timestamp", 0.0), ) def __str__(self) -> str: 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})" def __repr__(self) -> str: return str(self) class MapResets: def __init__(self, data: dict[int, list[MapResetData]]): self.data = data @staticmethod def init() -> MapResets: return MapResets({}) @staticmethod def read(stream: core.Data) -> MapResets: data: dict[int, list[MapResetData]] = {} for _ in range(stream.read_int()): key = stream.read_int() value: list[MapResetData] = [] for _ in range(stream.read_int()): value.append(MapResetData.read(stream)) data[key] = value return MapResets(data) def write(self, stream: core.Data): stream.write_int(len(self.data)) for key, value in self.data.items(): stream.write_int(key) stream.write_int(len(value)) for item in value: item.write(stream) def serialize(self) -> dict[int, list[dict[str, float]]]: return { key: [item.serialize() for item in value] for key, value in self.data.items() } @staticmethod def deserialize(data: dict[int, list[dict[str, float]]]) -> MapResets: return MapResets( { key: [MapResetData.deserialize(item) for item in value] for key, value in data.items() } ) def __str__(self) -> str: return f"MapResets(data={self.data!r})" def __repr__(self) -> str: return str(self) ================================================ FILE: src/bcsfe/core/game/map/outbreaks.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import dialog_creator, color class Outbreak: def __init__(self, cleared: bool): self.cleared = cleared @staticmethod def init() -> Outbreak: return Outbreak(False) @staticmethod def read(stream: core.Data) -> Outbreak: cleared = stream.read_bool() return Outbreak(cleared) def write(self, stream: core.Data): stream.write_bool(self.cleared) def serialize(self) -> bool: return self.cleared @staticmethod def deserialize(data: bool) -> Outbreak: return Outbreak(data) def __repr__(self) -> str: return f"Outbreak(cleared={self.cleared!r})" def __str__(self) -> str: return f"Outbreak(cleared={self.cleared!r})" class Chapter: def __init__(self, id: int, outbreaks: dict[int, Outbreak]): self.id = id self.outbreaks = outbreaks def get_true_id(self) -> int: if self.id < 3: return self.id return self.id - 1 @staticmethod def init(id: int) -> Chapter: return Chapter(id, {}) @staticmethod def read(stream: core.Data, id: int) -> Chapter: total = stream.read_int() outbreaks: dict[int, Outbreak] = {} for _ in range(total): outbreak_id = stream.read_int() outbreak = Outbreak.read(stream) outbreaks[outbreak_id] = outbreak return Chapter(id, outbreaks) def write(self, stream: core.Data): stream.write_int(len(self.outbreaks)) for outbreak_id, outbreak in self.outbreaks.items(): stream.write_int(outbreak_id) outbreak.write(stream) def serialize(self) -> dict[int, Any]: return { outbreak_id: outbreak.serialize() for outbreak_id, outbreak in self.outbreaks.items() } @staticmethod def deserialize(data: dict[int, Any], id: int) -> Chapter: return Chapter( id, { outbreak_id: Outbreak.deserialize(outbreak_data) for outbreak_id, outbreak_data in data.items() }, ) def __repr__(self) -> str: return f"Chapter(id={self.id!r}, outbreaks={self.outbreaks!r})" def __str__(self) -> str: return self.__repr__() class Outbreaks: def __init__(self, chapters: dict[int, Chapter]): self.chapters = chapters self.zombie_event_remaining_time = 0.0 self.current_outbreaks: dict[int, Chapter] = {} @staticmethod def init() -> Outbreaks: return Outbreaks({}) @staticmethod def read_chapters(stream: core.Data) -> Outbreaks: total = stream.read_int() chapters: dict[int, Chapter] = {} for _ in range(total): chapter_id = stream.read_int() chapter = Chapter.read(stream, chapter_id) chapters[chapter_id] = chapter return Outbreaks(chapters) def write_chapters(self, stream: core.Data): stream.write_int(len(self.chapters)) for chapter_id, chapter in self.chapters.items(): stream.write_int(chapter_id) chapter.write(stream) def read_2(self, stream: core.Data): self.zombie_event_remaining_time = stream.read_double() def write_2(self, stream: core.Data): stream.write_double(self.zombie_event_remaining_time) def read_current_outbreaks(self, stream: core.Data, gv: core.GameVersion): if gv <= 43: total_chapters = stream.read_int() for _ in range(total_chapters): stream.read_int() total_stage = stream.read_int() for _ in range(total_stage): stream.read_int() stream.read_bool() total = stream.read_int() current_outbreaks: dict[int, Chapter] = {} for _ in range(total): chapter_id = stream.read_int() chapter = Chapter.read(stream, chapter_id) current_outbreaks[chapter_id] = chapter self.current_outbreaks = current_outbreaks def write_current_outbreaks(self, stream: core.Data, gv: core.GameVersion): if gv <= 43: stream.write_int(0) stream.write_int(len(self.current_outbreaks)) for chapter_id, chapter in self.current_outbreaks.items(): stream.write_int(chapter_id) chapter.write(stream) def serialize(self) -> dict[str, Any]: return { "chapters": { chapter_id: chapter.serialize() for chapter_id, chapter in self.chapters.items() }, "zombie_event_remaining_time": self.zombie_event_remaining_time, "current_outbreaks": { chapter_id: chapter.serialize() for chapter_id, chapter in self.current_outbreaks.items() }, } @staticmethod def deserialize(data: dict[str, Any]) -> Outbreaks: outbreaks = Outbreaks( { chapter_id: Chapter.deserialize(chapter_data, chapter_id) for chapter_id, chapter_data in data.get("chapters", {}).items() } ) outbreaks.zombie_event_remaining_time = data.get( "zombie_event_remaining_time", 0.0 ) outbreaks.current_outbreaks = { chapter_id: Chapter.deserialize(chapter_data, chapter_id) for chapter_id, chapter_data in data.get("current_outbreaks", {}).items() } return outbreaks def __repr__(self) -> str: return f"Outbreaks(chapters={self.chapters!r}, zombie_event_remaining_time={self.zombie_event_remaining_time!r}, current_outbreaks={self.current_outbreaks!r})" def __str__(self) -> str: return self.__repr__() def get_chapter_from_true_id(self, true_id: int) -> Chapter | None: if true_id < 3: return self.chapters.get(true_id) return self.chapters.get(true_id + 1) def get_current_chapter_from_true_id(self, true_id: int) -> Chapter | None: if true_id < 3: return self.current_outbreaks.get(true_id) return self.current_outbreaks.get(true_id + 1) def clear_outbreak(self, chapter_id: int, stage_id: int, clear: bool): chapter = self.get_chapter_from_true_id(chapter_id) if chapter is not None: stage = chapter.outbreaks.get(stage_id) if stage is not None: stage.cleared = clear if clear: chapter = self.get_current_chapter_from_true_id(chapter_id) if chapter is not None: stage = chapter.outbreaks.get(stage_id) if stage is not None: stage.cleared = False @staticmethod def edit_outbreaks(save_file: core.SaveFile): outbreaks = save_file.outbreaks chapters = outbreaks.chapters if not chapters: color.ColoredText.localize("no_valid_outbreaks") return options = ["clear", "unclear"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="clear_unclear_outbreaks", single_choice=True ).single_choice() if choice is None: return choice -= 1 clear = choice == 0 selected_ids = core.StoryChapters.select_story_chapters( save_file, [chapter.get_true_id() for chapter in chapters.values()] ) if not selected_ids: return choice = core.StoryChapters.get_per_chapter(selected_ids) if choice is None: return if choice == 0: for chapter_id in selected_ids: stages = core.StoryChapters.select_stages(save_file, chapter_id) if not stages: continue for stage in stages: outbreaks.clear_outbreak(chapter_id, stage, clear) else: stages = core.StoryChapters.select_stages(save_file, 0) if not stages: return for stage in stages: for chapter_id in selected_ids: outbreaks.clear_outbreak(chapter_id, stage, clear) if clear: color.ColoredText.localize("clear_outbreaks_success") else: color.ColoredText.localize("unclear_outbreaks_success") ================================================ FILE: src/bcsfe/core/game/map/story.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times self.treasure = 0 self.itf_timed_score = 0 @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read_clear_times(stream: core.Data) -> Stage: return Stage(stream.read_int()) def read_treasure(self, stream: core.Data): self.treasure = stream.read_int() def read_itf_timed_score(self, stream: core.Data): self.itf_timed_score = stream.read_int() def write_clear_times(self, stream: core.Data): stream.write_int(self.clear_times) def write_treasure(self, stream: core.Data): stream.write_int(self.treasure) def write_itf_timed_score(self, stream: core.Data): stream.write_int(self.itf_timed_score) def serialize(self) -> dict[str, Any]: return { "clear_times": self.clear_times, "treasure": self.treasure, "itf_timed_score": self.itf_timed_score, } @staticmethod def deserialize(data: dict[str, Any]) -> Stage: stage = Stage(data.get("clear_times", 0)) stage.treasure = data.get("treasure", 0) stage.itf_timed_score = data.get("itf_timed_score", 0) return stage def __repr__(self): return f"Stage({self.clear_times}, {self.treasure}, {self.itf_timed_score})" def __str__(self): return self.__repr__() def clear_stage(self, clear_amount: int = 1): self.clear_times = clear_amount def unclear_stage(self): self.clear_times = 0 def is_cleared(self) -> bool: return self.clear_times > 0 def set_treasure(self, treasure: int): self.treasure = treasure class Chapter: def __init__(self, selected_stage: int): self.selected_stage = selected_stage self.progress = 0 self.stages = [Stage.init() for _ in range(51)] self.time_until_treasure_chance = 0 self.treasure_chance_duration = 0 self.treasure_chance_value = 0 self.treasure_chance_stage_id = 0 self.treasure_festival_type = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ): if overwrite_clear_progress: self.progress = index + 1 else: self.progress = max(self.progress, index + 1) self.stages[index].clear_stage(clear_amount) def set_treasure(self, stage_id: int, treasure: int): self.stages[stage_id].set_treasure(treasure) def is_stage_clear(self, stage_id: int) -> bool: return self.stages[stage_id].is_cleared() @staticmethod def init() -> Chapter: return Chapter(0) def get_treasure_stages(self) -> list[Stage]: return self.stages[:49] def get_valid_treasure_stages(self) -> list[Stage]: return self.stages[:48] @staticmethod def read_selected_stage(stream: core.Data) -> Chapter: return Chapter(stream.read_int()) def read_progress(self, stream: core.Data): self.progress = stream.read_int() def read_clear_times(self, stream: core.Data): total_stages = 51 self.stages = [Stage.read_clear_times(stream) for _ in range(total_stages)] def read_treasure(self, stream: core.Data): for stage in self.get_treasure_stages(): stage.read_treasure(stream) def read_time_until_treasure_chance(self, stream: core.Data): self.time_until_treasure_chance = stream.read_int() def read_treasure_chance_duration(self, stream: core.Data): self.treasure_chance_duration = stream.read_int() def read_treasure_chance_value(self, stream: core.Data): self.treasure_chance_value = stream.read_int() def read_treasure_chance_stage_id(self, stream: core.Data): self.treasure_chance_stage_id = stream.read_int() def read_treasure_festival_type(self, stream: core.Data): self.treasure_festival_type = stream.read_int() def read_itf_timed_scores(self, stream: core.Data): for stage in self.stages: stage.read_itf_timed_score(stream) def write_selected_stage(self, stream: core.Data): stream.write_int(self.selected_stage) def write_progress(self, stream: core.Data): stream.write_int(self.progress) def write_clear_times(self, stream: core.Data): for stage in self.stages: stage.write_clear_times(stream) def write_treasure(self, stream: core.Data): for stage in self.get_treasure_stages(): stage.write_treasure(stream) def write_time_until_treasure_chance(self, stream: core.Data): stream.write_int(self.time_until_treasure_chance) def write_treasure_chance_duration(self, stream: core.Data): stream.write_int(self.treasure_chance_duration) def write_treasure_chance_value(self, stream: core.Data): stream.write_int(self.treasure_chance_value) def write_treasure_chance_stage_id(self, stream: core.Data): stream.write_int(self.treasure_chance_stage_id) def write_treasure_festival_type(self, stream: core.Data): stream.write_int(self.treasure_festival_type) def write_itf_timed_scores(self, stream: core.Data): for stage in self.stages: stage.write_itf_timed_score(stream) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "progress": self.progress, "stages": [stage.serialize() for stage in self.stages], "time_until_treasure_chance": self.time_until_treasure_chance, "treasure_chance_duration": self.treasure_chance_duration, "treasure_chance_value": self.treasure_chance_value, "treasure_chance_stage_id": self.treasure_chance_stage_id, "treasure_festival_type": self.treasure_festival_type, } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: chapter = Chapter(data.get("selected_stage", 0)) chapter.progress = data.get("progress", 0) chapter.stages = [Stage.deserialize(stage) for stage in data.get("stages", [])] chapter.time_until_treasure_chance = data.get("time_until_treasure_chance", 0) chapter.treasure_chance_duration = data.get("treasure_chance_duration", 0) chapter.treasure_chance_value = data.get("treasure_chance_value", 0) chapter.treasure_chance_stage_id = data.get("treasure_chance_stage_id", 0) chapter.treasure_festival_type = data.get("treasure_festival_type", 0) return chapter def __repr__(self): return f"Chapter({self.selected_stage}, {self.progress}, {self.stages}, {self.time_until_treasure_chance}, {self.treasure_chance_duration}, {self.treasure_chance_value}, {self.treasure_chance_stage_id}, {self.treasure_festival_type})" def __str__(self): return f"Chapter({self.selected_stage}, {self.progress}, {self.stages}, {self.time_until_treasure_chance}, {self.treasure_chance_duration}, {self.treasure_chance_value}, {self.treasure_chance_stage_id}, {self.treasure_festival_type})" def apply_progress(self, progress: int, clear_times: list[int] | None = None): if clear_times is None: clear_times = [1] * progress self.progress = progress for i in range(progress + 1, 48): self.stages[i].unclear_stage() for i in range(progress): self.stages[i].clear_stage(clear_times[i]) def clear_chapter(self): self.apply_progress(48) class StoryChapters: def __init__(self, chapters: list[Chapter]): self.chapters = chapters def get_real_chapters(self) -> list[Chapter]: new_chapters: list[Chapter] = [] for i, chapter in enumerate(self.chapters): if i == 3: continue new_chapters.append(chapter) return new_chapters def clear_stage( self, map: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, chapters: list[Chapter] | None = None, ): if chapters is None: chapters = self.chapters chapters[map].clear_stage(stage, clear_amount, overwrite_clear_progress) def set_treasure(self, chapter: int, stage: int, treasure: int): self.chapters[chapter].set_treasure(stage, treasure) def is_stage_clear(self, chapter: int, stage: int) -> bool: return self.chapters[chapter].is_stage_clear(stage) @staticmethod def init() -> StoryChapters: chapters = [Chapter.init() for _ in range(10)] return StoryChapters(chapters) @staticmethod def read(stream: core.Data) -> StoryChapters: total_chapters = 10 chapters_l = [ Chapter.read_selected_stage(stream) for _ in range(total_chapters) ] chapters = StoryChapters(chapters_l) for chapter in chapters.chapters: chapter.read_progress(stream) for chapter in chapters.chapters: chapter.read_clear_times(stream) for chapter in chapters.chapters: chapter.read_treasure(stream) return chapters def read_treasure_festival(self, stream: core.Data): for chapter in self.chapters: chapter.read_time_until_treasure_chance(stream) for chapter in self.chapters: chapter.read_treasure_chance_duration(stream) for chapter in self.chapters: chapter.read_treasure_chance_value(stream) for chapter in self.chapters: chapter.read_treasure_chance_stage_id(stream) for chapter in self.chapters: chapter.read_treasure_festival_type(stream) def write(self, stream: core.Data): for chapter in self.chapters: chapter.write_selected_stage(stream) for chapter in self.chapters: chapter.write_progress(stream) for chapter in self.chapters: chapter.write_clear_times(stream) for chapter in self.chapters: chapter.write_treasure(stream) def write_treasure_festival(self, stream: core.Data): for chapter in self.chapters: chapter.write_time_until_treasure_chance(stream) for chapter in self.chapters: chapter.write_treasure_chance_duration(stream) for chapter in self.chapters: chapter.write_treasure_chance_value(stream) for chapter in self.chapters: chapter.write_treasure_chance_stage_id(stream) for chapter in self.chapters: chapter.write_treasure_festival_type(stream) def read_itf_timed_scores(self, stream: core.Data): # 0: eoc 1 # 1: eoc 2 # 2: eoc 3 # 3: _ # 4: itf 1 # 5: itf 2 # 6: itf 3 # 7: cotc 1 # 8: cotc 2 # 9: cotc 3 for i, chapter in enumerate(self.chapters): if i > 3 and i < 7: chapter.read_itf_timed_scores(stream) def write_itf_timed_scores(self, stream: core.Data): for i, chapter in enumerate(self.chapters): if i > 3 and i < 7: chapter.write_itf_timed_scores(stream) def serialize(self) -> list[dict[str, Any]]: chapters = [chapter.serialize() for chapter in self.chapters] return chapters @staticmethod def deserialize(data: list[dict[str, Any]]) -> StoryChapters: chapters = StoryChapters([Chapter.deserialize(chapter) for chapter in data]) return chapters def __repr__(self): return f"Chapters({self.chapters})" def __str__(self): return f"Chapters({self.chapters})" @staticmethod def clear_tutorial(save_file: core.SaveFile): save_file.tutorial_state = max(save_file.tutorial_state, 1) save_file.koreaSuperiorTreasureState = max( save_file.koreaSuperiorTreasureState, 2 ) save_file.ui6 = max(save_file.ui6, 1) new_length = len(save_file.new_dialogs_2) if new_length < 6: save_file.new_dialogs_2.extend([0] * (6 - new_length)) save_file.new_dialogs_2[1] = max(save_file.new_dialogs_2[1], 2) save_file.new_dialogs_2[5] = max(save_file.new_dialogs_2[5], 2) if save_file.story.chapters[0].stages[0].clear_times == 0: save_file.story.clear_stage(0, 0) @staticmethod def get_chapter_names( save_file: core.SaveFile, chapter_ids: list[int] | None = None ) -> list[str] | None: if chapter_ids is None: chapter_ids = [0, 1, 2, 3, 4, 5, 6, 7, 8] chapter_names: list[str] = [] localizable = core.core_data.get_localizable(save_file) eoc_name = localizable.get("everyplay_mapname_J") itf_name = localizable.get("everyplay_mapname_W") cotc_name = localizable.get("everyplay_mapname_P") if eoc_name is None or itf_name is None or cotc_name is None: return None for chapter_id in chapter_ids: if chapter_id < 3: chapter_names.append(eoc_name.replace("%d", str(chapter_id + 1))) elif chapter_id < 6: chapter_names.append(itf_name.replace("%d", str(chapter_id - 2))) else: chapter_names.append(cotc_name.replace("%d", str(chapter_id - 5))) return chapter_names @staticmethod def select_story_chapters( save_file: core.SaveFile, chapters: list[int] | None = None ) -> list[int] | None: chapter_names = StoryChapters.get_chapter_names(save_file, chapters) if chapter_names is None: return None selected_chapters, _ = dialog_creator.ChoiceInput.from_reduced( chapter_names, dialog="select_story_chapters" ).multiple_choice(localized_options=False) return selected_chapters @staticmethod def get_selected_chapter_progress(max_stages: int = 48) -> int | None: progress = dialog_creator.IntInput( min=0, max=max_stages ).get_input_locale_while("edit_chapter_progress_all", {"max": max_stages}) if progress is None: return None return progress @staticmethod def edit_chapter_progress( save_file: core.SaveFile, chapter_id: int, chapter_name: str, clear_amount: int, clear_amount_choose: int, ) -> bool: max_stages = 48 chapter = save_file.story.get_real_chapters()[chapter_id] progress = dialog_creator.IntInput( min=0, max=max_stages ).get_input_locale_while( "edit_chapter_progress", {"max": max_stages, "chapter_name": chapter_name}, ) if progress is None: return False clear_amounts = [1] * progress if clear_amount_choose == 0: clear_amount2 = core.EventChapters.ask_clear_amount() if clear_amount2 is None: return False clear_amounts = [clear_amount2] * progress elif clear_amount_choose == 1: clear_amounts = [clear_amount] * progress elif clear_amount_choose == 2: for i in range(progress): StoryChapters.print_current_stage(save_file, chapter_id, i) clear_amount2 = core.EventChapters.ask_clear_amount() if clear_amount2 is None: return False clear_amounts[i] = clear_amount2 chapter.apply_progress(progress, clear_amounts) return progress != 0 @staticmethod def convert_stage_id(index: int) -> int: if index == 46: return 46 if index == 47: return 47 index = 45 - index return index @staticmethod def ask_clear_count() -> int | None: clear_count = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("stage_clear_count"), ).get_input_locale_while("edit_stage_clear_count", {}) return clear_count @staticmethod def ask_if_individual_clear_counts() -> bool | None: options = ["individual_clear_counts", "all_clear_counts"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="individual_clear_counts_dialog", single_choice=True ).single_choice() if choice is None: return None choice -= 1 return choice == 0 @staticmethod def edit_stage_clear_count( save_file: core.SaveFile, chapter_id: int, stage_id: int ): chapter = save_file.story.get_real_chapters()[chapter_id] stage = chapter.stages[stage_id] clear_count = StoryChapters.ask_clear_count() if clear_count is None: return stage.clear_times = clear_count def clear_previous_chapters(self, chapter_id: int): chapters = self.get_real_chapters() """ 0: eoc 1 1: eoc 2 - requires eoc 1 2: eoc 3 - requires eoc 1 + eoc 2 3: itf 1 - requires eoc 1 4: itf 2 - requires eoc 1 + itf 1 5: itf 3 - requires eoc 1 + itf 1 + itf 2 6: cotc 1 - requires eoc 1 + itf 1 7: cotc 2 - requires eoc 1 + itf 1 + cotc 1 8: cotc 3 - requires eoc 1 + itf 1 + cotc 1 + cotc 2 """ if chapter_id == 1: # eoc 2 chapters[0].clear_chapter() elif chapter_id == 2: # eoc 3 chapters[0].clear_chapter() chapters[1].clear_chapter() elif chapter_id == 3: # itf 1 chapters[0].clear_chapter() elif chapter_id == 4: # itf 2 chapters[0].clear_chapter() chapters[3].clear_chapter() elif chapter_id == 5: # itf 3 chapters[0].clear_chapter() chapters[3].clear_chapter() chapters[4].clear_chapter() elif chapter_id == 6: # cotc 1 chapters[0].clear_chapter() chapters[3].clear_chapter() elif chapter_id == 7: # cotc 2 chapters[0].clear_chapter() chapters[3].clear_chapter() chapters[6].clear_chapter() elif chapter_id == 8: # cotc 3 chapters[0].clear_chapter() chapters[3].clear_chapter() chapters[6].clear_chapter() chapters[7].clear_chapter() @staticmethod def print_current_chapter(save_file: core.SaveFile, chapter_id: int): chapter_names = StoryChapters.get_chapter_names(save_file) if chapter_names is None: return chapter_name = chapter_names[chapter_id] color.ColoredText.localize("current_chapter", chapter_name=chapter_name) @staticmethod def print_current_treasure_group( save_file: core.SaveFile, chapter_id: int, treasure_group_id: int ): chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id) treasure_group_names = TreasureGroupNames( save_file, chapter_type ).treasure_group_names if treasure_group_names is None: return treasure_group_name = treasure_group_names[treasure_group_id] color.ColoredText.localize( "current_treasure_group", treasure_group_name=treasure_group_name ) @staticmethod def clear_story(save_file: core.SaveFile): story = save_file.story story.edit_chapters( save_file, ) def edit_chapters(self, save_file: core.SaveFile): chapters = self.get_real_chapters() names = StoryChapters.get_chapter_names(save_file) if names is None: return map_choices = StoryChapters.select_story_chapters(save_file) if not map_choices: return clear_type_choice = dialog_creator.ChoiceInput.from_reduced( ["clear_whole_chapters", "clear_specific_stages"], dialog="select_clear_type", single_choice=True, ).single_choice() if clear_type_choice is None: return clear_type_choice -= 1 modify_clear_amounts = dialog_creator.YesNoInput().get_input_once( "modify_clear_amounts" ) if modify_clear_amounts is None: return clear_amount = 1 clear_amount_type = -1 if modify_clear_amounts: if len(map_choices) == 1: clear_amount_type = 0 else: options = ["clear_amount_chapter", "clear_amount_all"] if clear_type_choice == 1: options.append("clear_amount_stages") clear_amount_type = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_clear_amount_type", single_choice=True ).single_choice() if clear_amount_type is None: return clear_amount_type -= 1 if clear_amount_type == 1: clear_amount = core.EventChapters.ask_clear_amount() if clear_amount is None: return for id in map_choices: stage_names = StageNames( save_file, chapter=str(self.get_chapter_type_from_index(id)) ) stage_names = stage_names.stage_names if stage_names is None: return new_stage_names: list[str] = [] for i in range(48): index_stage_id = StoryChapters.convert_stage_id(i) new_stage_names.append(stage_names[index_stage_id]) stage_names = new_stage_names map_name = names[id] color.ColoredText.localize("current_sol_chapter", name=map_name, id=id) if clear_type_choice: stages = core.EventChapters.ask_stages_stage_names(stage_names) if stages is None: return else: stages = list(range(48)) if clear_amount_type == 0: clear_amount = core.EventChapters.ask_clear_amount() if clear_amount is None: return could_unclear_stages = False if chapters[id].progress > max(stages) + 1: could_unclear_stages = True for stage in range(max(stages) + 1, 48): if chapters[id].stages[stage].clear_times: could_unclear_stages = True if could_unclear_stages: unclear_other_stages = dialog_creator.YesNoInput().get_input_once( "unclear_other_stages" ) if unclear_other_stages is None: return else: unclear_other_stages = False if unclear_other_stages: chapters[id].progress = 0 for stage in range(max(stages), 48): chapters[id].stages[stage].clear_times = 0 for stage in stages: if clear_amount_type == 2: stage_name = stage_names[stage] color.ColoredText.localize( "current_sol_stage", name=stage_name, id=stage ) if clear_amount_type == 2: clear_amount = core.EventChapters.ask_clear_amount() if clear_amount is None: return self.clear_stage( id, stage, overwrite_clear_progress=True, clear_amount=clear_amount, chapters=chapters, ) color.ColoredText.localize("map_chapters_edited") @staticmethod def ask_treasure_level(save_file: core.SaveFile) -> int | None: treasure_text = core.core_data.get_treasure_text(save_file).treasure_text if treasure_text is None: return None if len(treasure_text) < 3: return None options = [ "no_treasure", treasure_text[0], treasure_text[1], treasure_text[2], "custom_treasure_level", ] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="treasure_level_dialog", single_choice=True ).single_choice() if choice is None: return None choice -= 1 max_treasure_level = core.core_data.max_value_manager.get("treasure_level") if choice == 4: treasure_level = dialog_creator.IntInput( min=0, max=max_treasure_level ).get_input_locale_while("custom_treasure_level_dialog", {}) if treasure_level is None: return None return treasure_level return choice @staticmethod def get_per_chapter(chapters: list[int]) -> int | None: if len(chapters) == 1: return 0 options = ["per_chapter", "all_selected_chapters"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="edit_per_chapter", single_choice=True ).single_choice() if choice is None: return None choice -= 1 return choice @staticmethod def edit_treasures_whole_chapters(save_file: core.SaveFile, chapters: list[int]): choice = StoryChapters.get_per_chapter(chapters) if choice is None: return if choice == 0: for chapter_id in chapters: StoryChapters.print_current_chapter(save_file, chapter_id) chapter = save_file.story.get_real_chapters()[chapter_id] treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return for stage in chapter.get_valid_treasure_stages(): stage.set_treasure(treasure_level) else: treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return for chapter_id in chapters: chapter = save_file.story.get_real_chapters()[chapter_id] for stage in chapter.get_valid_treasure_stages(): stage.set_treasure(treasure_level) @staticmethod def get_chapter_type_from_index(index: int) -> int: if index < 3: return 0 if index < 6: return 1 return 2 @staticmethod def select_stages(save_file: core.SaveFile, chapter_id: int) -> list[int] | None: options = ["select_stage_by_id", "select_stage_by_name"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_stage_dialog", single_choice=True ).single_choice() if choice is None: return None choice -= 1 if choice == 0: stage_ids = dialog_creator.RangeInput(48, 1).get_input_locale( "select_stage_id", {} ) if stage_ids is None: return None stage_ids = [stage_id - 1 for stage_id in stage_ids] return stage_ids chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id) stage_names = StageNames(save_file, str(chapter_type)).stage_names if not stage_names: return None new_stage_names: list[str] = [] for i in range(48): index_stage_id = StoryChapters.convert_stage_id(i) new_stage_names.append(stage_names[index_stage_id]) selected_stages, _ = dialog_creator.ChoiceInput.from_reduced( new_stage_names, dialog="select_stages_name" ).multiple_choice(localized_options=False) if not selected_stages: return None return selected_stages @staticmethod def edit_treasures_individual_stages(save_file: core.SaveFile, chapters: list[int]): choice = StoryChapters.get_per_chapter(chapters) if choice is None: return if choice == 0: for chapter_id in chapters: StoryChapters.print_current_chapter(save_file, chapter_id) chapter = save_file.story.get_real_chapters()[chapter_id] stage_ids = StoryChapters.select_stages(save_file, chapter_id) if stage_ids is None: return treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return for stage_id in stage_ids: real_stage_id = StoryChapters.convert_stage_id(stage_id) chapter.set_treasure(real_stage_id, treasure_level) else: stage_ids = StoryChapters.select_stages(save_file, 0) if stage_ids is None: return treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return for chapter_id in chapters: chapter = save_file.story.get_real_chapters()[chapter_id] for stage_id in stage_ids: real_stage_id = StoryChapters.convert_stage_id(stage_id) chapter.set_treasure(real_stage_id, treasure_level) @staticmethod def edit_treasures_groups(save_file: core.SaveFile, chapters: list[int]): for chapter_id in chapters: StoryChapters.print_current_chapter(save_file, chapter_id) chapter = save_file.story.get_real_chapters()[chapter_id] chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id) treasure_group_data = TreasureGroupData( save_file, chapter_type ).treasure_group_data treasure_group_names = TreasureGroupNames( save_file, chapter_type ).treasure_group_names if not treasure_group_data or not treasure_group_names: return treasure_group_names_new: list[str] = [] for i in range(len(treasure_group_data)): treasure_group_names_new.append(treasure_group_names[i]) selected_treasure_groups, _ = dialog_creator.ChoiceInput.from_reduced( treasure_group_names_new, dialog="select_treasure_groups" ).multiple_choice(localized_options=False) if not selected_treasure_groups: return options = ["group_individual", "group_all_at_once"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="select_treasure_groups_individual" ).single_choice() if choice is None: return choice -= 1 if choice == 0: for treasure_group_id in selected_treasure_groups: StoryChapters.print_current_treasure_group( save_file, chapter_id, treasure_group_id ) treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return treasure_group = treasure_group_data[treasure_group_id] for stage_id in treasure_group: chapter.set_treasure(stage_id, treasure_level) else: treasure_level = StoryChapters.ask_treasure_level(save_file) if treasure_level is None: return for treasure_group_id in selected_treasure_groups: treasure_group = treasure_group_data[treasure_group_id] for stage_id in treasure_group: chapter.set_treasure(stage_id, treasure_level) @staticmethod def edit_treasures(save_file: core.SaveFile): selected_chapters = StoryChapters.select_story_chapters(save_file) if not selected_chapters: return options = ["whole_chapters", "individual_stages", "treasure_groups"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="treasure_dialog", single_choice=True ).single_choice() if choice is None: return choice -= 1 if choice == 0: StoryChapters.edit_treasures_whole_chapters(save_file, selected_chapters) elif choice == 1: StoryChapters.edit_treasures_individual_stages(save_file, selected_chapters) elif choice == 2: StoryChapters.edit_treasures_groups(save_file, selected_chapters) color.ColoredText.localize("treasures_edited") @staticmethod def edit_itf_timed_scores(save_file: core.SaveFile): selected_chapters = StoryChapters.select_story_chapters( save_file, chapters=[3, 4, 5] ) if not selected_chapters: return options = ["whole_chapters", "individual_stages"] choice = dialog_creator.ChoiceInput.from_reduced( options, dialog="itf_timed_scores_dialog", single_choice=True ).single_choice() if choice is None: return choice -= 1 selected_chapters = [chapter_id + 3 for chapter_id in selected_chapters] if choice == 0: StoryChapters.edit_itf_timed_scores_whole_chapters( save_file, selected_chapters ) elif choice == 1: StoryChapters.edit_itf_timed_scores_individual_stages( save_file, selected_chapters ) color.ColoredText.localize("itf_timed_scores_edited") @staticmethod def edit_itf_timed_scores_whole_chapters( save_file: core.SaveFile, chapters: list[int] ): choice = StoryChapters.get_per_chapter(chapters) if choice is None: return if choice == 0: for chapter_id in chapters: print(chapter_id) StoryChapters.print_current_chapter(save_file, chapter_id) chapter = save_file.story.get_real_chapters()[chapter_id] score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return for stage in chapter.get_valid_treasure_stages(): stage.itf_timed_score = score else: score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return for chapter_id in chapters: chapter = save_file.story.get_real_chapters()[chapter_id] for stage in chapter.get_valid_treasure_stages(): stage.itf_timed_score = score @staticmethod def print_current_stage(save_file: core.SaveFile, chapter_id: int, stage_id: int): chapter_names = StoryChapters.get_chapter_names(save_file) if chapter_names is None: return chapter_name = chapter_names[chapter_id] chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id) stage_names = StageNames(save_file, str(chapter_type)).stage_names if stage_names is None: return stage_id = StoryChapters.convert_stage_id(stage_id) stage_name = stage_names[stage_id] color.ColoredText.localize( "current_stage", chapter_name=chapter_name, stage_name=stage_name ) @staticmethod def edit_itf_timed_scores_individual_stages( save_file: core.SaveFile, chapters: list[int] ): choice = StoryChapters.get_per_chapter(chapters) if choice is None: return options = ["individual_stages", "all_selected_stages"] choice2 = dialog_creator.ChoiceInput.from_reduced( options, dialog="itf_timed_scores_individual_dialog", single_choice=True, ).single_choice() if choice2 is None: return choice2 -= 1 if choice == 0: for chapter_id in chapters: StoryChapters.print_current_chapter(save_file, chapter_id) chapter = save_file.story.get_real_chapters()[chapter_id] stage_ids = StoryChapters.select_stages(save_file, chapter_id) if stage_ids is None: return if choice2 == 0: for stage_id in stage_ids: StoryChapters.print_current_stage( save_file, chapter_id, stage_id ) score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return chapter.stages[stage_id].itf_timed_score = score elif choice2 == 1: score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return for stage_id in stage_ids: chapter.stages[stage_id].itf_timed_score = score else: stage_ids = StoryChapters.select_stages(save_file, 3) if stage_ids is None: return if choice2 == 0: for stage_id in stage_ids: StoryChapters.print_current_stage(save_file, 3, stage_id) score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return for chapter_id in chapters: chapter = save_file.story.get_real_chapters()[chapter_id] chapter.stages[stage_id].itf_timed_score = score elif choice2 == 1: score = dialog_creator.IntInput( min=0, max=core.core_data.max_value_manager.get("itf_timed_score"), ).get_input_locale_while("itf_timed_score_dialog", {}) if score is None: return for chapter_id in chapters: chapter = save_file.story.get_real_chapters()[chapter_id] for stage_id in stage_ids: chapter.stages[stage_id].itf_timed_score = score class StageNames: def __init__(self, save_file: core.SaveFile, chapter: str, max_stages: int = 48): self.save_file = save_file self.chapter = chapter self.max_stages = max_stages self.stage_names = self.get_stage_names() def get_file_name(self) -> str: if self.chapter.isdigit(): return ( f"StageName{self.chapter}_{core.core_data.get_lang(self.save_file)}.csv" ) return f"StageName_{self.chapter}_{core.core_data.get_lang(self.save_file)}.csv" def get_stage_names(self) -> list[str] | None: file_name = self.get_file_name() gdg = core.core_data.get_game_data_getter(self.save_file) file = gdg.download("resLocal", file_name) if file is None: return None csv = core.CSV( file, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), ) stage_names: list[str] = [] if self.chapter.isdigit(): for row in csv: stage_names.append(row[0].to_str()) else: for row in csv: for value in row: stage_names.append(value.to_str()) return stage_names[: self.max_stages] def get_stage_name(self, stage_id: int) -> str | None: if self.stage_names is None: return None return self.stage_names[stage_id] class TreasureText: def __init__(self, save_file: core.SaveFile): self.save_file = save_file self.treasure_text = self.get_treasure_text() def get_tt_file_name(self) -> str: return f"Treasure2_{core.core_data.get_lang(self.save_file)}.csv" def get_treasure_text(self) -> list[str] | None: file_name = self.get_tt_file_name() gdg = core.core_data.get_game_data_getter(self.save_file) file = gdg.download("resLocal", file_name) if file is None: return None csv = core.CSV( file, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), ) treasure_text: list[str] = [] for row in csv: treasure_text.append(row[0].to_str()) return treasure_text class TreasureGroupData: def __init__(self, save_file: core.SaveFile, chapter_type: int): self.save_file = save_file self.chapter_type = chapter_type self.treasure_group_data = self.get_treasure_group_data() def get_tgd_file_name(self) -> str: if self.chapter_type == 0: return "treasureData0.csv" if self.chapter_type == 1: return "treasureData1.csv" if self.chapter_type == 2: return "treasureData2_0.csv" return "" def get_treasure_group_data(self) -> list[list[int]] | None: gdg = core.core_data.get_game_data_getter(self.save_file) file = gdg.download("DataLocal", self.get_tgd_file_name()) if file is None: return None csv = core.CSV(file) treasure_group_data: list[list[int]] = [] for row in csv.lines[11:22]: treasure_group_data.append( [value.to_int() for value in row if value.to_int() != -1] ) return treasure_group_data class TreasureGroupNames: def __init__(self, save_file: core.SaveFile, chapter_type: int): self.save_file = save_file self.chapter_type = chapter_type self.treasure_group_names = self.get_treasure_group_names() def get_tgn_file_name(self) -> str: lang = core.core_data.get_lang(self.save_file) if self.chapter_type == 0: return f"Treasure3_0_{lang}.csv" if self.chapter_type == 1: return f"Treasure3_1_{lang}.csv" if self.chapter_type == 2: return f"Treasure3_2_0_{lang}.csv" return "" def get_treasure_group_names(self) -> list[str] | None: gdg = core.core_data.get_game_data_getter(self.save_file) file = gdg.download("resLocal", self.get_tgn_file_name()) if file is None: return None csv = core.CSV( file, delimiter=core.Delimeter.from_country_code_res(self.save_file.cc), ) treasure_group_names: list[str] = [] for row in csv: treasure_group_names.append(row[0].to_str()) return treasure_group_names ================================================ FILE: src/bcsfe/core/game/map/timed_score.py ================================================ from __future__ import annotations from bcsfe import core class Stage: def __init__(self, score: int): self.score = score @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(stream: core.Data) -> Stage: return Stage(stream.read_int()) def write(self, stream: core.Data): stream.write_int(self.score) def serialize(self) -> int: return self.score @staticmethod def deserialize(data: int) -> Stage: return Stage(data) def __repr__(self) -> str: return f"Stage(score={self.score})" def __str__(self) -> str: return self.__repr__() class SubChapter: def __init__(self, stages: list[Stage]): self.stages = stages @staticmethod def init(total_stages: int) -> SubChapter: return SubChapter([Stage.init() for _ in range(total_stages)]) @staticmethod def read(stream: core.Data, total_stages: int) -> SubChapter: stages: list[Stage] = [] for _ in range(total_stages): stages.append(Stage.read(stream)) return SubChapter(stages) def write(self, stream: core.Data): for stage in self.stages: stage.write(stream) def serialize(self) -> list[int]: return [stage.serialize() for stage in self.stages] @staticmethod def deserialize(data: list[int]) -> SubChapter: return SubChapter([Stage.deserialize(stage) for stage in data]) def __repr__(self) -> str: return f"SubChapter(stages={self.stages})" def __str__(self) -> str: return self.__repr__() class SubChapterStars: def __init__(self, sub_chapters: list[SubChapter]): self.sub_chapters = sub_chapters @staticmethod def init(total_stages: int, total_stars: int) -> SubChapterStars: return SubChapterStars( [SubChapter.init(total_stages) for _ in range(total_stars)] ) @staticmethod def read( stream: core.Data, total_stages: int, total_stars: int, ) -> SubChapterStars: sub_chapters: list[SubChapter] = [] for _ in range(total_stars): sub_chapters.append(SubChapter.read(stream, total_stages)) return SubChapterStars(sub_chapters) def write(self, stream: core.Data): for sub_chapter in self.sub_chapters: sub_chapter.write(stream) def serialize(self) -> list[list[int]]: return [sub_chapter.serialize() for sub_chapter in self.sub_chapters] @staticmethod def deserialize(data: list[list[int]]) -> SubChapterStars: return SubChapterStars( [SubChapter.deserialize(sub_chapter) for sub_chapter in data] ) def __repr__(self) -> str: return f"SubChapterStars(sub_chapters={self.sub_chapters})" def __str__(self) -> str: return self.__repr__() class TimedScoreChapters: def __init__(self, sub_chapters: list[SubChapterStars]): self.sub_chapters = sub_chapters @staticmethod def init(gv: core.GameVersion) -> TimedScoreChapters: if gv < 20: return TimedScoreChapters([]) if gv <= 33: total_subchapters = 50 total_stages = 12 total_stars = 3 elif gv <= 34: total_subchapters = 0 total_stages = 12 total_stars = 3 else: total_subchapters = 0 total_stages = 0 total_stars = 0 return TimedScoreChapters( [ SubChapterStars.init(total_stages, total_stars) for _ in range(total_subchapters) ] ) @staticmethod def read(stream: core.Data, gv: core.GameVersion) -> TimedScoreChapters: if gv < 20: return TimedScoreChapters([]) if gv <= 33: total_subchapters = 50 total_stages = 12 total_stars = 3 elif gv <= 34: total_subchapters = stream.read_int() total_stages = 12 total_stars = 3 else: total_subchapters = stream.read_int() total_stages = stream.read_int() total_stars = stream.read_int() sub_chapters: list[SubChapterStars] = [] for _ in range(total_subchapters): sub_chapters.append( SubChapterStars.read(stream, total_stages, total_stars) ) return TimedScoreChapters(sub_chapters) def write(self, stream: core.Data, gv: core.GameVersion): if gv < 20: return if gv <= 33: pass elif gv <= 34: stream.write_int(len(self.sub_chapters)) else: stream.write_int(len(self.sub_chapters)) try: stream.write_int( len(self.sub_chapters[0].sub_chapters[0].stages) ) except IndexError: stream.write_int(0) try: stream.write_int(len(self.sub_chapters[0].sub_chapters)) except IndexError: stream.write_int(0) for sub_chapter in self.sub_chapters: sub_chapter.write(stream) def serialize(self) -> list[list[list[int]]]: return [sub_chapter.serialize() for sub_chapter in self.sub_chapters] @staticmethod def deserialize(data: list[list[list[int]]]) -> TimedScoreChapters: return TimedScoreChapters( [SubChapterStars.deserialize(sub_chapter) for sub_chapter in data] ) def __repr__(self) -> str: return f"Chapters(sub_chapters={self.sub_chapters})" def __str__(self) -> str: return self.__repr__() ================================================ FILE: src/bcsfe/core/game/map/tower.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class TowerChapters: def __init__(self, chapters: core.Chapters): self.chapters = chapters self.item_obtain_states: list[list[bool]] = [] @staticmethod def init() -> TowerChapters: return TowerChapters(core.Chapters.init()) @staticmethod def read(data: core.Data) -> TowerChapters: ch = core.Chapters.read(data) return TowerChapters(ch) def write(self, data: core.Data): self.chapters.write(data) def read_item_obtain_states(self, data: core.Data): total_stars = data.read_int() total_stages = data.read_int() self.item_obtain_states: list[list[bool]] = [] for _ in range(total_stars): self.item_obtain_states.append(data.read_bool_list(total_stages)) def write_item_obtain_states(self, data: core.Data): data.write_int(len(self.item_obtain_states)) try: data.write_int(len(self.item_obtain_states[0])) except IndexError: data.write_int(0) for item_obtain_state in self.item_obtain_states: data.write_bool_list(item_obtain_state, write_length=False) def serialize(self) -> dict[str, Any]: return { "chapters": self.chapters.serialize(), "item_obtain_states": self.item_obtain_states, } @staticmethod def deserialize(data: dict[str, Any]) -> TowerChapters: tower = TowerChapters( core.Chapters.deserialize(data.get("chapters", {})), ) tower.item_obtain_states = data.get("item_obtain_states", []) return tower def __repr__(self): return f"Tower({self.chapters}, {self.item_obtain_states})" def __str__(self): return self.__repr__() def get_total_stars(self, chapter_id: int) -> int: return len(self.chapters.chapters[chapter_id].chapters) def get_total_stages(self, chapter_id: int, star: int) -> int: return len(self.chapters.chapters[chapter_id].chapters[star].stages) @staticmethod def edit_towers(save_file: core.SaveFile): towers = save_file.tower towers.chapters.edit_chapters(save_file, "V", 7000) ================================================ FILE: src/bcsfe/core/game/map/uncanny.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator class UncannyChapters: def __init__(self, chapters: core.Chapters, unknown: list[int]): self.chapters = chapters self.unknown = unknown @staticmethod def init() -> UncannyChapters: return UncannyChapters(core.Chapters.init(), []) @staticmethod def read(data: core.Data) -> UncannyChapters: ch = core.Chapters.read(data, read_every_time=False) unknown = data.read_int_list(length=len(ch.chapters)) return UncannyChapters(ch, unknown) def write(self, data: core.Data): self.chapters.write(data, write_every_time=False) data.write_int_list(self.unknown, write_length=False) def serialize(self) -> dict[str, Any]: return { "chapters": self.chapters.serialize(), "unknown": self.unknown, } @staticmethod def deserialize(data: dict[str, Any]) -> UncannyChapters: return UncannyChapters( core.Chapters.deserialize(data.get("chapters", {})), data.get("unknown", []), ) def __repr__(self): return f"Uncanny({self.chapters}, {self.unknown})" def __str__(self): return self.__repr__() @staticmethod def edit_uncanny(save_file: core.SaveFile): uncanny = save_file.uncanny uncanny.chapters.edit_chapters(save_file, "NA", 13000) @staticmethod def edit_catamin_stages(save_file: core.SaveFile): choice = dialog_creator.ChoiceInput.from_reduced( ["change_clear_amount_catamin", "clear_unclear_stage_catamin"], dialog="catamin_stage_clear_q", ).single_choice() if choice is None: return None if choice == 1: names = core.MapNames(save_file, "B", base_index=14000) map_ids = core.EventChapters.select_map_names(names.map_names) if map_ids is None: return None if len(map_ids) >= 2: choice2 = dialog_creator.ChoiceInput.from_reduced( ["individual", "all_at_once"], dialog="catamin_clear_amounts_q" ).single_choice() if choice2 is None: return None else: choice2 = 1 if choice2 == 2: clear_amount = dialog_creator.IntInput().get_input( "enter_clear_amount_catamin", {} )[0] if clear_amount is None: return None for map_id in map_ids: save_file.event_stages.chapter_completion_count[14_000 + map_id] = ( clear_amount ) elif choice == 1: for map_id in map_ids: name = names.map_names.get(map_id) or core.localize("unknown_map") clear_amount = dialog_creator.IntInput().get_input( "enter_clear_amount_catamin_map", {"name": name, "id": map_id} )[0] if clear_amount is None: return None save_file.event_stages.chapter_completion_count[14_000 + map_id] = ( clear_amount ) color.ColoredText.localize("catamin_stage_success") elif choice == 2: completed_chapters = save_file.catamin_stages.chapters.edit_chapters( save_file, "B", 14000 ) if completed_chapters is None: return None # TODO: maybe in the future ask if the user wants to modify the chapter clear amounts ================================================ FILE: src/bcsfe/core/game/map/zero_legends.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe.cli import edits, color class Stage: def __init__(self, clear_times: int): self.clear_times = clear_times @staticmethod def init() -> Stage: return Stage(0) @staticmethod def read(data: core.Data) -> Stage: clear_times = data.read_short() return Stage(clear_times) def write(self, data: core.Data): data.write_short(self.clear_times) def serialize(self) -> int: return self.clear_times @staticmethod def deserialize(data: int) -> Stage: return Stage( data, ) def __repr__(self): return f"Stage({self.clear_times})" def __str__(self): return self.__repr__() def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False): if ensure_cleared_only: self.clear_times = self.clear_times or clear_amount else: self.clear_times = clear_amount def unclear_stage(self): self.clear_times = 0 class Chapter: def __init__( self, selected_stage: int, clear_progress: int, unlock_state: int, stages: list[Stage], ): self.selected_stage = selected_stage self.clear_progress = clear_progress self.unlock_state = unlock_state self.stages = stages self.total_stages = 0 def clear_stage( self, index: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: if overwrite_clear_progress: self.clear_progress = index + 1 else: self.clear_progress = max(self.clear_progress, index + 1) self.stages[index].clear_stage(clear_amount, ensure_cleared_only) self.chapter_unlock_state = 3 if index == self.total_stages - 1: return True return False def unclear_stage(self, index: int) -> bool: self.clear_progress = min(self.clear_progress, index) self.stages[index].unclear_stage() return True @staticmethod def init() -> Chapter: return Chapter(0, 0, 0, []) @staticmethod def read(data: core.Data) -> Chapter: selected_stage = data.read_byte() clear_progress = data.read_byte() unlock_state = data.read_byte() total_stages = data.read_short() stages = [Stage.read(data) for _ in range(total_stages)] return Chapter( selected_stage, clear_progress, unlock_state, stages, ) def write(self, data: core.Data): data.write_byte(self.selected_stage) data.write_byte(self.clear_progress) data.write_byte(self.unlock_state) data.write_short(len(self.stages)) for stage in self.stages: stage.write(data) def serialize(self) -> dict[str, Any]: return { "selected_stage": self.selected_stage, "clear_progress": self.clear_progress, "unlock_state": self.unlock_state, "stages": [stage.serialize() for stage in self.stages], } @staticmethod def deserialize(data: dict[str, Any]) -> Chapter: return Chapter( data.get("selected_stage", 0), data.get("clear_progress", 0), data.get("unlock_state", 0), [Stage.deserialize(stage) for stage in data.get("stages", [])], ) def __repr__(self): return f"Chapter({self.selected_stage}, {self.clear_progress}, {self.unlock_state}, {self.stages})" def __str__(self): return self.__repr__() class ChaptersStars: def __init__(self, unknown: int, chapters: list[Chapter]): self.unknown = unknown self.chapters = chapters def clear_stage( self, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: finished = self.chapters[star].clear_stage( stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished: if star + 1 < len(self.chapters): self.chapters[star + 1].chapter_unlock_state = 1 return finished def unclear_stage(self, star: int, stage: int) -> bool: finished = self.chapters[star].unclear_stage(stage) if finished and star + 1 < len(self.chapters): for chapter in self.chapters[star + 1 :]: chapter.chapter_unlock_state = 0 return finished @staticmethod def init() -> ChaptersStars: return ChaptersStars(0, []) @staticmethod def read(data: core.Data) -> ChaptersStars: unknown = data.read_byte() total_stars = data.read_byte() chapters = [Chapter.read(data) for _ in range(total_stars)] return ChaptersStars( unknown, chapters, ) def write(self, data: core.Data): data.write_byte(self.unknown) data.write_byte(len(self.chapters)) for chapter in self.chapters: chapter.write(data) def serialize(self) -> dict[str, Any]: return { "unknown": self.unknown, "chapters": [chapter.serialize() for chapter in self.chapters], } @staticmethod def deserialize(data: dict[str, Any]) -> ChaptersStars: return ChaptersStars( data.get("unknown", 0), [Chapter.deserialize(chapter) for chapter in data.get("chapters", [])], ) def __repr__(self): return f"ChaptersStars({self.unknown}, {self.chapters})" def __str__(self): return self.__repr__() class ZeroLegendsChapters: def __init__(self, chapters: list[ChaptersStars]): self.chapters = chapters def clear_stage( self, map: int, star: int, stage: int, clear_amount: int = 1, overwrite_clear_progress: bool = False, ensure_cleared_only: bool = False, ) -> bool: self.create(map) finished = self.chapters[map].clear_stage( star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only ) if finished and map + 1 < len(self.chapters): self.chapters[map + 1].chapters[0].chapter_unlock_state = 1 return finished def unclear_stage(self, map: int, star: int, stage: int) -> bool: self.create(map) finished = self.chapters[map].unclear_stage(star, stage) if finished and map + 1 < len(self.chapters) and star == 0: for chapter in self.chapters[map + 1].chapters: chapter.chapter_unlock_state = 0 return finished @staticmethod def init() -> ZeroLegendsChapters: return ZeroLegendsChapters([]) @staticmethod def read(data: core.Data) -> ZeroLegendsChapters: total_chapters = data.read_short() chapters = [ChaptersStars.read(data) for _ in range(total_chapters)] return ZeroLegendsChapters( chapters, ) def write(self, data: core.Data): data.write_short(len(self.chapters)) for chapter in self.chapters: chapter.write(data) def serialize(self) -> list[dict[str, Any]]: return [chapter.serialize() for chapter in self.chapters] @staticmethod def deserialize(data: list[dict[str, Any]]) -> ZeroLegendsChapters: return ZeroLegendsChapters( [ChaptersStars.deserialize(chapter) for chapter in data], ) def __repr__(self): return f"Chapters({self.chapters})" def __str__(self): return self.__repr__() def get_total_stars(self, chapter_id: int) -> int: return len(self.chapters[chapter_id].chapters) def get_total_stages(self, chapter_id: int, star: int) -> int: return len(self.chapters[chapter_id].chapters[star].stages) def create(self, chapter_id: int): diff = chapter_id - len(self.chapters) if diff >= 0: for _ in range(diff + 1): stages = [Stage(0)] * self.get_total_stages(0, 0) chapters = [Chapter(0, 0, 0, stages)] * self.get_total_stars(0) chapters_stars = ChaptersStars(0, chapters) self.chapters.append(chapters_stars) @staticmethod def edit_zero_legends(save_file: core.SaveFile): color.ColoredText.localize("zero_legends_warning") zero_legends_chapters = save_file.zero_legends zero_legends_chapters.edit_chapters(save_file, "ND", base_index=34000) @staticmethod def edit_catclaw_championships(save_file: core.SaveFile): zero_legends_chapters = save_file.dojo_chapters zero_legends_chapters.edit_chapters(save_file, "G", 37000, True) def edit_chapters( self, save_file: core.SaveFile, letter_code: str, base_index: int, no_r_prefix: bool = False, ): edits.map.edit_chapters( save_file, self, letter_code, no_r_prefix=no_r_prefix, base_index=base_index ) def unclear_rest(self, stages: list[int], stars: int, id: int): if not stages: return for star in range(stars, self.get_total_stars(id)): for stage in range(max(stages), self.get_total_stages(id, star)): self.chapters[id].chapters[star].stages[stage].clear_times = 0 self.chapters[id].chapters[star].clear_progress = 0 def set_total_stages(self, map: int, total_stages: int): self.create(map) for chapter in self.chapters[map].chapters: chapter.total_stages = total_stages ================================================ FILE: src/bcsfe/core/game_version.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class GameVersion: """A class to represent a game version.""" def __init__(self, game_version: int): """Initializes a new instance of the GameVersion class. Args: game_version (int): Game version as an integer. e.g 120102 for 12.1.2 """ self.game_version = game_version def to_string(self) -> str: """Converts the game version to a string. Returns: str: Game version as a string. e.g 12.1.2 """ split_gv = str(self.game_version).zfill(6) split_gv = [ str(int(split_gv[i : i + 2])) for i in range(0, len(split_gv), 2) ] return ".".join(split_gv) def get_parts_zfill(self) -> list[str]: """Gets the parts of the game version as a list of strings with leading zeros. Returns: list[str]: Game version parts as strings with leading zeros. e.g ["12", "01", "02"] """ return [part.zfill(2) for part in self.to_string().split(".")] def get_parts(self) -> list[int]: """Gets the parts of the game version as a list of integers. Returns: list[int]: Game version parts as integers. e.g [12, 1, 2] """ return [int(part) for part in self.get_parts_zfill()] def format(self) -> str: """Formats the game version as a string with leading zeros. Returns: str: Game version as a string with leading zeros. e.g 12.01.02 """ parts = self.get_parts_zfill() string = "" for part in parts: string += f"{part}." return f"{string[:-1]}" def __str__(self) -> str: """Converts the game version to a string. Returns: str: Game version as a string. e.g 12.1.2 """ return self.to_string() def __repr__(self) -> str: """Converts the game version object to a string. Returns: str: Game version object as a string. e.g game_version(120102) 12.1.2 """ return f"game_version({self.game_version}) {self.to_string()}" @staticmethod def read(data: core.Data) -> GameVersion: """Reads a 4 byte int from a Data object. Args: data (core.Data): Data object to read from. Returns: GameVersion: Game version read from the Data object. """ return GameVersion(data.read_int()) def write(self, data: core.Data): """Writes the 4 byte game version to a Data object. Args: data (core.Data): Data object to write to. """ data.write_int(self.game_version) def serialize(self) -> dict[str, Any]: """Serializes the game version to a dictionary. Returns: dict[str, Any]: Serialized game version. """ return {"game_version": self.game_version} @staticmethod def deserialize(game_version: dict[str, Any]) -> GameVersion: """Deserializes a game version from a dictionary. Args: game_version (dict[str, Any]): Serialized game version. Returns: GameVersion: Deserialized game version. """ return GameVersion(game_version["game_version"]) @staticmethod def from_string(game_version: str) -> GameVersion: """Converts a string to a GameVersion object. Args: game_version (str): Game version as a string. e.g 12.1.2 Returns: GameVersion: Game version as a GameVersion object. """ split_gv = game_version.split(".") if len(split_gv) == 2: split_gv.append("0") final = "" for split in split_gv: final += split.zfill(2) return GameVersion(int(final)) def __eq__(self, other: Any) -> bool: """Checks if the game version is equal to another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is equal to the other object, False otherwise. """ if isinstance(other, GameVersion): return self.game_version == other.game_version elif isinstance(other, int): return self.game_version == other elif isinstance(other, str): return ( self.game_version == GameVersion.from_string(other).game_version ) else: return False def __ne__(self, other: Any) -> bool: """Checks if the game version is not equal to another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is not equal to the other object, False otherwise. """ return not self.__eq__(other) def __lt__(self, other: Any) -> bool: """Checks if the game version is less than another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is less than the other object, False otherwise. """ if isinstance(other, GameVersion): return self.game_version < other.game_version elif isinstance(other, int): return self.game_version < other elif isinstance(other, str): return ( self.game_version < GameVersion.from_string(other).game_version ) else: return False def __le__(self, other: Any) -> bool: """Checks if the game version is less than or equal to another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is less than or equal to the other object, False otherwise. """ return self.__lt__(other) or self.__eq__(other) def __gt__(self, other: Any) -> bool: """Checks if the game version is greater than another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is greater than the other object, False otherwise. """ return not self.__le__(other) def __ge__(self, other: Any) -> bool: """Checks if the game version is greater than or equal to another object. Args: other (Any): Object to compare to. Returns: bool: True if the game version is greater than or equal to the other object, False otherwise. """ return not self.__lt__(other) def __add__(self, other: Any) -> GameVersion: """Adds the game version to another object. Args: other (Any): Object to add to. Returns: GameVersion: Game version added to the other object. """ if isinstance(other, GameVersion): return GameVersion(self.game_version + other.game_version) elif isinstance(other, int): return GameVersion(self.game_version + other) elif isinstance(other, str): return GameVersion( self.game_version + GameVersion.from_string(other).game_version ) else: return NotImplemented def __sub__(self, other: Any) -> GameVersion: """Subtracts the game version from another object. Args: other (Any): Object to subtract from. Returns: GameVersion: Game version subtracted from the other object. """ return self.__add__(-other) ================================================ FILE: src/bcsfe/core/io/__init__.py ================================================ from bcsfe.core.io import ( bc_csv, path, data, command, yaml, config, json_file, save, thread_helper, root_handler, adb_handler, git_handler, waydroid, ) __all__ = [ "bc_csv", "path", "data", "command", "yaml", "config", "json_file", "save", "thread_helper", "root_handler", "adb_handler", "git_handler", "waydroid", ] ================================================ FILE: src/bcsfe/core/io/adb_handler.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.core import io from bcsfe.cli import dialog_creator, color class DeviceIDNotSet(Exception): pass class AdbNotInstalled(Exception): def __init__(self, result: core.CommandResult): self.result = result class AdbHandler(io.root_handler.RootHandler): def __init__(self, root: bool = True): self.root_avail = root adb_path = core.Path(core.core_data.config.get_str(core.ConfigKey.ADB_PATH)) self.check_adb_installed(adb_path) self.adb_path = adb_path self.start_server() self.device_id = None self.package_name = None self.root_result = None @staticmethod def display_no_adb_error(e: AdbNotInstalled): color.ColoredText.localize( "adb_not_installed", path=core.core_data.config.get_str(core.ConfigKey.ADB_PATH), error=e, ) def check_adb_installed(self, path: core.Path): result = path.run("version") if not result.success: raise AdbNotInstalled(result) def adb_root_success(self) -> bool: if self.root_result is None: return False result = self.root_result.result.strip() return ( result != "adbd cannot run as root in production builds" and result != "not available in Waydroid" ) def start_server(self) -> core.CommandResult: return self.adb_path.run("start-server") def kill_server(self) -> core.CommandResult: return self.adb_path.run("kill-server") def root(self) -> core.CommandResult: return self.adb_path.run(f"-s {self.get_device()} root") def get_connected_devices(self) -> list[str]: devices = self.adb_path.run("devices").result.split("\n") devices = [device.split("\t")[0] for device in devices[1:-2]] return devices def set_device(self, device_id: str): self.device_id = device_id if self.root_avail: self.root_result = self.root() def get_device(self) -> str: if self.device_id is None: raise DeviceIDNotSet("Device ID is not set") return self.device_id def get_device_name(self) -> str: return self.run_shell("getprop ro.product.model").result.strip() def run_shell(self, command: str) -> core.CommandResult: return self.adb_path.run(f'-s {self.get_device()} shell "{command}"') def run_root_shell(self, command: str) -> core.CommandResult: return self.run_shell(f"su -c '{command}'") def adb_pull_file( self, device_path: core.Path, local_path: core.Path ) -> core.CommandResult: return self.adb_path.run( f'-s {self.get_device()} pull "{device_path.to_str_forwards()}" "{local_path}"', ) def pull_file( self, device_path: core.Path, local_path: core.Path ) -> core.CommandResult: if not self.adb_root_success(): result = self.run_root_shell( f"cp {device_path.to_str_forwards()} /sdcard/{device_path.basename()} && chmod o+rw /sdcard/{device_path.basename()}" ) if result.exit_code != 0: return result device_path = core.Path("/sdcard/").add(device_path.basename()) result = self.adb_pull_file(device_path, local_path) if not result.success: return result if not self.adb_root_success(): result2 = self.run_shell(f"rm /sdcard/{device_path.basename()}") if result2.exit_code != 0: return result2 return result def adb_push_file( self, local_path: core.Path, device_path: core.Path ) -> core.CommandResult: return self.adb_path.run( f'-s {self.get_device()} push "{local_path}" "{device_path.to_str_forwards()}"' ) def push_file( self, local_path: core.Path, device_path: core.Path ) -> core.CommandResult: orignal_device_path = device_path.copy_object() if not self.adb_root_success(): device_path = core.Path("/sdcard/").add(device_path.basename()) result = self.adb_push_file(local_path, device_path) if not result.success: return result if not self.adb_root_success(): result2 = self.run_root_shell( f"cp '/sdcard/{device_path.basename()}' '{orignal_device_path.to_str_forwards()}' && chmod o+rw '{orignal_device_path.to_str_forwards()}'" ) result3 = self.run_shell(f"rm '/sdcard/{device_path.basename()}'") if result2.exit_code != 0: return result2 if result3.exit_code != 0: return result3 return result def stat_file(self, device_path: core.Path) -> core.CommandResult: return self.run_shell(f"stat {device_path.to_str_forwards()}") def does_file_exist(self, device_path: core.Path) -> bool: return self.stat_file(device_path).success def get_battlecats_packages(self) -> list[str]: cmd = "find /data/data/ -name SAVE_DATA -mindepth 3 -maxdepth 3" result = self.run_root_shell(cmd) if not result.success: return [] packages: list[str] = [] for package in result.result.split("\n"): parts = package.split("/") if len(parts) < 4: continue packages.append(package.split("/")[3]) return packages def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult: result = self.pull_file(self.get_battlecats_save_path(), local_path) return result def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult: return self.push_file(local_path, self.get_battlecats_save_path()) def close_game(self) -> core.CommandResult: return self.run_shell(f"am force-stop {self.get_package_name()}") def run_game(self) -> core.CommandResult: return self.run_shell(f"monkey --pct-syskeys 0 -p {self.get_package_name()} 1") def select_device(self) -> bool: devices = self.get_connected_devices() device = dialog_creator.ChoiceInput.from_reduced( devices, dialog="select_device", single_choice=True ).single_choice() if not device: color.ColoredText.localize("no_device_error") return False self.set_device(devices[device - 1]) return True ================================================ FILE: src/bcsfe/core/io/bc_csv.py ================================================ from __future__ import annotations import csv as csv_module import enum from typing import Any import typing from bcsfe import core class DelimeterType(enum.Enum): COMMA = "," TAB = "\t" PIPE = "|" class Delimeter: def __init__(self, de: DelimeterType | str): if isinstance(de, str): self.delimiter = DelimeterType(de) else: self.delimiter = de @staticmethod def from_country_code_res(cc: core.CountryCode) -> Delimeter: if cc.get_cc_lang() == core.CountryCodeType.JP: return Delimeter(DelimeterType.COMMA) return Delimeter(DelimeterType.PIPE) def __str__(self) -> str: return self.delimiter.value class Cell: def __init__(self, dt: core.Data): self.data = dt def to_str(self) -> str: return self.data.to_str() def to_int(self) -> int: return self.data.to_int() def to_bool(self) -> bool: return self.data.to_bool() def __repr__(self) -> str: return f"Cell({self.data})" def __str__(self) -> str: return self.data.to_str() class Row: def __init__(self, cells: list[Cell]): self.cells = cells self.index = 0 @typing.overload def __getitem__(self, index: int) -> Cell: ... @typing.overload def __getitem__(self, index: slice) -> Row: ... def __getitem__(self, index: int | slice) -> Cell | Row: if isinstance(index, int): try: return self.cells[index] except IndexError: return Cell(core.Data("")) try: return Row(self.cells[index]) except IndexError: return Row([]) def __len__(self) -> int: return len(self.cells) @staticmethod def from_list(dt: list[core.Data]) -> Row: cells: list[Cell] = [] for item in dt: cells.append(Cell(item)) return Row(cells) def __repr__(self) -> str: return f"Row({self.cells})" def __str__(self) -> str: return self.__repr__() def __iter__(self): self.index = 0 return iter(self.cells) def __next__(self): if self.index >= len(self.cells): raise StopIteration else: self.index += 1 return self.cells[self.index - 1] def next(self): return next(self) def next_opt(self) -> Cell | None: if self.done(): return None return self.next() def done(self): return self.index >= len(self.cells) def next_int(self) -> int: return self.next().to_int() def next_str(self) -> str: return self.next().to_str() def next_bool(self) -> bool: return self.next().to_bool() def next_int_opt(self) -> int | None: val = self.next_opt() if val is None: return None return val.to_int() def next_str_opt(self) -> str | None: val = self.next_opt() if val is None: return None return val.to_str() def next_bool_opt(self) -> bool | None: val = self.next_opt() if val is None: return None return val.to_bool() def to_str_list(self) -> list[str]: return [cell.to_str() for cell in self.cells] def to_int_list(self) -> list[int]: return [cell.to_int() for cell in self.cells] class CSV: def __init__( self, file_data: core.Data, delimiter: Delimeter | str = Delimeter(DelimeterType.COMMA), remove_padding: bool = False, remove_comments: bool = True, remove_empty: bool = True, ): self.file_data = file_data if remove_padding: data = self.file_data.unpad_pkcs7() if data is None: self.file_data = self.file_data else: self.file_data = data self.delimiter = delimiter self.remove_comments = remove_comments self.remove_empty = remove_empty self.index = 0 self.parse() def parse(self): reader = csv_module.reader( self.file_data.data.decode("utf-8").splitlines(), delimiter=str(self.delimiter), ) self.lines: list[Row] = [] for row in reader: new_row: list[core.Data] = [] full_row = f"{str(self.delimiter)}".join(row) if self.remove_comments: full_row = full_row.split("//")[0] row = full_row.split(str(self.delimiter)) if row or not self.remove_empty: for item in row: item = item.strip() if item or not self.remove_empty: new_row.append(core.Data(item)) if new_row or not self.remove_empty: self.lines.append(Row.from_list(new_row)) def get_row(self, index: int) -> Row: try: return self.lines[index] except IndexError: return Row([]) def __getitem__(self, index: int) -> Row: return self.get_row(index) def __len__(self) -> int: return len(self.lines) @staticmethod def from_file( pt: core.Path, delimiter: Delimeter = Delimeter(DelimeterType.COMMA) ) -> CSV: return CSV(pt.read(), delimiter) def add_line(self, line: list[Any] | Any): if not isinstance(line, list): line = [line] new_line: list[core.Data] = [] for item in line: new_line.append(core.Data(str(item))) self.lines.append(Row.from_list(new_line)) def set_line(self, index: int, line: list[Any]): new_line: list[core.Data] = [] for item in line: new_line.append(core.Data(item)) try: self.lines[index] = Row.from_list(new_line) except IndexError: self.lines.append(Row.from_list(new_line)) def to_data(self) -> core.Data: csv: list[str] = [] for line in self.lines: for i, item in enumerate(line): csv.append(str(item)) if i != len(line) - 1: csv.append(str(self.delimiter)) csv.append("\r\n") return core.Data("".join(csv)) def read_line(self) -> Row | None: try: line = self.lines[self.index] except IndexError: return None self.index += 1 return line def reset_index(self): self.index = 0 def has_line(self) -> bool: return self.index < len(self.lines) def __iter__(self): return self def __next__(self) -> Row: line = self.read_line() if line is None: raise StopIteration return line def extend(self, length: int, sub_length: int = 0): for _ in range(length): if sub_length == 0: self.lines.append(Row.from_list([])) else: self.lines.append(Row.from_list([core.Data("")] * sub_length)) ================================================ FILE: src/bcsfe/core/io/command.py ================================================ from __future__ import annotations import subprocess import threading class CommandResult: def __init__(self, result: str, exit_code: int): self.result = result self.exit_code = exit_code def __str__(self) -> str: return self.result def __repr__(self) -> str: return f"Result({self.result!r}, {self.exit_code!r})" @property def success(self) -> bool: return self.exit_code == 0 @staticmethod def create_success(result: str = "") -> CommandResult: return CommandResult(result, 0) @staticmethod def create_failure(result: str = "") -> CommandResult: return CommandResult(result, 1) class Command: def __init__(self, command: str, display_output: bool = True): self.command = command self.display_output = display_output def run(self, inputData: str = "\n") -> CommandResult: self.process = subprocess.Popen( self.command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, shell=True, universal_newlines=True, ) output, _ = self.process.communicate(inputData) return_code = self.process.wait() return CommandResult(output, return_code) def run_in_thread(self, inputData: str = "\n") -> None: self.thread = threading.Thread(target=self.run, args=(inputData,)) self.thread.start() ================================================ FILE: src/bcsfe/core/io/config.py ================================================ from __future__ import annotations import enum from typing import Any from bcsfe import core from bcsfe.cli import color, dialog_creator import requests class ConfigKey(enum.Enum): UPDATE_TO_BETA = "update_to_beta" SHOW_UPDATE_MESSAGE = "show_update_message" LOCALE = "locale" SHOW_MISSING_LOCALE_KEYS = "show_missing_locale_keys" DISABLE_MAXES = "disable_maxes" MAX_BACKUPS = "max_backups" THEME = "theme" RESET_CAT_DATA = "reset_cat_data" SET_CAT_CURRENT_FORMS = "set_cat_current_forms" STRICT_UPGRADE = "strict_upgrade" SEPARATE_CAT_EDIT_OPTIONS = "separate_cat_edit_options" STRICT_BAN_PREVENTION = "strict_ban_prevention" MAX_REQUEST_TIMEOUT = "max_request_timeout" GAME_DATA_REPO = "game_data_repo" FORCE_LANG_GAME_DATA = "force_lang_game_data" CLEAR_TUTORIAL_ON_LOAD = "clear_tutorial_on_load" REMOVE_BAN_MESSAGE_ON_LOAD = "remove_ban_message_on_load" UNLOCK_CAT_ON_EDIT = "unlock_cat_on_edit" USE_FILE_DIALOG = "use_file_dialog" ADB_PATH = "adb_path" IGNORE_PARSE_ERROR = "ignore_parse_error" USE_WAYDROID = "use_waydroid" USE_PKEXEC_WAYDROID = "use_pkexec_waydroid" class Config: def __init__(self, path: core.Path | None, print_yaml_err: bool = True): if path is None: path = Config.get_config_path() config = core.YamlFile(path, print_yaml_err) self.config: dict[ConfigKey, Any] = {} for key, value in config.yaml.items(): try: self.config[ConfigKey(key)] = value except ValueError: pass self.config_object = config self.initialize_config() @staticmethod def get_config_path() -> core.Path: return core.Path.get_config_folder().add("config.yaml") def __getitem__(self, key: ConfigKey) -> Any: return self.config[key] def __setitem__(self, key: ConfigKey, value: Any) -> None: self.config[key] = value def __contains__(self, key: ConfigKey) -> bool: return key in self.config @staticmethod def get_defaults() -> dict[ConfigKey, Any]: initial_values = { ConfigKey.UPDATE_TO_BETA: False, ConfigKey.SHOW_UPDATE_MESSAGE: True, ConfigKey.LOCALE: "en", ConfigKey.SHOW_MISSING_LOCALE_KEYS: False, ConfigKey.DISABLE_MAXES: False, ConfigKey.MAX_BACKUPS: 50, ConfigKey.THEME: "default", ConfigKey.RESET_CAT_DATA: True, ConfigKey.SET_CAT_CURRENT_FORMS: True, ConfigKey.STRICT_UPGRADE: False, ConfigKey.SEPARATE_CAT_EDIT_OPTIONS: True, ConfigKey.STRICT_BAN_PREVENTION: False, ConfigKey.MAX_REQUEST_TIMEOUT: 30, ConfigKey.GAME_DATA_REPO: "https://git.battlecatsmodding.org/fieryhenry/BCData/raw/branch/main/metadata.json", ConfigKey.FORCE_LANG_GAME_DATA: False, ConfigKey.CLEAR_TUTORIAL_ON_LOAD: True, ConfigKey.REMOVE_BAN_MESSAGE_ON_LOAD: True, ConfigKey.UNLOCK_CAT_ON_EDIT: True, ConfigKey.USE_FILE_DIALOG: True, ConfigKey.ADB_PATH: "adb", ConfigKey.IGNORE_PARSE_ERROR: False, ConfigKey.USE_WAYDROID: False, ConfigKey.USE_PKEXEC_WAYDROID: True, } return initial_values def get_default(self, key: ConfigKey) -> Any: value = Config.get_defaults()[key] return value def set_default(self, key: ConfigKey): value = self.get_default(key) self.config[key] = value self.save() return value def initialize_config(self): initial_values = Config.get_defaults() for key, value in initial_values.items(): if key not in self.config: self.config[key] = value self.save() def save(self): for key, value in self.config.items(): self.config_object.yaml[key.value] = value self.config_object.save() def get(self, key: ConfigKey) -> Any: value = self.config[key] if value is None: return self.set_default(key) return value def get_game_data_repo(self, fix_old_repo: bool = True) -> str: if fix_old_repo and self.get_str(ConfigKey.GAME_DATA_REPO) in [ "https://raw.githubusercontent.com/fieryhenry/BCData/master/", "https://git.fyhenry.uk/henry/BCData/raw/branch/main/info.json", ]: self.set( ConfigKey.GAME_DATA_REPO, self.get_default(ConfigKey.GAME_DATA_REPO) ) return self.get_str(ConfigKey.GAME_DATA_REPO) def get_str(self, key: ConfigKey) -> str: value = self.get(key) if not isinstance(value, str): return self.set_default(key) return value def get_bool(self, key: ConfigKey) -> bool: value = self.get(key) if not isinstance(value, bool): return self.set_default(key) return value def get_int(self, key: ConfigKey) -> int: value = self.get(key) if not isinstance(value, int): return self.set_default(key) return value def reset(self): self.config.clear() self.config_object.remove() self.initialize_config() def set(self, key: ConfigKey, value: Any): self.config[key] = value self.save() def get_bool_text(self, value: bool) -> str: if value: return core.core_data.local_manager.get_key("enabled") return core.core_data.local_manager.get_key("disabled") def get_full_input_localized( self, key: ConfigKey, current_value: str, default_value: str ) -> str: return core.core_data.local_manager.get_key( "config_full", key_desc=core.core_data.local_manager.get_key( Config.get_desc_key(key), current_value=current_value, default_value=default_value, escape=False, ), escape=False, ) def edit_bool(self, key: ConfigKey): value = self.get_bool(key) color.ColoredText( self.get_full_input_localized( key, self.get_bool_text(value), self.get_bool_text(self.get_default(key)), ), ) choice = dialog_creator.ChoiceInput( ["enable", "disable"], ["enable", "disable"], [], {}, "enable_disable_dialog", True, ).single_choice() if choice is None: return choice -= 1 if choice == 0: value = True elif choice == 1: value = False self.set(key, value) print() color.ColoredText.localize( "config_success", ) @staticmethod def get_desc_key(key: ConfigKey) -> str: return key.value + "_desc" def edit_int(self, key: ConfigKey): text = self.get_full_input_localized( key, str(self.get_int(key)), str(self.get_default(key)) ) color.ColoredText.localize(text) value = dialog_creator.SingleEditor( key.value, self.get_int(key), signed=False, localized_item=True ).edit() self.set(key, value) color.ColoredText.localize( "config_success", ) def edit_game_data_repo(self): text = self.get_full_input_localized( ConfigKey.GAME_DATA_REPO, self.get_str(ConfigKey.GAME_DATA_REPO), self.get_default(ConfigKey.GAME_DATA_REPO), ) color.ColoredText.localize(text) value = dialog_creator.StringInput().get_input_locale( "game_data_repo_dialog", {} ) if value is None: value = self.get_default(ConfigKey.GAME_DATA_REPO) color.ColoredText.localize("validating_game_repo") try: resp = core.RequestHandler(value).get() except (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema): color.ColoredText.localize("invalid_url") return if resp is None: color.ColoredText.localize("no_internet_or_connection_error") return if resp.status_code != 200: color.ColoredText.localize( "invalid_response", response_code=resp.status_code ) return self.set(ConfigKey.GAME_DATA_REPO, value) color.ColoredText.localize( "config_success", ) def edit_str(self, key: ConfigKey): text = self.get_full_input_localized( key, self.get_str(key), self.get_default(key), ) color.ColoredText.localize(text) str_val = core.core_data.local_manager.get_key(key.value) value = dialog_creator.StringInput().get_input_locale( "string_config_dialog", {"val": str_val} ) if value is None: return self.set(key, value) color.ColoredText.localize("config_success") def edit_locale(self): text = self.get_full_input_localized( ConfigKey.LOCALE, self.get_str(ConfigKey.LOCALE), self.get_default(ConfigKey.LOCALE), ) color.ColoredText.localize(text) all_locales = core.LocalManager.get_all_locales() options = all_locales.copy() + ["add_locale", "remove_locale"] value = dialog_creator.ChoiceInput.from_reduced( options, dialog="locale_dialog", single_choice=True, ).single_choice() if value is None: return value -= 1 if value == len(all_locales) + 1: # remove_locale options: list[str] = [] for locale in all_locales: if locale.startswith("ext-"): options.append(locale) if not options: color.ColoredText.localize( "no_external_locales", ) return options.append("cancel") choices, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="locale_remove_dialog" ).multiple_choice() if choices is None: return for choice in choices: if choice == len(options) - 1: return core.LocalManager.remove_locale(options[choice]) color.ColoredText.localize( "locale_removed", locale_name=options[choice], ) return elif value == len(all_locales): # add_locale if not core.GitHandler.is_git_installed(): color.ColoredText.localize( "git_not_installed", ) return git_repo = color.ColoredInput().localize("enter_locale_git_repo").strip() external_locale = core.ExternalLocale.from_git_repo(git_repo) if external_locale is None: color.ColoredText.localize( "invalid_git_repo", ) return locale_name = external_locale.get_full_name() if locale_name in all_locales: if not dialog_creator.YesNoInput().get_input_once( "locale_already_exists", {"locale_name": locale_name}, ): color.ColoredText.localize( "locale_cancelled", ) return external_locale.save() value = locale_name color.ColoredText.localize( "locale_added", ) else: value = all_locales[value] self.set(ConfigKey.LOCALE, value) color.ColoredText.localize( "locale_changed", locale_name=value, ) color.ColoredText.localize( "config_success", ) def edit_theme(self): themes = core.ThemeHandler.get_all_themes() current_theme = self.get_str(ConfigKey.THEME) if current_theme not in themes: current_theme = "default" text = self.get_full_input_localized( ConfigKey.THEME, current_theme, self.get_default(ConfigKey.THEME), ) color.ColoredText.localize(text) options = themes.copy() + ["add_theme", "remove_theme"] value = dialog_creator.ChoiceInput.from_reduced( options, dialog="theme_dialog", single_choice=True, ).single_choice() if value is None: return value -= 1 if value == len(themes) + 1: # remove_theme options: list[str] = [] for theme in themes: if theme.startswith("ext-"): options.append(theme) if not options: color.ColoredText.localize( "no_external_themes", ) return options.append("cancel") choices, _ = dialog_creator.ChoiceInput.from_reduced( options, dialog="theme_remove_dialog" ).multiple_choice() if choices is None: return for choice in choices: if choice == len(options) - 1: return core.ThemeHandler.remove_theme(options[choice]) color.ColoredText.localize( "theme_removed", theme_name=options[choice], ) return elif value == len(themes): # add_theme if not core.GitHandler.is_git_installed(): color.ColoredText.localize( "git_not_installed", ) return git_repo = color.ColoredInput().localize("enter_theme_git_repo").strip() external_theme = core.ExternalTheme.from_git_repo(git_repo) if external_theme is None: color.ColoredText.localize( "invalid_git_repo", ) return theme_name = external_theme.get_full_name() if theme_name in themes: if not dialog_creator.YesNoInput().get_input_once( "theme_already_exists", {"theme_name": theme_name}, ): color.ColoredText.localize( "theme_cancelled", ) return external_theme.save() value = theme_name color.ColoredText.localize( "theme_added", ) else: value = themes[value] self.set(ConfigKey.THEME, value) color.ColoredText.localize( "theme_changed", theme_name=value, ) @staticmethod def edit_config(_: Any = None): config = core.core_data.config features = list(ConfigKey) choice = dialog_creator.ChoiceInput.from_reduced( [key.value for key in features], dialog="config_dialog", single_choice=True, ).single_choice() if choice is None: return choice -= 1 feature = features[choice] print() if isinstance(config.get(feature), bool): core.core_data.config.edit_bool(feature) elif isinstance(config.get(feature), int): core.core_data.config.edit_int(feature) elif feature == ConfigKey.LOCALE: core.core_data.config.edit_locale() elif feature == ConfigKey.THEME: core.core_data.config.edit_theme() elif feature == ConfigKey.GAME_DATA_REPO: core.core_data.config.edit_game_data_repo() elif isinstance(config.get(feature), str): core.core_data.config.edit_str(feature) print() ================================================ FILE: src/bcsfe/core/io/data.py ================================================ from __future__ import annotations import base64 import enum from io import BytesIO import struct import typing from typing import Any, Literal from bcsfe import core import datetime class PaddingType(enum.Enum): PKCS7 = enum.auto() ZERO = enum.auto() NONE = enum.auto() class Data: def __init__( self, data: bytes | str | None | int | bool | Data | Any = None ): if isinstance(data, str): self.data = data.encode("utf-8") elif isinstance(data, bytes): self.data = data elif isinstance(data, bool): value = 1 if data else 0 self.data = str(value).encode("utf-8") elif isinstance(data, int): self.data = str(data).encode("utf-8") elif isinstance(data, Data): self.data = data.data elif data is None: self.data = b"" elif hasattr(data, "__bytes__"): self.data = bytes(data) else: raise TypeError( f"data must be bytes, str, int, bool, Data, or None, not {type(data)}" ) self.pos = 0 self.set_little_endiness() self.buffer_enabled = False def __bytes__(self) -> bytes: return self.data def __buffer__(self, flags: int, /) -> memoryview: return memoryview(self.data) @staticmethod def from_hex(hex: str) -> Data: return Data(bytes.fromhex(hex)) def enable_buffer(self): self.data_buffer: list[bytes] = [] self.buffer_enabled = True def end_buffer(self): self.buffer_enabled = False self.data = b"".join(self.data_buffer) self.data_buffer = [] def set_endiness(self, endiness: Literal["<", ">"]): self.endiness = endiness def set_little_endiness(self): self.set_endiness("<") def set_big_endiness(self): self.set_endiness(">") def is_empty(self) -> bool: return len(self.data) == 0 def to_file(self, path: core.Path): with open(path.path, "wb") as f: f.write(self.data) def copy(self) -> Data: return Data(self.data) @staticmethod def from_file(path: core.Path) -> Data: with open(path.path, "rb") as f: return Data(f.read()) def set_pos(self, pos: int): if pos < 0: pos = len(self.data) + pos self.pos = pos def reset_pos(self): self.pos = 0 def clear(self): self.data = b"" self.pos = 0 def get_pos(self) -> int: return self.pos def to_hex(self) -> str: return self.data.hex() def __len__(self) -> int: return len(self.data) def __add__(self, other: Data) -> Data: return Data(self.data + other.data) @typing.overload def __getitem__(self, key: int) -> int: pass @typing.overload def __getitem__(self, key: slice) -> Data: pass def __getitem__(self, key: int | slice) -> int | Data: if isinstance(key, int): return self.data[key] elif isinstance(key, slice): # type: ignore return Data(self.data[key]) else: raise TypeError("key must be int or slice") def __eq__(self, other: Any) -> bool: if isinstance(other, Data): return self.data == other.data else: return False def get_bytes(self) -> bytes: return self.data def read_bytes(self, length: int) -> bytes: result = self.data[self.pos : self.pos + length] self.pos += length return result def read_to_end(self, remaining_data: int = 0) -> bytes: length = len(self.data) - self.pos - remaining_data return self.read_bytes(length) def read_int(self) -> int: result = struct.unpack(f"{self.endiness}i", self.read_bytes(4))[0] return result def read_variable_length_int(self) -> int: i = 0 for _ in range(4): i3 = i << 7 read = self.read_ubyte() i = i3 | (read & 0x7F) if read & 0x80 == 0: return i return i def write_variable_length_int(self, value: int): value = int(value) i2 = 0 i3 = 0 while value >= 128: i2 |= ((value & 0x7F) | 0x8000) << (i3 * 8) i3 += 1 value >>= 7 i4 = i2 | (value << (i3 * 8)) i5 = i3 + 1 for i6 in range(i5): self.write_ubyte((i4 >> (((i5 - i6) - 1) * 8)) & 0xFF) def read_int_list(self, length: int | None = None) -> list[int]: if length is None: length = self.read_int() result: list[int] = [] for _ in range(length): result.append(self.read_int()) return result def read_bool_list(self, length: int | None = None) -> list[bool]: if length is None: length = self.read_int() result: list[bool] = [] for _ in range(length): result.append(self.read_bool()) return result def read_string_list(self, length: int | None = None) -> list[str]: if length is None: length = self.read_int() result: list[str] = [] for _ in range(length): result.append(self.read_string()) return result def read_byte_list(self, length: int | None = None) -> list[int]: if length is None: length = self.read_int() result: list[int] = [] for _ in range(length): result.append(self.read_byte()) return result def read_short_list(self, length: int | None = None) -> list[int]: if length is None: length = self.read_int() result: list[int] = [] for _ in range(length): result.append(self.read_short()) return result def read_uint(self) -> int: result = struct.unpack(f"{self.endiness}I", self.read_bytes(4))[0] return result def read_short(self) -> int: result = struct.unpack(f"{self.endiness}h", self.read_bytes(2))[0] return result def read_ushort(self) -> int: result = struct.unpack(f"{self.endiness}H", self.read_bytes(2))[0] return result def read_byte(self) -> int: result = struct.unpack(f"{self.endiness}b", self.read_bytes(1))[0] return result def read_ubyte(self) -> int: result = struct.unpack(f"{self.endiness}B", self.read_bytes(1))[0] return result def read_float(self) -> float: result = struct.unpack(f"{self.endiness}f", self.read_bytes(4))[0] return result def read_double(self) -> float: result = struct.unpack(f"{self.endiness}d", self.read_bytes(8))[0] return result def read_string(self, length: int | None = None) -> str: if length is None: length = self.read_int() result = self.read_bytes(length).decode("utf-8") return result def read_utf8_string_by_char_length(self, length: int | None = None) -> str: if length is None: length = self.read_int() if length == 0: return "" result_bytes = b"" result_str = "" while True: byte = self.read_bytes(1)[0] result_bytes += bytes([byte]) try: result_str = result_bytes.decode("utf-8") except UnicodeDecodeError: continue if len(result_str) == length: break return result_str def read_long(self) -> int: result = struct.unpack(f"{self.endiness}q", self.read_bytes(8))[0] return result def read_ulong(self) -> int: result = struct.unpack(f"{self.endiness}Q", self.read_bytes(8))[0] return result def read_date(self): year = self.read_int() month = self.read_int() day = self.read_int() hour = self.read_int() minute = self.read_int() second = self.read_int() return datetime.datetime(year, month, day, hour, minute, second) def write_date(self, date: datetime.datetime): self.write_int(date.year) self.write_int(date.month) self.write_int(date.day) self.write_int(date.hour) self.write_int(date.minute) self.write_int(date.second) def write_bytes(self, data: bytes): if self.buffer_enabled: self.data_buffer.append(data) else: self.data += data self.pos += len(data) def write_int(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}i", value)) def write_uint(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}I", value)) def write_short(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}h", value)) def write_ushort(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}H", value)) def write_byte(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}b", value)) def write_ubyte(self, value: int): value = int(value) self.write_bytes(struct.pack(f"{self.endiness}B", value)) def write_float(self, value: float): self.write_bytes(struct.pack(f"{self.endiness}f", value)) def write_double(self, value: float): self.write_bytes(struct.pack(f"{self.endiness}d", value)) def write_string(self, value: str, write_length: bool = True): if write_length: self.write_int(len(value.encode("utf-8"))) self.write_bytes(value.encode("utf-8")) def write_long(self, value: int): self.write_bytes(struct.pack(f"{self.endiness}q", value)) def write_ulong(self, value: int): self.write_bytes(struct.pack(f"{self.endiness}Q", value)) def write_list( self, value: list[Any], data_type: str, empty_value: Any = None, write_length: bool = True, length: int | None = None, ): if length is None: length = len(value) if write_length: self.write_int(length) if length > len(value): value += [empty_value] * (length - len(value)) elif length < len(value): value = value[:length] for item in value: getattr(self, f"write_{data_type}")(item) def write_int_list( self, value: list[int], write_length: bool = True, length: int | None = None, ): self.write_list(value, "int", 0, write_length, length) def write_bool_list( self, value: list[bool], write_length: bool = True, length: int | None = None, ): self.write_list(value, "bool", False, write_length, length) def write_string_list( self, value: list[str], write_length: bool = True, length: int | None = None, ): self.write_list(value, "string", "", write_length, length) def write_byte_list( self, value: list[int], write_length: bool = True, length: int | None = None, ): self.write_list(value, "byte", 0, write_length, length) def write_short_list( self, value: list[int], write_length: bool = True, length: int | None = None, ): self.write_list(value, "short", 0, write_length, length) def read_bool(self) -> bool: return self.read_byte() != 0 def write_bool(self, value: bool): self.write_byte(int(value)) def read_int_tuple(self) -> tuple[int, int]: return self.read_int(), self.read_int() def read_int_tuple_list( self, length: int | None = None ) -> list[tuple[int, int]]: if length is None: length = self.read_int() result: list[tuple[int, int]] = [] for _ in range(length): result.append(self.read_int_tuple()) return result def write_int_tuple(self, value: tuple[int, int]): self.write_int(value[0]) self.write_int(value[1]) def write_int_tuple_list( self, value: list[tuple[int, int]], write_length: bool = True, length: int | None = None, ): self.write_list(value, "int_tuple", (0, 0), write_length, length) def read_int_bool_dict(self, length: int | None = None) -> dict[int, bool]: if length is None: length = self.read_int() result: dict[int, bool] = {} for _ in range(length): key = self.read_int() value = self.read_bool() result[key] = value return result def write_int_bool_dict( self, value: dict[int, bool], write_length: bool = True ): if write_length: self.write_int(len(value)) for key, item in value.items(): self.write_int(key) self.write_bool(item) def read_int_int_dict(self, length: int | None = None) -> dict[int, int]: if length is None: length = self.read_int() result: dict[int, int] = {} for _ in range(length): key = self.read_int() value = self.read_int() result[key] = value return result def write_int_int_dict( self, value: dict[int, int], write_length: bool = True ): if write_length: self.write_int(len(value)) for key, item in value.items(): self.write_int(key) self.write_int(item) def read_int_double_dict( self, length: int | None = None ) -> dict[int, float]: if length is None: length = self.read_int() result: dict[int, float] = {} for _ in range(length): key = self.read_int() value = self.read_double() result[key] = value return result def write_int_double_dict( self, value: dict[int, float], write_length: bool = True ): if write_length: self.write_int(len(value)) for key, item in value.items(): self.write_int(key) self.write_double(item) def read_short_bool_dict( self, length: int | None = None ) -> dict[int, bool]: if length is None: length = self.read_int() result: dict[int, bool] = {} for _ in range(length): key = self.read_short() value = self.read_bool() result[key] = value return result def write_short_bool_dict( self, value: dict[int, bool], write_length: bool = True ): if write_length: self.write_int(len(value)) for key, item in value.items(): self.write_short(key) self.write_bool(item) def unpad_pkcs7(self) -> Data | None: try: pad = self.data[-1] except IndexError: return None if pad > len(self.data): return None if self.data[-pad:] != bytes([pad] * pad): return None return Data(self.data[:-pad]) def split(self, separator: bytes, maxsplit: int = -1) -> list[Data]: data_list: list[Data] = [] for line in self.data.split(separator, maxsplit): data_list.append(Data(line)) return data_list def to_int(self) -> int: try: return int(self.data.decode()) except ValueError: return 0 def to_int_little(self) -> int: return int.from_bytes(self.data, "little") def to_str(self) -> str: try: return self.data.decode(encoding="utf-8") except UnicodeDecodeError: return "" def to_bool(self) -> bool: return bool(self.to_int()) @staticmethod def int_list_data_list(int_list: list[int]) -> list[Data]: data_list: list[Data] = [] for integer in int_list: data_list.append(Data(str(integer))) return data_list @staticmethod def string_list_data_list(string_list: list[Any]) -> list[Data]: data_list: list[Data] = [] for string in string_list: data_list.append(Data(str(string))) return data_list @staticmethod def data_list_int_list(data_list: list[Data]) -> list[int]: int_list: list[int] = [] for data in data_list: int_list.append(data.to_int()) return int_list @staticmethod def data_list_string_list(data_list: list[Data]) -> list[str]: string_list: list[str] = [] for data in data_list: string_list.append(data.to_str()) return string_list def to_bytes(self) -> bytes: return self.data @staticmethod def from_many(others: list[Data], joiner: Data | None = None) -> Data: data_lst: list[bytes] = [] for other in others: data_lst.append(other.data) if joiner is None: return Data(b"".join(data_lst)) else: return Data(joiner.data.join(data_lst)) @staticmethod def from_int_list( int_list: list[int], endianess: Literal["little", "big"] ) -> Data: bytes_data = b"" for integer in int_list: bytes_data += integer.to_bytes(4, endianess) return Data(bytes_data) def strip(self) -> Data: return Data(self.data.strip()) def replace(self, old_data: Data, new_data: Data) -> Data: return Data(self.data.replace(old_data.data, new_data.data)) def set(self, value: bytes | str | None | int | bool) -> None: self.data = Data(value).data def to_bytes_io(self) -> BytesIO: return BytesIO(self.data) def __repr__(self) -> str: return f"Data({self.data!r})" def __str__(self) -> str: return self.to_str() def to_base_64(self) -> str: return base64.b64encode(self.data).decode() @staticmethod def from_base_64(string: str) -> Data: return Data(base64.b64decode(string)) def to_csv(self, *args: Any, **kwargs: Any) -> core.CSV: return core.CSV(self, *args, **kwargs) def search(self, search_data: Data, start: int = 0) -> int: return self.data.find(search_data.data, start) def add_line( self, line: Data | str | None | bytes | int | bool = None ) -> Data: line = Data(line) self.data += line.data + b"\r\n" return self class PaddedInt: def __init__(self, value: int, size: int): self.value = value self.size = size def __int__(self): return self.value def __str__(self): return str(self.value).zfill(self.size) def __repr__(self): return f"PaddedInt({self.value}, {self.size})" def to_str(self): return str(self) ================================================ FILE: src/bcsfe/core/io/git_handler.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color class Repo: def __init__(self, url: str, output_error: bool = True): self.url = url self.output_error = output_error self.success = self.clone() def get_repo_name(self) -> str: return self.url.split("/")[-1] def get_path(self) -> core.Path: path = GitHandler.get_repo_folder().add(self.get_repo_name()) path.generate_dirs() return path def run_cmd(self, cmd: str) -> bool: result = core.Command(cmd).run() success = result.exit_code == 0 if not success and self.output_error: color.ColoredText.localize("failed_to_run_git_cmd", cmd=cmd) return success def clone_to_temp(self, path: core.Path) -> bool: cmd = f"git clone {self.url} {path}" return self.run_cmd(cmd) def clone(self) -> bool: if self.is_cloned(): return True cmd = f"git clone {self.url} {self.get_path()}" success = self.run_cmd(cmd) if not success: self.get_path().remove() return success def pull(self) -> bool: cmd = f"git -C {self.get_path()} pull" return self.run_cmd(cmd) def fetch(self) -> bool: cmd = f"git -C {self.get_path()} fetch" return self.run_cmd(cmd) def get_file(self, file_path: core.Path) -> core.Data | None: path = self.get_path().add(file_path) try: return path.read() except FileNotFoundError: return None def get_temp_file(self, temp_folder: core.Path, file_path: core.Path) -> core.Data: path = temp_folder.add(file_path) return path.read() def get_folder(self, folder_path: core.Path) -> core.Path | None: path = self.get_path().add(folder_path) if path.exists(): return path return None def is_cloned(self) -> bool: return ( len(self.get_path().get_dirs()) > 0 or len(self.get_path().get_paths_dir()) > 0 ) class GitHandler: @staticmethod def get_repo_folder() -> core.Path: repo_folder = core.Path.get_data_folder().add("repos") repo_folder.generate_dirs() return repo_folder def get_repo(self, repo_url: str, output_error: bool = True) -> Repo | None: repo = Repo(repo_url) if repo.success: return repo if output_error: color.ColoredText.localize("failed_to_get_repo", url=repo_url) return None @staticmethod def is_git_installed() -> bool: cmd = "git --version" return core.Command(cmd).run().exit_code == 0 ================================================ FILE: src/bcsfe/core/io/json_file.py ================================================ from __future__ import annotations import json from typing import Any from bcsfe import core class JsonFile: def __init__(self, data: core.Data): self.json = json.loads(data.data) @staticmethod def from_path(path: core.Path) -> JsonFile: return JsonFile(path.read()) @staticmethod def from_object(js: Any) -> JsonFile: return JsonFile(core.Data(json.dumps(js))) @staticmethod def from_data(data: core.Data) -> JsonFile: return JsonFile(data) def to_data(self, indent: int | None = 4) -> core.Data: return core.Data(json.dumps(self.json, indent=indent)) def to_file(self, path: core.Path) -> None: path.write(self.to_data()) def to_object(self) -> Any: return self.json def get(self, key: str) -> Any: return self.json[key] def set(self, key: str, value: Any) -> None: self.json[key] = value def __str__(self) -> str: return str(self.json) def __getitem__(self, key: str) -> Any: return self.json[key] def __setitem__(self, key: str, value: Any) -> None: self.json[key] = value ================================================ FILE: src/bcsfe/core/io/path.py ================================================ from __future__ import annotations import glob import os import shutil from bcsfe import core import re class Path: def __init__(self, path: str = "", is_relative: bool = False): if isinstance(path, Path): path = path.path if is_relative: self.path = self.get_relative_path(path) else: self.path = path def is_relative(self) -> bool: return not os.path.isabs(self.path) @staticmethod def get_root() -> Path: return Path(os.sep) def get_relative_path(self, path: str) -> str: return os.path.join(self.get_files_folder().path, path) @staticmethod def get_files_folder() -> Path: file = Path(os.path.realpath(__file__)) if file.get_extension() == "pyc": path = file.parent().parent().parent().parent().add("files") else: path = file.parent().parent().parent().add("files") return path def strip_trailing_slash(self) -> Path: return Path(self.path.rstrip("/")) def open(self): self.generate_dirs() if os.name == "nt": os.startfile(self.path) # type: ignore elif os.name == "posix": cmd = f"dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call --print-reply /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:'file://{self.path}' string:''" core.Command(cmd, display_output=False).run_in_thread() elif os.name == "mac": core.Command(f"open {self.path}", display_output=False).run() else: raise OSError("Unknown OS") def open_file(self): os_name = os.name if os_name == "nt": os.startfile(self.path) # type: ignore elif os_name == "posix": cmd = f"xdg-open {self.path}" core.Command(cmd, display_output=False).run_in_thread() elif os_name == "mac": core.Command(f"open {self.path}", display_output=False).run() else: raise OSError("Unknown OS") def run(self, arg: str = "", display_output: bool = False) -> core.CommandResult: cmd_text = self.path + " " + arg cmd = core.Command(cmd_text, display_output=display_output) return cmd.run() def to_str(self) -> str: return self.path def to_str_forwards(self) -> str: return self.path.replace("\\", "/") @staticmethod def get_data_folder(app_name: str = "bcsfe") -> Path: os_name = os.name if os_name == "nt": path = Path.join(os.environ["USERPROFILE"], "Documents", app_name) elif os_name == "posix": data_home = os.environ.get("XDG_DATA_HOME") if data_home is None: path = Path.join(os.environ["HOME"], ".local", "share", app_name) else: path = Path.join(data_home, app_name) elif os_name == "mac": path = Path.join(os.environ["HOME"], "Documents", app_name) else: raise OSError("Unknown OS") path.generate_dirs() return path @staticmethod def get_config_folder(app_name: str = "bcsfe") -> Path: os_name = os.name if os_name != "posix": return Path.get_data_folder() data_home = os.environ.get("XDG_CONFIG_HOME") if data_home is None: path = Path.join(os.environ["HOME"], ".config", app_name) else: path = Path.join(data_home, app_name) path.generate_dirs() return path @staticmethod def get_state_folder(app_name: str = "bcsfe") -> Path: os_name = os.name if os_name != "posix": return Path.get_data_folder() data_home = os.environ.get("XDG_STATE_HOME") if data_home is None: path = Path.join(os.environ["HOME"], ".local", "state", app_name) else: path = Path.join(data_home, app_name) path.generate_dirs() return path def is_empty(self) -> bool: return self.path == "" def generate_dirs(self: Path) -> Path: if self.is_empty(): return self if not self.exists(): try: self.__make_dirs() except OSError as e: print(e, self) return self def create(self) -> Path: if not self.exists(): self.write(core.Data("test")) return self def exists(self) -> bool: return os.path.exists(self.path) def __make_dirs(self) -> Path: os.makedirs(self.path) return self def basename(self) -> str: return os.path.basename(self.path) @staticmethod def join(*paths: str | Path) -> Path: _paths: list[str] = [str(path) for path in paths] return Path(os.path.join(*_paths)) def add(self, *paths: str | Path) -> Path: _paths: list[str] = [str(path) for path in paths] return Path(os.path.join(self.path, *_paths)) def strip_leading_slash(self) -> Path: return Path(self.path.lstrip("/").lstrip("\\")) def __str__(self) -> str: return self.path def __repr__(self) -> str: return self.path def remove_tree(self, ignoreErrors: bool = False) -> Path: if self.exists(): shutil.rmtree(self.path, ignore_errors=ignoreErrors) return self def remove(self): if self.exists(): if self.is_directory(): self.remove_tree() else: os.remove(self.path) def has_files(self) -> bool: return len(os.listdir(self.path)) > 0 def copy(self, target: Path): if self.exists(): if self.is_directory(): self.copy_tree(target) else: try: target.parent().generate_dirs() shutil.copy(self.path, target.path) except shutil.SameFileError: pass else: raise FileNotFoundError(f"File not found: {self.path}") def copy_thread(self, target: Path): core.Thread("copy", self.copy, (target,)).start() def copy_tree(self, target: Path): if target.exists(): target.remove_tree() if self.exists(): target.parent().generate_dirs() shutil.copytree(self.path, target.path) def read(self, create: bool = False) -> core.Data: if self.exists(): return core.Data.from_file(self) elif create: self.write(core.Data()) return self.read() else: raise FileNotFoundError(f"File not found: {self.path}") def write(self, data: core.Data): data.to_file(self) def get_paths_dir(self, regex: str | None = None) -> list[Path]: if self.exists(): if regex is None: return [self.add(file) for file in os.listdir(self.path)] else: files: list[Path] = [] for file in os.listdir(self.path): if re.search(regex, file): files.append(self.add(file)) return files return [] def get_files(self, regex: str | None = None) -> list[Path]: return [file for file in self.get_paths_dir(regex) if file.is_file()] def is_file(self) -> bool: return os.path.isfile(self.path) def get_dirs(self) -> list["Path"]: return [file for file in self.get_paths_dir() if file.is_directory()] def glob(self, pattern: str, recursive: bool = False) -> list[Path]: return [ Path(path) for path in glob.glob(self.add(pattern).path, recursive=recursive) ] def strip_path_from(self, path: Path) -> Path: return Path(self.path.replace(path.path, "")).strip_leading_slash() def is_directory(self) -> bool: return os.path.isdir(self.path) def change_name(self, name: str) -> Path: return self.parent().add(name) def rename(self, name: str, overwrite: bool = False): if not self.exists(): raise FileNotFoundError(f"File not found: {self.path}") new_path = self.change_name(name) if new_path.path == self.path: return if new_path.exists(): if overwrite: new_path.remove() else: raise FileExistsError(f"File already exists: {new_path}") os.rename(self.path, new_path.path) self.path = new_path.path def parent(self) -> Path: return Path(os.path.dirname(self.path)) def change_extension(self, extension: str) -> Path: if extension.startswith("."): extension = extension[1:] return Path(self.path.rsplit(".", 1)[0] + "." + extension) def remove_extension(self) -> Path: return Path(self.path.rsplit(".", 1)[0]) def get_extension(self) -> str: try: return self.path.rsplit(".", 1)[1] except IndexError: return "" def get_file_name(self) -> str: return os.path.basename(self.path) def get_file_name_path(self) -> Path: return Path(self.get_file_name()) def get_file_name_without_extension(self) -> str: return self.get_file_name().rsplit(".", 1)[0] def get_file_size(self) -> int: return os.path.getsize(self.path) def get_absolute_path(self) -> Path: return Path(os.path.abspath(self.path)) def copy_object(self) -> Path: return Path(self.path) ================================================ FILE: src/bcsfe/core/io/root_handler.py ================================================ from __future__ import annotations from bcsfe import core import tempfile class PackageNameNotSet(Exception): pass class RootHandler: def __init__(self): self.package_name = None def is_android(self) -> bool: return core.Path.get_root().add("system").exists() def set_package_name(self, package_name: str): self.package_name = package_name def is_rooted(self) -> bool: try: core.Path.get_root().add("data").add("data").get_dirs() except PermissionError: return False return True def get_battlecats_packages(self) -> list[str]: packages = core.Path.get_root().add("data").add("data").get_dirs() packages = [ package.basename() for package in packages if package.add("files").add("SAVE_DATA").exists() ] return packages def get_package_name(self) -> str: if self.package_name is None: raise PackageNameNotSet("Package name is not set") return self.package_name def get_battlecats_path(self) -> core.Path: return core.Path.get_root().add("data").add("data").add(self.get_package_name()) def get_battlecats_save_path(self) -> core.Path: return self.get_battlecats_path().add("files").add("SAVE_DATA") def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult: self.get_battlecats_save_path().copy(local_path) return core.CommandResult.create_success() def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult: local_path.copy(self.get_battlecats_save_path()) return core.CommandResult.create_success() def close_game(self) -> core.CommandResult: cmd = core.Command( f"sudo pkill -f {self.get_package_name()}", ) return cmd.run() def run_game(self) -> core.CommandResult: cmd = core.Command( f"sudo monkey -p {self.get_package_name()} -c android.intent.category.LAUNCHER 1", ) return cmd.run() def rerun_game(self) -> core.CommandResult: result = self.close_game() if not result.success: return result result = self.run_game() if not result.success: return result return core.CommandResult.create_success() def save_locally( self, local_path: core.Path | None = None ) -> tuple[core.Path | None, core.CommandResult]: if local_path is None: local_path = core.Path.get_data_folder().add("saves").add("SAVE_DATA") local_path.parent().generate_dirs() result = self.save_battlecats_save(local_path) if not result.success: return None, result return local_path, result def load_locally(self, local_path: core.Path) -> core.CommandResult: success = self.load_battlecats_save(local_path) if not success: return core.CommandResult.create_failure() success = self.rerun_game() if not success: return core.CommandResult.create_failure() return core.CommandResult.create_success() def load_save( self, save: core.SaveFile, rerun_game: bool = True ) -> core.CommandResult: with tempfile.TemporaryDirectory() as temp_dir: local_path = core.Path(temp_dir).add("SAVE_DATA") save.to_data().to_file(local_path) result = self.load_battlecats_save(local_path) if not result.success: return result if rerun_game: result = self.rerun_game() return result ================================================ FILE: src/bcsfe/core/io/save.py ================================================ from __future__ import annotations import base64 from typing import Any from bcsfe import core, __version__, cli import datetime from bcsfe.cli.color import ColoredText from bcsfe.core.io.config import ConfigKey class SaveError(Exception): pass class CantDetectSaveCCError(SaveError): pass class SaveFileInvalid(SaveError): pass class FailedToLoadError(SaveError): pass class FailedToSaveError(SaveError): pass class SaveFile: def __init__( self, dt: core.Data | None = None, cc: core.CountryCode | None = None, load: bool = True, gv: core.GameVersion | None = None, package_name: str | None = None, ): self.package_name = package_name self.save_path: core.Path | None = None if dt is None: self.data = core.Data() else: self.data = dt detected_cc = self.detect_cc() if detected_cc is None: if cc is None: raise CantDetectSaveCCError( core.core_data.local_manager.get_key("cant_detect_cc") ) self.cc = cc else: self.cc = detected_cc self.used_storage = False self.localizable: core.Localizable | None = None self.init_save(gv) if dt is not None and load: self.load_wrapper() def get_localizable(self) -> core.Localizable: if self.localizable is None: self.localizable = core.Localizable(self) return self.localizable def load_save_file(self, other: SaveFile): self.data = other.data self.cc = other.cc self.game_version = other.game_version self.init_save(other.game_version) self.load_wrapper() def detect_cc(self) -> core.CountryCode | None: for cc in core.CountryCode.get_all(): self.cc = cc if self.verify_hash(): return cc return None def get_salt(self) -> str: """Get the salt for the save file. This is used for hashing the save file. Returns: str: The salt """ salt = f"battlecats{self.cc.get_patching_code()}" return salt def get_current_hash(self) -> str: """Get the current hash for the save file. This is used for hashing the save file. Returns: str: The current hash """ self.data.reset_pos() self.data.set_pos(-32) hash = self.data.read_string(32) return hash def get_new_hash(self, existing_hash: bool = True) -> str: """Get the new hash for the save file. This is used for hashing the save file. Returns: str: The new hash """ salt = self.get_salt() self.data.reset_pos() if existing_hash: data_to_hash = self.data.read_bytes(len(self.data) - 32) else: data_to_hash = self.data.read_bytes(len(self.data)) salted_data = core.Data(salt.encode("utf-8") + data_to_hash) hash = core.Hash(core.HashAlgorithm.MD5).get_hash(salted_data) return hash.to_hex() def set_hash(self, add: bool = False): """Set the hash of the save file.""" hash = self.get_new_hash(existing_hash=not add) if not add: self.data.set_pos(-32) else: self.data.set_pos(len(self.data)) self.data.write_string(hash, write_length=False) def verify_hash(self) -> bool: """Verify the hash of the save file. Returns: bool: Whether the hash is valid """ return self.get_current_hash() == self.get_new_hash() def load_wrapper(self): try: self.load() except Exception as e: ignore_error = core.core_data.config.get_bool(ConfigKey.IGNORE_PARSE_ERROR) if not ignore_error: raise FailedToLoadError( core.core_data.local_manager.get_key("failed_to_load_save") ) from e else: from traceback import format_exc ColoredText.localize("parse_ignored_error", error=format_exc()) def set_gv(self, gv: core.GameVersion): self.game_version = gv def set_cc(self, cc: core.CountryCode): self.cc = cc self.set_package_name(None) def set_package_name(self, package_name: str | None): self.package_name = package_name def load(self): """Load the save file. For most of this stuff I have no idea what it is used for""" self.data.reset_pos() self.dst_index = 0 self.dsts: list[bool] = [] self.game_version: core.GameVersion = core.GameVersion(self.data.read_int()) if self.game_version >= 10 or self.not_jp(): self.ub1 = self.data.read_bool() self.mute_bgm = self.data.read_bool() self.mute_se = self.data.read_bool() self.catfood = self.data.read_int() self.current_energy = self.data.read_int() year = self.data.read_int() self.year = self.data.read_int() month = self.data.read_int() self.month = self.data.read_int() day = self.data.read_int() self.day = self.data.read_int() self.timestamp = self.data.read_double() hour = self.data.read_int() minute = self.data.read_int() second = self.data.read_int() self.read_dst() self.date = datetime.datetime(year, month, day, hour, minute, second) self.ui1 = self.data.read_int() self.stamp_value_save = self.data.read_int() self.ui2 = self.data.read_int() self.upgrade_state = self.data.read_int() self.xp = self.data.read_int() self.tutorial_state = self.data.read_int() self.ui3 = self.data.read_int() self.koreaSuperiorTreasureState = self.data.read_int() self.unlock_popups_11 = self.data.read_int_list(3) self.ui5 = self.data.read_int() self.unlock_enemy_guide = self.data.read_int() self.ui6 = self.data.read_int() self.ub0 = self.data.read_bool() self.ui7 = self.data.read_int() self.cleared_eoc_1 = self.data.read_int() self.ui8 = self.data.read_int() self.unlocked_ending = self.data.read_int() self.lineups = core.LineUps.read(self.data, self.game_version) self.stamp_data = core.StampData.read(self.data) self.story = core.StoryChapters.read(self.data) if 20 <= self.game_version and self.game_version <= 25: self.enemy_guide = self.data.read_int_list(231) else: self.enemy_guide = self.data.read_int_list() self.cats = core.Cats.read_unlocked(self.data, self.game_version) self.cats.read_upgrade(self.data, self.game_version) self.cats.read_current_form(self.data, self.game_version) self.special_skills = core.SpecialSkills.read_upgrades(self.data) if self.game_version <= 25: self.menu_unlocks = self.data.read_int_list(5) self.unlock_popups_0 = self.data.read_int_list(5) elif self.game_version == 26: self.menu_unlocks = self.data.read_int_list(6) self.unlock_popups_0 = self.data.read_int_list(6) else: self.menu_unlocks = self.data.read_int_list() self.unlock_popups_0 = self.data.read_int_list() self.battle_items = core.BattleItems.read_items(self.data) if self.game_version <= 26: self.new_dialogs_2 = self.data.read_int_list(17) else: self.new_dialogs_2 = self.data.read_int_list() self.uil1 = self.data.read_int_list(length=20) self.moneko_bonus = self.data.read_int_list(length=1) self.daily_reward_initialized = self.data.read_int_list(length=1) self.battle_items.read_locked_items(self.data) self.read_dst() self.date_2 = self.data.read_date() self.story.read_treasure_festival(self.data) self.read_dst() self.date_3 = self.data.read_date() if self.game_version <= 37: self.ui0 = self.data.read_int() self.stage_unlock_cat_value = self.data.read_int() self.show_ending_value = self.data.read_int() self.chapter_clear_cat_unlock = self.data.read_int() self.ui9 = self.data.read_int() self.ios_android_month = self.data.read_int() self.ui10 = self.data.read_int() self.save_data_4_hash = self.data.read_string() self.mysale = core.MySale.read_bonus_hash(self.data) self.chara_flags = self.data.read_int_list(length=2) if self.game_version <= 37: self.uim1 = self.data.read_int() self.ubm1 = self.data.read_bool() self.chara_flags_2 = self.data.read_int_list(length=2) self.normal_tickets = self.data.read_int() self.rare_tickets = self.data.read_int() self.cats.read_gatya_seen(self.data, self.game_version) self.special_skills.read_gatya_seen(self.data) self.cats.read_storage(self.data, self.game_version) self.event_stages = core.EventChapters.read(self.data, self.game_version) self.itf1_ending = self.data.read_int() self.continue_flag = self.data.read_int() if 20 <= self.game_version: self.unlock_popups_8 = self.data.read_int_list(length=36) if 20 <= self.game_version and self.game_version <= 25: self.unit_drops = self.data.read_int_list(length=110) elif 26 <= self.game_version: self.unit_drops = self.data.read_int_list() self.gatya = core.Gatya.read_rare_normal_seed(self.data, self.game_version) self.get_event_data = self.data.read_bool() self.achievements = self.data.read_bool_list(length=7) self.os_value = self.data.read_int() self.read_dst() self.date_4 = self.data.read_date() self.gatya.read2(self.data) if self.not_jp(): self.player_id = self.data.read_string() self.order_ids = self.data.read_string_list() if self.not_jp(): self.g_timestamp = self.data.read_double() self.g_servertimestamp = self.data.read_double() self.m_gettimesave = self.data.read_double() self.usl1 = self.data.read_string_list() self.energy_notification = self.data.read_bool() self.full_gameversion = self.data.read_int() self.lineups.read_2(self.data, self.game_version) self.event_stages.read_legend_restrictions(self.data, self.game_version) if self.game_version <= 37: self.uil2 = self.data.read_int_list(length=7) self.uil3 = self.data.read_int_list(length=7) self.uil4 = self.data.read_int_list(length=7) self.g_timestamp_2 = self.data.read_double() self.g_servertimestamp_2 = self.data.read_double() self.m_gettimesave_2 = self.data.read_double() self.unknown_timestamp = self.data.read_double() self.gatya.read_trade_progress(self.data) if self.game_version <= 37: self.usl2 = self.data.read_string_list() if self.not_jp(): self.m_dGetTimeSave2 = self.data.read_double() self.ui11 = 0 else: self.ui11 = self.data.read_int() if 20 <= self.game_version and self.game_version <= 25: self.ubl1 = self.data.read_bool_list(length=12) elif 26 <= self.game_version and self.game_version < 39: self.ubl1 = self.data.read_bool_list() self.cats.read_max_upgrade_levels(self.data, self.game_version) self.special_skills.read_max_upgrade_levels(self.data) self.user_rank_rewards = core.UserRankRewards.read(self.data, self.game_version) if not self.not_jp(): self.m_dGetTimeSave2 = self.data.read_double() self.cats.read_unlocked_forms(self.data, self.game_version) self.transfer_code = self.data.read_string() self.confirmation_code = self.data.read_string() self.transfer_flag = self.data.read_bool() if 20 <= self.game_version: self.item_reward_stages = core.ItemRewardChapters.read( self.data, self.game_version ) self.timed_score_stages = core.TimedScoreChapters.read( self.data, self.game_version ) self.inquiry_code = self.data.read_string() self.officer_pass = core.OfficerPass.read(self.data) self.has_account = self.data.read_byte() self.backup_state = self.data.read_int() if self.not_jp(): self.ub2 = self.data.read_bool() assert self.data.read_int() == 44 self.itf1_complete = self.data.read_int() self.story.read_itf_timed_scores(self.data) self.title_chapter_bg = self.data.read_int() if self.game_version > 26: self.combo_unlocks = self.data.read_int_list() self.combo_unlocked_10k_ur = self.data.read_bool() assert self.data.read_int() == 45 if 21 <= self.game_version: assert self.data.read_int() == 46 self.gatya.read_event_seed(self.data, self.game_version) if self.game_version < 34: self.event_capsules = self.data.read_int_list(length=100) self.event_capsules_counter = self.data.read_int_list(length=100) else: self.event_capsules = self.data.read_int_list() self.event_capsules_counter = self.data.read_int_list() assert self.data.read_int() == 47 if 22 <= self.game_version: assert self.data.read_int() == 48 if 23 <= self.game_version: if not self.not_jp(): self.energy_notification = self.data.read_bool() self.m_dGetTimeSave3 = self.data.read_double() if self.game_version < 26: self.gatya_seen_lucky_drops = self.data.read_int_list(length=44) else: self.gatya_seen_lucky_drops = self.data.read_int_list() self.show_ban_message = self.data.read_bool() self.catfood_beginner_purchased = self.data.read_bool_list(length=3) self.next_week_timestamp = self.data.read_double() self.catfood_beginner_expired = self.data.read_bool_list(length=3) self.rank_up_sale_value = self.data.read_int() assert self.data.read_int() == 49 if 24 <= self.game_version: assert self.data.read_int() == 50 if 25 <= self.game_version: assert self.data.read_int() == 51 if 26 <= self.game_version: self.cats.read_catguide_collected(self.data) assert self.data.read_int() == 52 if 27 <= self.game_version: self.time_since_time_check_cumulative = self.data.read_double() self.server_timestamp = self.data.read_double() self.last_checked_energy_recovery_time = self.data.read_double() self.time_since_check = self.data.read_double() self.last_checked_expedition_time = self.data.read_double() self.catfruit = self.data.read_int_list() self.cats.read_fourth_forms(self.data) self.cats.read_catseyes_used(self.data) self.catseyes = self.data.read_int_list() self.catamins = self.data.read_int_list() self.gamatoto = core.Gamatoto.read(self.data) self.unlock_popups_6 = self.data.read_bool_list() self.ex_stages = core.ExChapters.read(self.data) assert self.data.read_int() == 53 if 29 <= self.game_version: self.gamatoto.read_2(self.data) assert self.data.read_int() == 54 self.item_pack = core.ItemPack.read(self.data) assert self.data.read_int() == 54 if self.game_version >= 30: self.gamatoto.read_skin(self.data) self.platinum_tickets = self.data.read_int() self.logins = core.LoginBonus.read(self.data, self.game_version) if self.game_version < 101000: self.reset_item_reward_flags = self.data.read_bool_list() self.reward_remaining_time = self.data.read_double() self.last_checked_reward_time = self.data.read_double() self.announcements = self.data.read_int_tuple_list(length=16) self.backup_counter = self.data.read_int() self.ui12 = self.data.read_int() self.ui13 = self.data.read_int() self.ui14 = self.data.read_int() assert self.data.read_int() == 55 if self.game_version >= 31: self.ub3 = self.data.read_bool() self.item_reward_stages.read_item_obtains(self.data) self.gatya.read_stepup(self.data) self.backup_frame = self.data.read_int() assert self.data.read_int() == 56 if self.game_version >= 32: self.ub4 = self.data.read_bool() self.cats.read_favorites(self.data) assert self.data.read_int() == 57 if self.game_version >= 33: self.dojo = core.Dojo.read_chapters(self.data) self.dojo.read_item_locks(self.data) assert self.data.read_int() == 58 if self.game_version >= 34: self.last_checked_zombie_time = self.data.read_double() self.outbreaks = core.Outbreaks.read_chapters(self.data) self.outbreaks.read_2(self.data) self.scheme_items = core.SchemeItems.read(self.data) if self.game_version >= 35: self.outbreaks.read_current_outbreaks(self.data, self.game_version) self.first_locks = self.data.read_int_bool_dict() self.energy_penalty_timestamp = self.data.read_double() assert self.data.read_int() == 60 if self.game_version >= 36: self.cats.read_chara_new_flags(self.data) self.shown_maxcollab_mg = self.data.read_bool() self.item_pack.read_displayed_packs(self.data) assert self.data.read_int() == 61 if self.game_version >= 38: self.unlock_popups = core.UnlockPopups.read(self.data) assert self.data.read_int() == 63 if self.game_version >= 39: self.ototo = core.Ototo.read(self.data) self.ototo.read_2(self.data, self.game_version) self.last_checked_castle_time = self.data.read_double() assert self.data.read_int() == 64 if self.game_version >= 40: self.beacon_base = core.BeaconEventListScene.read(self.data) assert self.data.read_int() == 65 if self.game_version >= 41: self.tower = core.TowerChapters.read(self.data) self.missions = core.Missions.read(self.data, self.game_version) self.tower.read_item_obtain_states(self.data) assert self.data.read_int() == 66 if self.game_version >= 42: self.dojo.read_ranking(self.data, self.game_version) self.item_pack.read_three_days(self.data) self.challenge = core.ChallengeChapters.read(self.data) self.challenge.read_scores(self.data) self.challenge.read_popup(self.data) assert self.data.read_int() == 67 if self.game_version >= 43: self.missions.read_weekly_missions(self.data) self.dojo.ranking.read_did_win_rewards(self.data) self.event_update_flags = self.data.read_bool() assert self.data.read_int() == 68 if self.game_version >= 44: self.event_stages.read_dicts(self.data) self.cotc_1_complete = self.data.read_int() assert self.data.read_int() == 69 if self.game_version >= 46: self.gamatoto.read_collab_data(self.data) assert self.data.read_int() == 71 if self.game_version < 90300: self.map_resets = core.MapResets.read(self.data) assert self.data.read_int() == 72 if self.game_version >= 51: self.uncanny = core.UncannyChapters.read(self.data) assert self.data.read_int() == 76 if self.game_version >= 77: self.catamin_stages = core.UncannyChapters.read(self.data) self.lucky_tickets = self.data.read_int_list() self.ub5 = self.data.read_bool() assert self.data.read_int() == 77 if self.game_version >= 80000: self.officer_pass.read_gold_pass(self.data, self.game_version) self.cats.read_talents(self.data) self.np = self.data.read_int() self.ub6 = self.data.read_bool() assert self.data.read_int() == 80000 if self.game_version >= 80200: self.ub7 = self.data.read_bool() self.leadership = self.data.read_short() self.officer_pass.read_cat_data(self.data) assert self.data.read_int() == 80200 if self.game_version >= 80300: self.filibuster_stage_id = self.data.read_byte() self.filibuster_stage_enabled = self.data.read_bool() assert self.data.read_int() == 80300 if self.game_version >= 80500: self.stage_ids_10s = self.data.read_int_list() assert self.data.read_int() == 80500 if self.game_version >= 80600: length = self.data.read_short() self.uil6 = self.data.read_int_list(length=length) self.legend_quest = core.LegendQuestChapters.read(self.data) self.ush1 = self.data.read_short() self.uby1 = self.data.read_byte() assert self.data.read_int() == 80600 if self.game_version >= 80700: length = self.data.read_int() self.uiid1: dict[int, list[int]] = {} for _ in range(length): key = self.data.read_int() value = self.data.read_int_list() self.uiid1[key] = value assert self.data.read_int() == 80700 if self.game_version >= 100600: if self.is_en(): self.uby2 = self.data.read_byte() assert self.data.read_int() == 100600 if self.game_version >= 81000: self.restart_pack = self.data.read_byte() assert self.data.read_int() == 81000 if self.game_version >= 90000: self.medals = core.Medals.read(self.data) self.wildcat_slots = core.GamblingEvent.read(self.data, self.game_version) assert self.data.read_int() == 90000 if self.game_version >= 90100: self.ush2 = self.data.read_short() self.ush3 = self.data.read_short() self.ui15 = self.data.read_int() self.ud1 = self.data.read_double() assert self.data.read_int() == 90100 if self.game_version >= 90300: length = self.data.read_short() self.utl1: list[tuple[int, int, int, int, int, int, int]] = [] for _ in range(length): i1 = self.data.read_int() i2 = self.data.read_int() i3 = self.data.read_short() i4 = self.data.read_int() i5 = self.data.read_int() i6 = self.data.read_int() i7 = self.data.read_short() self.utl1.append((i1, i2, i3, i4, i5, i6, i7)) length = self.data.read_short() self.uidd1 = self.data.read_int_double_dict(length) self.gauntlets = core.GauntletChapters.read(self.data) assert self.data.read_int() == 90300 if self.game_version >= 90400: self.enigma_clears = core.GauntletChapters.read(self.data) self.enigma = core.Enigma.read(self.data, self.game_version) self.cleared_slots = core.ClearedSlots.read(self.data) assert self.data.read_int() == 90400 if self.game_version >= 90500: self.collab_gauntlets = core.GauntletChapters.read(self.data) self.ub8 = self.data.read_bool() self.ud2 = self.data.read_double() self.ud3 = self.data.read_double() self.ui16 = self.data.read_int() if self.game_version >= 100300: self.uby3 = self.data.read_byte() self.ub9 = self.data.read_bool() self.ud4 = self.data.read_double() self.ud5 = self.data.read_double() if self.game_version >= 130700: length = self.data.read_short() self.uiid3: dict[int, int] = {} for _ in range(length): key = self.data.read_int() value = self.data.read_byte() self.uiid3[key] = value length = self.data.read_short() self.uidd2: dict[int, float] = {} for _ in range(length): key = self.data.read_int() value = self.data.read_double() self.uidd2[key] = value if self.game_version >= 140100: length = self.data.read_short() self.uidd3: dict[int, float] = {} for _ in range(length): key = self.data.read_int() value = self.data.read_double() self.uidd3[key] = value assert self.data.read_int() == 90500 if self.game_version >= 90700: self.talent_orbs = core.TalentOrbs.read(self.data, self.game_version) length = self.data.read_short() self.uidiid2: dict[int, dict[int, int]] = {} for _ in range(length): key = self.data.read_short() length = self.data.read_byte() for _ in range(length): key2 = self.data.read_byte() value = self.data.read_short() if key not in self.uidiid2: self.uidiid2[key] = {} self.uidiid2[key][key2] = value if length == 0: self.uidiid2[key] = {} self.ub10 = self.data.read_bool() assert self.data.read_int() == 90700 if self.game_version >= 90800: length = self.data.read_short() self.uil7 = self.data.read_int_list(length) self.ubl2 = self.data.read_bool_list(10) assert self.data.read_int() == 90800 if self.game_version >= 90900: self.cat_shrine = core.CatShrine.read(self.data) self.ud6 = self.data.read_double() self.ud7 = self.data.read_double() assert self.data.read_int() == 90900 if self.game_version >= 91000: self.lineups.read_slot_names(self.data, self.game_version) assert self.data.read_int() == 91000 if self.game_version >= 100000: self.legend_tickets = self.data.read_int() length = self.data.read_byte() self.uiil1: list[tuple[int, int]] = [] for _ in range(length): i1 = self.data.read_byte() i2 = self.data.read_int() self.uiil1.append((i1, i2)) self.ub11 = self.data.read_bool() self.ub12 = self.data.read_bool() self.password_refresh_token = self.data.read_string() self.ub13 = self.data.read_bool() self.uby4 = self.data.read_byte() self.uby5 = self.data.read_byte() self.ud8 = self.data.read_double() self.ud9 = self.data.read_double() assert self.data.read_int() == 100000 if self.game_version >= 100100: self.date_int = self.data.read_int() assert self.data.read_int() == 100100 if self.game_version >= 100300: self.battle_items.read_endless_items(self.data) assert self.data.read_int() == 100300 if self.game_version >= 100400: length = self.data.read_byte() self.event_capsules_2 = self.data.read_int_list(length) self.two_battle_lines = self.data.read_bool() assert self.data.read_int() == 100400 if self.game_version >= 100600: self.ud10 = self.data.read_double() self.platinum_shards = self.data.read_int() self.ub15 = self.data.read_bool() assert self.data.read_int() == 100600 if self.game_version >= 100700: self.cat_scratcher = core.GamblingEvent.read(self.data, self.game_version) assert self.data.read_int() == 100700 if self.game_version >= 100900: self.aku = core.AkuChapters.read(self.data) self.ub16 = self.data.read_bool() self.ub17 = self.data.read_bool() length = self.data.read_short() self.ushdshd2: dict[int, list[int]] = {} for _ in range(length): key = self.data.read_short() length = self.data.read_short() for _ in range(length): value = self.data.read_short() if key not in self.ushdshd2: self.ushdshd2[key] = [] self.ushdshd2[key].append(value) if length == 0: self.ushdshd2[key] = [] length = self.data.read_short() self.ushdd: dict[int, float] = {} for _ in range(length): key = self.data.read_short() value = self.data.read_double() self.ushdd[key] = value length = self.data.read_short() self.ushdd2: dict[int, float] = {} for _ in range(length): key = self.data.read_short() value = self.data.read_double() self.ushdd2[key] = value self.ub18 = self.data.read_bool() assert self.data.read_int() == 100900 if self.game_version >= 101000: self.uby6 = self.data.read_byte() assert self.data.read_int() == 101000 if self.game_version >= 110000: length = self.data.read_short() self.uidtii: dict[int, tuple[int, int]] = {} for _ in range(length): key = self.data.read_int() value = ( self.data.read_byte(), self.data.read_byte(), ) self.uidtii[key] = value assert self.data.read_int() == 110000 if self.game_version >= 110500: self.behemoth_culling = core.GauntletChapters.read(self.data) self.ub19 = self.data.read_bool() assert self.data.read_int() == 110500 if self.game_version >= 110600: self.ub20 = self.data.read_bool() assert self.data.read_int() == 110600 if self.game_version >= 110700: length = self.data.read_int() self.uidtff: dict[int, tuple[float, float]] = {} for _ in range(length): key = self.data.read_int() value = ( self.data.read_double(), self.data.read_double(), ) self.uidtff[key] = value if self.not_jp(): self.ub20 = self.data.read_bool() assert self.data.read_int() == 110700 if self.game_version >= 110800: self.cat_shrine.read_dialogs(self.data) self.ub21 = self.data.read_bool() self.dojo_3x_speed = self.data.read_bool() self.ub22 = self.data.read_bool() self.ub23 = self.data.read_bool() assert self.data.read_int() == 110800 if self.game_version >= 111000: self.ui17 = self.data.read_int() self.ush4 = self.data.read_short() self.uby7 = self.data.read_byte() self.uby8 = self.data.read_byte() self.ub24 = self.data.read_bool() self.uby9 = self.data.read_byte() length = self.data.read_byte() self.ushl1 = self.data.read_short_list(length) length = self.data.read_short() self.ushl2 = self.data.read_short_list(length) length = self.data.read_short() self.ushl3 = self.data.read_short_list(length) self.ui18 = self.data.read_int() self.ui19 = self.data.read_int() self.ui20 = self.data.read_int() self.ush5 = self.data.read_short() self.ush6 = self.data.read_short() self.ush7 = self.data.read_short() self.ush8 = self.data.read_short() self.uby10 = self.data.read_byte() self.ub25 = self.data.read_bool() self.ub26 = self.data.read_bool() self.ub27 = self.data.read_bool() self.ub28 = self.data.read_bool() self.ub29 = self.data.read_bool() self.ub30 = self.data.read_bool() self.uby11 = self.data.read_byte() length = self.data.read_short() self.ushl4 = self.data.read_short_list(length) self.ubl3 = self.data.read_bool_list(14) length = self.data.read_byte() self.labyrinth_medals = self.data.read_short_list(length) assert self.data.read_int() == 111000 if self.game_version >= 120000: self.zero_legends = core.ZeroLegendsChapters.read(self.data) self.uby12 = self.data.read_byte() assert self.data.read_int() == 120000 if self.game_version >= 120100: length = self.data.read_short() self.ushl6 = self.data.read_short_list(length) assert self.data.read_int() == 120100 if self.game_version >= 120200: self.ub31 = self.data.read_bool() self.ush9 = self.data.read_short() length = self.data.read_byte() self.ushshd: dict[int, int] = {} for _ in range(length): key = self.data.read_short() value = self.data.read_short() self.ushshd[key] = value assert self.data.read_int() == 120200 if self.game_version >= 120400: self.ud11 = self.data.read_double() self.ud12 = self.data.read_double() assert self.data.read_int() == 120400 if self.game_version >= 120500: self.ub32 = self.data.read_bool() self.ub33 = self.data.read_bool() self.ub34 = self.data.read_bool() self.ui21 = self.data.read_int() self.golden_cpu_count = self.data.read_byte() assert self.data.read_int() == 120500 if self.game_version >= 120600: self.sound_effects_volume = self.data.read_byte() self.background_music_volume = self.data.read_byte() assert self.data.read_int() == 120600 if (self.not_jp() and self.game_version >= 120700) or ( self.is_jp() and self.game_version >= 130000 ): length = self.data.read_byte() self.ustl1: list[tuple[str, str]] = [] for _ in range(length): s1 = self.data.read_string() s2 = self.data.read_string() self.ustl1.append((s1, s2)) if self.not_jp(): assert self.data.read_int() == 120700 else: assert self.data.read_int() == 130000 if self.game_version >= 130100: length = self.data.read_int() self.utl3: list[tuple[int, int]] = [] for _ in range(length): i1 = self.data.read_int() i2 = self.data.read_long() self.utl3.append((i1, i2)) assert self.data.read_int() == 130100 if self.game_version >= 130301: length = self.data.read_int() self.ustid1: dict[str, tuple[int, float]] = {} for _ in range(length): key = self.data.read_string() value_1 = self.data.read_int() value_2 = self.data.read_double() self.ustid1[key] = (value_1, value_2) assert self.data.read_int() == 130301 if self.game_version >= 130400: self.ud13 = self.data.read_double() self.ud14 = self.data.read_double() assert self.data.read_int() == 130400 if self.game_version >= 130500: self.utl4: list[tuple[int, list[tuple[int, int, int, list[int]]]]] = [] length1 = self.data.read_short() for _ in range(length1): id = self.data.read_byte() length2 = self.data.read_byte() ls2: list[tuple[int, int, int, list[int]]] = [] for _ in range(length2): v1 = self.data.read_byte() v2 = self.data.read_byte() v3 = self.data.read_byte() length3 = self.data.read_short() ls1: list[int] = [] for _ in range(length3): val = self.data.read_short() ls1.append(val) ls2.append((v1, v2, v3, ls1)) self.utl4.append((id, ls2)) assert self.data.read_int() == 130500 if self.game_version >= 130600: self.uby14 = self.data.read_byte() if self.not_jp(): self.ush12 = self.data.read_short() assert self.data.read_int() == 130600 if self.game_version >= 130700: if self.is_jp(): self.ush12 = self.data.read_short() self.ud15 = self.data.read_double() self.uby15 = self.data.read_byte() self.uby16 = self.data.read_byte() self.ush11 = self.data.read_short() self.uby17 = self.data.read_byte() self.uby18 = self.data.read_byte() self.uby19 = self.data.read_byte() self.ud16 = self.data.read_double() length1 = self.data.read_short() self.ushd1: dict[int, tuple[int, int, dict[int, int]]] = {} for _ in range(length1): key = self.data.read_short() value = self.data.read_short() value_2 = self.data.read_int() length2 = self.data.read_short() data2: dict[int, int] = {} for _ in range(length2): key2 = self.data.read_short() value3 = self.data.read_short() data2[key2] = value3 self.ushd1[key] = (value, value_2, data2) assert self.data.read_int() == 130700 if self.game_version >= 140000: self.ui22 = self.data.read_int() self.ud17 = self.data.read_double() self.uby20 = self.data.read_byte() length = self.data.read_byte() self.uild1: dict[int, list[int]] = {} for _ in range(length): key = self.data.read_int() length2 = self.data.read_byte() data3: list[int] = [] for _ in range(length2): value = self.data.read_byte() data3.append(value) self.uild1[key] = data3 self.dojo_chapters = core.ZeroLegendsChapters.read(self.data) length = self.data.read_short() self.uil9: list[int] = [] for _ in range(length): self.uil9.append(self.data.read_int()) self.ub35 = self.data.read_bool() self.ud18 = self.data.read_double() length = self.data.read_short() self.ushd2: dict[int, int] = {} for _ in range(length): key = self.data.read_short() value = self.data.read_byte() self.ushd2[key] = value assert self.data.read_int() == 140000 if self.game_version >= 140100 and self.game_version < 140500: self.uby21 = self.data.read_byte() assert self.data.read_int() == 140100 if self.game_version >= 140200: length = self.data.read_byte() self.uil10: list[ tuple[ int, int, bool, bool, bool, int, int, int, bool, bool, bool, str | None, bool, ] ] = [] for _ in range(length): val_1 = self.data.read_int() val_2 = self.data.read_int() val_3 = self.data.read_bool() val_4 = self.data.read_bool() val_5 = self.data.read_bool() val_6 = self.data.read_int() val_7 = self.data.read_int() val_8 = self.data.read_int() val_9 = self.data.read_bool() val_10 = self.data.read_bool() val_11 = self.data.read_bool() val_12 = None if self.game_version >= 140500: # game seems to read more than just this, may break in the future val_12 = self.data.read_string() val_13 = self.data.read_bool() self.uil10.append( ( val_1, val_2, val_3, val_4, val_5, val_6, val_7, val_8, val_9, val_10, val_11, val_12, val_13, ) ) length = self.data.read_byte() self.uid1: dict[int, float] = {} for _ in range(length): key = self.data.read_int() value = self.data.read_double() self.uid1[key] = value self.hundred_million_ticket = self.data.read_int() assert self.data.read_int() == 140200 if self.game_version >= 140300: length = self.data.read_byte() self.uil11: list[int] = [] for _ in range(length): val = self.data.read_byte() self.uil11.append(val) if self.game_version >= 150300: self.ui24 = self.data.read_int() self.ub38 = self.data.read_bool() self.ub36 = self.data.read_bool() length = self.data.read_byte() self.treasure_chests: list[int] = [] for _ in range(length): value = self.data.read_int() self.treasure_chests.append(value) self.ui23 = self.data.read_int() length = self.data.read_short() self.uil13: list[int] = [] for _ in range(length): self.uil13.append(self.data.read_int()) self.ub37 = self.data.read_bool() assert self.data.read_int() == 140300 self.remaining_data = self.data.read_to_end(32) def save(self, data: core.Data): self.data = data self.dst_index = 0 self.data.clear() self.data.enable_buffer() self.data.write_int(self.game_version.game_version) if self.game_version >= 10 or self.not_jp(): self.data.write_bool(self.ub1) self.data.write_bool(self.mute_bgm) self.data.write_bool(self.mute_se) self.data.write_int(self.catfood) self.data.write_int(self.current_energy) self.data.write_int(self.date.year) self.data.write_int(self.year) self.data.write_int(self.date.month) self.data.write_int(self.month) self.data.write_int(self.date.day) self.data.write_int(self.day) self.data.write_double(self.timestamp) self.data.write_int(self.date.hour) self.data.write_int(self.date.minute) self.data.write_int(self.date.second) self.write_dst() self.data.write_int(self.ui1) self.data.write_int(self.stamp_value_save) self.data.write_int(self.ui2) self.data.write_int(self.upgrade_state) self.data.write_int(self.xp) self.data.write_int(self.tutorial_state) self.data.write_int(self.ui3) self.data.write_int(self.koreaSuperiorTreasureState) self.data.write_int_list(self.unlock_popups_11, write_length=False, length=3) self.data.write_int(self.ui5) self.data.write_int(self.unlock_enemy_guide) self.data.write_int(self.ui6) self.data.write_bool(self.ub0) self.data.write_int(self.ui7) self.data.write_int(self.cleared_eoc_1) self.data.write_int(self.ui8) self.data.write_int(self.unlocked_ending) self.lineups.write(self.data, self.game_version) self.stamp_data.write(self.data) self.story.write(self.data) if 20 <= self.game_version and self.game_version <= 25: self.data.write_int_list(self.enemy_guide, write_length=False, length=231) else: self.data.write_int_list(self.enemy_guide) self.cats.write_unlocked(self.data, self.game_version) self.cats.write_upgrade(self.data, self.game_version) self.cats.write_current_form(self.data, self.game_version) self.special_skills.write_upgrades(self.data) if self.game_version <= 25: self.data.write_int_list(self.menu_unlocks, write_length=False, length=5) self.data.write_int_list(self.unlock_popups_0, write_length=False, length=5) elif self.game_version <= 26: self.data.write_int_list(self.menu_unlocks, write_length=False, length=6) self.data.write_int_list(self.unlock_popups_0, write_length=False, length=6) else: self.data.write_int_list(self.menu_unlocks) self.data.write_int_list(self.unlock_popups_0) self.battle_items.write_items(self.data) if self.game_version <= 26: self.data.write_int_list(self.new_dialogs_2, write_length=False, length=17) else: self.data.write_int_list(self.new_dialogs_2) self.data.write_int_list(self.uil1, write_length=False, length=20) self.data.write_int_list(self.moneko_bonus, write_length=False, length=1) self.data.write_int_list( self.daily_reward_initialized, write_length=False, length=1 ) self.battle_items.write_locked_items(self.data) self.write_dst() self.data.write_date(self.date_2) self.story.write_treasure_festival(self.data) self.write_dst() self.data.write_date(self.date_3) if self.game_version <= 37: self.data.write_int(self.ui0) self.data.write_int(self.stage_unlock_cat_value) self.data.write_int(self.show_ending_value) self.data.write_int(self.chapter_clear_cat_unlock) self.data.write_int(self.ui9) self.data.write_int(self.ios_android_month) self.data.write_int(self.ui10) self.data.write_string(self.save_data_4_hash) self.mysale.write_bonus_hash(self.data) self.data.write_int_list(self.chara_flags, write_length=False, length=2) if self.game_version <= 37: self.data.write_int(self.uim1) self.data.write_bool(self.ubm1) self.data.write_int_list(self.chara_flags_2, write_length=False, length=2) self.data.write_int(self.normal_tickets) self.data.write_int(self.rare_tickets) self.cats.write_gatya_seen(self.data, self.game_version) self.special_skills.write_gatya_seen(self.data) self.cats.write_storage(self.data, self.game_version) self.event_stages.write(self.data, self.game_version) self.data.write_int(self.itf1_ending) self.data.write_int(self.continue_flag) if 20 <= self.game_version: self.data.write_int_list( self.unlock_popups_8, write_length=False, length=36 ) if 20 <= self.game_version and self.game_version <= 25: self.data.write_int_list(self.unit_drops, write_length=False, length=110) elif 26 <= self.game_version: self.data.write_int_list(self.unit_drops) self.gatya.write_rare_normal_seed(self.data) self.data.write_bool(self.get_event_data) self.data.write_bool_list(self.achievements, write_length=False, length=7) self.data.write_int(self.os_value) self.write_dst() self.data.write_date(self.date_4) self.gatya.write2(self.data) if self.not_jp(): self.data.write_string(self.player_id) self.data.write_string_list(self.order_ids) if self.not_jp(): self.data.write_double(self.g_timestamp) self.data.write_double(self.g_servertimestamp) self.data.write_double(self.m_gettimesave) self.data.write_string_list(self.usl1) self.data.write_bool(self.energy_notification) self.data.write_int(self.full_gameversion) self.lineups.write_2(self.data, self.game_version) self.event_stages.write_legend_restrictions(self.data, self.game_version) if self.game_version <= 37: self.data.write_int_list(self.uil2, write_length=False, length=7) self.data.write_int_list(self.uil3, write_length=False, length=7) self.data.write_int_list(self.uil4, write_length=False, length=7) self.data.write_double(self.g_timestamp_2) self.data.write_double(self.g_servertimestamp_2) self.data.write_double(self.m_gettimesave_2) self.data.write_double(self.unknown_timestamp) self.gatya.write_trade_progress(self.data) if self.game_version <= 37: self.data.write_string_list(self.usl2) if self.not_jp(): self.data.write_double(self.m_dGetTimeSave2) else: self.data.write_int(self.ui11) if 20 <= self.game_version and self.game_version <= 25: self.data.write_bool_list(self.ubl1, write_length=False, length=12) elif 26 <= self.game_version and self.game_version < 39: self.data.write_bool_list(self.ubl1) self.cats.write_max_upgrade_levels(self.data, self.game_version) self.special_skills.write_max_upgrade_levels(self.data) self.user_rank_rewards.write(self.data, self.game_version) if self.is_jp(): self.data.write_double(self.m_dGetTimeSave2) self.cats.write_unlocked_forms(self.data, self.game_version) self.data.write_string(self.transfer_code) self.data.write_string(self.confirmation_code) self.data.write_bool(self.transfer_flag) if 20 <= self.game_version: self.item_reward_stages.write(self.data, self.game_version) self.timed_score_stages.write(self.data, self.game_version) self.data.write_string(self.inquiry_code) self.officer_pass.write(self.data) self.data.write_byte(self.has_account) self.data.write_int(self.backup_state) if self.not_jp(): self.data.write_bool(self.ub2) self.data.write_int(44) self.data.write_int(self.itf1_complete) self.story.write_itf_timed_scores(self.data) self.data.write_int(self.title_chapter_bg) if self.game_version > 26: self.data.write_int_list(self.combo_unlocks) self.data.write_bool(self.combo_unlocked_10k_ur) self.data.write_int(45) if 21 <= self.game_version: self.data.write_int(46) self.gatya.write_event_seed(self.data) if self.game_version < 34: self.data.write_int_list( self.event_capsules, write_length=False, length=100 ) self.data.write_int_list( self.event_capsules_counter, write_length=False, length=100 ) else: self.data.write_int_list(self.event_capsules) self.data.write_int_list(self.event_capsules_counter) self.data.write_int(47) if 22 <= self.game_version: self.data.write_int(48) if 23 <= self.game_version: if self.is_jp(): self.data.write_bool(self.energy_notification) self.data.write_double(self.m_dGetTimeSave3) if self.game_version < 26: self.data.write_int_list( self.gatya_seen_lucky_drops, write_length=False, length=44, ) else: self.data.write_int_list(self.gatya_seen_lucky_drops) self.data.write_bool(self.show_ban_message) self.data.write_bool_list( self.catfood_beginner_purchased, write_length=False, length=3, ) self.data.write_double(self.next_week_timestamp) self.data.write_bool_list( self.catfood_beginner_expired, write_length=False, length=3 ) self.data.write_int(self.rank_up_sale_value) self.data.write_int(49) if 24 <= self.game_version: self.data.write_int(50) if 25 <= self.game_version: self.data.write_int(51) if 26 <= self.game_version: self.cats.write_catguide_collected(self.data) self.data.write_int(52) if 27 <= self.game_version: self.data.write_double(self.time_since_time_check_cumulative) self.data.write_double(self.server_timestamp) self.data.write_double(self.last_checked_energy_recovery_time) self.data.write_double(self.time_since_check) self.data.write_double(self.last_checked_expedition_time) self.data.write_int_list(self.catfruit) self.cats.write_fourth_forms(self.data) self.cats.write_catseyes_used(self.data) self.data.write_int_list(self.catseyes) self.data.write_int_list(self.catamins) self.gamatoto.write(self.data) self.data.write_bool_list(self.unlock_popups_6) self.ex_stages.write(self.data) self.data.write_int(53) if 29 <= self.game_version: self.gamatoto.write_2(self.data) self.data.write_int(54) self.item_pack.write(self.data) self.data.write_int(54) if self.game_version >= 30: self.gamatoto.write_skin(self.data) self.data.write_int(self.platinum_tickets) self.logins.write(self.data, self.game_version) if self.game_version < 101000: self.data.write_bool_list(self.reset_item_reward_flags) self.data.write_double(self.reward_remaining_time) self.data.write_double(self.last_checked_reward_time) self.data.write_int_tuple_list( self.announcements, write_length=False, length=16 ) self.data.write_int(self.backup_counter) self.data.write_int(self.ui12) self.data.write_int(self.ui13) self.data.write_int(self.ui13) self.data.write_int(55) if self.game_version >= 31: self.data.write_bool(self.ub3) self.item_reward_stages.write_item_obtains(self.data) self.gatya.write_stepup(self.data) self.data.write_int(self.backup_frame) self.data.write_int(56) if self.game_version >= 32: self.data.write_bool(self.ub4) self.cats.write_favorites(self.data) self.data.write_int(57) if self.game_version >= 33: self.dojo.write_chapters(self.data) self.dojo.write_item_locks(self.data) self.data.write_int(58) if self.game_version >= 34: self.data.write_double(self.last_checked_zombie_time) self.outbreaks.write_chapters(self.data) self.outbreaks.write_2(self.data) self.scheme_items.write(self.data) if self.game_version >= 35: self.outbreaks.write_current_outbreaks(self.data, self.game_version) self.data.write_int_bool_dict(self.first_locks) self.data.write_double(self.energy_penalty_timestamp) self.data.write_int(60) if self.game_version >= 36: self.cats.write_chara_new_flags(self.data) self.data.write_bool(self.shown_maxcollab_mg) self.item_pack.write_displayed_packs(self.data) self.data.write_int(61) if self.game_version >= 38: self.unlock_popups.write(self.data) self.data.write_int(63) if self.game_version >= 39: self.ototo.write(self.data) self.ototo.write_2(self.data, self.game_version) self.data.write_double(self.last_checked_castle_time) self.data.write_int(64) if self.game_version >= 40: self.beacon_base.write(self.data) self.data.write_int(65) if self.game_version >= 41: self.tower.write(self.data) self.missions.write(self.data, self.game_version) self.tower.write_item_obtain_states(self.data) self.data.write_int(66) if self.game_version >= 42: self.dojo.write_ranking(self.data, self.game_version) self.item_pack.write_three_days(self.data) self.challenge.write(self.data) self.challenge.write_scores(self.data) self.challenge.write_popup(self.data) self.data.write_int(67) if self.game_version >= 43: self.missions.write_weekly_missions(self.data) self.dojo.ranking.write_did_win_rewards(self.data) self.data.write_bool(self.event_update_flags) self.data.write_int(68) if self.game_version >= 44: self.event_stages.write_dicts(self.data) self.data.write_int(self.cotc_1_complete) self.data.write_int(69) if self.game_version >= 46: self.gamatoto.write_collab_data(self.data) self.data.write_int(71) if self.game_version < 90300: self.map_resets.write(self.data) self.data.write_int(72) if self.game_version >= 51: self.uncanny.write(self.data) self.data.write_int(76) if self.game_version >= 77: self.catamin_stages.write(self.data) self.data.write_int_list(self.lucky_tickets) self.data.write_bool(self.ub5) self.data.write_int(77) if self.game_version >= 80000: self.officer_pass.write_gold_pass(self.data, self.game_version) self.cats.write_talents(self.data) self.data.write_int(self.np) self.data.write_bool(self.ub6) self.data.write_int(80000) if self.game_version >= 80200: self.data.write_bool(self.ub7) self.data.write_short(self.leadership) self.officer_pass.write_cat_data(self.data) self.data.write_int(80200) if self.game_version >= 80300: self.data.write_byte(self.filibuster_stage_id) self.data.write_bool(self.filibuster_stage_enabled) self.data.write_int(80300) if self.game_version >= 80500: self.data.write_int_list(self.stage_ids_10s) self.data.write_int(80500) if self.game_version >= 80600: self.data.write_short(len(self.uil6)) self.data.write_int_list(self.uil6, write_length=False) self.legend_quest.write(self.data) self.data.write_short(self.ush1) self.data.write_byte(self.uby1) self.data.write_int(80600) if self.game_version >= 80700: self.data.write_int(len(self.uiid1)) for key, value in self.uiid1.items(): self.data.write_int(key) self.data.write_int_list(value) self.data.write_int(80700) if self.game_version >= 100600: if self.is_en(): self.data.write_byte(self.uby2) self.data.write_int(100600) if self.game_version >= 81000: self.data.write_byte(self.restart_pack) self.data.write_int(81000) if self.game_version >= 90000: self.medals.write(self.data) self.wildcat_slots.write(self.data, self.game_version) self.data.write_int(90000) if self.game_version >= 90100: self.data.write_short(self.ush2) self.data.write_short(self.ush3) self.data.write_int(self.ui15) self.data.write_double(self.ud1) self.data.write_int(90100) if self.game_version >= 90300: self.data.write_short(len(self.utl1)) for tuple_ in self.utl1: tuple_len = len(tuple_) i1, i2, i3, i4, i5, i6, i7 = 0, 0, 0, 0, 0, 0, 0 if tuple_len >= 1: i1 = tuple_[0] if tuple_len >= 2: i2 = tuple_[1] if tuple_len >= 3: i3 = tuple_[2] if tuple_len >= 4: i4 = tuple_[3] if tuple_len >= 5: i5 = tuple_[4] if tuple_len >= 6: i6 = tuple_[5] if tuple_len >= 7: i7 = tuple_[6] self.data.write_int(i1) self.data.write_int(i2) self.data.write_short(i3) self.data.write_int(i4) self.data.write_int(i5) self.data.write_int(i6) self.data.write_short(i7) self.data.write_short(len(self.uidd1)) self.data.write_int_double_dict(self.uidd1, write_length=False) self.gauntlets.write(self.data) self.data.write_int(90300) if self.game_version >= 90400: self.enigma_clears.write(self.data) self.enigma.write(self.data, self.game_version) self.cleared_slots.write(self.data) self.data.write_int(90400) if self.game_version >= 90500: self.collab_gauntlets.write(self.data) self.data.write_bool(self.ub8) self.data.write_double(self.ud2) self.data.write_double(self.ud3) self.data.write_int(self.ui16) if self.game_version >= 100300: self.data.write_byte(self.uby3) self.data.write_bool(self.ub9) self.data.write_double(self.ud4) self.data.write_double(self.ud5) if self.game_version >= 130700: self.data.write_short(len(self.uiid3)) for key, value in self.uiid3.items(): self.data.write_int(key) self.data.write_byte(value) self.data.write_short(len(self.uidd2)) for key, value in self.uidd2.items(): self.data.write_int(key) self.data.write_double(value) if self.game_version >= 140100: self.data.write_short(len(self.uidd3)) for key, value in self.uidd3.items(): self.data.write_int(key) self.data.write_double(value) self.data.write_int(90500) if self.game_version >= 90700: self.talent_orbs.write(self.data, self.game_version) self.data.write_short(len(self.uidiid2)) for key, value in self.uidiid2.items(): self.data.write_short(key) self.data.write_byte(len(value)) for key2, value2 in value.items(): self.data.write_byte(key2) self.data.write_short(value2) self.data.write_bool(self.ub10) self.data.write_int(90700) if self.game_version >= 90800: self.data.write_short(len(self.uil7)) self.data.write_int_list(self.uil7, write_length=False) self.data.write_bool_list(self.ubl2, write_length=False, length=10) self.data.write_int(90800) if self.game_version >= 90900: self.cat_shrine.write(self.data) self.data.write_double(self.ud6) self.data.write_double(self.ud7) self.data.write_int(90900) if self.game_version >= 91000: self.lineups.write_slot_names(self.data, self.game_version) self.data.write_int(91000) if self.game_version >= 100000: self.data.write_int(self.legend_tickets) self.data.write_byte(len(self.uiil1)) for key, value in self.uiil1: self.data.write_byte(key) self.data.write_int(value) self.data.write_bool(self.ub11) self.data.write_bool(self.ub12) self.data.write_string(self.password_refresh_token) self.data.write_bool(self.ub13) self.data.write_byte(self.uby4) self.data.write_byte(self.uby5) self.data.write_double(self.ud8) self.data.write_double(self.ud9) self.data.write_int(100000) if self.game_version >= 100100: self.data.write_int(self.date_int) self.data.write_int(100100) if self.game_version >= 100300: self.battle_items.write_endless_items(self.data) self.data.write_int(100300) if self.game_version >= 100400: self.data.write_byte(len(self.event_capsules_2)) self.data.write_int_list(self.event_capsules_2, write_length=False) self.data.write_bool(self.two_battle_lines) self.data.write_int(100400) if self.game_version >= 100600: self.data.write_double(self.ud10) self.data.write_int(self.platinum_shards) self.data.write_bool(self.ub15) self.data.write_int(100600) if self.game_version >= 100700: self.cat_scratcher.write(self.data, self.game_version) self.data.write_int(100700) if self.game_version >= 100900: self.aku.write(self.data) self.data.write_bool(self.ub16) self.data.write_bool(self.ub17) self.data.write_short(len(self.ushdshd2)) for key, value in self.ushdshd2.items(): self.data.write_short(key) self.data.write_short(len(value)) for item in value: self.data.write_short(item) self.data.write_short(len(self.ushdd)) for key, value in self.ushdd.items(): self.data.write_short(key) self.data.write_double(value) self.data.write_short(len(self.ushdd2)) for key, value in self.ushdd2.items(): self.data.write_short(key) self.data.write_double(value) self.data.write_bool(self.ub18) self.data.write_int(100900) if self.game_version >= 101000: self.data.write_byte(self.uby6) self.data.write_int(101000) if self.game_version >= 110000: self.data.write_short(len(self.uidtii)) for key, value in self.uidtii.items(): self.data.write_int(key) self.data.write_byte(value[0]) self.data.write_byte(value[1]) self.data.write_int(110000) if self.game_version >= 110500: self.behemoth_culling.write(self.data) self.data.write_bool(self.ub19) self.data.write_int(110500) if self.game_version >= 110600: self.data.write_bool(self.ub20) self.data.write_int(110600) if self.game_version >= 110700: self.data.write_int(len(self.uidtff)) for key, value in self.uidtff.items(): self.data.write_int(key) self.data.write_double(value[0]) self.data.write_double(value[1]) if self.not_jp(): self.data.write_bool(self.ub20) self.data.write_int(110700) if self.game_version >= 110800: self.cat_shrine.write_dialogs(self.data) self.data.write_bool(self.ub21) self.data.write_bool(self.dojo_3x_speed) self.data.write_bool(self.ub22) self.data.write_bool(self.ub23) self.data.write_int(110800) if self.game_version >= 111000: self.data.write_int(self.ui17) self.data.write_short(self.ush4) self.data.write_byte(self.uby7) self.data.write_byte(self.uby8) self.data.write_bool(self.ub24) self.data.write_byte(self.uby9) self.data.write_byte(len(self.ushl1)) self.data.write_short_list(self.ushl1, write_length=False) self.data.write_short(len(self.ushl2)) self.data.write_short_list(self.ushl2, write_length=False) self.data.write_short(len(self.ushl3)) self.data.write_short_list(self.ushl3, write_length=False) self.data.write_int(self.ui18) self.data.write_int(self.ui19) self.data.write_int(self.ui20) self.data.write_short(self.ush5) self.data.write_short(self.ush6) self.data.write_short(self.ush7) self.data.write_short(self.ush8) self.data.write_byte(self.uby10) self.data.write_bool(self.ub25) self.data.write_bool(self.ub26) self.data.write_bool(self.ub27) self.data.write_bool(self.ub28) self.data.write_bool(self.ub29) self.data.write_bool(self.ub30) self.data.write_byte(self.uby11) self.data.write_short(len(self.ushl4)) self.data.write_short_list(self.ushl4, write_length=False) self.data.write_bool_list(self.ubl3, write_length=False, length=14) self.data.write_byte(len(self.labyrinth_medals)) self.data.write_short_list(self.labyrinth_medals, write_length=False) self.data.write_int(111000) if self.game_version >= 120000: self.zero_legends.write(self.data) self.data.write_byte(self.uby12) self.data.write_int(120000) if self.game_version >= 120100: self.data.write_short(len(self.ushl6)) self.data.write_short_list(self.ushl6, write_length=False) self.data.write_int(120100) if self.game_version >= 120200: self.data.write_bool(self.ub31) self.data.write_short(self.ush9) self.data.write_byte(len(self.ushshd)) for key, value in self.ushshd.items(): self.data.write_short(key) self.data.write_short(value) self.data.write_int(120200) if self.game_version >= 120400: self.data.write_double(self.ud11) self.data.write_double(self.ud12) self.data.write_int(120400) if self.game_version >= 120500: self.data.write_bool(self.ub32) self.data.write_bool(self.ub33) self.data.write_bool(self.ub34) self.data.write_int(self.ui21) self.data.write_byte(self.golden_cpu_count) self.data.write_int(120500) if self.game_version >= 120600: self.data.write_byte(self.sound_effects_volume) self.data.write_byte(self.background_music_volume) self.data.write_int(120600) if (self.not_jp() and self.game_version >= 120700) or ( self.is_jp() and self.game_version >= 130000 ): self.data.write_byte(len(self.ustl1)) for str1, str2 in self.ustl1: self.data.write_string(str1) self.data.write_string(str2) if self.not_jp(): self.data.write_int(120700) else: self.data.write_int(130000) if self.game_version >= 130100: self.data.write_int(len(self.utl3)) for i, long in self.utl3: self.data.write_int(i) self.data.write_long(long) self.data.write_int(130100) if self.game_version >= 130301: self.data.write_int(len(self.ustid1)) for key, (v1, v2) in self.ustid1.items(): self.data.write_string(key) self.data.write_int(v1) self.data.write_double(v2) self.data.write_int(130301) if self.game_version >= 130400: self.data.write_double(self.ud13) self.data.write_double(self.ud14) self.data.write_int(130400) if self.game_version >= 130500: self.data.write_short(len(self.utl4)) for id, ls2 in self.utl4: self.data.write_byte(id) self.data.write_byte(len(ls2)) for v1, v2, v3, ls1 in ls2: self.data.write_byte(v1) self.data.write_byte(v2) self.data.write_byte(v3) self.data.write_short(len(ls1)) for val in ls1: self.data.write_short(val) self.data.write_int(130500) if self.game_version >= 130600: self.data.write_byte(self.uby14) if self.not_jp(): self.data.write_short(self.ush12) self.data.write_int(130600) if self.game_version >= 130700: if self.is_jp(): self.data.write_short(self.ush12) self.data.write_double(self.ud15) self.data.write_byte(self.uby15) self.data.write_byte(self.uby16) self.data.write_short(self.ush11) self.data.write_byte(self.uby17) self.data.write_byte(self.uby18) self.data.write_byte(self.uby19) self.data.write_double(self.ud16) self.data.write_short(len(self.ushd1)) for key, (value, value_2, data_2) in self.ushd1.items(): self.data.write_short(key) self.data.write_short(value) self.data.write_int(value_2) self.data.write_short(len(data_2)) for key2, value3 in data_2.items(): self.data.write_short(key2) self.data.write_short(value3) self.data.write_int(130700) if self.game_version >= 140000: self.data.write_int(self.ui22) self.data.write_double(self.ud17) self.data.write_byte(self.uby20) self.data.write_byte(len(self.uild1)) for key, value in self.uild1.items(): self.data.write_int(key) self.data.write_byte(len(value)) for val in value: self.data.write_byte(val) self.dojo_chapters.write(self.data) self.data.write_short(len(self.uil9)) for val in self.uil9: self.data.write_int(val) self.data.write_bool(self.ub35) self.data.write_double(self.ud18) self.data.write_short(len(self.ushd2)) for key, value in self.ushd2.items(): self.data.write_short(key) self.data.write_byte(value) self.data.write_int(140000) if self.game_version >= 140100 and self.game_version < 140500: self.data.write_byte(self.uby21) self.data.write_int(140100) if self.game_version >= 140200: self.data.write_byte(len(self.uil10)) for v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13 in self.uil10: self.data.write_int(v1) self.data.write_int(v2) self.data.write_bool(v3) self.data.write_bool(v4) self.data.write_bool(v5) self.data.write_int(v6) self.data.write_int(v7) self.data.write_int(v8) self.data.write_bool(v9) self.data.write_bool(v10) self.data.write_bool(v11) if self.game_version >= 140500: # game seems to write more than this, may not work with all saves self.data.write_string(v12 or "") self.data.write_bool(v13) self.data.write_byte(len(self.uid1)) for key, value in self.uid1.items(): self.data.write_int(key) self.data.write_double(value) self.data.write_int(self.hundred_million_ticket) self.data.write_int(140200) if self.game_version >= 140300: self.data.write_byte(len(self.uil11)) for val in self.uil11: self.data.write_byte(val) if self.game_version >= 150300: self.data.write_int(self.ui24) self.data.write_bool(self.ub38) self.data.write_bool(self.ub36) self.data.write_byte(len(self.treasure_chests)) for val in self.treasure_chests: self.data.write_int(val) self.data.write_int(self.ui23) self.data.write_short(len(self.uil13)) for val in self.uil13: self.data.write_int(val) self.data.write_bool(self.ub37) self.data.write_int(140300) self.data.write_bytes(self.remaining_data) self.data.end_buffer() def to_data(self) -> core.Data: dt = core.Data() self.save_wrapper(dt) self.set_hash(add=True) return dt def save_wrapper(self, data: core.Data) -> None: try: self.save(data) except Exception as e: raise FailedToSaveError( core.core_data.local_manager.get_key("failed_to_save_save") ) from e def to_file_thread(self, path: core.Path): core.Thread("to_file", self.to_file, [path]).start() def to_file(self, path: core.Path) -> None: path.parent().generate_dirs() dt = self.to_data() try: dt.to_file(path) except Exception as e: print(e) @staticmethod def get_temp_path() -> core.Path: save_temp_path = core.Path.get_data_folder().add("save.temp") save_temp_path.parent().generate_dirs() return save_temp_path def to_dict(self) -> dict[str, Any]: data: dict[str, Any] = { "editor_version": __version__, "cc": self.cc.get_code(), "dsts": self.dsts, "game_version": self.game_version.game_version, "ub1": self.ub1, "mute_bgm": self.mute_bgm, "mute_se": self.mute_se, "catfood": self.catfood, "current_energy": self.current_energy, "year": self.year, "month": self.month, "day": self.day, "timestamp": self.timestamp, "date": self.date.timestamp(), "ui1": self.ui1, "stamp_value_save": self.stamp_value_save, "ui2": self.ui2, "upgrade_state": self.upgrade_state, "xp": self.xp, "tutorial_state": self.tutorial_state, "ui3": self.ui3, "koreaSuperiorTreasureState": self.koreaSuperiorTreasureState, "unlock_popups_11": self.unlock_popups_11, "ui5": self.ui5, "unlock_enemy_guide": self.unlock_enemy_guide, "ui6": self.ui6, "ub0": self.ub0, "ui7": self.ui7, "cleared_eoc_1": self.cleared_eoc_1, "ui8": self.ui8, "unlocked_ending": self.unlocked_ending, "lineups": self.lineups.serialize(), "stamp_data": self.stamp_data.serialize(), "story": self.story.serialize(), "enemy_guide": self.enemy_guide, "cats": self.cats.serialize(), "special_skills": self.special_skills.serialize(), "menu_unlocks": self.menu_unlocks, "unlock_popups_0": self.unlock_popups_0, "battle_items": self.battle_items.serialize(), "new_dialogs_2": self.new_dialogs_2, "uil1": self.uil1, "moneko_bonus": self.moneko_bonus, "daily_reward_initialized": self.daily_reward_initialized, "date_2": self.date_2.timestamp(), "date_3": self.date_3.timestamp(), "ui0": self.ui0, "stage_unlock_cat_value": self.stage_unlock_cat_value, "show_ending_value": self.show_ending_value, "chapter_clear_cat_unlock": self.chapter_clear_cat_unlock, "ui9": self.ui9, "ios_android_month": self.ios_android_month, "ui10": self.ui10, "save_data_4_hash": self.save_data_4_hash, "mysale": self.mysale.serialize(), "chara_flags": self.chara_flags, "uim1": self.uim1, "ubm1": self.ubm1, "chara_flags_2": self.chara_flags_2, "normal_tickets": self.normal_tickets, "rare_tickets": self.rare_tickets, "event_stages": self.event_stages.serialize(), "itf1_ending": self.itf1_ending, "continue_flag": self.continue_flag, "unlock_popups_8": self.unlock_popups_8, "unit_drops": self.unit_drops, "gatya": self.gatya.serialize(), "get_event_data": self.get_event_data, "achievements": self.achievements, "os_value": self.os_value, "date_4": self.date_4.timestamp(), "player_id": self.player_id, "order_ids": self.order_ids, "g_timestamp": self.g_timestamp, "g_servertimestamp": self.g_servertimestamp, "m_gettimesave": self.m_gettimesave, "usl1": self.usl1, "energy_notification": self.energy_notification, "full_gameversion": self.full_gameversion, "uil2": self.uil2, "uil3": self.uil3, "uil4": self.uil4, "g_timestamp_2": self.g_timestamp_2, "g_servertimestamp_2": self.g_servertimestamp_2, "m_gettimesave_2": self.m_gettimesave_2, "unknown_timestamp": self.unknown_timestamp, "usl2": self.usl2, "m_dGetTimeSave2": self.m_dGetTimeSave2, "ui11": self.ui11, "ubl1": self.ubl1, "user_rank_rewards": self.user_rank_rewards.serialize(), "transfer_code": self.transfer_code, "confirmation_code": self.confirmation_code, "transfer_flag": self.transfer_flag, "item_reward_stages": self.item_reward_stages.serialize(), "timed_score_stages": self.timed_score_stages.serialize(), "inquiry_code": self.inquiry_code, "officer_pass": self.officer_pass.serialize(), "has_account": self.has_account, "backup_state": self.backup_state, "ub2": self.ub2, "itf1_complete": self.itf1_complete, "title_chapter_bg": self.title_chapter_bg, "combo_unlocks": self.combo_unlocks, "combo_unlocked_10k_ur": self.combo_unlocked_10k_ur, "event_capsules": self.event_capsules, "event_capsules_counter": self.event_capsules_counter, "m_dGetTimeSave3": self.m_dGetTimeSave3, "gatya_seen_lucky_drops": self.gatya_seen_lucky_drops, "banned": self.show_ban_message, "catfood_beginner_purchased": self.catfood_beginner_purchased, "next_week_timestamp": self.next_week_timestamp, "catfood_beginner_expired": self.catfood_beginner_expired, "rank_up_sale_value": self.rank_up_sale_value, "time_since_time_check_cumulative": self.time_since_time_check_cumulative, "server_timestamp": self.server_timestamp, "last_checked_energy_recovery_time": self.last_checked_energy_recovery_time, "time_since_check": self.time_since_check, "last_checked_expedition_time": self.last_checked_expedition_time, "catfruit": self.catfruit, "catseyes": self.catseyes, "catamins": self.catamins, "gamatoto": self.gamatoto.serialize(), "unlock_popups_6": self.unlock_popups_6, "ex_stages": self.ex_stages.serialize(), "item_pack": self.item_pack.serialize(), "platinum_tickets": self.platinum_tickets, "logins": self.logins.serialize(), "reset_item_reward_flags": self.reset_item_reward_flags, "reward_remaining_time": self.reward_remaining_time, "last_checked_reward_time": self.last_checked_reward_time, "announcements": self.announcements, "backup_counter": self.backup_counter, "ui12": self.ui12, "ui13": self.ui13, "ui14": self.ui14, "ub3": self.ub3, "backup_frame": self.backup_frame, "ub4": self.ub4, "dojo": self.dojo.serialize(), "last_checked_zombie_time": self.last_checked_zombie_time, "outbreaks": self.outbreaks.serialize(), "scheme_items": self.scheme_items.serialize(), "first_locks": self.first_locks, "energy_penalty_timestamp": self.energy_penalty_timestamp, "shown_maxcollab_mg": self.shown_maxcollab_mg, "unlock_popups": self.unlock_popups.serialize(), "ototo": self.ototo.serialize(), "last_checked_castle_time": self.last_checked_castle_time, "beacon_base": self.beacon_base.serialize(), "tower": self.tower.serialize(), "missions": self.missions.serialize(), "challenge": self.challenge.serialize(), "event_update_flags": self.event_update_flags, "cotc_1_complete": self.cotc_1_complete, "map_resets": self.map_resets.serialize(), "uncanny": self.uncanny.serialize(), "catamin_stages": self.catamin_stages.serialize(), "lucky_tickets": self.lucky_tickets, "ub5": self.ub5, "np": self.np, "ub6": self.ub6, "ub7": self.ub7, "leadership": self.leadership, "filibuster_stage_id": self.filibuster_stage_id, "filibuster_stage_enabled": self.filibuster_stage_enabled, "stage_ids_10s": self.stage_ids_10s, "uil6": self.uil6, "legend_quest": self.legend_quest.serialize(), "ush1": self.ush1, "uby1": self.uby1, "uiid1": self.uiid1, "uby2": self.uby2, "restart_pack": self.restart_pack, "medals": self.medals.serialize(), "wildcat_slots": self.wildcat_slots.serialize(), "ush2": self.ush2, "ush3": self.ush3, "ui15": self.ui15, "ud1": self.ud1, "utl1": self.utl1, "uidd1": self.uidd1, "gauntlets": self.gauntlets.serialize(), "enigma_clears": self.enigma_clears.serialize(), "enigma": self.enigma.serialize(), "cleared_slots": self.cleared_slots.serialize(), "collab_gauntlets": self.collab_gauntlets.serialize(), "ub8": self.ub8, "ud2": self.ud2, "ud3": self.ud3, "ui16": self.ui16, "uby3": self.uby3, "ub9": self.ub9, "ud4": self.ud4, "ud5": self.ud5, "uiid3": self.uiid3, "uidd2": self.uidd2, "uidd3": self.uidd3, "talent_orbs": self.talent_orbs.serialize(), "uidiid2": self.uidiid2, "ub10": self.ub10, "uil7": self.uil7, "ubl2": self.ubl2, "cat_shrine": self.cat_shrine.serialize(), "ud6": self.ud6, "ud7": self.ud7, "legend_tickets": self.legend_tickets, "uiil1": self.uiil1, "ub11": self.ub11, "ub12": self.ub12, "password_refresh_token": self.password_refresh_token, "ub13": self.ub13, "uby4": self.uby4, "uby5": self.uby5, "ud8": self.ud8, "ud9": self.ud9, "date_int": self.date_int, "event_capsules_2": self.event_capsules_2, "two_battle_lines": self.two_battle_lines, "ud10": self.ud10, "platinum_shards": self.platinum_shards, "ub15": self.ub15, "cat_scratcher": self.cat_scratcher.serialize(), "aku": self.aku.serialize(), "ub16": self.ub16, "ub17": self.ub17, "ushdshd2": self.ushdshd2, "ushdd": self.ushdd, "ushdd2": self.ushdd2, "ub18": self.ub18, "uby6": self.uby6, "uidtii": self.uidtii, "behemoth_culling": self.behemoth_culling.serialize(), "ub19": self.ub19, "ub20": self.ub20, "uidtff": self.uidtff, "ub21": self.ub21, "dojo_3x_speed": self.dojo_3x_speed, "ub22": self.ub22, "ub23": self.ub23, "ui17": self.ui17, "ush4": self.ush4, "uby7": self.uby7, "uby8": self.uby8, "ub24": self.ub24, "uby9": self.uby9, "ushl1": self.ushl1, "ushl2": self.ushl2, "ushl3": self.ushl3, "ui18": self.ui18, "ui19": self.ui19, "ui20": self.ui20, "ush5": self.ush5, "ush6": self.ush6, "ush7": self.ush7, "ush8": self.ush8, "uby10": self.uby10, "ub25": self.ub25, "ub26": self.ub26, "ub27": self.ub27, "ub28": self.ub28, "ub29": self.ub29, "ub30": self.ub30, "uby11": self.uby11, "ushl4": self.ushl4, "ubl3": self.ubl3, "labyrinth_medals": self.labyrinth_medals, "zero_legends": self.zero_legends.serialize(), "uby12": self.uby12, "ushl6": self.ushl6, "ub31": self.ub31, "ush9": self.ush9, "ushshd": self.ushshd, "ud11": self.ud11, "ud12": self.ud12, "ub32": self.ub32, "ub33": self.ub33, "ub34": self.ub34, "ui21": self.ui21, "golden_cpu_count": self.golden_cpu_count, "sound_effects_volume": self.sound_effects_volume, "background_music_volume": self.background_music_volume, "ustl1": self.ustl1, "utl3": self.utl3, "ustid1": self.ustid1, "ud13": self.ud13, "ud14": self.ud14, "utl4": self.utl4, "uby14": self.uby14, "ush12": self.ush12, "ud15": self.ud15, "uby15": self.uby15, "uby16": self.uby16, "ush11": self.ush11, "uby17": self.uby17, "uby18": self.uby18, "uby19": self.uby19, "ud16": self.ud16, "ushd1": self.ushd1, "ui22": self.ui22, "ud17": self.ud17, "uby20": self.uby20, "uild1": self.uild1, "dojo_chapters": self.dojo_chapters.serialize(), "uil9": self.uil9, "ub35": self.ub35, "ud18": self.ud18, "ushd2": self.ushd2, "uby21": self.uby21, "uil10": self.uil10, "uid1": self.uid1, "hundred_million_ticket": self.hundred_million_ticket, "uil11": self.uil11, "ub36": self.ub36, "treasure_chests": self.treasure_chests, "ui23": self.ui23, "uil13": self.uil13, "ub37": self.ub37, "ub38": self.ub38, "ui24": self.ui24, "remaining_data": base64.b64encode(self.remaining_data).decode("utf-8"), } return data @staticmethod def from_dict(data: dict[str, Any], warn: bool = True) -> SaveFile: editor_version = data.get("editor_version", "0.0.0") if editor_version != __version__ and warn: cli.color.ColoredText.localize( "editor_version_mismatch", json_version=editor_version, editor_version=__version__, ) cc = data.get("cc") if cc is not None: cc = core.CountryCode(cc) else: cc = None save_file = SaveFile(cc=cc) save_file.dsts = data.get("dsts", []) save_file.game_version = core.GameVersion(data.get("game_version", 0)) save_file.ub1 = data.get("ub1", False) save_file.mute_bgm = data.get("mute_bgm", False) save_file.mute_se = data.get("mute_se", False) save_file.catfood = data.get("catfood", 0) save_file.current_energy = data.get("current_energy", 0) save_file.year = data.get("year", 0) save_file.month = data.get("month", 0) save_file.day = data.get("day", 0) save_file.timestamp = data.get("timestamp", 0.0) save_file.date = datetime.datetime.fromtimestamp(data.get("date", 0)) save_file.ui1 = data.get("ui1", 0) save_file.stamp_value_save = data.get("stamp_value_save", 0) save_file.ui2 = data.get("ui2", 0) save_file.upgrade_state = data.get("upgrade_state", 0) save_file.xp = data.get("xp", 0) save_file.tutorial_state = data.get("tutorial_state", 0) save_file.ui3 = data.get("ui3", 0) save_file.koreaSuperiorTreasureState = data.get("koreaSuperiorTreasureState", 0) save_file.unlock_popups_11 = data.get("unlock_popups_11", []) save_file.ui5 = data.get("ui5", 0) save_file.unlock_enemy_guide = data.get("unlock_enemy_guide", 0) save_file.ui6 = data.get("ui6", 0) save_file.ub0 = data.get("ub0", False) save_file.ui7 = data.get("ui7", 0) save_file.cleared_eoc_1 = data.get("cleared_eoc_1", 0) save_file.ui8 = data.get("ui8", 0) save_file.unlocked_ending = data.get("unlocked_ending", 0) save_file.lineups = core.LineUps.deserialize(data.get("lineups", {})) save_file.stamp_data = core.StampData.deserialize(data.get("stamp_data", {})) save_file.story = core.StoryChapters.deserialize(data.get("story", [])) save_file.enemy_guide = data.get("enemy_guide", []) save_file.cats = core.Cats.deserialize(data.get("cats", {})) save_file.special_skills = core.SpecialSkills.deserialize( data.get("special_skills", []) ) save_file.menu_unlocks = data.get("menu_unlocks", []) save_file.unlock_popups_0 = data.get("unlock_popups_0", []) save_file.battle_items = core.BattleItems.deserialize( data.get("battle_items", {}) ) save_file.new_dialogs_2 = data.get("new_dialogs_2", []) save_file.uil1 = data.get("uil1", []) save_file.moneko_bonus = data.get("moneko_bonus", []) save_file.daily_reward_initialized = data.get("daily_reward_initialized", []) save_file.date_2 = datetime.datetime.fromtimestamp(data.get("date_2", 0)) save_file.date_3 = datetime.datetime.fromtimestamp(data.get("date_3", 0)) save_file.ui0 = data.get("ui0", 0) save_file.stage_unlock_cat_value = data.get("stage_unlock_cat_value", 0) save_file.show_ending_value = data.get("show_ending_value", 0) save_file.chapter_clear_cat_unlock = data.get("chapter_clear_cat_unlock", 0) save_file.ui9 = data.get("ui9", 0) save_file.ios_android_month = data.get("ios_android_month", 0) save_file.ui10 = data.get("ui10", 0) save_file.save_data_4_hash = data.get("save_data_4_hash", "") save_file.mysale = core.MySale.deserialize(data.get("mysale", {})) save_file.chara_flags = data.get("chara_flags", []) save_file.uim1 = data.get("uim1", 0) save_file.ubm1 = data.get("ubm1", False) save_file.chara_flags_2 = data.get("chara_flags_2", []) save_file.normal_tickets = data.get("normal_tickets", 0) save_file.rare_tickets = data.get("rare_tickets", 0) save_file.event_stages = core.EventChapters.deserialize( data.get("event_stages", {}) ) save_file.itf1_ending = data.get("itf1_ending", 0) save_file.continue_flag = data.get("continue_flag", 0) save_file.unlock_popups_8 = data.get("unlock_popups_8", []) save_file.unit_drops = data.get("unit_drops", []) save_file.gatya = core.Gatya.deserialize(data.get("gatya", {})) save_file.get_event_data = data.get("get_event_data", False) save_file.achievements = data.get("achievements", []) save_file.os_value = data.get("os_value", 0) save_file.date_4 = datetime.datetime.fromtimestamp(data.get("date_4", 0)) save_file.player_id = data.get("player_id", "") save_file.order_ids = data.get("order_ids", []) save_file.g_timestamp = data.get("g_timestamp", 0.0) save_file.g_servertimestamp = data.get("g_servertimestamp", 0.0) save_file.m_gettimesave = data.get("m_gettimesave", 0.0) save_file.usl1 = data.get("usl1", []) save_file.energy_notification = data.get("energy_notification", False) save_file.full_gameversion = data.get("full_gameversion", 0) save_file.uil2 = data.get("uil2", []) save_file.uil3 = data.get("uil3", []) save_file.uil4 = data.get("uil4", []) save_file.g_timestamp_2 = data.get("g_timestamp_2", 0.0) save_file.g_servertimestamp_2 = data.get("g_servertimestamp_2", 0.0) save_file.m_gettimesave_2 = data.get("m_gettimesave_2", 0.0) save_file.unknown_timestamp = data.get("unknown_timestamp", 0.0) save_file.usl2 = data.get("usl2", []) save_file.m_dGetTimeSave2 = data.get("m_dGetTimeSave2", 0.0) save_file.ui11 = data.get("ui11", 0) save_file.ubl1 = data.get("ubl1", []) save_file.user_rank_rewards = core.UserRankRewards.deserialize( data.get("user_rank_rewards", []) ) save_file.transfer_code = data.get("transfer_code", "") save_file.confirmation_code = data.get("confirmation_code", "") save_file.transfer_flag = data.get("transfer_flag", False) save_file.item_reward_stages = core.ItemRewardChapters.deserialize( data.get("item_reward_stages", {}) ) save_file.timed_score_stages = core.TimedScoreChapters.deserialize( data.get("timed_score_stages", []) ) save_file.inquiry_code = data.get("inquiry_code", "") save_file.officer_pass = core.OfficerPass.deserialize( data.get("officer_pass", {}) ) save_file.has_account = data.get("has_account", False) save_file.backup_state = data.get("backup_state", 0) save_file.ub2 = data.get("ub2", False) save_file.itf1_complete = data.get("itf1_complete", 0) save_file.title_chapter_bg = data.get("title_chapter_bg", 0) save_file.combo_unlocks = data.get("combo_unlocks", []) save_file.combo_unlocked_10k_ur = data.get("combo_unlocked_10k_ur", False) save_file.event_capsules = data.get("event_capsules", []) save_file.event_capsules_counter = data.get("event_capsules_counter", []) save_file.m_dGetTimeSave3 = data.get("m_dGetTimeSave3", 0.0) save_file.gatya_seen_lucky_drops = data.get("gatya_seen_lucky_drops", []) save_file.show_ban_message = data.get("banned", False) save_file.catfood_beginner_purchased = data.get( "catfood_beginner_purchased", [] ) save_file.next_week_timestamp = data.get("next_week_timestamp", 0.0) save_file.catfood_beginner_expired = data.get("catfood_beginner_expired", []) save_file.rank_up_sale_value = data.get("rank_up_sale_value", 0) save_file.time_since_time_check_cumulative = data.get( "time_since_time_check_cumulative", 0.0 ) save_file.server_timestamp = data.get("server_timestamp", 0.0) save_file.last_checked_energy_recovery_time = data.get( "last_checked_energy_recovery_time", 0.0 ) save_file.time_since_check = data.get("time_since_check", 0.0) save_file.last_checked_expedition_time = data.get( "last_checked_expedition_time", 0.0 ) save_file.catfruit = data.get("catfruit", []) save_file.catseyes = data.get("catseyes", []) save_file.catamins = data.get("catamins", []) save_file.gamatoto = core.Gamatoto.deserialize(data.get("gamatoto", {})) save_file.unlock_popups_6 = data.get("unlock_popups_6", []) save_file.ex_stages = core.ExChapters.deserialize(data.get("ex_stages", [])) save_file.item_pack = core.ItemPack.deserialize(data.get("item_pack", {})) save_file.platinum_tickets = data.get("platinum_tickets", 0) save_file.logins = core.LoginBonus.deserialize(data.get("logins", {})) save_file.reset_item_reward_flags = data.get("reset_item_reward_flags", []) save_file.reward_remaining_time = data.get("reward_remaining_time", 0.0) save_file.last_checked_reward_time = data.get("last_checked_reward_time", 0.0) save_file.announcements = data.get("announcements", []) save_file.backup_counter = data.get("backup_counter", 0) save_file.ui12 = data.get("ui12", 0) save_file.ui13 = data.get("ui13", 0) save_file.ui14 = data.get("ui14", 0) save_file.ub3 = data.get("ub3", False) save_file.backup_frame = data.get("backup_frame", 0) save_file.ub4 = data.get("ub4", False) save_file.dojo = core.Dojo.deserialize(data.get("dojo", {})) save_file.last_checked_zombie_time = data.get("last_checked_zombie_time", 0.0) save_file.outbreaks = core.Outbreaks.deserialize(data.get("outbreaks", {})) save_file.scheme_items = core.SchemeItems.deserialize( data.get("scheme_items", {}) ) save_file.first_locks = data.get("first_locks", {}) save_file.energy_penalty_timestamp = data.get("energy_penalty_timestamp", 0.0) save_file.shown_maxcollab_mg = data.get("shown_maxcollab_mg", False) save_file.unlock_popups = core.UnlockPopups.deserialize( data.get("unlock_popups", {}) ) save_file.ototo = core.Ototo.deserialize(data.get("ototo", {})) save_file.last_checked_castle_time = data.get("last_checked_castle_time", 0.0) save_file.beacon_base = core.BeaconEventListScene.deserialize( data.get("beacon_base", {}) ) save_file.tower = core.TowerChapters.deserialize(data.get("tower", {})) save_file.missions = core.Missions.deserialize(data.get("missions", {})) save_file.challenge = core.ChallengeChapters.deserialize( data.get("challenge", {}) ) save_file.event_update_flags = data.get("event_update_flags", []) save_file.cotc_1_complete = data.get("cotc_1_complete", False) save_file.map_resets = core.MapResets.deserialize(data.get("map_resets", {})) save_file.uncanny = core.UncannyChapters.deserialize(data.get("uncanny", {})) save_file.catamin_stages = core.UncannyChapters.deserialize( data.get("catamin_stages", {}) ) save_file.lucky_tickets = data.get("lucky_tickets", []) save_file.ub5 = data.get("ub5", False) save_file.np = data.get("np", 0) save_file.ub6 = data.get("ub6", False) save_file.ub7 = data.get("ub7", False) save_file.leadership = data.get("leadership", 0) save_file.filibuster_stage_id = data.get("filibuster_stage_id", 0) save_file.filibuster_stage_enabled = data.get("filibuster_stage_enabled", False) save_file.stage_ids_10s = data.get("stage_ids_10s", []) save_file.uil6 = data.get("uil6", []) save_file.legend_quest = core.LegendQuestChapters.deserialize( data.get("legend_quest", {}) ) save_file.ush1 = data.get("ush1", 0) save_file.uby1 = data.get("uby1", 0) save_file.uiid1 = data.get("uiid1", {}) save_file.uby2 = data.get("uby2", 0) save_file.restart_pack = data.get("restart_pack", 0) save_file.medals = core.Medals.deserialize(data.get("medals", {})) save_file.wildcat_slots = core.GamblingEvent.deserialize( data.get("wildcat_slots", {}) ) save_file.ush2 = data.get("ush2", 0) save_file.ush3 = data.get("ush3", 0) save_file.ui15 = data.get("ui15", 0) save_file.ud1 = data.get("ud1", 0.0) save_file.utl1 = data.get("utl1", []) save_file.uidd1 = data.get("uidd1", {}) save_file.gauntlets = core.GauntletChapters.deserialize( data.get("gauntlets", {}) ) save_file.enigma_clears = core.GauntletChapters.deserialize( data.get("enigma_clears", {}) ) save_file.enigma = core.Enigma.deserialize(data.get("enigma", {})) save_file.cleared_slots = core.ClearedSlots.deserialize( data.get("cleared_slots", {}) ) save_file.collab_gauntlets = core.GauntletChapters.deserialize( data.get("collab_gauntlets", {}) ) save_file.ub8 = data.get("ub8", False) save_file.ud2 = data.get("ud2", 0.0) save_file.ud3 = data.get("ud3", 0.0) save_file.ui16 = data.get("ui16", 0) save_file.uby3 = data.get("uby3", 0) save_file.ub9 = data.get("ub9", False) save_file.ud4 = data.get("ud4", 0.0) save_file.ud5 = data.get("ud5", 0.0) save_file.uiid3 = data.get("uiid3", {}) save_file.uidd2 = data.get("uidd2", {}) save_file.uidd3 = data.get("uidd3", {}) save_file.talent_orbs = core.TalentOrbs.deserialize(data.get("talent_orbs", {})) save_file.uidiid2 = data.get("uidiid2", {}) save_file.ub10 = data.get("ub10", False) save_file.uil7 = data.get("uil7", []) save_file.ubl2 = data.get("ubl2", []) save_file.cat_shrine = core.CatShrine.deserialize(data.get("cat_shrine", {})) save_file.ud6 = data.get("ud6", 0.0) save_file.ud7 = data.get("ud7", 0) save_file.legend_tickets = data.get("legend_tickets", 0) save_file.uiil1 = data.get("uiil1", []) save_file.ub11 = data.get("ub11", False) save_file.ub12 = data.get("ub12", False) save_file.password_refresh_token = data.get("password_refresh_token", "") save_file.ub13 = data.get("ub13", False) save_file.uby4 = data.get("uby4", 0) save_file.uby5 = data.get("uby5", 0) save_file.ud8 = data.get("ud8", 0.0) save_file.ud9 = data.get("ud9", 0.0) save_file.date_int = data.get("date_int", 0) save_file.event_capsules_2 = data.get("event_capsules_2", []) save_file.two_battle_lines = data.get("two_battle_lines", False) save_file.ud10 = data.get("ud10", 0.0) save_file.platinum_shards = data.get("platinum_shards", 0) save_file.ub15 = data.get("ub15", False) save_file.cat_scratcher = core.GamblingEvent.deserialize( data.get("cat_scratcher", {}) ) save_file.aku = core.AkuChapters.deserialize(data.get("aku", {})) save_file.ub16 = data.get("ub16", False) save_file.ub17 = data.get("ub17", False) save_file.ushdshd2 = data.get("ushdshd2", {}) save_file.ushdd = data.get("ushdd", {}) save_file.ushdd2 = data.get("ushdd2", {}) save_file.ub18 = data.get("ub18", False) save_file.uby6 = data.get("uby6", 0) save_file.uidtii = data.get("uidtii", {}) save_file.behemoth_culling = core.GauntletChapters.deserialize( data.get("behemoth_culling", {}) ) save_file.ub19 = data.get("ub19", False) save_file.ub20 = data.get("ub20", False) save_file.uidtff = data.get("uidtff", {}) save_file.ub21 = data.get("ub21", False) save_file.dojo_3x_speed = data.get("dojo_3x_speed", False) save_file.ub22 = data.get("ub22", False) save_file.ub23 = data.get("ub23", False) save_file.ui17 = data.get("ui17", 0) save_file.ush4 = data.get("ush4", 0) save_file.uby7 = data.get("uby7", 0) save_file.uby8 = data.get("uby8", 0) save_file.ub24 = data.get("ub24", False) save_file.uby9 = data.get("uby9", 0) save_file.ushl1 = data.get("ushl1", []) save_file.ushl2 = data.get("ushl2", []) save_file.ushl3 = data.get("ushl3", []) save_file.ui18 = data.get("ui18", 0) save_file.ui19 = data.get("ui19", 0) save_file.ui20 = data.get("ui20", 0) save_file.ush5 = data.get("ush5", 0) save_file.ush6 = data.get("ush6", 0) save_file.ush7 = data.get("ush7", 0) save_file.ush8 = data.get("ush8", 0) save_file.uby10 = data.get("uby10", 0) save_file.ub25 = data.get("ub25", False) save_file.ub26 = data.get("ub26", False) save_file.ub27 = data.get("ub27", False) save_file.ub28 = data.get("ub28", False) save_file.ub29 = data.get("ub29", False) save_file.ub30 = data.get("ub30", False) save_file.uby11 = data.get("uby11", 0) save_file.ushl4 = data.get("ushl4", []) save_file.ubl3 = data.get("ubl3", []) save_file.labyrinth_medals = data.get("labyrinth_medals", []) save_file.zero_legends = core.ZeroLegendsChapters.deserialize( data.get("zero_legends", []) ) save_file.uby12 = data.get("uby12", 0) save_file.ushl6 = data.get("ushl6", []) save_file.ub31 = data.get("ub31", False) save_file.ush9 = data.get("ush9", 0) save_file.ushshd = data.get("ushshd", {}) save_file.ud11 = data.get("ud11", 0.0) save_file.ud12 = data.get("ud12", 0.0) save_file.ub32 = data.get("ub32", False) save_file.ub33 = data.get("ub33", False) save_file.ub34 = data.get("ub34", False) save_file.ui21 = data.get("ui21", 0) save_file.golden_cpu_count = data.get("golden_cpu_count", 0) save_file.sound_effects_volume = data.get("sound_effects_volume", 0) save_file.background_music_volume = data.get("background_music_volume", 0) save_file.ustl1 = data.get("ustl1", []) save_file.utl3 = data.get("utl3", []) save_file.ustid1 = data.get("ustid1", {}) save_file.ud13 = data.get("ud13", 0.0) save_file.ud14 = data.get("ud14", 0.0) save_file.utl4 = data.get("utl4", []) save_file.uby14 = data.get("uby14", 0) save_file.ush12 = data.get("ush12", 0) save_file.ud15 = data.get("ud15", 0.0) save_file.uby15 = data.get("uby15", 0) save_file.uby16 = data.get("uby16", 0) save_file.ush11 = data.get("ush11", 0) save_file.uby17 = data.get("uby17", 0) save_file.uby18 = data.get("uby18", 0) save_file.uby19 = data.get("uby19", 0) save_file.ud16 = data.get("ud16", 0.0) save_file.ushd1 = data.get("ushd1", {}) save_file.ui22 = data.get("ui22", 0) save_file.ud17 = data.get("ud17", 0.0) save_file.uby20 = data.get("uby20", 0) save_file.uild1 = data.get("uild1", {}) save_file.dojo_chapters = core.ZeroLegendsChapters.deserialize( data.get("dojo_chapters", []) ) save_file.uil9 = data.get("uil9", []) save_file.ub35 = data.get("ub35", False) save_file.ud18 = data.get("ud18", 0.0) save_file.ushd2 = data.get("ushd2", {}) save_file.uil10 = data.get("uil10", []) save_file.uid1 = data.get("uid1", {}) save_file.hundred_million_ticket = data.get("hundred_million_ticket", 0) save_file.uil11 = data.get("uil11", []) save_file.ub36 = data.get("ub36", False) save_file.treasure_chests = data.get("treasure_chests", []) save_file.ui23 = data.get("ui23", 0) save_file.uil13 = data.get("uil13", []) save_file.ub37 = data.get("ub37", False) save_file.ub38 = data.get("ub38", False) save_file.ui24 = data.get("ui24", 0) save_file.remaining_data = base64.b64decode(data.get("remaining_data", "")) return save_file def init_save(self, gv: core.GameVersion | None = None): self.dsts = [] self.dst_index = 0 if gv is None: gv = core.GameVersion(120200) self.set_gv(gv) self.ubm1 = False self.ubm = False self.ub0 = False self.ub1 = False self.ub2 = False self.ub3 = False self.ub4 = False self.ub5 = False self.ub6 = False self.ub7 = False self.ub8 = False self.ub9 = False self.ub10 = False self.ub11 = False self.ub12 = False self.ub13 = False self.ub15 = False self.ub16 = False self.ub17 = False self.ub18 = False self.ub19 = False self.ub20 = False self.ub21 = False self.ub22 = False self.ub23 = False self.ub24 = False self.ub25 = False self.ub26 = False self.ub27 = False self.ub28 = False self.ub29 = False self.ub30 = False self.ub31 = False self.ub32 = False self.ub33 = False self.ub34 = False self.ub35 = False self.ub36 = False self.ub37 = False self.ub38 = False self.mute_bgm = False self.mute_se = False self.get_event_data = False self.energy_notification = False self.transfer_flag = False self.combo_unlocked_10k_ur = False self.show_ban_message = False self.shown_maxcollab_mg = False self.event_update_flags = False self.filibuster_stage_enabled = False self.dojo_3x_speed = False self.two_battle_lines = False self.uim1 = 0 self.ui0 = 0 self.ui1 = 0 self.ui2 = 0 self.ui3 = 0 self.ui4 = 0 self.ui5 = 0 self.ui6 = 0 self.ui7 = 0 self.ui8 = 0 self.ui9 = 0 self.ui10 = 0 self.ui11 = 0 self.ui12 = 0 self.ui13 = 0 self.ui14 = 0 self.ui15 = 0 self.ui16 = 0 self.ui17 = 0 self.ui18 = 0 self.ui19 = 0 self.ui20 = 0 self.ui21 = 0 self.ui22 = 0 self.ui23 = 0 self.ui24 = 0 self.catfood = 0 self.current_energy = 0 self.year = 0 self.month = 0 self.day = 0 self.stamp_value_save = 0 self.upgrade_state = 0 self.xp = 0 self.tutorial_state = 0 self.koreaSuperiorTreasureState = 0 self.unlock_enemy_guide = 0 self.cleared_eoc_1 = 0 self.unlocked_ending = 0 self.stage_unlock_cat_value = 0 self.show_ending_value = 0 self.chapter_clear_cat_unlock = 0 self.ios_android_month = 0 self.normal_tickets = 0 self.rare_tickets = 0 self.itf1_ending = 0 self.continue_flag = 0 self.os_value = 0 self.full_gameversion = 0 self.backup_state = 0 self.itf1_complete = 0 self.title_chapter_bg = 0 self.rank_up_sale_value = 0 self.platinum_tickets = 0 self.backup_counter = 0 self.backup_frame = 0 self.cotc_1_complete = 0 self.np = 0 self.legend_tickets = 0 self.date_int = 0 self.platinum_shards = 0 self.sound_effects_volume = 0 self.background_music_volume = 0 self.hundred_million_ticket = 0 self.ud1 = 0.0 self.ud2 = 0.0 self.ud3 = 0.0 self.ud4 = 0.0 self.ud5 = 0.0 self.ud6 = 0.0 self.ud7 = 0.0 self.ud8 = 0.0 self.ud9 = 0.0 self.ud10 = 0.0 self.ud11 = 0.0 self.ud12 = 0.0 self.ud13 = 0.0 self.ud14 = 0.0 self.ud15 = 0.0 self.ud16 = 0.0 self.ud17 = 0.0 self.ud18 = 0.0 self.timestamp = 0.0 self.g_timestamp = 0.0 self.g_servertimestamp = 0.0 self.m_gettimesave = 0.0 self.g_timestamp_2 = 0.0 self.g_servertimestamp_2 = 0.0 self.m_gettimesave_2 = 0.0 self.unknown_timestamp = 0.0 self.m_dGetTimeSave2 = 0.0 self.m_dGetTimeSave3 = 0.0 self.next_week_timestamp = 0.0 self.time_since_time_check_cumulative = 0.0 self.server_timestamp = 0.0 self.last_checked_energy_recovery_time = 0.0 self.time_since_check = 0.0 self.last_checked_expedition_time = 0.0 self.reward_remaining_time = 0.0 self.last_checked_reward_time = 0.0 self.last_checked_zombie_time = 0.0 self.energy_penalty_timestamp = 0.0 self.last_checked_castle_time = 0.0 self.date = datetime.datetime.fromtimestamp(0) self.date_2 = datetime.datetime.fromtimestamp(0) self.date_3 = datetime.datetime.fromtimestamp(0) self.date_4 = datetime.datetime.fromtimestamp(0) self.uil1 = [0] * 20 self.uil2 = [0] * 7 self.uil3 = [0] * 7 self.uil4 = [0] * 7 self.stage_ids_10s = [] self.uil6 = [] self.uil7 = [] self.event_capsules_2 = [] self.uil9 = [] self.uil10 = [] self.uil11 = [] self.treasure_chests = [] self.uil13 = [] self.uiil1 = [] self.usl1 = [] self.usl2 = [] self.ustl1 = [] if 20 <= gv and gv <= 25: self.ubl1 = [False] * 12 else: self.ubl1 = [] self.ubl2 = [False] * 10 self.ubl3 = [False] * 14 self.ushl1 = [] self.ushl2 = [] self.ushl3 = [] self.ushl4 = [] self.ushl6 = [] self.utl1 = [] self.utl3 = [] self.utl4 = [] self.unlock_popups_11 = [0] * 3 if 20 <= gv and gv <= 25: self.enemy_guide = [0] * 231 else: self.enemy_guide = [] if gv <= 25: self.menu_unlocks = [0] * 5 self.unlock_popups_0 = [0] * 5 elif gv <= 26: self.menu_unlocks = [0] * 6 self.unlock_popups_0 = [0] * 6 else: self.menu_unlocks = [] self.unlock_popups_0 = [] if gv <= 26: self.new_dialogs_2 = [0] * 17 else: self.new_dialogs_2 = [] self.moneko_bonus = [0] * 1 self.daily_reward_initialized = [0] * 1 self.chara_flags = [0] * 2 self.chara_flags_2 = [0] * 2 self.unlock_popups_8 = [0] * 36 if 20 <= gv and gv <= 25: self.unit_drops = [0] * 10 else: self.unit_drops = [] self.achievements = [False] * 7 self.order_ids = [] self.combo_unlocks = [] if gv < 34: self.event_capsules = [0] * 100 self.event_capsules_counter = [0] * 100 else: self.event_capsules = [] self.event_capsules_counter = [] if gv < 26: self.gatya_seen_lucky_drops = [0] * 44 else: self.gatya_seen_lucky_drops = [] self.catfood_beginner_purchased = [False] * 3 self.catfood_beginner_expired = [False] * 3 self.catfruit = [] self.catseyes = [] self.catamins = [] self.unlock_popups_6 = [] self.reset_item_reward_flags = [] self.announcements = [(0, 0)] * 16 self.lucky_tickets = [] self.labyrinth_medals = [] self.save_data_4_hash = "" self.player_id = "" self.transfer_code: str = "" self.confirmation_code: str = "" self.inquiry_code: str = "" self.password_refresh_token: str = "" self.uby1 = 0 self.uby2 = 0 self.uby3 = 0 self.uby4 = 0 self.uby5 = 0 self.uby6 = 0 self.uby7 = 0 self.uby8 = 0 self.uby9 = 0 self.uby10 = 0 self.uby11 = 0 self.uby12 = 0 self.golden_cpu_count = 0 self.uby14 = 0 self.uby15 = 0 self.uby16 = 0 self.uby17 = 0 self.uby18 = 0 self.uby19 = 0 self.uby20 = 0 self.uby21 = 0 self.has_account = 0 self.filibuster_stage_id = 0 self.restart_pack = 0 self.ush1 = 0 self.ush2 = 0 self.ush3 = 0 self.ush4 = 0 self.ush5 = 0 self.ush6 = 0 self.ush7 = 0 self.ush8 = 0 self.ush9 = 0 self.ush10 = 0 self.ush11 = 0 self.ush12 = 0 self.leadership = 0 self.lineups = core.LineUps.init(self.game_version) self.stamp_data = core.StampData.init() self.story = core.StoryChapters.init() self.cats = core.Cats.init(self.game_version) self.special_skills = core.SpecialSkills.init() self.battle_items = core.BattleItems.init() self.mysale = core.MySale.init() self.event_stages = core.EventChapters.init(self.game_version) self.gatya = core.Gatya.init() self.user_rank_rewards = core.UserRankRewards.init(self.game_version) self.item_reward_stages = core.ItemRewardChapters.init(self.game_version) self.timed_score_stages = core.TimedScoreChapters.init(self.game_version) self.officer_pass = core.OfficerPass.init() self.gamatoto = core.Gamatoto.init() self.ex_stages = core.ExChapters.init() self.item_pack = core.ItemPack.init() self.logins = core.LoginBonus.init(self.game_version) self.dojo = core.Dojo.init() self.outbreaks = core.Outbreaks.init() self.scheme_items = core.SchemeItems.init() self.unlock_popups = core.UnlockPopups.init() self.ototo = core.Ototo.init(self.game_version) self.beacon_base = core.BeaconEventListScene.init() self.tower = core.TowerChapters.init() self.missions = core.Missions.init() self.challenge = core.ChallengeChapters.init() self.map_resets = core.MapResets.init() self.uncanny = core.UncannyChapters.init() self.catamin_stages = core.UncannyChapters.init() self.legend_quest = core.LegendQuestChapters.init() self.medals = core.Medals.init() self.gauntlets = core.GauntletChapters.init() self.enigma_clears = core.GauntletChapters.init() self.enigma = core.Enigma.init() self.cleared_slots = core.ClearedSlots.init() self.collab_gauntlets = core.GauntletChapters.init() self.talent_orbs = core.TalentOrbs.init() self.cat_shrine = core.CatShrine.init() self.aku = core.AkuChapters.init() self.behemoth_culling = core.GauntletChapters.init() self.zero_legends = core.ZeroLegendsChapters.init() self.dojo_chapters = core.ZeroLegendsChapters.init() self.wildcat_slots = core.GamblingEvent.init() self.cat_scratcher = core.GamblingEvent.init() self.uiid1 = {} self.uidd1 = {} self.uidiid2 = {} self.ushdshd2 = {} self.ushdd = {} self.ushdd2 = {} self.uidtii = {} self.uidtff = {} self.ushshd = {} self.ustid1 = {} self.uiid3 = {} self.uidd2 = {} self.uidd3 = {} self.ushd1 = {} self.uild1 = {} self.ushd2 = {} self.uid1 = {} self.first_locks = {} self.remaining_data = b"" def is_jp(self) -> bool: return self.cc == core.CountryCodeType.JP def not_jp(self) -> bool: return self.cc != core.CountryCodeType.JP def is_en(self) -> bool: return self.cc == core.CountryCodeType.EN def should_read_dst(self) -> bool: if self.is_jp(): return False return self.game_version >= 49 def read_dst(self): if self.should_read_dst(): self.dsts.append(self.data.read_bool()) def write_dst(self): if self.should_read_dst(): try: self.data.write_bool(self.dsts[self.dst_index]) except IndexError: self.data.write_bool(False) self.dst_index += 1 def calculate_user_rank(self): user_rank = 0 for cat in self.cats.cats: if not cat.unlocked: continue user_rank += cat.upgrade.base + 1 user_rank += cat.upgrade.plus for i, skill in enumerate(self.special_skills.skills): if i == 1: continue user_rank += skill.upgrade.base + 1 user_rank += skill.upgrade.plus return user_rank @staticmethod def get_string_identifier(identifier: str) -> str: return f"_bcsfe:{identifier}" def store_string(self, identifier: str, string: str, overwrite: bool = True): if overwrite: for i, order in enumerate(self.order_ids): if order.startswith(SaveFile.get_string_identifier(identifier)): self.order_ids[i] = ( f"{SaveFile.get_string_identifier(identifier)}:{string}" ) return self.order_ids.append(f"{SaveFile.get_string_identifier(identifier)}:{string}") def get_string(self, identifier: str) -> str | None: for order in self.order_ids: if order.startswith(SaveFile.get_string_identifier(identifier)): return order.split(":")[2] return None def get_strings(self, identifier: str) -> list[str]: strings: list[str] = [] for order in self.order_ids: if order.startswith(SaveFile.get_string_identifier(identifier)): strings.append(order.split(":")[2]) return strings def remove_string(self, identifier: str): for i, order in enumerate(self.order_ids): if order.startswith(SaveFile.get_string_identifier(identifier)): self.order_ids.pop(i) return def remove_strings(self, identifier: str): new_order_ids: list[str] = [] for order in self.order_ids: if not order.startswith(SaveFile.get_string_identifier(identifier)): new_order_ids.append(order) self.order_ids = new_order_ids def store_dict( self, identifier: str, dictionary: dict[str, str], overwrite: bool = True, ): if overwrite: for i, order in enumerate(self.order_ids): if order.startswith(SaveFile.get_string_identifier(identifier)): self.order_ids.pop(i) for key, value in dictionary.items(): self.order_ids.append( f"{SaveFile.get_string_identifier(identifier)}:{key}:{value}" ) def get_dict(self, identifier: str) -> dict[str, str] | None: dictionary: dict[str, str] = {} for order in self.order_ids: if order.startswith(SaveFile.get_string_identifier(identifier)): dictionary[order.split(":")[2]] = order.split(":")[3] return dictionary def remove_dict(self, identifier: str): new_order_ids: list[str] = [] for order in self.order_ids: if not order.startswith(SaveFile.get_string_identifier(identifier)): new_order_ids.append(order) self.order_ids = new_order_ids @staticmethod def get_saves_path() -> core.Path: return core.Path.get_data_folder().add("saves").generate_dirs() @staticmethod def get_save_path() -> core.Path: return SaveFile.get_saves_path().add("SAVE_DATA") def get_default_path(self) -> core.Path: core.Thread("check-backups", SaveFile.check_backups, []).start() date = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") local_path = ( self.get_saves_path() .add("backups") .add(f"{self.cc.get_code()}") .add(self.inquiry_code) ) local_path.generate_dirs() local_path = local_path.add(date) return local_path @staticmethod def check_backups(): max_backups = core.core_data.config.get_int(core.ConfigKey.MAX_BACKUPS) if max_backups == -1: return saves_path = SaveFile.get_saves_path().add("backups") saves_path.generate_dirs() all_saves: list[tuple[core.Path, datetime.datetime]] = [] for cc in saves_path.get_dirs(): for inquiry in cc.get_dirs(): for save in inquiry.get_paths_dir(): name = save.basename() try: date = datetime.datetime.strptime(name, "%Y-%m-%d_%H-%M-%S") except ValueError: continue all_saves.append((save, date)) all_saves.sort(key=lambda x: x[1], reverse=True) for i, save_info in enumerate(all_saves): if i >= max_backups: save_info[0].remove() for cc in saves_path.get_dirs(): dirs = cc.get_dirs() if len(dirs) == 0: cc.remove() for inquiry in dirs: saves = inquiry.get_paths_dir() if len(saves) == 0: inquiry.remove() def unlock_equip_menu(self): self.menu_unlocks[2] = max(self.menu_unlocks[2], 1) def get_xp(self) -> int: return self.xp def set_xp(self, xp: int): self.xp = xp def get_catfood(self) -> int: return self.catfood def set_catfood(self, catfood: int): self.catfood = catfood def get_normal_tickets(self) -> int: return self.normal_tickets def set_normal_tickets(self, normal_tickets: int): self.normal_tickets = normal_tickets def get_rare_tickets(self) -> int: return self.rare_tickets def set_rare_tickets(self, rare_tickets: int): self.rare_tickets = rare_tickets def get_platinum_tickets(self) -> int: return self.platinum_tickets def set_platinum_tickets(self, platinum_tickets: int): self.platinum_tickets = platinum_tickets def get_legend_tickets(self) -> int: return self.legend_tickets def set_legend_tickets(self, legend_tickets: int): self.legend_tickets = legend_tickets def get_platinum_shards(self) -> int: return self.platinum_shards def set_platinum_shards(self, platinum_shards: int): self.platinum_shards = platinum_shards def get_np(self) -> int: return self.np def set_np(self, np: int): self.np = np def get_leadership(self) -> int: return self.leadership def set_leadership(self, leadership: int): self.leadership = leadership def max_rank_up_sale(self): self.rank_up_sale_value = 0x7FFFFFFF ================================================ FILE: src/bcsfe/core/io/thread_helper.py ================================================ from __future__ import annotations from typing import Callable, Any, Iterable import threading class Thread: def __init__( self, name: str, target: Callable[..., Any], args: Iterable[Any] | None = None, ): self.name = name self.target = target self.args: Iterable[Any] = args if args is not None else [] self._thread: threading.Thread | None = None def start(self): self._thread = threading.Thread( target=self.target, args=self.args, name=self.name ) self._thread.start() def join(self): if self._thread is not None: self._thread.join() def is_alive(self) -> bool: if self._thread is not None: return self._thread.is_alive() return False @staticmethod def run(name: str, target: Callable[..., None], args: Any): thread = Thread(name, target, args) thread.start() return thread def thread_run_many_helper(funcs: list[Callable[..., Any]], *args: list[Any]): for i in range(len(funcs)): args_ = args[i] funcs[i](*args_) return def thread_run_many( funcs: list[Callable[..., Any]], args: Any = None, max_threads: int = 16 ) -> list[Thread]: chunk_size = len(funcs) // max_threads if chunk_size == 0: chunk_size = 1 callable_chunks: list[list[Callable[..., Any]]] = [] args_chunks: list[list[Any]] = [] for i in range(0, len(funcs), chunk_size): callable_chunks.append(funcs[i : i + chunk_size]) args_chunks.append(args[i : i + chunk_size]) threads: list[Thread] = [] for i in range(len(callable_chunks)): args_ = args_chunks[i] if args is None: args_ = [] threads.append( Thread.run( "run_many_helper", thread_run_many_helper, (callable_chunks[i], *args_), ) ) for thread in threads: thread.join() return threads ================================================ FILE: src/bcsfe/core/io/waydroid.py ================================================ from __future__ import annotations from bcsfe import core from bcsfe.cli import color from bcsfe.core import io from bcsfe.core.io.command import CommandResult class WayDroidNotInstalledError(Exception): def __init__(self, result: CommandResult): self.result = result class WayDroidHandler(io.root_handler.RootHandler): def __init__(self): self.check_waydroid_installed() self.adb_handler = io.adb_handler.AdbHandler(root=False) self.package_name = None def set_package_name(self, package_name: str): self.package_name = package_name self.adb_handler.set_package_name(self.package_name) @staticmethod def display_waydroid_not_installed(e: WayDroidNotInstalledError): color.ColoredText.localize("waydroid_not_installed", error=e) return @staticmethod def check_waydroid_installed(): result = io.command.Command("waydroid -V").run() if not result.success: raise WayDroidNotInstalledError(result) def run_shell_cmd(self, command: str) -> core.CommandResult: cmd = "waydroid shell" use_pkexec = core.core_data.config.get_bool(core.ConfigKey.USE_PKEXEC_WAYDROID) if use_pkexec: cmd = "pkexec " + cmd return io.command.Command(cmd).run(f"{command}") def pull_file( self, device_path: core.Path, local_path: core.Path ) -> core.CommandResult: # copy file to sdcard result = self.run_shell_cmd( f"cp {device_path.to_str_forwards()} /sdcard/{device_path.basename()} && chmod o+rw /sdcard/{device_path.basename()}" ) if not result.success: return result device_path = core.Path("/sdcard/").add(device_path.basename()) # adb pull result = self.adb_handler.adb_pull_file(device_path, local_path) if not result.success: return result # delete /sdcard file again # return self.adb_handler.run_shell(f"rm /sdcard/{device_path.basename()}") def push_file( self, local_path: core.Path, device_path: core.Path ) -> core.CommandResult: original_device_path = device_path.copy_object() device_path = core.Path("/sdcard/").add(device_path.basename()) # push to /sdcard with adb import time time.sleep(0.25) result = self.adb_handler.adb_push_file(local_path, device_path) if not result.success: return result result = self.run_shell_cmd( f"cp '/sdcard/{device_path.basename()}' '{original_device_path.to_str_forwards()}' && chmod o+rw '{original_device_path.to_str_forwards()}'" ) if not result.success: return result # remove temp file # return self.adb_handler.run_shell(f"rm '/sdcard/{device_path.basename()}'") def get_battlecats_packages(self) -> list[str]: cmd = "find /data/data/ -name SAVE_DATA -mindepth 3 -maxdepth 3" result = self.run_shell_cmd(cmd) if not result.success: return [] packages: list[str] = [] for package in result.result.split("\n"): parts = package.split("/") if len(parts) < 4: continue packages.append(package.split("/")[3]) return packages def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult: return self.pull_file(self.get_battlecats_save_path(), local_path) def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult: return self.push_file(local_path, self.get_battlecats_save_path()) def run_game(self) -> core.CommandResult: return self.adb_handler.run_game() def close_game(self) -> core.CommandResult: return self.adb_handler.close_game() ================================================ FILE: src/bcsfe/core/io/yaml.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core from bcsfe import cli import yaml class YamlFile: def __init__(self, path: core.Path, print_err: bool = True): self.path = path self.yaml: dict[str, Any] = {} if self.path.exists(): self.data = path.read() try: yml = yaml.safe_load(self.data.data) if not isinstance(yml, dict): self.yaml = {} self.save(print_err) else: self.yaml = yml except yaml.YAMLError: self.yaml = {} self.save(print_err) else: self.yaml = {} self.save(print_err) def save(self, print_err: bool = True) -> None: self.path.parent().generate_dirs() try: with open(self.path.path, "w", encoding="utf-8") as f: yaml.dump(self.yaml, f) except FileNotFoundError: if print_err: cli.color.ColoredText.localize("yaml_create_error", path=self.path.path) def __getitem__(self, key: str) -> Any: return self.yaml[key] def __setitem__(self, key: str, value: Any) -> None: self.yaml[key] = value def __delitem__(self, key: str) -> None: del self.yaml[key] def __contains__(self, key: str) -> bool: return key in self.yaml def __iter__(self): return iter(self.yaml) def __len__(self) -> int: return len(self.yaml) def __repr__(self) -> str: return self.yaml.__repr__() def __str__(self) -> str: return self.yaml.__str__() def get(self, key: str) -> Any: return self.yaml.get(key) def remove(self) -> None: self.path.remove() self.yaml = {} ================================================ FILE: src/bcsfe/core/locale_handler.py ================================================ from __future__ import annotations import dataclasses import tempfile from typing import Any from bcsfe import core from bcsfe.cli import color class PropertySet: """Represents a set of properties in a property file.""" def __init__(self, locale: str, property: str): """Initializes a new instance of the PropertySet class. Args: locale (str): Language code of the locale. property (str): Name of the property file. """ self.locale = locale self.property = property self.path = LocalManager.get_locale_folder(locale).add(property + ".properties") self.properties: dict[str, tuple[str, str]] = {} self.parse() def parse(self): """Parses the property file. Raises: KeyError: If a key is already defined in the property file. """ lines = self.path.read().to_str().splitlines() i = 0 in_multi_line = False multi_line_text = "" multi_line_key = "" while i < len(lines): line = lines[i] finish_multiline = False if (in_multi_line and not line.startswith(">")) or ( in_multi_line and i == len(lines) - 1 ): in_multi_line = False finish_multiline = True if multi_line_key in self.properties: raise KeyError( f"Key {multi_line_key} already exists in property file" ) if line.startswith(">"): multi_line_text += line[1:] else: multi_line_text = multi_line_text[:-1] # remove extra newline self.properties[multi_line_key] = (multi_line_text, self.property) multi_line_text = "" multi_line_key = "" if line.startswith("#") or not line: i += 1 continue if line.startswith(">") and in_multi_line: multi_line_text += line[1:] + "\n" parts = line.split("=") if line.strip().endswith("="): in_multi_line = True multi_line_key = parts[0] if not in_multi_line and not finish_multiline: key = parts[0] value = "=".join(parts[1:]) if key in self.properties: raise KeyError(f"Key {key} already exists in property file") self.properties[key] = (value, self.property) i += 1 def get_key(self, key: str) -> str: """Gets a key from the property file. Args: key (str): Key to get. Returns: str: Value of the key. """ return ( self.properties.get(key, key)[0].replace("\\n", "\n").replace("\\t", "\t") ) @staticmethod def from_config(property: str) -> PropertySet: """Gets a PropertySet from the language code in the config. Args: property (str): Name of the property file. Returns: PropertySet: PropertySet for the property file. """ return PropertySet( core.core_data.config.get_str(core.ConfigKey.LOCALE), property ) class LocalManager: """Manages properties for a locale""" def __init__(self, locale: str | None = None): """Initializes a new instance of the LocalManager class. Args: locale (str): Language code of the locale. """ if locale is None: lc = core.core_data.config.get_str(core.ConfigKey.LOCALE) else: lc = locale self.locale = lc self.path = LocalManager.get_locale_folder(lc) self.properties: dict[str, PropertySet] = {} self.all_properties: dict[str, tuple[str, str]] = {} self.en_properties: dict[str, tuple[str, str]] = {} self.en_properties_path = LocalManager.get_locale_folder("en") self.authors: list[str] = ["fieryhenry"] self.name: str = "English" self.parse() if self.locale == "en": self.en_properties = self.all_properties if core.core_data.config.get_bool(core.ConfigKey.SHOW_MISSING_LOCALE_KEYS): key = self.get_key("missing_locale_keys") print(key) print() missing = self.get_missing_keys() for key in missing: print(f"{key[2]}\n{key[0]}={key[1]}\n") if not missing: print(self.get_key("none")) print() key = self.get_key("extra_locale_keys") print(key) print() extra = self.get_extra_keys() for key in extra: print(f"{key[2]}\n{key[0]}={key[1]}\n") if not extra: print(self.get_key("none")) print() def get_missing_keys(self) -> list[tuple[str, str, str]]: missing = set(self.en_properties.keys()) - set(self.all_properties.keys()) return [ ( key, self.en_properties[key][0], self.en_properties[key][1] + ".properties", ) for key in missing ] def get_extra_keys(self) -> list[tuple[str, str, str]]: extra = set(self.all_properties.keys()) - set(self.en_properties.keys()) return [ ( key, self.all_properties[key][0], self.all_properties[key][1] + ".properties", ) for key in extra ] def parse(self): """Parses all property files in the locale folder recursively.""" for file in self.path.glob("**/*.properties", recursive=True): file_name = file.strip_path_from(self.path).path property_set = PropertySet(self.locale, file_name[:-11]) self.all_properties.update(property_set.properties) self.properties[file_name[:-11]] = property_set metadata_path = self.path.add("metadata.json") if metadata_path.exists(): data = core.JsonFile.from_path(metadata_path) self.authors = data.get("authors") or ["fieryhenry"] self.name = data.get("name") or "English" if self.locale != "en": for file in self.en_properties_path.glob("**/*.properties", recursive=True): file_name = file.strip_path_from(self.en_properties_path).path property_set = PropertySet("en", file_name[:-11]) self.en_properties.update(property_set.properties) def get_key(self, key: str, escape: bool = True, **kwargs: Any) -> str: """Gets a key from the property file. Args: key (str): Key to get. Returns: str: Value of the key. """ try: text = self.get_key_recursive(key, kwargs, escape) except RecursionError: text = key for kwarg_key, kwarg_value in kwargs.items(): value = str(kwarg_value) if escape: value = LocalManager.escape_string(value) text = text.replace("{" + kwarg_key + "}", value) if "$(" in text: text = self.parse_condition(text, kwargs) return text def parse_condition(self, text: str, kwargs: dict[str, Any]) -> str: counter = 0 final_text = "" in_expression = False expression_text = "" count_down = 0 while counter < len(text): char = text[counter] if counter == len(text) - 1: final_text += char break next_char = text[counter + 1] if char == "\\": final_text += next_char counter += 2 continue if char == "$" and next_char == "(": count_down = 0 in_expression = True elif char == "/" and next_char == "$": count_down = 2 in_expression = False if len(expression_text) < 3: counter += 1 continue new_expression_text = expression_text[2:-1] expression_text = "" parts = new_expression_text.split(":") if len(parts) < 2: counter += 1 continue keyword = parts[0].strip() expression = parts[1].strip() conditions = expression.split("$,") string = "" for i, condition in enumerate(conditions): condition = condition.strip() if not condition: continue if i == len(conditions) - 1: string = condition break condition_parts = condition.split("($") if len(condition_parts) < 2: continue logic = condition_parts[0].strip() word = condition_parts[1].strip() if not word: continue word = word[:-1] value = kwargs.get(keyword) if value is None: continue equality = None if logic.startswith("=="): equality = "==" elif logic.startswith("!="): equality = "!=" elif logic.startswith(">="): equality = ">=" elif logic.startswith("<="): equality = "<=" elif logic.startswith(">"): equality = ">" elif logic.startswith("<"): equality = "<" if equality is None: continue logic_parts = logic.split(equality) if len(logic_parts) < 2: continue logic_value = logic_parts[1].strip() if isinstance(value, int): if not logic_value.isdigit(): continue logic_value = int(logic_value) if equality == "==": if logic_value == value: string = word break elif equality == "!=": if logic_value != value: string = word break if isinstance(logic_value, int) and not string: if equality == ">": if logic_value > value: string = word break elif equality == ">=": if logic_value >= value: string = word break elif equality == "<": if logic_value < value: string = word break elif equality == "<=": if logic_value <= value: string = word break final_text += string if in_expression: expression_text += char else: if count_down <= 0: final_text += char else: count_down -= 1 counter += 1 return final_text @staticmethod def get_special_chars() -> list[str]: return ["<", ">", "/"] @staticmethod def escape_string(string: str) -> str: for char in LocalManager.get_special_chars(): string = string.replace(char, "\\" + char) return string def get_key_recursive( self, key: str, kwargs: dict[str, Any], escape: bool = True, ) -> str: value = self.all_properties.get(key) if value is None: value = self.en_properties.get(key, (key, key)) value = value[0].replace("\\n", "\n").replace("\\t", "\t") # replace {{key}} with the value of the key if "{{" not in value: return value char_index = 0 while char_index < len(value): if value[char_index] == "{" and value[char_index + 1] == "{": key_name = "" char_index += 2 while value[char_index] != "}": key_name += value[char_index] char_index += 1 if key_name != key: value = value.replace( "{{" + key_name + "}}", self.get_key(key_name, escape, **kwargs), ) char_index += 1 return value @staticmethod def get_all_aliases(value: str) -> list[str]: """Gets all aliases from a string. Aliases are separated by |. Args: value (str): String to get aliases from. Returns: list[str]: List of aliases. """ if "|" not in value: return [value] i = 0 aliases: list[str] = [] while i < len(value): char = value[i] prev_char = value[i - 1] if i > 0 else "" if char == "|" and prev_char != "\\": aliases.append(value[:i]) value = value[i + 1 :] i = 0 i += 1 aliases.append(value) return aliases @staticmethod def from_config() -> LocalManager: """Gets a LocalManager from the language code in the config. Returns: LocalManager: LocalManager for the locale. """ return LocalManager(core.core_data.config.get_str(core.ConfigKey.LOCALE)) def check_duplicates(self): """Checks for duplicate keys in all property files. Raises: KeyError: If a key is already defined in the property file. """ keys: set[str] = set() for property in self.properties.values(): for key in property.properties.keys(): if key in keys: raise KeyError(f"Duplicate key {key}") keys.add(key) @staticmethod def get_all_locales() -> list[str]: """Gets all locales in the locales folder. Returns: list[str]: List of locales. """ locales: list[str] = [] for folder in LocalManager.get_locales_folder().get_dirs(): locales.append(folder.basename()) for folder in LocalManager.get_external_locales_folder().get_dirs(): locales.append(folder.basename()) return locales @staticmethod def get_locales_folder() -> core.Path: """Gets the locales folder. Returns: core.Path: Path to the locales folder. """ return core.Path("locales", True) @staticmethod def get_external_locales_folder() -> core.Path: """Gets the external locales folder. Returns: core.Path: Path to the external locales folder. """ return core.Path.get_data_folder().add("external_locales") @staticmethod def get_locale_folder(locale: str) -> core.Path: """Gets the folder for a locale. Args: locale (str): Language code of the locale. Returns: core.Path: Path to the locale folder. """ if locale.startswith("ext-"): return LocalManager.get_external_locales_folder().add(locale) return LocalManager.get_locales_folder().add(locale) @staticmethod def remove_locale(locale: str): """Removes a locale. Args: locale (str): Language code of the locale. """ if locale not in LocalManager.get_all_locales(): return if locale.startswith("ext-"): extern = ExternalLocaleManager.get_external_locale(locale) if extern is not None: ExternalLocaleManager.delete_locale(extern) LocalManager.get_external_locales_folder().add(locale).remove() else: LocalManager.get_locales_folder().add(locale).remove() if core.core_data.config.get_str(core.ConfigKey.LOCALE) == locale: core.core_data.config.set(core.ConfigKey.LOCALE, "en") @dataclasses.dataclass class ExternalLocale: short_name: str name: str description: str author: str version: str git_repo: str | None = None def to_json(self) -> dict[str, Any]: return dataclasses.asdict(self) @staticmethod def from_json(json_data: dict[str, Any]) -> ExternalLocale | None: short_name = json_data.get("short_name") name = json_data.get("name") description = json_data.get("description") author = json_data.get("author") version = json_data.get("version") git_repo = json_data.get("git_repo") if ( short_name is None or name is None or description is None or author is None or version is None ): return None return ExternalLocale( short_name, name, description, author, version, git_repo, ) @staticmethod def from_git_repo(git_repo: str) -> ExternalLocale | None: repo = core.GitHandler().get_repo(git_repo) if repo is None: return None locale_json = repo.get_file(core.Path("locale.json")) if locale_json is None: return None json_data = core.JsonFile.from_data(locale_json).to_object() json_data["git_repo"] = git_repo return ExternalLocale.from_json(json_data) def get_new_version(self) -> bool: if self.git_repo is None: return False repo = core.GitHandler().get_repo(self.git_repo) if repo is None: return False with tempfile.TemporaryDirectory() as tmp: temp_dir = core.Path(tmp) success = repo.clone_to_temp(temp_dir) if not success: return False external_locale = ExternalLocaleManager.parse_external_locale(temp_dir) if external_locale is None: return False version = external_locale.version if version == self.version: return False self.name = external_locale.name self.short_name = external_locale.short_name self.description = external_locale.description self.author = external_locale.author self.version = version success = repo.pull() if not success: return False self.save() return True def save(self): ExternalLocaleManager.save_locale(self) def get_full_name(self) -> str: return f"ext-{self.author}-{self.short_name}" class ExternalLocaleManager: @staticmethod def delete_locale(external_locale: ExternalLocale): if external_locale.git_repo is None: return folder = core.GitHandler.get_repo_folder().add( external_locale.git_repo.split("/")[-1] ) folder.remove() @staticmethod def save_locale( external_locale: ExternalLocale, ): """Saves an external locale. Args: external_locale (ExternalLocale): External locale to save. """ if external_locale.git_repo is None: return folder = LocalManager.get_external_locales_folder().add( external_locale.get_full_name() ) folder.generate_dirs() repo = core.GitHandler().get_repo(external_locale.git_repo) if repo is None: return files_dir = repo.get_folder(core.Path("files")) if files_dir is None: return files_dir.copy_tree(folder) json_data = external_locale.to_json() folder.add("locale.json").write(core.JsonFile.from_object(json_data).to_data()) @staticmethod def parse_external_locale(path: core.Path) -> ExternalLocale | None: """Parses an external locale. Args: path (core.Path): Path to the external locale. Returns: ExternalLocale: External locale. """ if not path.exists(): return None json_data = core.JsonFile.from_data(path.add("locale.json").read()).to_object() return ExternalLocale.from_json(json_data) @staticmethod def update_external_locale(external_locale: ExternalLocale): """Updates an external locale. Args: external_locale (ExternalLocale): External locale to update. """ if external_locale.git_repo is None: return color.ColoredText.localize( "checking_for_locale_updates", locale_name=external_locale.name, ) updated = external_locale.get_new_version() if updated: color.ColoredText.localize( "external_locale_updated", locale_name=external_locale.name, version=external_locale.version, ) else: color.ColoredText.localize( "external_locale_no_update", locale_name=external_locale.name, version=external_locale.version, ) print() @staticmethod def update_all_external_locales(_: Any = None): """Updates all external locales.""" dirs = LocalManager.get_external_locales_folder().get_dirs() if not dirs: color.ColoredText.localize( "no_external_locales", ) return if not core.GitHandler.is_git_installed(): color.ColoredText.localize( "git_not_installed", ) return for folder in dirs: locale = ExternalLocaleManager.parse_external_locale(folder) if locale is None: continue ExternalLocaleManager.update_external_locale(locale) @staticmethod def get_external_locale_config() -> ExternalLocale | None: """Gets the external locale from the config. Returns: ExternalLocale: External locale. """ locale = core.core_data.config.get_str(core.ConfigKey.LOCALE) if not locale.startswith("ext-"): return None return ExternalLocaleManager.parse_external_locale( LocalManager.get_locale_folder(locale) ) @staticmethod def get_external_locale(locale: str) -> ExternalLocale | None: """Gets the external locale from the code. Returns: ExternalLocale: External locale. """ if not locale.startswith("ext-"): return None return ExternalLocaleManager.parse_external_locale( LocalManager.get_locale_folder(locale) ) ================================================ FILE: src/bcsfe/core/log.py ================================================ from __future__ import annotations """Module for handling logging""" import traceback from bcsfe import core import time class Logger: def __init__(self, path: core.Path | None): """ Initializes a Logger object """ if path is None: path = Logger.get_log_path() self.log_file = path try: self.log_data = self.log_file.read(True).split(b"\n") except Exception as _: self.log_data = None @staticmethod def get_log_path() -> core.Path: return core.Path.get_state_folder().add("bcsfe.log") def is_log_enabled(self) -> bool: return self.log_data is not None def get_time(self) -> str: """ Returns the current time in the format: "HH:MM:SS" Returns: str: The current time """ return time.strftime("%d/%m/%Y %H:%M:%S", time.localtime()) def log_debug(self, message: str): """ Logs a debug message Args: message (str): The message to log """ if self.log_data is None: return self.log_data.append(core.Data(f"[DEBUG]::{self.get_time()} - {message}")) self.write() def log_info(self, message: str): """ Logs an info message Args: message (str): The message to log """ if self.log_data is None: return self.log_data.append(core.Data(f"[INFO]::{self.get_time()} - {message}")) self.write() def log_warning(self, message: str): """ Logs a warning message Args: message (str): The message to log """ if self.log_data is None: return self.log_data.append(core.Data(f"[WARNING]::{self.get_time()} - {message}")) self.write() def log_error(self, message: str): """ Logs an error message Args: message (str): The message to log """ if self.log_data is None: return self.log_data.append(core.Data(f"[ERROR]::{self.get_time()} - {message}")) self.write() def log_exception(self, exception: Exception, extra_msg: str = ""): tb = traceback.format_exc() if tb == "NoneType: None\n": try: raise exception except Exception: tb = traceback.format_exc() self.log_error( f"{extra_msg}: {exception.__class__.__name__}: {exception}\n{tb}" ) def write(self): """ Writes the log data to the log file """ if self.log_data is None: return self.log_file.write(core.Data.from_many(self.log_data, core.Data("\n")).strip()) def log_no_file_found(self, file_name: str): """ Logs that a file was not found Args: fileName (str): The name of the file """ self.log_warning(f"Could not find {file_name}") @staticmethod def get_traceback() -> str: """ Gets the traceback of the last exception Returns: str: The traceback """ tb = traceback.format_exc() if tb == "NoneType: None\n": return "" return tb ================================================ FILE: src/bcsfe/core/max_value_helper.py ================================================ from __future__ import annotations import enum from typing import Any from bcsfe import core class MaxValueType(enum.Enum): CATFOOD = "catfood" XP = "xp" NORMAL_TICKETS = "normal_tickets" HUNDRED_MILLION_TICKETS = "100_million_tickets" RARE_TICKETS = "rare_tickets" PLATINUM_TICKETS = "platinum_tickets" LEGEND_TICKETS = "legend_tickets" NP = "np" LEADERSHIP = "leadership" BATTLE_ITEMS = "battle_items" CATAMINS = "catamins" CATSEYES = "catseyes" CATFRUIT = "catfruit" BASE_MATERIALS = "base_materials" LABYRINTH_MEDALS = "labyrinth_medals" TALENT_ORBS = "talent_orbs" TREASURE_LEVEL = "treasure_level" STAGE_CLEAR_COUNT = "stage_clear_count" ITF_TIMED_SCORE = "itf_timed_score" EVENT_TICKETS = "event_tickets" TREASURE_CHESTS = "treasure_chests" class MaxValueHelper: def __init__(self): self.max_value_data = self.get_max_value_data() @staticmethod def convert_val_code(value_code: MaxValueType | str) -> str: if isinstance(value_code, MaxValueType): value_code = value_code.value return value_code def get_max_value_data(self) -> dict[str, Any]: file_path = core.Path("max_values.json", True) if not file_path.exists(): return {} try: return core.JsonFile.from_data(file_path.read()).to_object() except core.JSONDecodeError: return {} def get(self, value_code: str | MaxValueType) -> int: try: return int(self.max_value_data.get(self.convert_val_code(value_code), 0)) except ValueError: return 0 def get_property(self, value_code: str | MaxValueType, property: str) -> int: try: return int( self.max_value_data.get(self.convert_val_code(value_code), {}).get( property, 0 ) ) except ValueError: return 0 def get_old(self, value_code: str | MaxValueType) -> int: return self.get_property(value_code, "old") def get_new(self, value_code: str | MaxValueType) -> int: return self.get_property(value_code, "new") ================================================ FILE: src/bcsfe/core/server/__init__.py ================================================ from bcsfe.core.server import ( managed_item, headers, client_info, server_handler, game_data_getter, request, updater, event_data, ) __all__ = [ "managed_item", "server_handler", "headers", "client_info", "game_data_getter", "request", "updater", "event_data" ] ================================================ FILE: src/bcsfe/core/server/client_info.py ================================================ from __future__ import annotations from typing import Any from bcsfe import core class ClientInfo: def __init__(self, cc: core.CountryCode, gv: core.GameVersion): self.cc = cc self.gv = gv @staticmethod def from_save_file(save_file: core.SaveFile): return ClientInfo(save_file.cc, save_file.game_version) def get_client_info(self) -> dict[str, Any]: cc = self.cc.get_client_info_code() data = { "clientInfo": { "client": { "countryCode": cc, "version": self.gv.game_version, }, "device": { "model": "SM-G955F", }, "os": { "type": "android", "version": "9", }, }, "nonce": core.Random.get_hex_string(32), } return data ================================================ FILE: src/bcsfe/core/server/event_data.py ================================================ from __future__ import annotations from collections.abc import Callable from typing import Type, TypeVar from bcsfe import core class FilterDate: def __init__(self, start_mmdd: int, start_hhmm: int, end_mmdd: int, end_hhmm: int): self.start_mmdd = start_mmdd self.start_hhmm = start_hhmm self.end_mmdd = end_mmdd self.end_hhmm = end_hhmm @staticmethod def from_csv_row(row: core.Row) -> FilterDate: return FilterDate( row.next_int(), row.next_int(), row.next_int(), row.next_int() ) class FilterItem: def __init__( self, filter_date: FilterDate | None, filter_day_flags: list[bool], # 31 item array filter_week: int, filter_times_start_end_hhmm: list[tuple[int, int]], ): self.filter_date = filter_date self.filter_day_flags = filter_day_flags self.filter_week = filter_week self.filter_times_start_end_hhmm = filter_times_start_end_hhmm @staticmethod def from_csv_row(row: core.Row) -> FilterItem: filter_date_enabled = row.next_bool() filter_date = None if filter_date_enabled: filter_date = FilterDate.from_csv_row(row) filter_day_count = row.next_int() filter_day_flags: list[bool] = [False] * 31 for _ in range(filter_day_count): day_ind = row.next_int() - 1 if day_ind >= 0 and day_ind < len(filter_day_flags): filter_day_flags[day_ind] = True filter_week = row.next_int() filter_time_count = row.next_int() filter_times_start_end_hhmm: list[tuple[int, int]] = [] for _ in range(filter_time_count): start_hhmm = row.next_int() end_hhmm = row.next_int() filter_times_start_end_hhmm.append((start_hhmm, end_hhmm)) return FilterItem( filter_date, filter_day_flags, filter_week, filter_times_start_end_hhmm ) def split_yyyymmdd(yyyymmdd: int) -> tuple[int, int, int]: year = yyyymmdd // 10_000 month = (yyyymmdd % 10_000) // 100 day = yyyymmdd % 100 return year, month, day def split_hhmm(hhmm: int) -> tuple[int, int]: hour = hhmm // 100 minute = hhmm % 100 return hour, minute class FilterData: def __init__( self, start_yyyymmdd: int, start_hhmm: int, end_yyyymmdd: int, end_hhmm: int, min_game_version: int, max_game_version: int, platform_flag: int, filter_items: list[FilterItem], ): self.start_yyyymmdd = start_yyyymmdd self.start_hhmm = start_hhmm self.end_yyyymmdd = end_yyyymmdd self.end_hhmm = end_hhmm self.min_game_version = min_game_version self.max_game_version = max_game_version self.platform_flag = platform_flag self.filter_items = filter_items @staticmethod def from_csv_row(row: core.Row) -> FilterData: start_yyyymmdd = row.next_int() start_hhmm = row.next_int() end_yyyymmdd = row.next_int() end_hhmm = row.next_int() min_game_version = row.next_int() max_game_version = row.next_int() platform_flag = row.next_int() total_items = row.next_int() filter_items: list[FilterItem] = [] for _ in range(total_items): filter_items.append(FilterItem.from_csv_row(row)) return FilterData( start_yyyymmdd, start_hhmm, end_yyyymmdd, end_hhmm, min_game_version, max_game_version, platform_flag, filter_items, ) class Localization: def __init__(self, lang: str, title: str, message: str): self.lang = lang self.title = title self.message = message @staticmethod def from_csv_row(row: core.Row) -> Localization: return Localization(row.next_str(), row.next_str(), row.next_str()) class RarityGatya: def __init__(self, prob: int, guaranteed: int): self.prob = prob self.guaranteed = guaranteed @staticmethod def from_csv_row(row: core.Row) -> RarityGatya: return RarityGatya(row.next_int(), row.next_int()) class ServerGatyaDataSet: def __init__( self, number: int, catfood: int, stage_progress: int, flags: int, rarity_info: list[RarityGatya], message: str, collab_message: tuple[str, str] | None, ): self.number = number self.catfood = catfood self.stage_progress = stage_progress self.flags = flags self.rarity_info = rarity_info self.message = message self.other_event_message = collab_message @staticmethod def from_csv_row(row: core.Row, flag: int) -> ServerGatyaDataSet: number = row.next_int() catfood = row.next_int() stage_progress = row.next_int() flags = row.next_int() rarity_info: list[RarityGatya] = [] for _ in range(5): rarity_info.append(RarityGatya.from_csv_row(row)) message = row.next_str() collab_message = None if flag == 4: collab_message = (row.next_str(), row.next_str()) return ServerGatyaDataSet( number, catfood, stage_progress, flags, rarity_info, message, collab_message, ) def is_visible_silhouette(self) -> bool: return (self.flags & 1) != 0 def is_required_user_rank_1600(self) -> bool: return (self.flags & 2) != 0 def has_stepup_gatya(self) -> bool: return (self.flags & 4) != 0 class ServerGatyaDataItem: def __init__(self, filter: FilterData, flags: int, sets: list[ServerGatyaDataSet]): self.filter = filter self.flags = flags self.sets = sets @staticmethod def from_csv_row(row: core.Row) -> ServerGatyaDataItem: filter = FilterData.from_csv_row(row) flag = row.next_int() count = row.next_int() sets: list[ServerGatyaDataSet] = [] for _ in range(count): sets.append(ServerGatyaDataSet.from_csv_row(row, flag)) return ServerGatyaDataItem(filter, flag, sets) def get_normal_flag(self) -> bool: return self.flags == 0 def get_rare_flag(self) -> bool: return 1 <= self.flags <= 3 def get_collab_flag(self) -> bool: return self.flags == 4 def get_first_rare_flag(self) -> bool: return self.flags == 2 def get_first_rare_10_flag(self) -> bool: return self.flags == 3 class ServerItemDataItem: def __init__( self, filter: FilterData, event_number: int, # server item id item_number: int, item_unit: int, # base quanity, not cat unit (e.g 2 XP+1000s) title: str, message: str, stage_progress: int, stage_progress_flag: int, flags: int, locales: list[Localization] | None, ): self.filter = filter self.event_number = event_number self.item_number = item_number self.item_unit = item_unit self.title = title self.message = message self.stage_progress = stage_progress self.stage_progress_flag = stage_progress_flag self.flags = flags self.locales = locales def is_every_day(self) -> bool: return (self.flags & 1) != 0 def is_required_user_rank_1600(self) -> bool: return (self.flags & 2) != 0 @staticmethod def from_csv_row(row: core.Row) -> ServerItemDataItem: filter = FilterData.from_csv_row(row) event_number = row.next_int() item_number = row.next_int() item_unit = row.next_int() title = row.next_str() message = row.next_str() stage_progress = row.next_int() stage_progress_flag = row.next_bool() flags = row.next_int() locales: list[Localization] | None = None if not row.done(): locales = [] total_locales = row.next_int() for _ in range(total_locales): locales.append(Localization.from_csv_row(row)) return ServerItemDataItem( filter, event_number, item_number, item_unit, title, message, stage_progress, stage_progress_flag, flags, locales, ) Item = TypeVar("Item") T = TypeVar("T") def read_event_data( csv: core.CSV, read_func: Callable[[core.Row], Item], init_func: Callable[[list[Item]], T], ) -> T | None: start = csv.read_line() if start is None: return None if start.next_str() != "[start]": return None if not start.done(): return None items: list[Item] = [] while True: row = csv.read_line() if row is None: return None if len(row) == 0: return None if row[0].to_str() == "[end]": break item = read_func(row) items.append(item) return init_func(items) class ServerItemData: def __init__(self, items: list[ServerItemDataItem]): self.items = items @staticmethod def from_csv(csv: core.CSV) -> ServerItemData | None: return read_event_data(csv, ServerItemDataItem.from_csv_row, ServerItemData) @staticmethod def from_data(data: core.Data) -> ServerItemData | None: csv = core.CSV(data, delimiter="\t", remove_comments=False, remove_empty=False) return ServerItemData.from_csv(csv) class ServerGatyaData: def __init__(self, items: list[ServerGatyaDataItem]): self.items = items @staticmethod def from_csv(csv: core.CSV) -> ServerGatyaData | None: return read_event_data(csv, ServerGatyaDataItem.from_csv_row, ServerGatyaData) @staticmethod def from_data(data: core.Data) -> ServerGatyaData | None: csv = core.CSV(data, delimiter="\t", remove_comments=False, remove_empty=False) return ServerGatyaData.from_csv(csv) ================================================ FILE: src/bcsfe/core/server/game_data_getter.py ================================================ from __future__ import annotations from io import BytesIO from typing import Any, Callable from bcsfe.cli import color, dialog_creator import tarfile from bcsfe import core class GameDataGetter: def __init__( self, cc: core.CountryCode, gv: core.GameVersion, do_print: bool = True ): self.repo_url = core.core_data.config.get_game_data_repo() self.print = do_print self.lang = core.core_data.config.get_str(core.ConfigKey.LOCALE) self.cc = cc.get_cc_lang() self.real_cc = cc self.gv = gv self.cc = self.cc if not self.cc.is_lang() else self.real_cc self.version, exact_match = self.find_gv(self.cc, gv) self.all_versions = None self.url = None self.filepath = None if exact_match: return self.metadata = self.get_metadata() if self.metadata is None: return self.all_versions = self.get_versions(self.metadata) self.url = self.metadata.get("base_url") if self.all_versions is not None: self.version, self.filepath = self.get_version(self.all_versions, self.cc) def find_gv( self, cc: core.CountryCode, gv: core.GameVersion ) -> tuple[str | None, bool]: versions = GameDataGetter.get_all_downloaded_versions().get(cc.get_code()) if versions is None: return None, False versions_int = [ core.GameVersion.from_string(version).game_version for version in versions ] versions_int.sort() for version in versions_int: if version >= gv.game_version: return core.GameVersion(version).to_string(), version == gv.game_version return None, False def does_save_version_match(self, save_file: core.SaveFile) -> bool: if self.version is None: return False return save_file.game_version == self.version def get_version( self, versions: dict[str, dict[str, str]], cc: core.CountryCode ) -> tuple[str | None, str | None]: cc_versions = versions.get(cc.get_code()) if cc_versions is None: return None, None if not cc_versions: return None, None gv_string = self.gv.to_string() if gv_string not in cc_versions: cc_version_keys = list(cc_versions.keys()) cc_version_keys.sort() for version in cc_version_keys: if ( core.GameVersion.from_string(version).game_version >= self.gv.game_version ): return version, cc_versions[version] return cc_version_keys[-1], cc_versions[cc_version_keys[-1]] return gv_string, cc_versions[gv_string] def get_metadata(self, show_alt: bool = True) -> dict[str, Any] | None: response = core.RequestHandler(self.repo_url).get() if response is None: if ( self.repo_url == core.core_data.config.get_default(core.ConfigKey.GAME_DATA_REPO) and show_alt ): alt = "https://gitlab.com/fieryhenry/bcdata/-/raw/main/metadata.json" res = dialog_creator.YesNoInput().get_input_once( "use_alternative_repo", {"repo": alt}, ) if res: core.core_data.config.set(core.ConfigKey.GAME_DATA_REPO, alt) self.repo_url = alt return self.get_metadata(show_alt=False) return None try: data = response.json() except core.JSONDecodeError as e: print(e, f"Data:\n{response.text}") return None return data def get_versions(self, metdata: dict[str, Any]) -> dict[str, dict[str, str]] | None: return metdata.get("versions") def get_packname(self, packname: str) -> str: if packname != "resLocal": return packname if self.cc != core.CountryCodeType.EN: return packname langs = core.CountryCode.get_langs() if self.lang in langs: return f"{packname}_{self.lang}" return packname @staticmethod def get_game_data_dir() -> core.Path: path = core.get_game_data_path() if path is None: return core.Path.get_data_folder().add("game_data") return path def get_file_path(self, pack_name: str, file_name: str) -> core.Path | None: pack_name = self.get_packname(pack_name) path = self.get_version_path() if path is None: return None return path.add(pack_name).generate_dirs().add(file_name) def download_version_data(self): if self.url is None or self.filepath is None or self.version is None: return None url = self.url + self.filepath if self.print: color.ColoredText.localize("downloading_compressed_data", url=url) downloaded_data = core.RequestHandler(url).get() if downloaded_data is None: if self.print: color.ColoredText.localize("no_internet") return None archive = tarfile.open( name=self.filepath, fileobj=BytesIO(downloaded_data.content) ) outdir = ( GameDataGetter.get_game_data_dir().add(self.cc.get_code()).add(self.version) ).generate_dirs() archive.extractall(outdir.path) outdir.add("downloaded").write(core.Data()) return True def get_version_path(self) -> core.Path | None: if self.version is None: return None return ( GameDataGetter.get_game_data_dir().add(self.cc.get_code()).add(self.version) ).generate_dirs() def has_downloaded(self) -> bool: path = self.get_version_path() if path is None: return False return path.add("downloaded").exists() def get_file(self, pack_name: str, file_name: str) -> core.Data | bool: path = self.get_file_path(pack_name, file_name) if path is None: return False if path.exists(): return path.read() else: if self.has_downloaded(): return True if self.download_version_data() is None: return False path = self.get_file_path(pack_name, file_name) if path is None: return False if path.exists(): return path.read() return self.has_downloaded() def save_file(self, pack_name: str, file_name: str) -> core.Data | bool: pack_name = self.get_packname(pack_name) data = self.get_file(pack_name, file_name) if isinstance(data, bool): return data path = self.get_file_path(pack_name, file_name) if path is None: return False data.to_file(path) return data def save_file_data( self, pack_name: str, file_name: str, data: core.Data ) -> core.Data | None: pack_name = self.get_packname(pack_name) path = self.get_file_path(pack_name, file_name) if path is None: return None data.to_file(path) return data def is_downloaded(self, pack_name: str, file_name: str) -> bool: pack_name = self.get_packname(pack_name) path = self.get_file_path(pack_name, file_name) if path is None: return False return path.exists() def download_from_path( self, path: str, retries: int = 2, display_text: bool = True ) -> core.Data | None: pack_name, file_name = path.split("/") pack_name = self.get_packname(pack_name) return self.download(pack_name, file_name, retries, display_text) def download( self, pack_name: str, file_name: str, retries: int = 2, display_text: bool = True, ) -> core.Data | None: retries -= 1 pack_name = self.get_packname(pack_name) if self.is_downloaded(pack_name, file_name): path = self.get_file_path(pack_name, file_name) if path is None: return None try: return path.read() except FileNotFoundError: return None if retries == 0: return None version = self.version if version is None: if display_text: self.print_no_file(pack_name, file_name) return None if display_text and not self.has_downloaded(): color.ColoredText.localize( "downloading", file_name=file_name, pack_name=pack_name, country_code=self.cc.get_code(), version=version, ) data = self.save_file(pack_name, file_name) if isinstance(data, bool): if not data and display_text: self.print_no_file(pack_name, file_name) return None data = self.download(pack_name, file_name, retries, display_text) if data is None: if display_text: self.print_no_file(pack_name, file_name) return None return data def download_all( self, pack_name: str, file_names: list[str], display_text: bool = True, ) -> list[tuple[str, core.Data] | None]: pack_name = self.get_packname(pack_name) callables: list[Callable[..., Any]] = [] args: list[tuple[str, str, int, bool]] = [] for file_name in file_names: callables.append(self.download) args.append((pack_name, file_name, 2, display_text)) core.thread_run_many(callables, args) data_list: list[tuple[str, core.Data] | None] = [] for file_name in file_names: path = self.get_file_path(pack_name, file_name) if path is None: data_list.append(None) elif not path.exists(): data_list.append(None) else: data_list.append((file_name, path.read())) return data_list @staticmethod def get_all_downloaded_versions() -> dict[str, list[str]]: versions: dict[str, list[str]] = {} for cc in core.CountryCode.get_all_str(): dir = GameDataGetter.get_game_data_dir().add(cc) if not dir.exists(): continue for version in GameDataGetter.get_game_data_dir().add(cc).get_dirs(): if not version.exists(): continue if not version.add("downloaded").exists(): continue if cc in versions: versions[cc].append(version.basename()) else: versions[cc] = [version.basename()] return versions @staticmethod def delete_old_versions(to_keep: int) -> None: versions = GameDataGetter.get_all_downloaded_versions() for cc, cc_versions in versions.items(): cc_versions.sort(reverse=True) to_keep = min(to_keep, len(cc_versions)) for version in cc_versions[to_keep:]: path = GameDataGetter.get_game_data_dir().add(cc).add(version) path.remove() def print_no_file(self, packname: str, file_name: str) -> None: if self.version is None: color.ColoredText.localize("failed_to_get_game_versions") else: color.ColoredText.localize( "failed_to_download_game_data", file_name=file_name, pack_name=packname, country_code=self.cc.get_code(), version=self.version, url=self.url, ) ================================================ FILE: src/bcsfe/core/server/headers.py ================================================ from __future__ import annotations import time from bcsfe import core class AccountHeaders: def __init__(self, save_file: core.SaveFile, data: str): self.save_file = save_file self.data = data def get_headers(self) -> dict[str, str]: return AccountHeaders.get_headers_static( self.save_file.inquiry_code, self.data ) @staticmethod def get_headers_static(iq: str, data: str): return { "accept-enconding": "gzip", "connection": "keep-alive", "content-type": "application/json", "nyanko-signature": core.NyankoSignature( iq, data ).generate_signature(), "nyanko-timestamp": str(int(time.time())), "nyanko-signature-version": "1", "nyanko-signature-algorithm": "HMACSHA256", "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)", } ================================================ FILE: src/bcsfe/core/server/managed_item.py ================================================ from __future__ import annotations """ManagedItem class for bcsfe.""" from enum import Enum from typing import Any import uuid import time from bcsfe import core class DetailType(Enum): """Enum for the different types of details.""" GET = "get" USE = "use" class ManagedItemType(Enum): """Enum for the different types of managed items.""" CATFOOD = "catfood" RARE_TICKET = "rareTicket" PLATINUM_TICKET = "platinumTicket" LEGEND_TICKET = "legendTicket" class ManagedItem: """Managed item for backupmetadata""" def __init__( self, amount: int, detail_type: DetailType, managed_item_type: ManagedItemType, detail_code: str = "", detail_created_at: int = 0, ): self.amount = amount self.detail_type = detail_type self.managed_item_type = managed_item_type if not detail_code: detail_code = str(uuid.uuid4()) self.detail_code = detail_code if not detail_created_at: detail_created_at = int(time.time()) self.detail_created_at = detail_created_at @staticmethod def from_change( change: int, managed_item_type: ManagedItemType ) -> ManagedItem: """Create a managed item from a change.""" if change > 0: detail_type = DetailType.GET else: detail_type = DetailType.USE managed_item = ManagedItem(abs(change), detail_type, managed_item_type) return managed_item def to_dict(self) -> dict[str, Any]: """Convert the managed item to a dictionary.""" data = { "amount": self.amount, "detailCode": self.detail_code, "detailCreatedAt": self.detail_created_at, "detailType": self.detail_type.value, "managedItemType": self.managed_item_type.value, } return data def to_short_form(self) -> str: """Convert the managed item to a short form.""" return f"{self.amount}_{self.detail_created_at}_{self.managed_item_type.value}_{self.detail_type.value}" @staticmethod def from_short_form(short_form: str) -> ManagedItem: values = short_form.split("_") try: amount = int(values[0]) except (IndexError, ValueError): amount = 0 try: detail_created_at = int(values[1]) except (IndexError, ValueError): detail_created_at = 0 try: managed_item_type = values[2] except IndexError: managed_item_type = ManagedItemType.CATFOOD.value try: detail_type = values[3] except IndexError: detail_type = DetailType.GET.value return ManagedItem( amount, DetailType(detail_type), ManagedItemType(managed_item_type), detail_created_at=detail_created_at, ) def __str__(self) -> str: return f"{self.amount} {self.managed_item_type.value} ({self.detail_type.value})" def __repr__(self) -> str: return f"{self.amount} {self.managed_item_type.value} ({self.detail_type.value})" class BackupMetaData: def __init__( self, save_file: core.SaveFile, ): self.save_file = save_file self.identifier = "managed_items" def set_managed_items(self, managed_items: list[ManagedItem]): self.save_file.remove_strings(self.identifier) for managed_item in managed_items: string = managed_item.to_short_form() self.save_file.store_string( self.identifier, string, overwrite=False ) def get_managed_items(self) -> list[ManagedItem]: managed_items: list[ManagedItem] = [] managed_items_str = self.save_file.get_strings(self.identifier) for managed_item_str in managed_items_str: managed_item = ManagedItem.from_short_form(managed_item_str) if managed_item.amount == 0: continue managed_items.append(managed_item) return managed_items def add_managed_item(self, managed_item: ManagedItem): if managed_item.amount == 0: return managed_items = self.get_managed_items() managed_items.append(managed_item) self.set_managed_items(managed_items) def remove_managed_items(self) -> None: self.save_file.remove_strings(self.identifier) def create( self, save_key: str | None = None, add_managed_items: bool = True ) -> str: """Create the backup metadata.""" return BackupMetaData.create_static( self.save_file.inquiry_code, self.save_file.officer_pass.play_time, self.save_file.calculate_user_rank(), self.get_managed_items(), save_key, add_managed_items, ) @staticmethod def create_static( iq: str, playtime: int, userrank: int, items: list[ManagedItem], save_key: str | None = None, add_managed_items: bool = True, ): managed_items: list[dict[str, Any]] = [] if add_managed_items: for managed_item in items: if managed_item.amount == 0: continue managed_items.append(managed_item.to_dict()) managed_items_json = core.JsonFile.from_object(managed_items) managed_items_str = ( managed_items_json.to_data(indent=None).to_str().replace(" ", "") ) backup_metadata: dict[str, Any] = { "managedItemDetails": managed_items, "nonce": core.Random.get_hex_string(32), "playTime": playtime, "rank": userrank, "receiptLogIds": [], "signature_v1": core.NyankoSignature( iq, managed_items_str ).generate_signature_v1(), } if save_key is not None: backup_metadata["saveKey"] = save_key return ( core.JsonFile.from_object(backup_metadata) .to_data(indent=None) .to_str() .replace(" ", "") ) ================================================ FILE: src/bcsfe/core/server/request.py ================================================ from __future__ import annotations import requests from bcsfe import core class MultiPartFile: def __init__(self, content: bytes, content_type: str, filename: str | None = None): self.content = content self.content_type = content_type self.filename = filename class MultipartForm: def __init__(self): self.data: dict[str, MultiPartFile] = {} def into_files( self, ) -> dict[str, tuple[str | None, bytes, str]]: out = {} for name, data in self.data.items(): out[name] = (data.filename, data.content, data.content_type) return out def add_key( self, key: str, content: bytes, content_type: str, filename: str | None = None ): self.data[key] = MultiPartFile(content, content_type, filename) def get_all_type(self, content_type: str) -> str: data = "" for key, file in self.data.items(): if file.content_type == content_type: content = file.content.decode("utf-8", errors="ignore") data += f"key: {key}, data: {content}\n" return data class RequestHandler: """Handles HTTP requests.""" def __init__( self, url: str, headers: dict[str, str] | None = None, data: core.Data | None = None, form: MultipartForm | None = None, ): """Initializes a new instance of the RequestHandler class. Args: url (str): URL to request. headers (dict[str, str] | None, optional): Headers to send with the request. Defaults to None. data (core.Data | None, optional): Data to send with the request. Defaults to None. """ if data is None: data = core.Data() self.url = url self.headers = headers self.data = data self.form = form def get( self, stream: bool = False, no_timeout: bool = False, ) -> requests.Response | None: """Sends a GET request. Returns: requests.Response: Response from the server. """ try: return requests.get( self.url, headers=self.headers, timeout=( None if no_timeout else core.core_data.config.get_int( core.ConfigKey.MAX_REQUEST_TIMEOUT ) ), stream=stream, files=None if self.form is None else self.form.into_files(), ) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return None def post(self, no_timeout: bool = False) -> requests.Response | None: """Sends a POST request. Returns: requests.Response: Response from the server. """ try: return requests.post( self.url, headers=self.headers, data=self.data.data, timeout=( None if no_timeout else core.core_data.config.get_int( core.ConfigKey.MAX_REQUEST_TIMEOUT ) ), files=None if self.form is None else self.form.into_files(), ) except requests.exceptions.ConnectionError: return None ================================================ FILE: src/bcsfe/core/server/server_handler.py ================================================ from __future__ import annotations import base64 import time from typing import Any from bcsfe import core import jwt from bcsfe.cli import color class RequestResult: def __init__( self, url: str, response: core.Response | None, headers: dict[str, str], data: str, payload: dict[str, Any] | None = None, timestamp: str | None = None, ): self.url = url self.response = response self.headers = headers self.data = data self.payload = payload self.timestamp = timestamp class ServerHandler: auth_url = "https://nyanko-auth.ponosgames.com" save_url = "https://nyanko-save.ponosgames.com" backups_url = "https://nyanko-backups.ponosgames.com" aws_url = "https://nyanko-service-data-prd.s3.amazonaws.com" managed_item_url = "https://nyanko-managed-item.ponosgames.com" events_url = "https://nyanko-events.ponosgames.com" def __init__(self, save_file: core.SaveFile, print: bool = True): self.save_file = save_file self.print = print self.counter = 0 @staticmethod def get_password_key() -> str: return "password" @staticmethod def get_auth_token_key() -> str: return "auth_token" @staticmethod def get_save_key_key() -> str: return "save_key" def save_password(self, password: str): self.save_file.store_string(ServerHandler.get_password_key(), password) def get_stored_password(self) -> str | None: return self.save_file.get_string(ServerHandler.get_password_key()) def remove_stored_password(self): self.save_file.remove_string(ServerHandler.get_password_key()) def save_save_key_data(self, save_key: dict[str, Any]): self.save_file.store_dict(ServerHandler.get_save_key_key(), save_key) def get_stored_save_key_data(self) -> dict[str, Any] | None: save_key_data = self.save_file.get_dict(ServerHandler.get_save_key_key()) if save_key_data is None: return None if not self.validate_save_key_data(save_key_data): self.remove_stored_save_key_data() return None return save_key_data def validate_save_key_data(self, save_key_data: dict[str, Any]) -> bool: key = save_key_data.get("key") if key is None: return False if key.split("/")[2] != self.save_file.inquiry_code: return False policy = save_key_data.get("policy") if policy is None: return False policy = base64.b64decode(policy) json_policy = core.JsonFile.from_data(core.Data(policy)).to_object() expiration = json_policy.get("expiration") if expiration is None: return False expiration = int( time.mktime(time.strptime(expiration, "%Y-%m-%dT%H:%M:%S.%fZ")) ) if expiration < time.time(): return False return True def remove_stored_save_key_data(self): self.save_file.remove_dict(ServerHandler.get_save_key_key()) def save_auth_token(self, auth_token: str): self.save_file.store_string(ServerHandler.get_auth_token_key(), auth_token) def get_stored_auth_token(self) -> str | None: token = self.save_file.get_string(ServerHandler.get_auth_token_key()) return token def remove_stored_auth_token(self): self.save_file.remove_string(ServerHandler.get_auth_token_key()) def get_password_new(self) -> str | None: self.print_key("getting_password") url = f"{self.auth_url}/v1/users" data = { "accountCode": self.save_file.inquiry_code, "accountCreatedAt": int(self.save_file.energy_penalty_timestamp), "nonce": core.Random.get_hex_string(32), } password = self.do_password_request(url, data) return password @staticmethod def log_error(key: str, result: RequestResult): if "EXPECT_THIS_TO_FAIL" in result.data: return if result.response is None: log_text = "Failed to make request. Check your internet connection." core.core_data.logger.log_error(log_text) return log_text = ( f"Error: {key}\n" f"URL: {result.url}\n" f"Response Headers: {result.response.headers}\n" f"Response Body: {result.response.content.decode('utf-8')}\n" f"Status Code: {result.response.status_code}\n" f"Reason: {result.response.reason}\n" f"Request Headers: {result.headers}\n" f"Request Body: {result.data}\n" ) core.core_data.logger.log_error(log_text) def do_password_request(self, url: str, dict_data: dict[str, Any]) -> str | None: result = self.do_request(url, dict_data) if result.payload is None: ServerHandler.log_error("password_fail", result) return None payload = result.payload password = payload.get("password", None) if password is None: ServerHandler.log_error("password_fail", result) self.remove_stored_password() return None password_refresh_token = payload.get("passwordRefreshToken", None) if password_refresh_token is None: ServerHandler.log_error("password_fail", result) self.remove_stored_password() return None account_code = payload.get("accountCode", None) timestamp = result.timestamp self.save_file.password_refresh_token = password_refresh_token self.save_password(password) if account_code: self.save_file.inquiry_code = account_code self.remove_stored_auth_token() self.remove_stored_save_key_data() if timestamp is not None: self.save_file.energy_penalty_timestamp = int(timestamp) if not self.update_managed_items(): return None return password def do_request(self, url: str, dict_data: dict[str, Any]) -> RequestResult: data = ( core.JsonFile.from_object(dict_data) .to_data(indent=None) .to_str() .replace(" ", "") ) headers = core.AccountHeaders(self.save_file, data).get_headers() response = core.RequestHandler(url, headers, core.Data(data)).post() if response is None: self.log_no_internet(RequestResult(url, None, headers, data)) return RequestResult(url, response, headers, data) json: dict[str, Any] = response.json() status_code = json.get("statusCode", 0) if status_code != 1: return RequestResult(url, response, headers, data) timestamp = json.get("timestamp", None) payload = json.get("payload", {}) return RequestResult(url, response, headers, data, payload, timestamp) def refresh_password(self) -> str | None: self.print_key("refreshing_password") url = f"{self.auth_url}/v1/user/password" data = { "accountCode": self.save_file.inquiry_code, "passwordRefreshToken": self.save_file.password_refresh_token, "nonce": core.Random.get_hex_string(32), } return self.do_password_request(url, data) def get_auth_token_new(self, password: str) -> str | None: self.print_key("getting_auth_token") url = f"{self.auth_url}/v1/tokens" data = core.ClientInfo.from_save_file(self.save_file).get_client_info() data["password"] = password data["accountCode"] = self.save_file.inquiry_code result = self.do_request(url, data) if result.payload is None: ServerHandler.log_error("auth_token_fail", result) self.remove_stored_auth_token() self.remove_stored_password() return None payload = result.payload auth_token = payload.get("token", None) if auth_token is None: ServerHandler.log_error("auth_token_fail", result) self.remove_stored_auth_token() self.remove_stored_password() return None self.save_auth_token(auth_token) return auth_token def get_password(self, tries: int = 0) -> str | None: password = self.get_stored_password() if password is not None: return password password = self.refresh_password() if password is not None: return password password = self.get_password_new() if password is not None: return password self.create_new_account() if tries >= 1: return None return self.get_password(tries + 1) def validate_auth_token(self, auth_token: str) -> bool: token = jwt.decode( # type: ignore auth_token, algorithms=["HS256"], options={"verify_signature": False}, ) if not token: return False if token.get("exp", 0) < time.time(): return False if token.get("accountCode", None) != self.save_file.inquiry_code: return False return True def get_auth_token(self, tries: int = 1) -> str | None: auth_token = self.get_stored_auth_token() if auth_token is not None: if self.validate_auth_token(auth_token): return auth_token self.remove_stored_auth_token() password = self.get_password() if password is None: return None auth_token = self.get_stored_auth_token() if auth_token is not None: return auth_token auth_token = self.get_auth_token_new(password) if auth_token is not None: return auth_token if tries > 0: self.print_key("retry_auth_token") return self.get_auth_token(tries - 1) return None def log_no_internet(self, result: RequestResult): ServerHandler.log_error("no_internet", result) if self.print: core.print_no_internet() def get_save_key_new(self, auth_token: str) -> dict[str, Any] | None: self.print_key("getting_save_key") nonce = core.Random.get_hex_string(32) url = f"{self.save_url}/v2/save/key?nonce={nonce}" headers = { "accept-encoding": "gzip", "connection": "keep-alive", "authorization": "Bearer " + auth_token, "nyanko-timestamp": str(int(time.time())), "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)", } response = core.RequestHandler(url, headers).get() if response is None: self.log_no_internet(RequestResult(url, None, headers, "")) return None json: dict[str, Any] = response.json() status_code = json.get("statusCode", 0) if status_code != 1: ServerHandler.log_error( "save_key_fail", RequestResult(url, response, headers, "") ) self.remove_stored_auth_token() return None payload = json.get("payload", {}) self.save_save_key_data(payload) return payload def get_save_key(self) -> dict[str, Any] | None: # save_key = self.get_stored_save_key_data() # if save_key and save_key.get("key", None): # return save_key auth_token = self.get_auth_token() if auth_token is None: return None # save_key = self.get_stored_save_key_data() # if save_key: # return save_key save_key = self.get_save_key_new(auth_token) if save_key is not None: return save_key return None def get_upload_request_form( self, save_key: dict[str, str], ) -> core.MultipartForm: save_data = self.save_file.to_data() form_data = core.MultipartForm() for key, value in save_key.items(): if key == "url": continue form_data.add_key(key, value.encode(), "text/plain") form_data.add_key( "file", save_data.to_bytes(), "application/octet-stream", "file.sav" ) return form_data def upload_save_data(self, save_key: dict[str, Any]) -> bool: self.print_key("uploading_save_file") form = self.get_upload_request_form(save_key) if form is None: self.remove_stored_save_key_data() return False url = save_key.get("url") if url is None: url = f"{self.aws_url}/" headers = { "accept-encoding": "gzip", "connection": "keep-alive", "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)", } response = core.RequestHandler(url, headers, form=form).post(no_timeout=True) if response is None: self.log_no_internet(RequestResult(url, None, headers, "")) return False if response.status_code != 204: ServerHandler.log_error( "upload_fail_aws", RequestResult( url, response, headers, form.get_all_type("text-plain"), ), ) self.remove_stored_save_key_data() return False return True def print_key(self, key: str, **kwargs: Any): if self.print: color.ColoredText.localize(key, **kwargs) def get_codes(self, upload_managed_items: bool = True) -> tuple[str, str] | None: self.save_file.show_ban_message = False auth_token = self.get_auth_token() if auth_token is None: return None save_key = self.get_save_key() if save_key is None: self.remove_stored_save_key_data() return None if not self.upload_save_data(save_key): return None self.print_key("getting_codes") bmd = core.BackupMetaData(self.save_file) meta_data = bmd.create(save_key["key"], upload_managed_items) url = f"{self.save_url}/v2/transfers" headers = core.AccountHeaders(self.save_file, meta_data).get_headers() headers["authorization"] = "Bearer " + auth_token response = core.RequestHandler(url, headers, core.Data(meta_data)).post() if response is None: self.log_no_internet(RequestResult(url, None, headers, meta_data)) return None json: dict[str, Any] = response.json() status_code = json.get("statusCode", 0) if status_code != 1: ServerHandler.log_error( "upload_fail_transfers", RequestResult(url, response, headers, meta_data), ) self.remove_stored_auth_token() return None payload = json.get("payload", {}) transfer_code = payload.get("transferCode", None) confirmation_code = payload.get("pin", None) if transfer_code is None or confirmation_code is None: ServerHandler.log_error( "upload_fail_transfers", RequestResult(url, response, headers, ""), ) self.remove_stored_auth_token() return None bmd.remove_managed_items() if self.print: print() return (transfer_code, confirmation_code) def has_managed_items(self) -> bool: bmd = core.BackupMetaData(self.save_file) managed_items = bmd.get_managed_items() if len(managed_items) == 0: return False return True def upload_meta_data(self) -> bool: auth_token = self.get_auth_token() if auth_token is None: return False save_key = self.get_save_key() if save_key is None: self.remove_stored_save_key_data() return False if not self.upload_save_data(save_key): return False bmd = core.BackupMetaData(self.save_file) meta_data = bmd.create(save_key["key"]) url = f"{self.save_url}/v2/backups" headers = core.AccountHeaders(self.save_file, meta_data).get_headers() headers["authorization"] = "Bearer " + auth_token response = core.RequestHandler(url, headers, core.Data(meta_data)).post() if response is None: self.log_no_internet(RequestResult(url, None, headers, meta_data)) return False json: dict[str, Any] = response.json() status_code = json.get("statusCode", 0) if status_code != 1: self.remove_stored_auth_token() return False bmd.remove_managed_items() return True def get_new_inquiry_code(self) -> str | None: url = f"{self.backups_url}/?action=createAccount&referenceId=" response = core.RequestHandler(url).get() if response is None: self.log_no_internet(RequestResult(url, None, {}, "")) return None data = response.json() iq = data["accountId"] return iq def create_new_account(self) -> bool: new_iq = self.get_new_inquiry_code() if new_iq is None: return False self.save_file.inquiry_code = new_iq self.remove_stored_auth_token() self.remove_stored_save_key_data() self.remove_stored_password() fail_text = "EXPECT_THIS_TO_FAIL" start_count = (40 - len(fail_text)) // 2 end_count = 40 - len(fail_text) - start_count self.save_file.password_refresh_token = ( "_" * start_count + fail_text + "_" * end_count ) password = self.get_password() auth_token = self.get_auth_token() save_key_data = self.get_save_key() self.update_managed_items() self.save_file.show_ban_message = False if password is None or auth_token is None or save_key_data is None: return False return True @staticmethod def from_codes( transfer_code: str, confirmation_code: str, cc: core.CountryCode, gv: core.GameVersion, print: bool = True, save_backup: bool = True, ) -> tuple[ServerHandler | None, RequestResult | None]: url = f"{ServerHandler.save_url}/v2/transfers/{transfer_code}/reception" data = core.ClientInfo(cc, gv).get_client_info() data["pin"] = confirmation_code data_str = ( core.JsonFile.from_object(data) .to_data(indent=None) .to_str() .replace(" ", "") ) headers = { "content-type": "application/json", "accept-encoding": "gzip", "connection": "keep-alive", "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)", } response = core.RequestHandler(url, headers, core.Data(data_str)).post() if response is None: if print: core.print_no_internet() return None, None resp_headers = response.headers content_type = resp_headers.get("content-type", "") if content_type != "application/octet-stream": return None, RequestResult(url, response, headers, data_str) save_data = response.content if save_backup: temp_path = core.get_transfer_backup_path() if temp_path is None: temp_path = ( core.Path.get_data_folder() .add("saves") .generate_dirs() .add("transfer_backup") ) try: temp_path.write(core.Data(save_data)) except Exception as e: color.ColoredText.localize( "transfer_backup_fail", path=str(temp_path), error=e ) else: if print: color.ColoredText.localize("transfer_backup", path=str(temp_path)) save_file = core.SaveFile(core.Data(save_data), cc=cc) password_refresh_token = resp_headers.get("Nyanko-Password-Refresh-Token") if password_refresh_token is not None: save_file.password_refresh_token = password_refresh_token server_handler = ServerHandler(save_file) password = resp_headers.get("Nyanko-Password") if password is not None: server_handler.save_password(password) return server_handler, RequestResult(url, response, headers, data_str) def update_managed_items(self) -> bool: auth_token = self.get_auth_token() if auth_token is None: return False data = { "catfoodAmount": self.save_file.catfood, "isPaid": True, "legendTicketAmount": self.save_file.legend_tickets, "nonce": core.Random.get_hex_string(32), "platinumTicketAmount": self.save_file.platinum_tickets, "rareTicketAmount": self.save_file.rare_tickets, } data_str = ( core.JsonFile.from_object(data) .to_data(indent=None) .to_str() .replace(" ", "") ) url = f"{self.managed_item_url}/v1/managed-items" headers = core.AccountHeaders(self.save_file, data_str).get_headers() headers["authorization"] = "Bearer " + auth_token response = core.RequestHandler(url, headers, core.Data(data_str)).post() if response is None: self.log_no_internet(RequestResult(url, None, headers, data_str)) return False json: dict[str, Any] = response.json() status_code = json.get("statusCode", 0) if status_code != 1: self.remove_stored_auth_token() return False core.BackupMetaData(self.save_file).remove_managed_items() return True def download_event_data(self, filename: str) -> core.Data | None: url = ( self.events_url + f"/battlecats{self.save_file.cc.get_patching_code()}_production/{filename}" ) auth_token = self.get_auth_token() if auth_token is None: return None url += f"?jwt={auth_token}" headers = { "accept-encoding": "gzip", "connection": "keep-alive", "user-agent": "Dalvik/2.1.0 (Linux; U; Android 9; Pixel 2 Build/PQ3A.190801.002)", } resp = core.RequestHandler(url, headers).get() if resp is None: return None return core.Data(resp.content) def download_gatya_data(self) -> core.Data | None: return self.download_event_data("gatya.tsv") def download_item_data(self) -> core.Data | None: return self.download_event_data("item.tsv") def download_sale_data(self) -> core.Data | None: return self.download_event_data("sale.tsv") ================================================ FILE: src/bcsfe/core/server/updater.py ================================================ from __future__ import annotations import sys from typing import Any from bcsfe import core import bcsfe class Updater: package_name = "bcsfe" def __init__(self): pass def get_local_version(self) -> str: return bcsfe.__version__ def get_pypi_json(self) -> dict[str, Any] | None: url = f"https://pypi.org/pypi/{self.package_name}/json" # add a User-Agent since pypi started to block the default requests user-agent # this probably won't be needed in the future as i assume this block is temporary response = core.RequestHandler( url, headers={"User-Agent": "BCSFE-Updater"} ).get() if response is None: return None try: return response.json() except core.JSONDecodeError: return None def get_releases(self) -> list[str] | None: pypi_json = self.get_pypi_json() if pypi_json is None: return None releases = pypi_json.get("releases") if releases is None: return None return list(releases.keys()) def get_latest_version(self, prereleases: bool = False) -> str | None: releases = self.get_releases() if releases is None: return None releases.reverse() if prereleases: return releases[0] else: for release in releases: if "b" not in release: return release return releases[0] def get_latest_version_info( self, prereleases: bool = False ) -> dict[str, Any] | None: pypi_json = self.get_pypi_json() if pypi_json is None: return None releases = pypi_json.get("releases") if releases is None: return None return releases.get(self.get_latest_version(prereleases)) def update(self, target_version: str) -> bool: binary = sys.orig_argv[0] python_aliases = [binary, "py", "python", "python3"] for python_alias in python_aliases: cmd = f"{python_alias} -m pip install --upgrade {self.package_name}=={target_version}" result = core.Path().run(cmd) if result.exit_code == 0: break else: pip_aliases = ["pip", "pip3"] for pip_alias in pip_aliases: cmd = f"{pip_alias} install --upgrade {self.package_name}=={target_version}" result = core.Path().run(cmd) if result.exit_code == 0: break else: return False return True def has_enabled_pre_release(self) -> bool: return core.core_data.config.get_bool(core.ConfigKey.UPDATE_TO_BETA) ================================================ FILE: src/bcsfe/core/theme_handler.py ================================================ from __future__ import annotations import dataclasses import tempfile from typing import Any from bcsfe import core from bcsfe.cli import color class ThemeHandler: def __init__(self, theme_code: str | None = None): if theme_code is None: self.theme_code = core.core_data.config.get_str(core.ConfigKey.THEME) else: self.theme_code = theme_code self.theme_data = self.get_theme_data() @staticmethod def get_themes_folder() -> core.Path: return core.Path("themes", True).generate_dirs() @staticmethod def get_external_themes_folder() -> core.Path: return core.Path.get_data_folder().add("external_themes").generate_dirs() @staticmethod def get_theme_path(theme_code: str) -> core.Path: if theme_code.startswith("ext-"): return ThemeHandler.get_external_themes_folder().add(theme_code + ".json") return ThemeHandler.get_themes_folder().add(theme_code + ".json") def get_theme_data(self) -> dict[str, Any]: file_path = self.get_theme_path(self.theme_code) if not file_path.exists(): return {} try: return core.JsonFile.from_data(file_path.read()).to_object() except core.JSONDecodeError: return {} def get_short_name(self) -> str: return self.theme_data.get("short_name", "") def get_name(self) -> str: return self.theme_data.get("name", "") def get_description(self) -> str: return self.theme_data.get("description", "") def get_author(self) -> str: return self.theme_data.get("author", "") def get_version(self) -> str: return self.theme_data.get("version", "") def get_git_repo(self) -> str | None: return self.theme_data.get("git_repo", None) def get_theme_colors(self) -> dict[str, Any]: return self.theme_data.get("colors", {}) def get_theme_color(self, color_code: str) -> str: return self.get_theme_colors().get(color_code, "") def get_primary_color(self) -> str: return self.get_theme_color("primary") def get_secondary_color(self) -> str: return self.get_theme_color("secondary") def get_tertiary_color(self) -> str: return self.get_theme_color("tertiary") def get_quaternary_color(self) -> str: return self.get_theme_color("quaternary") def get_error_color(self) -> str: return self.get_theme_color("error") def get_warning_color(self) -> str: return self.get_theme_color("warning") def get_success_color(self) -> str: return self.get_theme_color("success") @staticmethod def get_all_themes() -> list[str]: themes = [ file.get_file_name_without_extension() for file in ThemeHandler.get_themes_folder().get_paths_dir( regex=r".*\.json" ) ] themes += [ folder.get_file_name_without_extension() for folder in ThemeHandler.get_external_themes_folder().get_paths_dir( regex=r".*\.json" ) ] return themes @staticmethod def remove_theme(theme_code: str): extern = ExternalThemeManager.get_external_theme(theme_code) if extern is not None: ExternalThemeManager.delete_theme(extern) ThemeHandler.get_theme_path(theme_code).remove() if theme_code == core.core_data.config.get_str(core.ConfigKey.THEME): core.core_data.config.set_default(core.ConfigKey.THEME) @dataclasses.dataclass class ExternalTheme: short_name: str name: str description: str author: str version: str colors: dict[str, Any] git_repo: str | None = None def to_json(self) -> dict[str, Any]: return dataclasses.asdict(self) @staticmethod def from_json(json_data: dict[str, Any]) -> ExternalTheme | None: try: return ExternalTheme(**json_data) except TypeError: return None @staticmethod def from_git_repo(git_repo: str) -> ExternalTheme | None: repo = core.GitHandler().get_repo(git_repo) if repo is None: return None theme_json = repo.get_file(core.Path("theme.json")) if theme_json is None: return None json_data = core.JsonFile.from_data(theme_json).to_object() json_data["git_repo"] = git_repo return ExternalTheme.from_json(json_data) def get_new_version(self) -> bool: if self.git_repo is None: return False repo = core.GitHandler().get_repo(self.git_repo) if repo is None: return False with tempfile.TemporaryDirectory() as tmp: temp_dir = core.Path(tmp) success = repo.clone_to_temp(temp_dir) if not success: return False external_theme = ExternalThemeManager.parse_external_theme( temp_dir.add("theme.json") ) if external_theme is None: return False version = external_theme.version if version == self.version: return False self.name = external_theme.name self.short_name = external_theme.short_name self.description = external_theme.description self.author = external_theme.author self.colors = external_theme.colors self.version = version success = repo.pull() if not success: return False self.save() return True def save(self): ExternalThemeManager.save_theme(self) def get_full_name(self) -> str: return f"ext-{self.author}-{self.short_name}" class ExternalThemeManager: @staticmethod def delete_theme(external_theme: ExternalTheme): if external_theme.git_repo is None: return folder = core.GitHandler.get_repo_folder().add( external_theme.git_repo.split("/")[-1] ) folder.remove() @staticmethod def save_theme( external_theme: ExternalTheme, ): """Saves an external theme. Args: external_theme (ExternalTheme): External theme to save. """ if external_theme.git_repo is None: return file = ThemeHandler.get_theme_path(external_theme.get_full_name()) json_data = external_theme.to_json() file.write(core.JsonFile.from_object(json_data).to_data()) @staticmethod def parse_external_theme(path: core.Path) -> ExternalTheme | None: """Parses an external theme. Args: path (core.Path): Path to the external theme. Returns: ExternalTheme: External theme. """ json_data = core.JsonFile.from_data(path.read()).to_object() return ExternalTheme.from_json(json_data) @staticmethod def update_external_theme(external_theme: ExternalTheme): """Updates an external theme. Args: external_theme (ExternalTheme): External theme to update. """ if external_theme.git_repo is None: return color.ColoredText.localize( "checking_for_theme_updates", theme_name=external_theme.name, ) updated = external_theme.get_new_version() if updated: color.ColoredText.localize( "external_theme_updated", theme_name=external_theme.name, version=external_theme.version, ) else: color.ColoredText.localize( "external_theme_no_update", theme_name=external_theme.name, version=external_theme.version, ) print() @staticmethod def update_all_external_themes(_: Any = None): """Updates all external themes.""" files = ThemeHandler.get_external_themes_folder().get_paths_dir() if not files: color.ColoredText.localize( "no_external_themes", ) return if not core.GitHandler.is_git_installed(): color.ColoredText.localize( "git_not_installed", ) return for file in files: theme = ExternalThemeManager.parse_external_theme(file) if theme is None: continue ExternalThemeManager.update_external_theme(theme) @staticmethod def get_external_theme_config() -> ExternalTheme | None: """Gets the external theme from the config. Returns: ExternalTheme: External theme. """ theme = core.core_data.config.get_str(core.ConfigKey.THEME) if not theme.startswith("ext-"): return None return ExternalThemeManager.parse_external_theme( ThemeHandler.get_theme_path(theme) ) @staticmethod def get_external_theme(theme: str) -> ExternalTheme | None: """Gets the external theme from the theme code. Returns: ExternalTheme: External theme. """ if not theme.startswith("ext-"): return None return ExternalThemeManager.parse_external_theme( ThemeHandler.get_theme_path(theme) ) ================================================ FILE: src/bcsfe/files/locales/en/core/config.properties ================================================ config=Config edit_config=Edit config default_value=(default value: <@q>{default_value}) current_value=(current value: <@q>{current_value}) config_value_txt=<@s>{{current_value}} {{default_value}} config_dialog=Select a config option to edit: update_to_beta_desc=Check for updates to beta versions {{config_value_txt}} update_to_beta=Update to Beta Versions show_update_message_desc=Show a message when a new version is available {{config_value_txt}} show_update_message=Show update message config_full=<@t>{key_desc} disable_maxes_desc=Disable maximum values when editing {{config_value_txt}} disable_maxes=Disable maximum values max_backups_desc=Maximum number of backups of save files to keep {{config_value_txt}} max_backups=Maximum save backups available_themes=Available themes: theme_desc=Theme to use {{config_value_txt}} theme=Theme show_missing_locale_keys=Show missing locale keys 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}} reset_cat_data_desc=Reset all cat data when removing a cat from the save file {{config_value_txt}} reset_cat_data=Reset cat data on cat removal filter_current_cats_desc=When selecting cats to edit, filter out cats that are not in the save file {{config_value_txt}} filter_current_cats=Filter current cats on cat selection set_cat_current_forms_desc=When true forming cats, set the cat's current form to the newly unlocked form {{config_value_txt}} set_cat_current_forms=Set cat current forms on form unlock 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}} strict_upgrade=Strict upgrade checks separate_cat_edit_options_desc=Separate the cat edit options into multiple features {{config_value_txt}} separate_cat_edit_options=Separate cat edit options strict_ban_prevention_desc=When doing anything server related, create a new account to reduce the chance of getting banned {{config_value_txt}} strict_ban_prevention=Strict ban prevention max_request_timeout_desc=Maximum time to wait for a request to complete (in seconds) {{config_value_txt}} max_request_timeout=Maximum request timeout game_data_repo_desc=Repository to use for game data {{config_value_txt}} game_data_repo=Game data repository game_data_repo_dialog=Enter a game data repository to use: 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}} force_lang_game_data=Force use game data for current locale clear_tutorial_on_load_desc=Clear the tutorial when you load a save file into the editor {{config_value_txt}} clear_tutorial_on_load=Clear tutorial on save load remove_ban_message_on_load_desc=Remove the ban message when you load a save file into the editor {{config_value_txt}} remove_ban_message_on_load=Remove ban message on save load unlock_cat_on_edit_desc=Unlock the cat when you edit its level, talents, form, etc. {{config_value_txt}} unlock_cat_on_edit=Unlock cat on edit use_file_dialog_desc=Use the tkinter file dialog to open and save files instead of the file input {{config_value_txt}} use_file_dialog=Use file dialog adb_path_desc=Path to the adb executable {{config_value_txt}} adb_path=ADB path use_waydroid=Use waydroid shell rather than adb use_waydroid_desc=Waydroid doesn't support adb root, so use waydroid shell instead {{config_value_txt}} use_pkexec_waydroid=Use the pkexec binary to run waydroid commands 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}} 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}} ignore_parse_error=Ignore Save Parsing Errors string_config_dialog=Enter a new value for <@q>{val}: enable_disable_dialog=Do you want to <@q>enable or <@q>disable this feature?: enable=Enable disable=Disable enabled=Enabled disabled=Disabled config_success=<@su>Successfully updated config 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? ================================================ FILE: src/bcsfe/files/locales/en/core/files.properties ================================================ another_path=Enter path manually select_files_dir=Select files in directory: enter_path=Enter file path / location: enter_path_dir=Enter folder path / location: enter_path_default=Enter file path / location (default: <@t>{default}): current_files_dir=Current files in directory <@t>{dir}: other_dir=Enter other directory no_files_dir=<@e>No files in directory path_not_exists=<@e>Path does not exist ================================================ FILE: src/bcsfe/files/locales/en/core/input.properties ================================================ input_int=Input a number between <@q>{min} and <@q>{max}: select_edit=Select options for <@t>{group_name}: input_int_default=Input a number between <@q>{min} and <@q>{max} (default <@q>{default}): input_many=Input numbers between <@q>{min} and <@q>{max} separated by spaces: input_single=Input a number between <@q>{min} and <@q>{max}: input=Enter a value for <@t>{name} (current value: <@q>{value}) (max value: <@q>{max}): input_min=Enter a value for <@t>{name} (current value: <@q>{value}) (range: <@q>{min} - <@q>{max}): input_non_max=Enter a value for <@t>{name} (current value: <@q>{value}): input_all=Enter a value for all <@t>{name} (max value: <@q>{max}): value_changed=<@su>Successfully changed <@s>{name} to <@s>{value} value_gave=<@su>Successfully gave the <@s>{name} all_at_once=Select all options at once invalid_input=<@e>Invalid input. Please try again. invalid_input_int=<@e>Invalid input. Please enter a number between <@s>{min} and <@s>{max} select_option=Select option: finish=Finish features=Features: go_back=Go back yes_key=y quit_key=q 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: select_features= >To select a feature, enter >- a <@q>number corresponding to the number on the left >- <@t>text to search for a feature >You can press <@t>enter to view all features >Some features are <@t>categories and so when selected, will display all of its <@t>sub-features >Input: individual=Individual edit_all_at_once=All at once ================================================ FILE: src/bcsfe/files/locales/en/core/locale.properties ================================================ available_locales=Available languages: locale_desc=Language to use {{config_value_txt}} locale=Language locale_dialog=Select a language: add_locale=Add Locale remove_locale=Remove Locale locale_remove_dialog=Select locales to remove: enter_locale_git_repo=Enter the git repository of the locale (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git): locale_already_exists=<@e>A locale with name <@s>{locale_name} already exists.\nWould you like to overwrite it? ({{y/n}}): locale_added=<@su>Successfully added localization checking_for_locale_updates=Checking for updates to external localization <@t>{locale_name}... external_locale_updated=<@su>Successfully updated external localization <@t>{locale_name} to version <@t>{version}<@t>.\n{{restart_to_see_changes}} external_locale_no_update=<@su>No update needed for external localization <@t>{locale_name} latest version is <@t>{version}<@t> invalid_git_repo=<@e>Invalid git repository locale_cancelled=<@e>Cancelled restart_to_see_changes=You will need to restart the editor to see all of the changes locale_changed=<@su>Successfully changed locale to <@t>{locale_name}.\n{{restart_to_see_changes}} locale_removed=<@su>Successfully removed locale <@t>{locale_name}.\n{{restart_to_see_changes}} no_external_locales=<@w>No external locales found missing_locale_keys=Missing Locale Keys: extra_locale_keys=Extra Locale Keys: locale_text= >Current Locale: <@s>{locale_name} (Version: <@s>{locale_version}) >Made by <@s>{locale_author} >Locale File Location: <@s>{locale_path} default_locale_text_authors= >Current Locale: <@s>{name} >Made by <@s>{authors} >Locale File Location: <@s>{path} ================================================ FILE: src/bcsfe/files/locales/en/core/main.properties ================================================ # Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files # color formatting # # <@p> = primary color # <@s> = secondary color # <@t> = tertiary color # <@q> = quaternary color # <@e> = error color # <@w> = warning color # <@su> = success color # # = close current color # When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes. # You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red. # If you want to write < or > or / in the text, escape them with a backslash (\) e.g. \< or \> or \/ # # <#rrggbb> = hex color # # = white # = black # = red # = green # = blue # = yellow # = magenta # = cyan # = dark yellow # = dark grey # = dark blue # = dark cyan # = dark magenta # = dark red # = dark green # = light grey # = orange downloading=<@su>Downloading <@s>{file_name} from <@s>{pack_name} with version <@s>{version} and country code <@s>{country_code} 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. failed_to_get_game_versions=<@e>Failed to get game versions. Maybe check your internet connection. no_device_error=<@e>No connected devices found 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. exit=Exit tkinter_not_found=<@e>tkinter was not found. If you are not on mobile, please install it and try again. tkinter_not_found_enter_path_file=Please enter the path/location of the {initialfile} file: tkinter_not_found_enter_path_file_save=Please enter the path/location to save the {initialfile} file: tkinter_not_found_enter_path_dir=Please enter the path/location of the {initialdir} folder instead: discord_url=https://discord.gg/DvmMgvn5ZB welcome= ><@t>Welcome to the <@s>Battle Cats Save File Editor! >Made by <@s>fieryhenry > >Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python >Discord: <@s>{{discord_url}} - Please report any bugs to <@s>#bug-reports and suggestions to <@s>#suggestions >Donate: <@s>https://ko-fi.com/fieryhenry > >Config File Location: <@s>{config_path} > >{theme_text} > >{locale_text} > ><@q>Thanks To: >- <@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/ >- <@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 >- Anyone who has supported my work for giving me motivation to keep working on this and similar projects: <@s>https://ko-fi.com/fieryhenry >- Everyone in the discord for giving me saves, reporting bugs, suggesting new features, and for being an amazing community: <@s>{{discord_url}} > ><@w>If you paid for this program, you have been scammed. This program is free and open source. > ><@w>Use this tool at your own risk. I am not responsible for any bans or damage caused to your save file. >Obviously, the save editor does try to prevent this from happening, but I cannot guarantee that your save is safe. >Though if your save does get corrupted please do still report it to the discord. >I recommend you to make backups of your save file before editing it. report_message=Please report this to <@s>#bug-reports on the discord: <@s>{{discord_url}} report_message_l=please report this to <@s>#bug-reports on the discord: <@s>{{discord_url}} try_again_message=Please try again. If error persists {{report_message_l}} all=All error=<@e>An error has occurred (<@s>{error}, editor version: <@s>{version}) {{report_message_l}}\n{traceback} see_log=<@e>Please see the log file for more details. max=max none=None unknown=Unknown leave=\n<@q>Thank you for using the Battle Cats Save File Editor! checking_for_changes=<@t>Checking for changes... no_changes=<@su>No changes found. changes_found=<@su>Changes found. y/n=y/n yes=yes git_not_installed=<@e>Git is not installed. Please install it, add it to PATH, and try again. failed_to_get_repo=<@e>Failed to get repo: "<@t>{url}". Maybe it doesn't exist, or you have no internet connection failed_to_run_git_cmd=<@e>Failed to run git command: "<@t>{cmd}". Maybe check your internet connection cancel=Cancel update_external=Update External Content updating_external_content=<@q>Updating external content... downloading_map_names=<@q>Getting map names... (code: <@t>{code}). This may take a while... select_device=Select device: continue_q=Continue? ({{y/n}}): no_data_version=<@e>The latest available game data version is not available. This is probably due to internet issues. Please try again. no_feature_with_name=<@e>No feature found with name: <@s>{name} ================================================ FILE: src/bcsfe/files/locales/en/core/save.properties ================================================ save_load_option=Select an option to load the save file download_save=Download save file using transfer and confirmation code select_save_file=Select save file from file adb_pull_save=Pull save file from device using adb waydroid_pull_save=Pull save from waydroid device load_save_data_json=Load save data from json root_storage_pull_save=Pull save file from root storage save_save_dialog=Save save file save_downloaded=<@su>Save file downloaded to <@s>{path} save_json_dialog=Save save data to json load_from_documents=Load save file from <@s>{path} save_file_not_found=<@e>Save file not found save_file_found=<@su>Loading save from: <@t>{path} parse_save_error=<@e>An error occurred while parsing your save file: {error}\n(editor version: <@s>{version}) (game version: <@s>{game_version}) (country code: <@s>{country_code})\n{{report_message}} load_json_fail=<@e>Failed to load save data from json ({error}) parse_json_fail=<@e>Failed to read json file, is your file actually in JSON format? editor_version_mismatch=<@w>Editor version mismatch. Save file may not be compatible with this editor. Json Version: <@t>{json_version}, Editor Version: <@t>{editor_version} save_management=Save Management save_save=Save save save_save_file=Save save to specific file save_save_documents=Save save to {path} save_upload=Upload save file to server and get transfer and confirmation code unban_account=Unban Account / Fix Save Used Elsewhere Error adb_push_rerun=Use adb to push the save file to a device (Rerun the game after pushing) adb_push=Use adb to push the save file to a device (Do not rerun the game after pushing) adb_push_success=<@su>Save file pushed to device adb_push_fail=<@e>Failed to push save file to device ({error}) adb_rerun_success=<@su>Successfully reran game adb_rerun_fail=<@e>Failed to rerun game ({error}) waydroid_push_rerun=Push the save file to a waydroid device (Also rerun the game after pushing) waydroid_push=Push the save file to a waydroid device (Do not rerun the game after pushing) waydroid_push_success=<@su>Save file pushed to waydroid device waydroid_push_fail=<@e>Failed to push save file to waydroid device ({error}) waydroid_rerun_success=<@su>Successfully reran game on waydroid device waydroid_rerun_fail=<@e>Failed to rerun game on waydroid device ({error}) export_save=Export save file to json save_success=<@su>Save file saved to <@s>{path} export_success=<@su>Save data exported to <@s>{path} init_save=Reset save file init_save_confirm=Are you sure you want to reset your save file? ({{y/n}}): init_save_success=<@su>Succesfully reset save file adb_pulling=<@q>Pulling save file from device with package name <@s>{package_name}with adb ... adb_pull_fail=<@e>Failed to pull save file from device with package name <@s>{package_name} ({error}) with adb waydroid_pulling=<@q>Pulling save file from device with package name <@s>{package_name} with waydroid ... waydroid_pull_fail=<@e>Failed to pull save file from device with package name <@s>{package_name} ({error}) with waydroid storage_pulling=<@q>Pulling save file from root storage with package name <@s>{package_name}... storage_pull_fail=<@e>Failed to pull save file from root storage with package name <@s>{package_name} ({error}) not_rooted_error=<@e>Device does not seem to be rooted, or the editor is not running as root upload_items=Upload managed items to server upload_items_success=<@su>Successfully uploaded managed items upload_items_fail=<@e>Failed to upload managed items load_save=Load save file load_save_success=<@su>Succesfully loaded save file account=Account save_before_exit=<@q>Save latest changes before exiting? (<@s>y/<@s>n): save_temp_success=<@su>Succesfully managed to recover save file from temp file save_temp_fail=<@e>Failed to recover save file from temp file. Latest save changes are lost ({error})\n{traceback} save_temp_not_found=<@e>Failed to recover save file from temp file. Latest save changes are lost (Temp file not found) cant_detect_cc=<@w>Failed to detect country code from save file. \nPlease enter your country code manually failed_to_load_save_gv=Save file loaded but certain values were not as expected. Error thrown to prevent save corruption failed_to_load_save=Failed to load save file failed_to_save_save=Failed to save save file game_version_dialog=Enter game version (e.g <@t>12.2.1): invalid_game_version=<@e>Invalid game version country_code_set=<@su>Succesfully set country code to <@s>{cc} game_version_set=<@su>Succesfully set game version to <@s>{version} convert_region=Convert country code (e.g en -\> jp) convert_version=Convert game version (e.g 12.2.1 -\> 12.2.0) cc_warning=<@w>Warning: This may cause issues with your save file. Bugs and crashes are expected! If reporting a bug, please ensure to mention you have used this feature. You should enter the cat base / upgrade menu after running this feature so the game makes the necessary changes to your save file.\nCurrent country code: <@t>{current} gv_warning=<@w>Warning: This may cause issues with your save file. Bugs and crashes are expected! If reporting a bug, plesae ensure to mention you have used this feature. You should enter the cat base / upgrade menu after running this feature so the game makes the necessary changes to your save file.\nCurrent game version: <@t>{current} create_new_save_success=<@su>Succesfully created new save file create_new_save=Create new save file create_new_save_warning=<@w>Warning: Many editor features will not work with an auto-created save file, you need to load it in the game first, then reload it in the editor\nThis is likely to change in later editor versions. parse_ignored_error=<@w>WARNING: <@e>{error}<>\n<@w>Ignoring due to the <@s>Ignore Parse Error config flag being set. This may cause issues! select_package_name=Select package name: adb_not_installed= ><@e>adb has not been added to your PATH environment variable or the executable path is incorrect. Try editing the adb path in the config >Current Value: <@s>{path} >Error: <@s>{error} waydroid_not_installed=<@e>Waydroid is not installed, or an error occured: {error} root_push_not_android_error=<@e>Root push is only available on android devices root_push_success=<@su>Successfully wrote save to root storage root_push_fail=<@e>Failed to write save to root storage. Error: <@s>{error} root_rerun_success=<@su>Successfully reran game root_rerun_fail=<@e>Failed to rerun game. Error: <@s>{error} root_push=Use root to push save directly to the game root_push_rerun=Use root to push save directly to the game (and rerun the game) select_recent=Select recent save: recent_save=<@s><@q>{inquiry_code} <@t>{cc}-<@t>{gv} @ <@t>{year}-<@t>{month}-<@t>{day} <@t>{hour}:<@t>{minute}:<@t>{second} - original path: <@q>{name} load_recent_saves=Load from recent saves and backups no_recent_saves=<@w>No recent saves current_save=\nCurrent Save: <@t>{cc}-<@t>{gv} Inquiry: <@t>{inquiry_code} ================================================ FILE: src/bcsfe/files/locales/en/core/server.properties ================================================ transfer_code=Transfer Code enter_transfer_code=Enter Transfer Code: confirmation_code=Confirmation Code enter_confirmation_code=Enter Confirmation Code: country_code=Country Code country_code_select=Select country code: invalid_codes_error=<@e>Failed to download save file. Please check your transfer code and confirmation code and country code and try again. display_response_debug_info_q=Do you want to display the response debug info? ({{y/n}}): response_text_display= >URL: <@q>{url} >Request Headers: <@q>{request_headers} >Request Body: <@q>{request_body} > >Response Headers: <@q>{response_headers} >Response Body: <@q>{response_body} downloading_save_file=Downloading save file from server (transfer code: <@q>{transfer_code}, confirmation code: <@q>{confirmation_code}, country code: <@q>{country_code})... upload_result= ><@su> >Transfer Code: <@s>{transfer_code} >Confirmation Code: <@s>{confirmation_code} > upload_fail=<@e>Failed to upload save file. {{try_again_message}} {{see_log}} unban_fail=<@e>Failed to unban account. {{try_again_message}} {{see_log}} unban_success=<@su>Account unbanned successfully. 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}}): strict_ban_prevention_enabled=<@w>Strict Ban Prevention Enabled. A new account will be created before uploading save file / managed items. create_new_account_success=<@su>Account created successfully. create_new_account_fail=<@e>Failed to create account. {{try_again_message}} {{see_log}} uploading_save_file=<@q>Uploading save file to server... getting_codes=<@q>Getting transfer code and confirmation code... getting_auth_token=<@q>Getting account auth token... refreshing_password=<@q>Refreshing account password... getting_password=<@q>Getting account password... getting_save_key=<@q>Getting account save key... 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}} 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}} no_internet=<@e>No internet connection. Please check your internet connection and try again. transfer_backup=<@su>Saved backup transfer save file to <@t>{path} transfer_backup_fail=<@e>Failed to save transfer backup file to <@t>{path} due to {error} retry_auth_token=<@e>Failed to get auth token, retrying... downloading_compressed_data=<@su>Downloading game data from <@s>{url} clear_game_data_q=Do you want to clear all downloaded game data? ({{y/n}}): cleared_game_data=<@su>Successfully cleared game data validating_game_repo=Validating game data repo... invalid_response=<@e>Invalid response code: <@s>{response_code}. Expected <@s>200 no_internet_or_connection_error=<@e>Failed to connect to game data repo invalid_url=<@e>Invalid URL use_alternative_repo=<@e>Failed to fetch game data repo, this may be an issue with your internet connection or the repo is blocked on your network. Would you like to swap to <@t>{repo} as an alternative game data repo? ({{y/n}}): ================================================ FILE: src/bcsfe/files/locales/en/core/theme.properties ================================================ theme_text= >Current Theme: <@s>{theme_name} (Version <@s>{theme_version}) >Made by <@s>{theme_author} >Theme File Location: <@s>{theme_path} default_theme_text= >Current Theme: <@s>Default >Theme File Location: <@s>{theme_path} checking_for_theme_updates=Checking for updates to external theme <@t>{theme_name}... external_theme_updated=<@su>Successfully updated external theme <@t>{theme_name} to version <@t>{version}<@t>.\n{{restart_to_see_changes}} external_theme_no_update=<@su>No update needed for external theme <@t>{theme_name} latest version is <@t>{version}<@t> theme_changed=<@su>Successfully changed theme to <@t>{theme_name}.\n{{restart_to_see_changes}} theme_removed=<@su>Successfully removed theme <@t>{theme_name}.\n{{restart_to_see_changes}} no_external_themes=<@w>No external themes found theme_dialog=Select a theme: add_theme=Add theme remove_theme=Remove theme theme_remove_dialog=Select themes to remove: enter_theme_git_repo=Enter the git repository of the theme (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git): theme_already_exists=<@e>A theme with name <@s>{theme_name} already exists.\nWould you like to overwrite it? ({{y/n}}): theme_added=<@su>Successfully added theme ================================================ FILE: src/bcsfe/files/locales/en/core/updater.properties ================================================ local_version=<@q>Local version: <@s>{local_version} latest_version=<@q>Latest version: <@s>{latest_version} update_check_fail=<@e>Failed to check for updates. Maybe check your internet connection? update_available= ><@q>An update is available: <@s>{latest_version} >Would you like to update? <@t>({{y/n}}): update_success= ><@t>Update successful >Please restart the application update_fail= ><@e>Update failed >Please update manually >Command: <@s>pip install --upgrade bcsfe version_line={{local_version}} | {{latest_version}} disable_update_message=Would you like to disable update messages? <@t>({{y/n}}): ================================================ FILE: src/bcsfe/files/locales/en/edits/bannable_items.properties ================================================ do_you_want_to_continue=Do you want to continue? ({{y/n}}): catfood_warning=<@w>WARNING: Editing in cat food can result in a ban. Use at your own risk.\n{{do_you_want_to_continue}} 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}} rare_ticket_warning= ><@w>WARNING: Editing in rare tickets can result in a ban. Use at your own risk. >You can use the rare ticket trade feature to get rare tickets with a lower risk of ban. platinum_ticket_warning= ><@w>WARNING: Editing in platinum tickets can result in a ban. Use at your own risk. >You can use the platinum shards feature to get platinum tickets with a lower risk of ban. select_an_option_to_continue=Select an option to continue editing {feature_name}: continue_editing=Continue editing {feature_name} go_to_safe_feature=Go to the safer {safer_feature_name} feature cancel_editing=Cancel editing {feature_name} rare_ticket_trade_enter=Enter the number of rare tickets you want to <@q>add (max value: <@q>{max}) (current amount: <@q>{current}): rare_ticket_trade_storage_full=<@e>ERROR: You don't have enough space in your cat storage, please free 1 space! rare_ticket_successfully_traded= ><@su>Successfully gave {rare_ticket_count} rare tickets. >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. rare_tickets_l=rare tickets rare_ticket_trade_l=rare ticket trade rare_ticket_trade_maxed=<@e>ERROR: You already have the maximum amount of rare tickets!\nPlease use some before running this feature! platinum_tickets_l=platinum tickets platinum_shards_l=platinum shards ================================================ FILE: src/bcsfe/files/locales/en/edits/cats.properties ================================================ total_selected_cats=<@t>{total} cats currently selected selected_cat=<@t>{name} (<@t>{id}) is selected cat=<@t>{name} (<@t>{id}) special_skill=<@t>{name} (<@t>{id}) item=<@t>{name} (<@t>{id}) unrecognised_storage_item=<@e>Unrecognised storage item. Item category: <@s>{item_type}. Item id: <@s>{id} current_storage_items=Current storage items: storage_is_empty=Storage is empty available_storage=Available storage space: <@t>{slots} display_storage=Display storage clear_storage=Clear storage add_cats=Add cats add_special_skills=Add special skills / base upgrades remove_items=Remove cats / skills too_many_cats_selected=<@e>Too many cats selected. Maximum is <@s>{max}. Got <@s>{current} too_many_skills_selected=<@e>Too many skills selected. Maximum is <@s>{max}. Got <@s>{current} need_x_more_space=<@e>Not enough storage space. Need <@s>{needs} more slots added_cats=Added cats: added_special_skills=Added special skills: select_special_skills=Select special skills removed_items=Removed items: cat_storage=Cat Storage storage_success=<@su>Successfully edited cat storage select_gv= >Enter the game versions to filter by. Examples are: >- Get cats in versions <@t>11.5.0 only: <@t>11.5.0, >- Get cats in versions <@t>12.4.0 and <@t>13.0.0 only <@t>12.4.0 13.0.0 >- Get all cats between versions <@t>12.4.0 and <@t>13.0.0 inclusive: <@t>12.4.0-13.0.0 >Do note that any cats which do not appear in the upgrade menu cannot be selected here since their game version is set to <@t>-1. >Input: possible_gvs=Possible game versions: no_valid_gvs_entered=<@w>No valid game versions entered select_cats_rarity=Select cats based on rarity select_cats_name=Select cats based on name select_cats_obtainable=Select all obtainable cats select_cats_not_obtainable=Select all unobtainable cats select_cats_gatya_banner=Select cats based on gacha banner select_cats_game_version=Select cats by game version select_cats_all=Select all cats select_cats=Select cats: and_mode_q=Do you want to filter down the current selection (<@t>1), add to it (<@t>2) or replace it (<@t>3)?: select_rarity=Select cat rarity: enter_name=Enter cat name: select_name=Select cat name: select_gatya_banner=Enter gacha banner ids {{range_input}} cats=Cats edit_cats=Edit cats enter_cat_ids=You can find cat IDs here: <@t>https://battlecats.miraheze.org/wiki/Cat_Release_Order\nEnter cat ids {{range_input}} select_cats_id=Select cats by id no_cats_found_name=<@w>No cats found with name <@s>{name} select_cats_again=Select additional cats unlock_cats=Unlock Cats|Get Cats remove_cats=Remove Cats upgrade_cats=Upgrade Cats true_form_cats=True Form Cats remove_true_form_cats=Remove Cat True Forms upgrade_talents_cats=Upgrade Cat Talents remove_talents_cats=Remove Cat Talents unlock_cat_guide=Claim Cat Guide remove_cat_guide=Unclaim Cat Guide finish_edit_cats=Finish editing cats select_edit_cats_option=Select an option to edit cats: upgrade_success=<@su>Successfully upgraded cats upgrade_cats_select_mod=Select an option to upgrade cats: upgrade_individual=Input an upgrade for each selected cat selected_cat_upgrades={{selected_cat}}: <@t>{base_level}<@s>+{plus_level} selected_cat_upgraded=<@t>{name} (<@t>{id}) has been upgraded to <@t>{base_level}<@s>+{plus_level} upgrade_all=Input an upgrade to apply to all selected cats upgrade_input= >Enter an upgrade level. Examples: ><@t>10<@s>+20 = Base level 10, plus level 20 ><@t>10<@s>+ = Base level 10, keep current plus level ><@t><@s>+20 = Keep current base level, plus level 20 ><@t>10 = Base level 10, plus level 0 ><@t>5<@q>-10<@s>+20<@q>-30 = Random base level between 5 and 10, random plus level between 20 and 30 ><@t>5<@q>-10<@s>+ = Random base level between 5 and 10, keep current plus level ><@t><@s>+20<@q>-30 = Keep current base level, random plus level between 20 and 30 ><@t>{{max}}<@s>+{{max}} = Max base level, max plus level ><@t>{{quit_key}} = Quit >Input: max_upgrade=Max Upgrade Level: <@t>{max_base}<@s>+{max_plus} invalid_upgrade_base=<@e>Invalid base level: <@s>{base} invalid_upgrade_base_random=<@e>Invalid base level range: <@s>{min}-<@s>{max} invalid_upgrade_plus=<@e>Invalid plus level: <@s>{plus} invalid_upgrade_plus_random=<@e>Invalid plus level range: <@s>{min}-<@s>{max} remove_true_form_success=<@su>Successfully removed true forms true_form_success=<@su>Successfully true formed cats remove_success=<@su>Successfully removed cats unlock_success=<@su>Successfully unlocked cats unlock_cat_guide_success=<@su>Successfully claimed cat guide entries remove_cat_guide_success=<@su>Successfully unclaimed cat guide entries select_cats_current=Select currently unlocked cats select_cats_not_unlocked=Select cats that are not unlocked talents_version_warning= ><@w>Warning: The editor's game data does not match this save file's game version. Talents may not work as expected. >Save Version: <@s>{save_version} >Game Data Version: <@s>{data_version} >If the game data is outdated, it should get updated within the next few days. talents_success=<@su>Successfully upgraded cat talents talents_remove_success=<@su>Successfully removed cat talents talents_individual=Edit talents for each selected cat talents_all=Max out talents for all selected cats upgrade_talents_select_mod=Select an option to edit cat talents: no_talent_data=<@w>There is no talent data for this cat talents=Talents upgrade_talent_cats=Upgrade Cat Talents force_true_form_cats=Force True Form Cats force_true_form_cats_warning= ><@w>Warning: Only use this if you know the cat has a true form, otherwise it will lead to a glitched true form. >The main use of this option is when the editor's game data is outdated and new true forms have not been added yet. filter_current_q=Do you want to only select from cats that you have currently unlocked (<@t>1) or all cats (<@t>2)?: select_cats_currently_option=Select from cats that you have currently unlocked (e.g when selecting cats for a specific rarity, only select cats of that rarity that you have currently unlocked) select_cats_all_option=Select from all cats unlock_remove_cats=Unlock Cats / Remove Cats true_form_remove_form_cats=True Form Cats / Remove Cat True Forms upgrade_talents_remove_talents_cats=Upgrade Talents / Remove Talents Cats unlock_remove_cat_guide=Claim / Unclaim Cat Guide Entries unlock_remove_q=Do you want to <@t>Unlock or <@t>Remove cats?: true_form_remove_form_q=Do you want to <@t>True Form cats or <@t>Remove Cat True Forms?: upgrade_talents_remove_talents_q=Do you want to <@t>Upgrade or <@t>Remove cat talents?: unlock_cat_guide_remove_guide_q=Do you want to <@t>Claim or <@t>Unclaim cat guide entries?: fourth_form_remove_form_cats=Ultra Form Cats / Remove Cat Ultra Forms (4th Forms) force_fourth_form_cats=Force Ultra Form Cats (4th Forms) fourth_form_success=<@su>Successfully ultra formed cats remove_fourth_form_success=<@su>Successfully removed ultra forms fourth_form_cats=Ultra Form Cats remove_fourth_form_cats=Remove Cat Ultra Forms fourth_form_remove_form_q=Do you want to <@t>Ultra Form cats or <@t>Remove Cat Ultra Forms?: force_fourth_form_cats_warning= ><@w>Warning: Only use this if you know the cat has an ultra form, otherwise it will lead to a glitched 4th form. >The main use of this option is when the editor's game data is outdated and new forms have not been added yet. gatya_info_progress=Downloading gacha info (<@t>{current}/<@t>{total}) unknown_banner=Unknown banner banner_txt={name} (<@s>{int}) filter_down_q_gatya=Do you want to remove duplicate and unknown banners from the list? ({{y/n}}): select_cats_non_gatya=Select Non-Gacha Cats finished_cats_selection=Have you finished selecting cats? ({{y/n}}): downloading_cat_names=<@su>Downloading cat names from <@s>{url} ================================================ FILE: src/bcsfe/files/locales/en/edits/enemy.properties ================================================ total_selected_enemies=<@t>{total} enemies currently selected unlock_enemy_guide_success=<@su>Successfully unlocked enemy guide entries remove_enemy_guide_success=<@su>Successfully removed enemy guide entries selected_enemy=<@t>{name} (<@t>{id}) is selected select_enemies_valid=Select all enemies in the enemy guide select_enemies_invalid=Select all enemies which are not in the enemy guide select_enemies_all=Select all enemies select_enemies_id=Select enemies by ID select_enemies_name=Select enemies by name select_enemies=Select enemies: enter_enemy_ids=You can find enemy IDs here: <@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order\nEnter enemy IDs {{range_input}}: enter_enemy_name=Enter enemy name: enemy_not_found_name=<@w>No enemies found with name <@s>{name} unlock_enemy_guide=Unlock enemy guide entries remove_enemy_guide=Remove enemy guide entries enemy_guide=Enemy Guide edit_enemy_guide=Enter an option to edit enemy guide entries: ================================================ FILE: src/bcsfe/files/locales/en/edits/fixes.properties ================================================ fix_gamatoto_crash=Fix gamatoto from crashing the game fix_time_errors=Fix time related issues fix_ototo_crash=Fix ototo from crashing the game fix_gamatoto_crash_success=<@su>Sucessfully fixed gamatoto from crashing the game fix_time_errors_success=<@su>Sucessfully fixed time related issues <@w>(Your device time on both devices must be correct for this to work) fix_ototo_crash_success=<@su>Successfully fixed ototo from crashing the game fixes=Fixes unlock_equip_menu=Unlock Equip Menu equip_menu_unlocked=<@su>Successfully unlocked equip menu ================================================ FILE: src/bcsfe/files/locales/en/edits/gambling.properties ================================================ reset_wildcat_slots=<@su>Successfully reset wildcat slots reset_cat_scratcher=<@su>Successfully reset cat scratcher lottery reset_gambling_events=Reset Wildcat Slots and Cat Scratcher Lottery ================================================ FILE: src/bcsfe/files/locales/en/edits/gamototo.properties ================================================ enter_raw_gamatoto_xp=Enter Raw Gamatoto XP enter_gamatoto_level=Enter Gamatoto Level edit_gamatoto_level_q=Enter an option to edit the gamatoto level: gamatoto_xp=Gamatoto XP gamatoto_level=Gamatoto Level gamatoto_level_success=<@su>Succesfully set gamatoto level to <@s>{level} (XP: <@s>{xp}) gamatoto_level_current=<@t>Current gamatoto level is <@q>{level} (XP: <@q>{xp}) gamatoto_xp_level=Gamatoto XP / Level current_gamatoto_helpers=Current Helpers: gamatoto_helper=Helper: <@t>{name} (rarity: <@t>{rarity_name}) new_gamatoto_helpers=New Helpers: gamatoto_helpers=Gamatoto Helpers ototo_cat_cannon=Ototo Cat Cannon current_cannon_stats=Current Cannon Stats: cannon_part=<@t><@q>{name}{buffer}(level <@s>{level}) development={buffer}(Development: <@q>{development}) cannon_stats={parts} foundation=Foundation style=Style effect=Effect improved_foundation=Improved Foundation improved_style=Improved Style unknown_stage=Unknown Stage (<@s>{stage}) selected_cannon=<@t>Selected cannon: <@q>{name} selected_cannon_stage=<@t>Cannon: <@q>{name} Current Stage: <@q>{stage} cannon_edit_type=Do you want to edit each cannon individually or apply edits to all selected cannons at once?: cannon_dev_level_q=Do you want to edit the development of the cannons or the levels of the cannons?: development_o=Development level_o=Levels select_development=Select development stage: select_cannon=Select Cannon cannon_level=Cannon Level cannon_success=<@su>Succesfully edited ototo cannons cat_shrine=Edit Cat Shrine shrine_level=Edit Shrine Level shrine_xp=Shrine XP current_shrine_xp_level=<@t>Current XP: <@q>{xp} (Level: <@q>{level}) cat_shrine_choice_dialog=What do you want to do?: shrine_level_dialog=Enter cat shrine level (max: <@q>{max_level}): shrine_xp_dialog=Enter cat shrine XP (max: <@q>{max_xp}): cat_shrine_edited=<@su>Succesfully edited cat shrine make_catshrine_appear=Show Cat Shrine in Game make_catshrine_disappear=Hide Cat Shrine in Game ================================================ FILE: src/bcsfe/files/locales/en/edits/gatya.properties ================================================ event_tickets=Event Tickets / Lucky Tickets downloading_gatya_data=Downloading gacha event data... download_gatya_data_success=<@su>Successfully downloaded gacha event data download_gatya_data_fail=<@e>Failed to download gacha event data. Maybe try again save_gatya_error=<@e>Failed to save gatya data due to {error} gatya_by_id_q=Do you want to select gacha banners by <@t>ID or <@t>name?: by_id=By ID by_name=By Name ================================================ FILE: src/bcsfe/files/locales/en/edits/gold_pass.properties ================================================ 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): gold_pass=Gold Pass / Officer Club gold_pass_remove_success=<@su>Succesfully removed the gold pass 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. officer_pass_fixed=<@su>Succesfully fixed the officer club from crashing fix_officer_pass_crash=Fix Officer Club Crashing ================================================ FILE: src/bcsfe/files/locales/en/edits/items.properties ================================================ # Note that not all items are here catamins=Catamins catfruit=Catfruit base_materials=Base Materials inquiry_code=Inquiry Code rare_gatya_seed=Rare Gacha Seed normal_gatya_seed=Normal Gacha Seed event_gatya_seed=Event Gacha Seed unlocked_slots=Unlocked Slots|Equip Slots|Lineups password_refresh_token=Password Refresh Token challenge_score=Challenge Score dojo_score=Dojo Score items=Items user_rank_rewards=Claim User Rank Rewards (Does not give rewards) catfood=Cat Food xp=XP normal_tickets=Normal Tickets|Basic Tickets|Silver Tickets rare_tickets=Rare Tickets|Gold Tickets platinum_tickets=Platinum Tickets legend_tickets=Legend Tickets 100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets 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 platinum_shards=Platinum Shards np=NP leadership=Leadership catseyes=Catseyes battle_items=Battle Items duration=<@t>{days} days, <@t>{hours} hours, <@t>{minutes} minutes, <@t>{seconds} seconds endless_item_item=<@s>{item} : <@s>{int} endless_items_success=<@su>Successfully edited endless items invalid_minute_count=<@e>Invalid minute amount enter_duration_minutes=Enter the duration in minutes for the endless items to last for (if you enter <@t>infinity the items last forever): infinity_duration=<@t>infinity infinity=Infinity 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): battle_items_endless=Endless Battle Items talent_orbs=Talent Orbs scheme_items=Scheme Items labyrinth_medals=Labyrinth Medals restart_pack=Restart Pack|Returner Mode engineers=Engineers gamototo=Gamatoto / Ototo special_skills=Special Skills / Base Abilities treasure_chests=Treasure Chests unknown_treasure_chest_name=Unknown Treasure Chest ({id}) rare_ticket_trade=Rare Ticket Trade rare_ticket_trade_feature_name=Rare Ticket Trade (Allows for unbannable rare tickets) other=Other gatya=Gacha levels=Levels / Story / Treasure cats_special_skills=Cats / Special Skills gatya_item_unknown_name=Unknown Item unknown_catamin_name=Unknown Catamin <@t>{id} unknown_catseye_name=Unknown Catseye <@t>{id} unknown_catfruit_name=Unknown Catfruit <@t>{id} unknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id} reset_golden_cat_cpus_success=<@su>Successfully reset golden cat CPU uses reset_golden_cat_cpus=Reset Golden Cat CPU Uses ================================================ FILE: src/bcsfe/files/locales/en/edits/map.properties ================================================ tutorial_already_cleared=<@w>You have already cleared the tutorial tutorial_cleared=<@su>Succesfully cleared tutorial clear_tutorial=Clear Tutorial clear_stages=Clear Stages unclear_stages=Unclear Stages clear_unclear_q=Do you want to <@t>clear or <@t>unclear stages?: add_enigma_stages=Add Enigma Stages clear_enigma_stages=Clear Enigma Stages current_enigma_stages=Current Enigma Stages: enigma_stage=Enigma Stage <@q>{name} (id: <@q>{id}) unknown_enigma_name=Unknown Enigma Name (id: <@q>{id}) enigma_select=Select Enigma Stages to Add enigma_success=<@su>Succesfully added Enigma Stages wipe_enigma=Do you want to wipe your current enigma stages? ({{y/n}}): aku_realm_unlocked=<@su>Succesfully unlocked Aku Realm unlock_aku_realm=Unlock Aku Realm select_story_chapters=Select Story Chapters chapter_progress_txt=(e.g <@q>0 = no stages cleared, <@q>1 = first stage cleared, <@q>2 = first and second stage cleared, ... <@q>{max} = all stages cleared) edit_chapter_progress_all=Enter the progress to set each chapter to {{chapter_progress_txt}}: edit_chapter_progress=Enter the progress to set <@t>{chapter_name} to {{chapter_progress_txt}}: edit_stage_clear_count=Enter the number of times to clear the stage: story_cleared=<@su>Succesfully cleared story individual_chapters=Individual Chapters all_chapters=All Chapters individual_chapters_dialog=Do you want to edit the clear progress of each chapter <@t>individually? or set <@t>all chapters to the same progress?: individual_clear_counts=Individual Clear Counts all_clear_counts=All Clear Counts individual_clear_counts_dialog=Do you want to edit the clear count of each stage <@t>individually? or set <@t>all stages to the same clear count?: clear_story=Main Story Chapters|Clear Story map_name_star={name} {star} Crown clear=Clear unclear=Unclear outbreaks=Outbreaks / Zombie Stages clear_unclear_outbreaks=Do you want to <@t>clear or <@t>unclear outbreaks?: clear_outbreaks_success=<@su>Succesfully cleared outbreaks unclear_outbreaks_success=<@su>Succesfully uncleared outbreaks no_valid_outbreaks=<@e>Error: no valid outbreaks found aku_chapters=Aku Realm Chapters aku_clear_success=<@su>Succesfully cleared Aku Realm aku_current_stage=Aku Realm Stage <@q>{name} (id: <@q>{id}) itf_timed_scores=Into the Future Timed Scores itf_timed_scores_dialog=Do you want to edit timed scores for <@t>whole chapters at once or <@t>individual stages? itf_timed_scores_edited=<@su>Succesfully edited Into The Future timed scores itf_timed_score_dialog=Enter the timed score: current_stage={chapter_name} <@t>{stage_name} itf_timed_scores_individual_dialog=Do you want to edit the timed score of each selected stage <@t>individually? or set <@t>all selected stages to the same timed score?: filibuster_stage_reclearing_allowed=<@su>Filibuster stage has successfully been re-enabled. filibuster_reclearing=Re-enable Filibuster Stage all_selected_stages=All Selected Stages unknown_map_name=Unknown Map Name (id: <@q>{id}) map_name={name} <@s>(id: <@q>{id}) edit_map_chapters=Select Chapters clear_whole_chapters=Clear Whole Chapters unclear_whole_chapters=Unclear Whole Chapters clear_specific_stages=Clear Specific Stages unclear_specific_stages=Unclear Specific Stages select_clear_type=Do you want to <@t>clear whole chapters or <@t>clear specific stages?: select_unclear_type=Do you want to <@t>unclear whole chapters or <@t>unclear specific stages?: custom_star_count_per_chapter_yn=Do you want to set a custom star/crown count for each chapter? ({{y/n}}): modify_clear_amounts=Setting clear times to <@t>1 for each selected stage. Do you want to change this? ({{y/n}}): clear_amount_chapter=Set a different clear amount for each selected chapter clear_amount_all=Set the same clear amount for all selected chapters clear_amount_stages=Set a different clear amount for each selected stage select_clear_amount_type=Enter the clear amount setting mode you want to use: clear_amount_enter=Enter the clear amount: custom_star_count_per_chapter=Enter star/crown count (max <@q>{max}): custom_star_count_per_chapter_unclear= >Enter the star/crown to remove: ><@s><@t>1 = unclear from whole map ><@s><@t>2 = unclear from 2nd, 3rd and 4th crown/star map ><@s><@t>3 = unclear from 3rd and 4th crown/star map ><@s><@t>4 = unclear from 4th crown/star map >(max <@q>{max}): current_sol_chapter=Chapter <@t>{name} (id: <@q>{id}) current_sol_star=Star/Crown: <@q>{star} current_sol_stage=Stage <@q>{name} (id: <@q>{id}) map_chapters_edited=<@su>Succesfully edited chapters sol=Stories of Legend event=Normal Event Stages collab=Collaboration Event Stages select_map=Select Map select_map_dialog= >Select the maps you want to edit >You can enter a range of numbers (e.g <@q>1-5), individual numbers (e.g <@q>1 3 5), or a combination of both (e.g <@q>1-3 5) >You can also enter the name / part of a name of the map (e.g <@t>{example}) to search / select it >You can also enter the word <@q>all to select all chapters >Input: no_map_found=<@e>No map found with name <@s>{name} finished_selecting_maps=Have you finished selecting maps? ({{y/n}}): current_maps=Current Maps: select_stage=Select Stage gauntlets=Gauntlets collab_gauntlets=Collaboration Gauntlets uncanny=Uncanny Legends catamin_stages=Catamin Stages behemoth_culling=Behemoth Culling legend_quest=Legend Quest towers=Towers zero_legends=Zero Legends unclear_other_stages=Do you want to overwrite your current progress in the chapter? ({{y/n}}) <@t>n = just change clear times for selected stages, <@t>y = unclear later stages in the chapter which were previously cleared: select_stage_progress=Enter the stage to clear up to and including: zero_legends_warning=<@w>Warning: If the version of the game you are using does not have a zero legends map, the game will crash if you try to edit it to be clear! stages_select=Enter numbers {{range_input}} change_clear_amount_catamin=Change Chapter Clear Amount clear_unclear_stage_catamin=Clear / Unclear Catamin Stages catamin_stage_clear_q=Do you want to <@t>Change the amount of times you've cleared a catamin chapter, or just <@t>clear or unclear the stages?: select_map_from_names=Select Map enter_clear_amount_catamin_map=Enter clear times to set for chapter <@t>{name} (ID: <@t>{id}) (<@t>0 = haven't cleared this chapter, <@t>3 or more means the chapter disappears): enter_clear_amount_catamin=Enter clear times to set for selected chapters (<@t>0 = haven't cleared the chapter, <@t>3 or more means the chapter disappears): catamin_stage_success=<@su>Successfully edited catamin stages catamin_clear_amounts_q=Do you want to edit the clear times for each chapter <@t>individually or <@t>all at once?: dojo_catclaw_championships=Clear Dojo Catclaw Championships finished=Finished edit_chapters_q=What do you want to edit?: clear_whole_chapter=Clear whole chapter clear_to_specific_stage=Clear to a specific stage clear_whole_q=Do you want to <@t>clear the whole chapter at once or <@t>clear up to a specific stage within the chapter?: clear_all=Clear all chapters handle_individually=Edit each chapter individually clear_chapters_q=Do you want to <@t>clear all selected chapters or <@t>edit the progress of each chapter individually?: max_stars=Enter the maximum crown (max: <@t>{max}): edit_map_all=Same clear count for all chapters edit_chapters_q_all=Do you want to set the <@t>same clear count for all selected chapters or edit the clear count of <@t>each chapter individually? edit_whole_chapter=Set the same clear count for the whole chapter edit_specific_stages=Edit the clear count of specific stages edit_chapter_q=Do you want to <@t>set the same clear count all stages in the chapter or <@t>edit the clear count for specific stages?: each_stage_individually=Set a different clear count for each selected stage stage_all_at_once=Set the same clear count for all selected stages set_clear_count_stage_q=Do you want to <@t>set a different clear count for each selected stage or <@t>set the same clear count for all selected stages?: unknown_stage_name=Unknown stage name (index: <@t>{index}) current_stage_map=<@t>{name} <@s>(index: <@q>{index}) edit_progress_clear=Edit Map Progress edit_progress_unclear=Edit Map Progress (Supports unclearing) edit_clear_counts=Edit Stage Clear Counts keep_selecting=Keep Selecting remove_selection=Remove Selection finish_selection=Finish Selection map_selection_q=What do you want to do? ================================================ FILE: src/bcsfe/files/locales/en/edits/medals.properties ================================================ medals=Meow Medals add_medals=Add Medals remove_medals=Remove Medals medal_add_remove_dialog=Do you want to <@t>add medals or <@t>remove medals?: medal_string={medal_name}: <@q>{medal_req} select_medals=Select medals: medals_added=<@su>Succesfully added meow medals medals_removed=<@su>Succesfully removed meow medals ================================================ FILE: src/bcsfe/files/locales/en/edits/missions.properties ================================================ missions=Catnip Challenges / Missions|Cat Missions complete_reward=Clear Missions and Don't Claim Rewards complete_claim=Complete Missions and Claim Rewards uncomplete=Uncomplete Mission 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? select_missions=Select missions to edit: missions_edited=<@su>Succesfully edited missions ================================================ FILE: src/bcsfe/files/locales/en/edits/playtime.properties ================================================ 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)/$) playtime_current=Current playtime: {{playtime_str}} playtime_edited=Successfully edited playtime to {{playtime_str}} playtime_hours_prompt=Enter the number of <@t>hours to set the playtime to: playtime_minutes_prompt=Enter the number of <@t>minutes to set the playtime to: playtime_seconds_prompt=Enter the number of <@t>seconds to set the playtime to: playtime=Play Time ================================================ FILE: src/bcsfe/files/locales/en/edits/scheme_items.properties ================================================ scheme_items_edit_success=<@su>Succesfully edited scheme items scheme_items_select_gain=Select scheme items to gain scheme_items_select_remove=Select scheme items to remove gain_remove_scheme_items=Do you want to <@t>gain or <@t>remove scheme items?: gain_scheme_items=Gain scheme items remove_scheme_items=Remove scheme items ================================================ FILE: src/bcsfe/files/locales/en/edits/special_skills.properties ================================================ special_skills_dialog=Select a base ability to upgrade upgrade_individual_skill=Input an upgrade for each selected skill upgrade_all_skills=Input an upgrade to apply to all selected skills upgrade_skills_select_mod=Select an option to upgrade skills: selected_skill=<@t>{name} is selected selected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+{plus_level} selected_skill_upgraded=<@t>{name} is upgraded to <@t>{base_level}<@s>+{plus_level} skills_edited=<@su>Succesfully edited special skills ================================================ FILE: src/bcsfe/files/locales/en/edits/talent_orbs.properties ================================================ total_current_orbs=Total Current Orbs: <@q>{total_orbs} total_current_orb_types=Total Current Orb Types: <@q>{total_types} current_orbs=Current Orbs: orb_select=Select talent orbs to edit: selected_orbs=Selected talent Orbs: edit_orbs_individually=Do you want to edit each orb individually (<@q>1) or all at once (<@q>2)?: edit_orbs_all=Input a value to edit all selected orbs to (max <@t>{max}): failed_to_load_orbs=Failed to load talent orbs edit_orbs_help= >Help: >Available grades: {all_grades_str} >Available attributes: {all_attributes_str} >Available effects: {all_effects_str} ><@w>Note: Not all grades and effects will be available for all attributes. >Example inputs: > aku - selects all aku orbs > red s - selects all red orbs with s grade > alien d 0 - selects the alien orb with d grade that increases attack. > c 1 - selects the boost stories of legend orb with grade c >If you want to select <@q>all orbs then input: > <@q>* >If you want to do <@q>multiple selections then separate them with a <@q>comma like this: > s black 4,d 3,floating > ================================================ FILE: src/bcsfe/files/locales/en/edits/treasures.properties ================================================ whole_chapters=Whole Chapters individual_stages=Individual Stages treasure_groups=Treasure Groups / Sets treasure_dialog=Do you want to edit treasures for <@t>whole chapters at once, <@t>individual stages or individual <@t>treasure groups?: treasures_edited=<@su>Succesfully edited treasures per_chapter=Per Chapter all_selected_chapters=All Selected Chapters edit_per_chapter=Do you want to edit data for <@t>all selected chapters or <@t>each chapter individually?: no_treasure=No Treasure custom_treasure_level=Custom Treasure Level (<@w>Only edit if you know what you're doing!) treasure_level_dialog=Enter the treasure level you want to set: custom_treasure_level_dialog=Enter the custom treasure level you want to set: select_stage_by_id=Select Stages by IDs select_stage_by_name=Select Stages by Names select_stage_dialog=Do you want to select stages by <@t>IDs or <@t>Names?: select_stage_id=Enter the stage IDs you want to select {{range_input}} select_stages_name=Select stages: select_treasure_groups=Select the treasure groups you want to edit: story_treasures=Story Treasures current_chapter=Current Chapter: <@t>{chapter_name} current_treasure_group=Current Treasure Group: <@t>{treasure_group_name} group_individual=Individual Groups group_all_at_once=All Selected Groups 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?: ================================================ FILE: src/bcsfe/files/locales/en/edits/user_rank.properties ================================================ claim=Claim unclaim=Unclaim fix_claimed=Fix Claimed 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?: select_ur=Select user rank rewards ur_claimed_success=<@su>Successfully claimed user rank rewards ur_unclaimed_success=<@su>Successfully unclaimed user rank rewards ur_string=Rank: <@s>{rank}: {description} ur_fix_claimed_success=<@su>Successfully fixed claimed user rank rewards ================================================ FILE: src/bcsfe/files/locales/tw/core/config.properties ================================================ config=設定 edit_config=編輯設定 default_value=(預設值: <@q>{default_value}) current_value=(目前數值: <@q>{current_value}) config_value_txt=<@s>{{current_value}} {{default_value}} config_dialog=選擇要編輯的設定項目: update_to_beta_desc=檢查是否有Beta版本更新 {{config_value_txt}} update_to_beta=更新至Beta版本 show_update_message_desc=有新版本時顯示訊息 {{config_value_txt}} show_update_message=顯示版本更新 config_full=<@t>{key_desc} disable_maxes_desc=編輯時停用最大值 {{config_value_txt}} disable_maxes=停用最大值 max_backups_desc=可保留的最大數量存檔備份 {{config_value_txt}} max_backups=存檔備份最大值 available_themes=可用的主題: theme_desc=要使用的主題 {{config_value_txt}} theme=主題 show_missing_locale_keys=顯示缺少的翻譯鍵值 show_missing_locale_keys_desc=顯示所有存在於英文 (en) ,但當前語系卻缺少的翻譯鍵值。這對除錯很有幫助: {{config_value_txt}} reset_cat_data_desc=從存檔移除貓咪時重設所有貓咪資料 {{config_value_txt}} reset_cat_data=移除貓咪同時重設資料 filter_current_cats_desc=選擇貓咪編輯時, 過濾掉不在存檔的貓咪 {{config_value_txt}} filter_current_cats=選擇貓咪時過濾掉現有貓咪 set_cat_current_forms_desc=三階貓咪時, 將貓咪設為新解鎖的型態 {{config_value_txt}} set_cat_current_forms=解鎖型態時切換到目前型態 strict_upgrade_desc=升級貓咪時檢查等級排行及遊戲進度使貓咪只能升級到特定等級 {{config_value_txt}} strict_upgrade=嚴格檢查升級 separate_cat_edit_options_desc=把貓咪編輯選項分為多個獨立功能 {{config_value_txt}} separate_cat_edit_options=拆分貓咪編輯選項 strict_ban_prevention_desc=執行任何與伺服器相關的操作時建立一個新帳號以降低被鎖帳的機率 {{config_value_txt}} strict_ban_prevention=嚴格防封鎖機制 max_request_timeout_desc=等待請求完成的最大時間(秒) {{config_value_txt}} max_request_timeout=最大請求逾時 game_data_repo_desc=用於遊戲資料的儲存庫 {{config_value_txt}} game_data_repo=遊戲資料儲存庫 game_data_repo_dialog=輸入要使用的遊戲資料儲存庫: force_lang_game_data_desc=強制編輯器使用目前語系的遊戲資料,即使存檔屬於不同的遊戲版本 {{config_value_txt}} force_lang_game_data=強制使用目前語系的遊戲資料 clear_tutorial_on_load_desc=將存檔載入編輯器時跳過新手教學 {{config_value_txt}} clear_tutorial_on_load=存檔時跳過新手教學 remove_ban_message_on_load_desc=把存檔載入編輯器時移除封帳訊息 {{config_value_txt}} remove_ban_message_on_load=存檔時移除封帳訊息 unlock_cat_on_edit_desc=當編輯貓咪的等級、本能、型態等數值時自動解鎖該貓咪 {{config_value_txt}} unlock_cat_on_edit=編輯貓咪時解鎖 use_file_dialog_desc=使用 tkinter 檔案對話框來開啟與儲存檔案,而非手動輸入檔案路徑 {{config_value_txt}} use_file_dialog=使用檔案對話框 adb_path_desc=adb執行檔路徑 {{config_value_txt}} adb_path=ADB路徑 use_waydroid=使用 waydroid shell 而非 adb use_waydroid_desc=Waydroid 不支援 adb root,因此改用 waydroid shell {{config_value_txt}} use_pkexec_waydroid=使用 pkexec 執行檔來執行 waydroid 指令 use_pkexec_waydroid_desc=執行 <@s>waydroid shell 需要root權限。 使用 <@s>pkexec 以避免用root執行編輯器 {{config_value_txt}} ignore_parse_error_desc=忽略解析錯誤並直接跳過解析剩餘的存檔資料。 <@w>警告:除非你的存檔已損毀才建議這麼做。任何解析問題都應該回報至 Discord 伺服器 {{config_value_txt}} ignore_parse_error=忽略存檔解析錯誤 string_config_dialog=為 <@q>{val}輸入新數值: enable_disable_dialog=是否要 <@q>啟用 或 <@q>停用 這項功能?: enable=啟用 disable=停用 enabled=已啟用 disabled=已停用 config_success=<@su>成功更新設定 yaml_create_error=<@e>在 <@s>{path}<@s>創建yaml檔案時失敗,可能是權限不足,請以root/系統管理員身分執行此編輯器 ================================================ FILE: src/bcsfe/files/locales/tw/core/files.properties ================================================ another_path=手動輸入路徑 select_files_dir=選擇目錄中的檔案: enter_path=輸入檔案路徑或位置: enter_path_dir=輸入資料夾路徑或位置: enter_path_default=輸入檔案路徑或位置: (預設: <@t>{default}): current_files_dir=目前在 <@t>{dir}的檔案: other_dir=輸入其他目錄 no_files_dir=<@e>目錄中沒有檔案 path_not_exists=<@e>路徑不存在 ================================================ FILE: src/bcsfe/files/locales/tw/core/input.properties ================================================ input_int=輸入 <@q>{min} 到 <@q>{max}之間的數值: select_edit=為 <@t>{group_name}選擇選項: input_int_default=輸入 <@q>{min} 到 <@q>{max}之間的數值: (預設值 <@q>{default}): input_many=輸入 <@q>{min} 到<@q>{max} 之間的數字並以空格分隔: input_single=輸入 <@q>{min} 到 <@q>{max}之間的數值: input=輸入 <@t>{name}的數值 (目前值: <@q>{value}) (最大值: <@q>{max}): input_min=輸入 <@t>{name}的數值 (目前值: <@q>{value}) (範圍: <@q>{min} - <@q>{max}): input_non_max=輸入 <@t>{name}的數值 (目前值: <@q>{value}): input_all=輸入所有 <@t>{name}的數值 (最大值: <@q>{max}): value_changed=<@su>成功將 <@s>{name} 變更為 <@s>{value} value_gave=<@su>成功發送 <@s>{name} all_at_once=一次全選 invalid_input=<@e>輸入無效。請重試。 invalid_input_int=<@e>輸入無效。請輸入一個介於 <@s>{min} 到 <@s>{max}的數值 select_option=選擇選項: finish=完成 features=功能: go_back=返回 yes_key=y quit_key=q range_input=以空格分隔 (例如<@t>1 2 3 192), 或輸入範圍 (例如 <@t>1-43) 或輸入 <@t>all: select_features= >若要選擇功能,輸入 >- 左側對應的 <@q>number 數字 >- <@t>text 來搜尋功能 >你可以按 <@t>enter 來檢視所有功能 >部分功能為 <@t>分類,選取後會顯示該分類下的所有 <@t>子功能 >輸入: individual=個別設定 edit_all_at_once=統一設定 ================================================ FILE: src/bcsfe/files/locales/tw/core/locale.properties ================================================ available_locales=可用的語言: locale_desc=要使用的語言 {{config_value_txt}} locale=語言 locale_dialog=選擇語言: add_locale=新增語系 remove_locale=移除語系 locale_remove_dialog=選擇要移除的語系: enter_locale_git_repo=輸入語系的 git 儲存庫 (例如 <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git): locale_already_exists=<@e>名為 <@s>{locale_name} 的語系已經存在\n是否要覆蓋 ({{y/n}}): locale_added=<@su>成功新增本地化語系 checking_for_locale_updates=正在檢查外部語系 <@t>{locale_name}的更新... external_locale_updated=<@su>成功將外部語系 <@t>{locale_name} 更新至版本 <@t>{version}<@t>.\n{{restart_to_see_changes}} external_locale_no_update=<@su>外部語系 <@t>{locale_name}無須更新, 最新版本是 <@t>{version}<@t> invalid_git_repo=<@e>無效的git儲存庫 locale_cancelled=<@e>已取消 restart_to_see_changes=重新啟動編輯器以檢視所有變更 locale_changed=<@su>成功將語言變更為 <@t>{locale_name}.\n{{restart_to_see_changes}} locale_removed=<@su>成功移除語系 <@t>{locale_name}.\n{{restart_to_see_changes}} no_external_locales=<@w>找不到外部語系 missing_locale_keys=缺少的語系鍵值: extra_locale_keys=多餘的語系鍵值: locale_text= >目前語言: <@s>{locale_name} (版本: <@s>{locale_version}) >作者:<@s>{locale_author} >此語系檔案位置: <@s>{locale_path} default_locale_text_authors= >目前語言: <@s>{name} >作者: <@s>{authors} >語系檔案位置: <@s>{path} ================================================ FILE: src/bcsfe/files/locales/tw/core/main.properties ================================================ # Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files # color formatting # # <@p> = primary color # <@s> = secondary color # <@t> = tertiary color # <@q> = quaternary color # <@e> = error color # <@w> = warning color # <@su> = success color # # = close current color # When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes. # You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red. # If you want to write < or > or / in the text, escape them with a backslash (\) e.g. \< or \> or \/ # # <#rrggbb> = hex color # # = white # = black # = red # = green # = blue # = yellow # = magenta # = cyan # = dark yellow # = dark grey # = dark blue # = dark cyan # = dark magenta # = dark red # = dark green # = light grey # = orange downloading=<@su>正在從 <@s>{file_name} 下載 <@s>{pack_name} (版本 <@s>{version} ,國家代碼: <@s>{country_code}) failed_to_download_game_data=<@e>無法從 <@s>{file_name} 下載遊戲資料 <@s>{pack_name} (版本: <@s>{version} ,國家代碼: <@s>{country_code}。網址: <@s>{url} 請檢查你的網路連線。 failed_to_get_game_versions=<@e>無法取得遊戲版本,檢查你的網路連線 no_device_error=<@e>未找到已連接的裝置 no_package_name_error=<@e>找不到貓咪大戰爭的安裝包。你的裝置可能沒有 root,或者確保至少進入過一次貓咪基地後重試。. exit=Exit tkinter_not_found=<@e>找不到 tkinter。如果你不是在手機上操作,請安裝後重試。 tkinter_not_found_enter_path_file=輸入 {initialfile} 檔案的路徑或位置: tkinter_not_found_enter_path_file_save=輸入儲存 {initialfile} 檔案的路徑或位置: tkinter_not_found_enter_path_dir=改為輸入 {initialdir} 資料夾的路徑或位置: discord_url=https://discord.gg/DvmMgvn5ZB welcome= ><@t>歡迎使用 <@s>貓咪大戰爭存檔編輯器! >由 <@s>fieryhenry製作 > >Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python >Discord: <@s>{{discord_url}} - 請到 <@s>#bug-reports 回報任何錯誤以及到 <@s>#suggestions提供建議 >贊助: <@s>https://ko-fi.com/fieryhenry > >設定檔位置: <@s>{config_path} > >{theme_text} > >{locale_text} > ><@q>感謝: >- <@s>Lethal的編輯器 給我靈感並幫助我初步如何修改存檔資料以及編輯貓罐頭: <@s>https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren/ >- <@s>Beeven 和 <@s>csehydrogen's 的程式碼,幫助我弄清楚如何修改存檔資料: https://github.com/beeven/battlecats and https://github.com/csehydrogen/BattleCatsHacker >- 任何支持我作品的人,給了我動力繼續開發這個以及類似的專案: <@s>https://ko-fi.com/fieryhenry >- Discord 伺服器裡的所有成員,提供存檔、回報錯誤、提供新功能建議,我們組成了一個超棒的社群: <@s>{{discord_url}} > ><@w>如果你有為此程式付費,代表你被騙了。本程式是完全免費且開源的。 > ><@w>使用此工具需自行承擔風險。我不對任何帳號封鎖或存檔損壞負責。 >當然,這個存檔編輯器會盡量防止這些情況發生,但我無法百分百保證你的存檔絕對安全。 >如果你的存檔真的損壞了請到discord頻道回報。 >強烈建議在編輯之前先備份你的存檔。 report_message=請將此問題回報至 Discord 的 <@s>#bug-reports 頻道: <@s>{{discord_url}} report_message_l=請將此問題回報至 Discord 的 <@s>#bug-reports 頻道: <@s>{{discord_url}} try_again_message=請再試一次。如果錯誤持續發生, {{report_message_l}} all=全部 error=<@e>發生錯誤 (<@s>{error}, 編輯器版本: <@s>{version}) {{report_message_l}}\n{traceback} see_log=<@e>請查閱紀錄檔獲取更多詳細資訊 max=最大值 none=無 unknown=未知的 leave=\n<@q>感謝你使用貓咪大戰爭存檔編輯器! checking_for_changes=<@t>正在檢查變更... no_changes=<@su>未發現任何變更。 changes_found=<@su>已發現變更。 y/n=y/n yes=yes git_not_installed=<@e>尚未安裝 Git。請先安裝並將其加入系統 PATH路徑之後重試一次 。 failed_to_get_repo=<@e>無法取得儲存庫: "<@t>{url}"。 網址可能不存在或你的網路連線中斷 failed_to_run_git_cmd=<@e>無法執行 git 指令: "<@t>{cmd}"。 檢查你的網路連線 cancel=取消 update_external=更新外部內容 updating_external_content=<@q>正在更新外部內容... downloading_map_names=<@q>正在取得地圖名稱.... (代碼: <@t>{code})。 這可能需要一點時間... select_device=選擇裝置: continue_q=是否繼續? ({{y/n}}): no_data_version=<@e>無法取得最新可用的遊戲版本資料。可能是網路問題,請再試一次。 no_feature_with_name=<@e>找不到名為 <@s>{name}的功能 ================================================ FILE: src/bcsfe/files/locales/tw/core/save.properties ================================================ save_load_option=選擇載入存檔的方式 download_save=使用轉移碼和認證碼下載存檔 select_save_file=從檔案中選擇存檔 adb_pull_save=使用 ADB 從裝置提取存檔 waydroid_pull_save=從 Waydroid 裝置提取存檔 load_save_data_json=從 JSON 載入存檔資料 root_storage_pull_save=從根目錄提取存檔 save_save_dialog=儲存存檔 save_downloaded=<@su>存檔已下載至 <@s>{path} save_json_dialog=將存檔資料儲存為 JSON load_from_documents=從文件資料夾載入存檔 save_file_not_found=<@e>找不到存檔 save_file_found=<@su>正在從 <@t>{path}<@t>中載入存檔 parse_save_error=<@e>解析存檔時發生錯誤: {error}\n(編輯器版本: <@s>{version}) (遊戲版本: <@s>{game_version}) (國家代碼: <@s>{country_code})\n{{report_message}} load_json_fail=<@e>無法從 JSON 載入存檔資料 ({error}) parse_json_fail=<@e>無法讀取 JSON 檔案,請確認檔案是否真的是 JSON 格式 editor_version_mismatch=<@w>輯器版本不符。存檔可能與此編輯器不相容。JSON 版本: <@t>{json_version}, 編輯器版本: <@t>{editor_version} save_management=管理存檔 save_save=儲存存檔 save_save_file=將存檔另存為特定檔案 save_save_documents=將存檔儲存至文件資料夾 save_upload=將存檔上傳至伺服器並獲取轉移碼和認證碼 unban_account=解鎖帳號 / 修復「存檔已在其他裝置使用」錯誤 adb_push_rerun=使用 ADB 將存檔推送至裝置(推送後重啟遊戲) adb_push=使用 ADB 將存檔推送至裝置(推送後不重啟遊戲) adb_push_success=<@su>已將存檔推送至裝置 adb_push_fail=<@e>無法將存檔推送至裝置 ({error}) adb_rerun_success=<@su>已成功重新啟動遊戲 adb_rerun_fail=<@e>無法重新啟動遊戲 ({error}) waydroid_push_rerun=將存檔推送至 Waydroid 裝置(推送後重啟遊戲) waydroid_push=將存檔推送至 Waydroid 裝置(推送後不重啟遊戲) waydroid_push_success=<@su>已將存檔推送至 Waydroid 裝置 waydroid_push_fail=<@e>>無法將存檔推送至 Waydroid 裝置 ({error}) waydroid_rerun_success=<@su>已成功在 Waydroid 裝置上重新啟動遊戲 waydroid_rerun_fail=<@e>無法在 Waydroid 裝置上重新啟動遊戲 ({error}) export_save=將存檔匯出為 JSON save_success=<@su>存檔已儲存至 <@s>{path} export_success=<@su>存檔資料已匯出至 <@s>{path} init_save=重設存檔 init_save_confirm=你確定要重設你的存檔嗎? ({{y/n}}): init_save_success=<@su>S已成功重設存檔 adb_pulling=<@q>正在使用 ADB 從名稱為 <@s>{package_name}的裝置提取存檔... adb_pull_fail=<@e>使用 ADB 從名稱為 <@s>{package_name} 的裝置提取存檔失敗({error}) waydroid_pulling=<@q>正在使用 Waydroid 從名稱為<@s>{package_name} 的裝置提取存檔... waydroid_pull_fail=<@e>使用 Waydroid 從名稱為 <@s>{package_name}的裝置提取存檔失敗 ({error}) storage_pulling=<@q>正在從根目錄提取名稱為 <@s>{package_name}的存檔... storage_pull_fail=<@e>從根目錄提取名稱為 <@s>{package_name}的存檔失敗 ({error}) not_rooted_error=<@e>裝置似乎沒有 root,或者編輯器未以 root 權限執行 upload_items=上傳管理項目到伺服器 upload_items_success=<@su>已成功上傳管理項目 upload_items_fail=<@e>上傳管理項目時失敗 load_save=載入存檔 load_save_success=<@su>成功載入存檔 account=帳號 save_before_exit=<@q>離開前是否儲存最新的變更? (<@s>y/<@s>n): save_temp_success=<@su>已成功從暫存檔中復原存檔 save_temp_fail=<@e>無法從暫存檔中復原存檔。最新的存檔變更已遺失 ({error})\n{traceback} save_temp_not_found=<@e>無法從暫存檔中復原存檔。最新的存檔變更已遺失 (Temp file not found) cant_detect_cc=<@w>無法從存檔中偵測到國家代碼。\n請手動輸入你的國家代碼 failed_to_load_save_gv=存檔已載入,但某些數值不符預期。 已停止操作防止存檔毀損 failed_to_load_save=無法載入存檔 failed_to_save_save=無法儲存存檔 game_version_dialog=輸入遊戲版本 (例如 <@t>12.2.1): invalid_game_version=<@e>遊戲版本無效 country_code_set=<@su>已成功將國家代碼設定為 <@s>{cc} game_version_set=<@su>已成功將遊戲版本設定為 <@s>{version} convert_region=轉換國家代碼 (例如 en -\> jp) convert_version=轉換遊戲版本 (例如 12.2.1 -\> 12.2.0) cc_warning=<@w>警告:這可能會對你的存檔出現問題、錯誤和當機情況!若要回報錯誤,請確保提及你曾使用過此功能。執行此功能後,你必須進入貓咪基地/升級選單,以便遊戲對存檔進行必要變更。\n目前國家代碼: <@t>{current} gv_warning=<@w>警告:這可能會對你的存檔出現問題、錯誤和當機情況!若要回報錯誤,請確保提及你曾使用過此功能。執行此功能後,你必須進入貓咪基地/升級選單,以便遊戲對存檔進行必要變更。\n目前遊戲版本: <@t>{current} create_new_save_success=<@su>已成功建立新存檔 create_new_save=建立新存檔 create_new_save_warning=<@w>警告:許多編輯器功能無法在使用自動建立的存檔時運作,你需要先在遊戲中載入存檔,然後再於編輯器中重新載入\n此情況可能會在後續的版本中改進。 parse_ignored_error=<@w>警告: <@e>{error}<>\n<@w>由於已設定 <@s>忽略存檔解析錯誤 因此將予以忽略。這可能會導致問題! select_package_name=選擇套件名稱: adb_not_installed= ><@e>adb 未加入 PATH 環境變數,或執行檔路徑不正確。請嘗試在設定中編輯 adb 路徑 >目前數值: <@s>{path} >錯誤: <@s>{error} waydroid_not_installed=<@e>尚未安裝 Waydroid或發生錯誤: {error} root_push_not_android_error=<@e>Root 推送功能僅適用於 Android 裝置 root_push_success=<@su>已成功將存檔寫入根目錄 root_push_fail=<@e>無法將存檔寫入根目錄。錯誤: <@s>{error} root_rerun_success=<@su>已成功重新啟動遊戲 root_rerun_fail=<@e>無法重新啟動遊戲。錯誤: <@s>{error} root_push=使用 root 將存檔直接推送至遊戲 root_push_rerun=使用 root 將存檔直接推送至遊戲 (並重新啟動遊戲) select_recent=選擇最近的存檔: recent_save=<@s><@q>{inquiry_code} <@t>{cc}-<@t>{gv} @ <@t>{year}-<@t>{month}-<@t>{day} <@t>{hour}:<@t>{minute}:<@t>{second} - 原始路徑: <@q>{name} load_recent_saves=從最近的存檔與備份中載入 no_recent_saves=<@w>沒有最近的存檔 current_save=\n目前存檔: <@t>{cc}-<@t>{gv} 詢問碼: <@t>{inquiry_code} ================================================ FILE: src/bcsfe/files/locales/tw/core/server.properties ================================================ transfer_code=轉移碼 enter_transfer_code=輸入轉移碼: confirmation_code=認證碼 enter_confirmation_code=輸入認證碼: country_code=國家代碼 country_code_select=選擇國家代碼: invalid_codes_error=<@e>無法下載存檔。請檢查你的轉移碼、認證碼和國家代碼之後重試 display_response_debug_info_q=是否要顯示回應除錯資訊?({{y/n}}): response_text_display= >網址: <@q>{url} >請求標頭: <@q>{request_headers} >請求主體: <@q>{request_body} > >回應標頭: <@q>{response_headers} >回應主體: <@q>{response_body} downloading_save_file=正在從伺服器下載存檔(轉移碼: <@q>{transfer_code}, 認證碼: <@q>{confirmation_code}, 國家代碼: <@q>{country_code})... upload_result= ><@su> >轉移碼: <@s>{transfer_code} >認證碼: <@s>{confirmation_code} > upload_fail=<@e>無法上傳存檔。 {{try_again_message}} {{see_log}} unban_fail=<@e>無法解除帳號封鎖狀態。 {{try_again_message}} {{see_log}} unban_success=<@su>已成功解除帳號封鎖。 upload_items_checker_confirm=目前的存檔中有部分管理項目尚未被追蹤。你要現在上傳它們嗎? ({{y/n}}): strict_ban_prevention_enabled=<@w>已啟用嚴格防封鎖機制。在上傳存檔 / 管理項目之前會建立一個新帳號。 create_new_account_success=<@su>已成功建立帳號。 create_new_account_fail=<@e>無法建立帳號。 {{try_again_message}} {{see_log}} uploading_save_file=<@q>正在將存檔上傳至伺服器... getting_codes=<@q>正在獲取轉移碼與認證碼... getting_auth_token=<@q>正在獲取帳號驗證權杖 (Auth Token)... refreshing_password=<@q>正在重新整理帳號密碼... getting_password=<@q>正在獲取帳號密碼... getting_save_key=<@q>正在獲取帳號存檔金鑰 (Save Key)... inquiry_code_warning=<@w>警告:改變詢問碼可能會導致帳號無法遊玩。請自行承擔風險。\n{{do_you_want_to_continue}} password_refresh_token_warning=<@w>警告:編輯密碼重整權杖可能會導致你的帳號無法遊玩。請自行承擔風險。\n{{do_you_want_to_continue}} no_internet=<@e沒有網路連線。請檢查網路狀態後再試一次。 transfer_backup=<@su>已將備份轉移存檔至 <@t>{path} transfer_backup_fail=<@e>因 {error},無法將備份轉移存檔儲存至 <@t>{path} retry_auth_token=<@e>無法獲取驗證權杖,正在重試... downloading_compressed_data=<@su>正在從 <@s>{url}下載遊戲資料 clear_game_data_q=你要清除所有已下載的遊戲資料嗎?({{y/n}}): cleared_game_data=<@su>已成功清除遊戲資料 validating_game_repo=正在驗證遊戲資料儲存庫... invalid_response=<@e>I無效的回應代碼: <@s>{response_code}。應為 <@s>200 no_internet_or_connection_error=<@e>無法連線至遊戲資料儲存庫 invalid_url=<@e>無效的網址 use_alternative_repo=<@e>無法取得遊戲資料儲存庫,可能因為網路連線有問題,或者該儲存庫在你的網路環境被封鎖。 是否要切換到 <@t>{repo} 作為替代的遊戲資料儲存庫? ({{y/n}}): ================================================ FILE: src/bcsfe/files/locales/tw/core/theme.properties ================================================ theme_text= >目前主題: <@s>{theme_name} (版本 <@s>{theme_version}) >作者 <@s>{theme_author} >主題檔案位置: <@s>{theme_path} default_theme_text= >目前主題: <@s>Default >主題檔案位置: <@s>{theme_path} checking_for_theme_updates=正在檢查外部主題 <@t>{theme_name} 的更新... external_theme_updated=<@su>已成功將外部主題 <@t>{theme_name} 更新至版本 <@t>{version}<@t>。\n{{restart_to_see_changes}} external_theme_no_update=<@su>外部主題 <@t>{theme_name} 無須更新,最新版本已是 <@t>{version}<@t> theme_changed=<@su>已成功將主題變更為 <@t>{theme_name}。\n{{restart_to_see_changes}} theme_removed=<@su>已成功移除主題 <@t>{theme_name}。\n{{restart_to_see_changes}} no_external_themes=<@w>找不到外部主題 theme_dialog=選擇一個主題: add_theme=新增主題 remove_theme=移除主題 theme_remove_dialog=選擇要移除的主題: enter_theme_git_repo=輸入主題的 git 儲存庫 (例如 <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git): theme_already_exists=<@e>名為 <@s>{theme_name} 的主題已經存在。\n要覆蓋它嗎?({{y/n}}): theme_added=<@su>成功新增主題 ================================================ FILE: src/bcsfe/files/locales/tw/core/updater.properties ================================================ local_version=<@q>本機版本:<@s>{local_version} latest_version=<@q>最新版本:<@s>{latest_version} update_check_fail=<@e>檢查更新失敗。檢查網路連線 update_available= ><@q>有可用的更新:<@s>{latest_version} >是否要更新?<@t>({{y/n}}): update_success= ><@t>更新成功 >請重新啟動程式 update_fail= ><@e>更新失敗 >請手動更新 >指令:<@s>pip install --upgrade bcsfe version_line={{local_version}} | {{latest_version}} disable_update_message=是否停用更新提示訊息?<@t>({{y/n}}): ================================================ FILE: src/bcsfe/files/locales/tw/edits/bannable_items.properties ================================================ do_you_want_to_continue=是否要繼續? ({{y/n}}): catfood_warning=<@w>警告:修改貓罐頭可能會導致被鎖帳。請自行承擔風險。\n{{do_you_want_to_continue}} legend_ticket_warning=<@w>警告:修改傳說券可能會導致被鎖帳。請自行承擔風險。\n{{do_you_want_to_continue}} rare_ticket_warning= ><@w>警告:修改稀有券可能會導致被封鎖。請自行承擔風險。 >你可以使用「稀有券兌換」功能來獲取稀有券,以降低被封鎖的風險。 platinum_ticket_warning= ><@w>警告:修改白金券可能會導致被封鎖。請自行承擔風險。 >你可以使用「白金券碎片」功能來獲取白金券,以降低被封鎖的風險。 select_an_option_to_continue=選擇一個選項以繼續編輯 {feature_name}: continue_editing=繼續編輯 {feature_name} go_to_safe_feature=前往更安全的 {safer_feature_name} 功能 cancel_editing=取消編輯 {feature_name} rare_ticket_trade_enter=輸入你想要<@q>增加的稀有券數量(最大值:<@q>{max})(目前數量:<@q>{current}): rare_ticket_trade_storage_full=<@e>錯誤:貓咪儲藏庫空間不足,請空出 1 個位子! rare_ticket_successfully_traded= ><@su>已成功給予 {rare_ticket_count} 張稀有券。 >你現在需要進入貓咪儲藏庫,按下<@q>全部使用按鈕,然後按下<@q>收集換券按鈕來獲取稀有券。 rare_tickets_l=稀有券 rare_ticket_trade_l=稀有券兌換 rare_ticket_trade_maxed=<@e>錯誤:你已經擁有數量上限的稀有券!\n請在使用此功能前先消耗一些! platinum_tickets_l=白金券 platinum_shards_l=白金券碎片 ================================================ FILE: src/bcsfe/files/locales/tw/edits/cats.properties ================================================ total_selected_cats=目前已選取 <@t>{total} 隻貓咪 selected_cat=已選取 <@t>{name} (<@t>{id}) cat=<@t>{name} (<@t>{id}) special_skill=<@t>{name} (<@t>{id}) item=<@t>{name} (<@t>{id}) unrecognised_storage_item=<@e>無法識別的儲藏庫物品。類別:<@s>{item_type}。物品 ID:<@s>{id} current_storage_items=目前的儲藏庫物品: storage_is_empty=儲藏庫是空的 available_storage=可用儲藏庫空間:<@t>{slots} display_storage=顯示儲藏庫 clear_storage=清空儲藏庫 add_cats=新增貓咪 add_special_skills=新增特殊能力 / 貓炮升級 remove_items=移除貓咪 / 能力 too_many_cats_selected=<@e>選取的貓咪過多。最大值為 <@s>{max}。目前已選取 <@s>{current} too_many_skills_selected=<@e>選取的能力過多。最大值為 <@s>{max}。目前已選取 <@s>{current} need_x_more_space=<@e>儲藏庫空間不足。還需要 <@s>{needs} 個空位 added_cats=已新增貓咪: added_special_skills=已新增特殊能力: select_special_skills=選擇特殊能力 removed_items=已移除物品: cat_storage=貓咪儲藏庫 storage_success=<@su>已成功編輯貓咪儲藏庫 select_gv= >輸入遊戲版本來進行篩選。例如: >- 僅獲取版本 <@t>11.5.0 的貓咪:<@t>11.5.0 >- 僅獲取版本 <@t>12.4.0 與 <@t>13.0.0 的貓咪:<@t>12.4.0 13.0.0 >- 獲取版本 <@t>12.4.0 與 <@t>13.0.0 之間(含)的所有貓咪:<@t>12.4.0-13.0.0 >請注意,任何沒有出現在升級選單裡的貓咪都無法在此選擇,因為它們的遊戲版本被設定為 <@t>-1。 >輸入: possible_gvs=可能的遊戲版本: no_valid_gvs_entered=<@w>未輸入有效的遊戲版本 select_cats_rarity=依稀有度選擇貓咪 select_cats_name=依名稱選擇貓咪 select_cats_obtainable=選擇所有可獲得的貓咪 select_cats_not_obtainable=選擇所有不可獲得的貓咪 select_cats_gatya_banner=依轉蛋系列選擇貓咪 select_cats_game_version=依遊戲版本選擇貓咪 select_cats_all=選擇所有貓咪 select_cats=選擇貓咪: and_mode_q=你想要縮減目前的選取範圍 (<@t>1)、加入到選取範圍 (<@t>2),還是取代它 (<@t>3)?: select_rarity=選擇貓咪稀有度: enter_name=輸入貓咪名稱: select_name=選擇貓咪名稱: select_gatya_banner=輸入轉蛋系列 ID {{range_input}} cats=貓咪 edit_cats=編輯貓咪 enter_cat_ids=你可以在這裡找到貓咪 ID:<@t>https://battlecats.miraheze.org/wiki/Cat_Release_Order\n輸入貓咪 ID {{range_input}} select_cats_id=依 ID 選擇貓咪 no_cats_found_name=<@w>找不到名稱為 <@s>{name} 的貓咪 select_cats_again=選擇額外貓咪 unlock_cats=解鎖貓咪|獲取貓咪 remove_cats=移除貓咪 upgrade_cats=升級貓咪 true_form_cats=三階進化 (第三型態/覺醒) remove_true_form_cats=移除貓咪三階 upgrade_talents_cats=升級貓咪本能 remove_talents_cats=移除貓咪本能 unlock_cat_guide=解鎖貓咪圖鑑 remove_cat_guide=取消解鎖貓咪圖鑑 finish_edit_cats=完成貓咪編輯 select_edit_cats_option=選擇一個選項來編輯貓咪: upgrade_success=<@su>已成功升級貓咪 upgrade_cats_select_mod=選擇升級貓咪的選項: upgrade_individual=為每隻選取的貓咪個別輸入等級 selected_cat_upgrades={{selected_cat}}:<@t>{base_level}<@s>+{plus_level} selected_cat_upgraded=<@t>{name} (<@t>{id}) 已升級至 <@t>{base_level}<@s>+{plus_level} upgrade_all=輸入等級並套用至所有選取的貓咪 upgrade_input= >輸入等級,範例如下: ><@t>10<@s>+20 = 等級 10, 加值 20 ><@t>10<@s>+ =等級 10, 維持目前加值 ><@t><@s>+20 = 維持目前等級, 加值 20 ><@t>10 = 等級 10, 加值 0 ><@t>5<@q>-10<@s>+20<@q>-30 = 5 到 10 之間的隨機等級,20 到 30 之間的隨機加值 ><@t>5<@q>-10<@s>+ = 5 到 10 之間的隨機等級,維持目前加值 ><@t><@s>+20<@q>-30 = 維持目前等級,20 到 30 之間的隨機加值 ><@t>{{max}}<@s>+{{max}} = 最大等級,最高加值 ><@t>{{quit_key}} = 退出 >Input: max_upgrade=最大升級上限: <@t>{max_base}<@s>+{max_plus} invalid_upgrade_base=<@e>無效的等級:<@s>{base} invalid_upgrade_base_random=<@e>無效的等級範圍:<@s>{min}-<@s>{max} invalid_upgrade_plus=<@e>無效的加值:<@s>{plus} invalid_upgrade_plus_random=<@e>無效的加值範圍:<@s>{min}-<@s>{max} remove_true_form_success=<@su>已成功移除第三型態 true_form_success=<@su>已成功將貓咪進化為第三型態 remove_success=<@su>已成功移除貓咪 unlock_success=<@su>已成功解鎖貓咪 unlock_cat_guide_success=<@su>已成功領取貓咪圖鑑獎勵 remove_cat_guide_success=<@su>已成功取消領取貓咪圖鑑獎勵 select_cats_current=選擇目前已解鎖的貓咪 select_cats_not_unlocked=選擇尚未解鎖的貓咪 talents_version_warning= ><@w>警告:編輯器的遊戲資料與此存檔的遊戲版本不符。本能可能無法如預期運作。 >存檔版本:<@s>{save_version} >遊戲資料版本:<@s>{data_version} >如果遊戲資料過舊,應會在幾天內更新。 talents_success=<@su>已成功升級貓咪本能 talents_remove_success=<@su>已成功移除貓咪本能 talents_individual=個別編輯選取貓咪的本能 talents_all=將所有選取貓咪的本能升至滿級 upgrade_talents_select_mod=選擇編輯貓咪本能的選項: no_talent_data=<@w>這隻貓咪沒有本能資料 talents=本能 upgrade_talent_cats=升級貓咪本能 force_true_form_cats=強制進化為第三型態 force_true_form_cats_warning= ><@w>警告:只有當你確定該貓咪擁有第三型態時才使用此功能,否則會導致第三型態出現錯誤。 >此選項主要用途是編輯器的遊戲資料過舊,尚未新增更新的第三型態時。 filter_current_q=你想要只從目前已解鎖的貓咪中選擇 (<@t>1),還是從所有貓咪中選擇 (<@t>2)?: select_cats_currently_option=從你目前已解鎖的貓咪中選擇(例如,當選擇特定稀有度的貓咪時,只會選擇你目前擁有該稀有度的貓咪) select_cats_all_option=從所有貓咪中選擇 unlock_remove_cats=解鎖貓咪 / 移除貓咪 true_form_remove_form_cats=進化為第三型態 / 移除第三型態 upgrade_talents_remove_talents_cats=升級本能 / 移除本能 unlock_remove_cat_guide=領取 / 取消領取貓咪圖鑑獎勵 unlock_remove_q=你想要<@t>解鎖還是<@t>移除貓咪?: true_form_remove_form_q=你想要將貓咪<@t>進化為第三型態還是<@t>移除第三型態?: upgrade_talents_remove_talents_q=你想要<@t>升級還是<@t>移除本能?: unlock_cat_guide_remove_guide_q=你想要<@t>領取還是<@t>取消領取貓咪圖鑑獎勵?: fourth_form_remove_form_cats=超進化貓咪 (第四型態) / 移除貓咪第四型態 force_fourth_form_cats=強制超進化貓咪 (第四型態) fourth_form_success=<@su>已成功將貓咪超進化 (第四型態) remove_fourth_form_success=<@su>已成功移除貓咪第四型態 fourth_form_cats=超進化貓咪 (第四型態) remove_fourth_form_cats=移除貓咪第四型態 fourth_form_remove_form_q=你想要將貓咪<@t>超進化還是<@t>移除第四型態?: force_fourth_form_cats_warning= ><@w>警告:只有當你確定該貓咪擁有第四型態 (超進化) 時才使用此功能,否則會導致第四型態出現錯誤。 >此選項的主要用途是當編輯器的遊戲資料過舊,且尚未新增新的型態時。 gatya_info_progress=正在下載轉蛋資訊 (<@t>{current}/<@t>{total}) unknown_banner=未知的轉蛋系列 banner_txt={name} (<@s>{int}) filter_down_q_gatya=你要從清單中移除重複及未知的轉蛋系列嗎?({{y/n}}): select_cats_non_gatya=選擇非轉蛋貓咪 finished_cats_selection=你已經完成貓咪選擇了嗎?({{y/n}}): downloading_cat_names=<@su>正在從 <@s>{url} 下載貓咪名稱 ================================================ FILE: src/bcsfe/files/locales/tw/edits/enemy.properties ================================================ total_selected_enemies=目前已選取 <@t>{total} 種敵人 unlock_enemy_guide_success=<@su>已成功解鎖敵人圖鑑項目 remove_enemy_guide_success=<@su>已成功移除敵人圖鑑項目 selected_enemy=已選取 <@t>{name} (<@t>{id}) select_enemies_valid=選擇敵人圖鑑中的所有敵人 select_enemies_invalid=選擇不在敵人圖鑑中的所有敵人 select_enemies_all=選擇所有敵人 select_enemies_id=依 ID 選擇敵人 select_enemies_name=依名稱選擇敵人 select_enemies=選擇敵人: enter_enemy_ids=你可以在這裡找到敵人 ID:<@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order\n輸入敵人 ID {{range_input}}: enter_enemy_name=輸入敵人名稱: enemy_not_found_name=<@w>找不到名稱為 <@s>{name} 的敵人 unlock_enemy_guide=解鎖敵人圖鑑 remove_enemy_guide=移除敵人圖鑑 enemy_guide=敵人圖鑑 edit_enemy_guide=輸入選項以編輯敵人圖鑑項目: ================================================ FILE: src/bcsfe/files/locales/tw/edits/fixes.properties ================================================ fix_gamatoto_crash=修復加碼多多造成的遊戲閃退 fix_time_errors=修復時間相關錯誤 fix_ototo_crash=修復城堡寶開發隊造成的遊戲閃退 fix_gamatoto_crash_success=<@su>已成功修復加碼多多造成的遊戲閃退 fix_time_errors_success=<@su>已成功修復時間相關錯誤 <@w>(兩台裝置時間都必須正確此功能才生效) fix_ototo_crash_success=<@su>已成功修復城堡寶開發隊造成的遊戲閃退 fixes=錯誤修復 unlock_equip_menu=解鎖隊伍編成選單 equip_menu_unlocked=<@su>已成功解鎖隊伍編成選單 ================================================ FILE: src/bcsfe/files/locales/tw/edits/gambling.properties ================================================ reset_wildcat_slots=<@su>已成功重置貓咪拉霸機 reset_cat_scratcher=<@su>已成功重置貓咪刮刮樂 reset_gambling_events=重置貓咪拉霸機與貓咪刮刮樂 ================================================ FILE: src/bcsfe/files/locales/tw/edits/gamototo.properties ================================================ enter_raw_gamatoto_xp=輸入加碼多多經驗值 enter_gamatoto_level=輸入加碼多多等級 edit_gamatoto_level_q=輸入選項以編輯加碼多多等級: gamatoto_xp=加碼多多經驗值 gamatoto_level=加碼多多等級 gamatoto_level_success=<@su>已成功將加碼多多等級設定為 <@s>{level} (XP:<@s>{xp}) gamatoto_level_current=<@t>目前的加碼多多等級為 <@q>{level} (XP:<@q>{xp}) gamatoto_xp_level=加碼多多經驗值 / 等級 current_gamatoto_helpers=目前的隊員: gamatoto_helper=隊員:<@t>{name} (稀有度:<@t>{rarity_name}) new_gamatoto_helpers=新的隊員: gamatoto_helpers=加碼多多隊員 ototo_cat_cannon=城堡寶開發隊_貓咪城 current_cannon_stats=目前的貓砲狀態: cannon_part=<@t><@q>{name}{buffer}(等級 <@s>{level}) development={buffer}(開發進度:<@q>{development}) cannon_stats={parts} foundation=基座 style=裝飾 effect=主炮 improved_foundation=強化基座 improved_style=強化裝飾 unknown_stage=未知階段 (<@s>{stage}) selected_cannon=<@t>已選取的貓砲:<@q>{name} selected_cannon_stage=<@t>貓砲:<@q>{name} 目前階段:<@q>{stage} cannon_edit_type=你想要個別編輯每座貓砲,還是將變更套用至所有已選的貓砲?: cannon_dev_level_q=你想要編輯貓砲的開發進度還是等級?: development_o=開發進度 level_o=等級 select_development=選擇開發階段: select_cannon=選擇貓砲 cannon_level=貓砲等級 cannon_success=<@su>已成功編輯城堡開發隊貓砲 cat_shrine=編輯貓咪神社 shrine_level=編輯神社等級 shrine_xp=神社 XP (經驗值) current_shrine_xp_level=<@t>目前的 XP:<@q>{xp} (等級:<@q>{level}) cat_shrine_choice_dialog=你想要做什麼?: shrine_level_dialog=輸入貓咪神社等級(最大值:<@q>{max_level}): shrine_xp_dialog=輸入貓咪神社 XP(最大值:<@q>{max_xp}): cat_shrine_edited=<@su>已成功編輯貓咪神社 make_catshrine_appear=在遊戲中顯示貓咪神社 make_catshrine_disappear=在遊戲中隱藏貓咪神社 ================================================ FILE: src/bcsfe/files/locales/tw/edits/gatya.properties ================================================ event_tickets=活動轉蛋券 / 招福轉蛋券 downloading_gatya_data=正在下載轉蛋活動資料... download_gatya_data_success=<@su>已成功下載轉蛋活動資料 download_gatya_data_fail=<@e>下載轉蛋活動資料失敗。請再試一次 save_gatya_error=<@e>因 {error} 導致儲存轉蛋資料失敗 gatya_by_id_q=你想要依 <@t>ID 還是 <@t>名稱 來選擇轉蛋系列?: by_id=依 ID by_name=依名稱 ================================================ FILE: src/bcsfe/files/locales/tw/edits/gold_pass.properties ================================================ gold_pass_dialog=輸入你想要的 <@t>會員 ID(留 <@q>白 則產生 <@q>隨機 ID,或者輸入 <@q>-1 來 <@q>移除 黃金會員): gold_pass=黃金會員 / 貓咪俱樂部 gold_pass_remove_success=<@su>已成功移除黃金會員 gold_pass_get_success=<@su>已成功獲取黃金會員 (ID:<@t>{id})。<@w>注意:如果遊戲連線後發現實際沒有購買,可能會自動移除黃金會員。這是遊戲本身的防護機制,我無法修復此問題,因此請不要將其作為 bug 回報。 officer_pass_fixed=<@su>已成功修復貓咪俱樂部導致的閃退問題 fix_officer_pass_crash=修復貓咪俱樂部閃退 ================================================ FILE: src/bcsfe/files/locales/tw/edits/items.properties ================================================ # Note that not all items are here catamins=喵力達 catfruit=貓薄荷 base_materials=城堡開發材料 inquiry_code=詢問碼 rare_gatya_seed=稀有轉蛋種子碼 normal_gatya_seed=一般轉蛋種子碼 event_gatya_seed=活動轉蛋種子碼 unlocked_slots=解鎖出陣列表|編成欄位 password_refresh_token=密碼重新整理權杖 challenge_score=挑戰賽分數 dojo_score=貓咪道場分數 items=道具 user_rank_rewards=領取等級排行獎勵(不會實際領取獎勵) catfood=貓罐頭 xp=XP(經驗值) normal_tickets=貓咪轉蛋券|基本轉蛋券|銀券 rare_tickets=稀有券|金券 platinum_tickets=白金券 legend_tickets=傳說券 100_million_tickets=一億次下載紀念券|一億下載紀念券 100_million_warn=<@w>注意:只有在「一億下載紀念轉蛋」活動進行期間,才能看到並使用這些轉蛋券 platinum_shards=白金券碎片 np=NP leadership=首領旗 catseyes=貓眼石 battle_items=戰鬥道具 duration=<@t>{days} 天,<@t>{hours} 小時,<@t>{minutes} 分鐘,<@t>{seconds} 秒 endless_item_item=<@s>{item}:<@s>{int} endless_items_success=<@su>已成功編輯無限道具 invalid_minute_count=<@e>無效的分鐘數 enter_duration_minutes=輸入無限道具持續的分鐘數(如果輸入 <@t>infinity 則道具將永久有效): infinity_duration=<@t>infinity infinity=無限 enter_duration_minutes_item=輸入無限 <@t>{item} 持續的分鐘數(如果輸入 <@t>infinity 則道具將永久有效): battle_items_endless=無限戰鬥道具 talent_orbs=本能玉 scheme_items=獸石 / 獸結晶 labyrinth_medals=地底迷宮獎章 restart_pack=回歸禮包 engineers=城堡開發隊員 gamototo=加碼多多 / 城堡寶開發隊 special_skills=特殊能力 / 貓炮 treasure_chests=神秘寶箱 unknown_treasure_chest_name=未知的寶箱 ({id}) rare_ticket_trade=稀有券兌換 rare_ticket_trade_feature_name=稀有券兌換 (可獲得不會被封帳的稀有券) other=其他 gatya=轉蛋 levels=關卡 / 傳奇故事 / 寶物 cats_special_skills=貓咪 / 特殊能力 gatya_item_unknown_name=未知的道具 unknown_catamin_name=未知的喵力達 <@t>{id} unknown_catseye_name=未知的貓眼石 <@t>{id} unknown_catfruit_name=未知的貓薄荷 <@t>{id} unknown_labyrinth_medal_name=未知的地底迷宮獎章 <@t>{id} reset_golden_cat_cpus_success=<@su>已成功重置跳過戰鬥的使用次數 reset_golden_cat_cpus=重置跳過戰鬥使用次數 ================================================ FILE: src/bcsfe/files/locales/tw/edits/map.properties ================================================ tutorial_already_cleared=<@w>你已經通過教學關卡 tutorial_cleared=<@su>已成功通過教學關卡 clear_tutorial=通過教學關卡 clear_stages=通關關卡 unclear_stages=取消通關關卡 clear_unclear_q=你要<@t>通關還是<@t>取消通關關卡?: add_enigma_stages=新增發掘關卡 clear_enigma_stages=通關發掘關卡 current_enigma_stages=目前的發掘關卡: enigma_stage=發掘關卡 <@q>{name} (ID:<@q>{id})  unknown_enigma_name=未知的發掘關卡名稱 (ID:<@q>{id}) enigma_select=選擇要新增的發掘關卡 enigma_success=<@su>已成功新增發掘關卡 wipe_enigma=你要清除目前的發掘關卡嗎?({{y/n}}): aku_realm_unlocked=<@su>已成功解鎖魔界篇 unlock_aku_realm=解鎖魔界篇 select_story_chapters=選擇傳奇故事章節 chapter_progress_txt=(例如:<@q>0 = 未通關任何關卡,<@q>1 = 通關第一關,<@q>2 = 通關第一和第二關,... <@q>{max} = 全數通關) edit_chapter_progress_all=輸入進度以將每個章節設定為 {{chapter_progress_txt}}: edit_chapter_progress=輸入進度以將 <@t>{chapter_name} 設定為 {{chapter_progress_txt}}: edit_stage_clear_count=輸入關卡通關次數: story_cleared=<@su>已成功通關傳奇故事章節 individual_chapters=個別章節 all_chapters=所有章節 individual_chapters_dialog=你要<@t>個別編輯每個章節的通關進度?還是將<@t>所有章節設定為相同的進度?: individual_clear_counts=個別通關次數 all_clear_counts=統一通關次數 individual_clear_counts_dialog=你要<@t>個別編輯每個關卡的通關次數?還是將<@t>所有關卡設定為相同的通關次數?: clear_story=主線故事章節|通關故事章節 map_name_star={name} {star} 皇冠 clear=通關 unclear=取消通關 outbreaks=不死生物襲擊 / 殭屍關卡 clear_unclear_outbreaks=你要<@t>通關還是<@t>取消通關不死生物襲擊?: clear_outbreaks_success=<@su>已成功通關不死生物襲擊 unclear_outbreaks_success=<@su>已成功取消通關不死生物襲擊 no_valid_outbreaks=<@e>錯誤:找不到有效的不死生物襲擊 aku_chapters=魔界篇章節 aku_clear_success=<@su>已成功通關魔界篇 aku_current_stage=魔界篇關卡 <@q>{name} (ID:<@q>{id}) itf_timed_scores=未來篇時間積分 itf_timed_scores_dialog=你要編輯<@t>整個章節的分數,還是<@t>個別關卡的分數? itf_timed_scores_edited=<@su>已成功編輯未來篇時間分數 itf_timed_score_dialog=輸入時間分數: current_stage={chapter_name} <@t>{stage_name} itf_timed_scores_individual_dialog=你要<@t>個別編輯每個選取關卡的時間分數?還是將<@t>所有選取的關卡設定為相同的時間分數?: filibuster_stage_reclearing_allowed=<@su>已成功重啟議事阻撓貓關卡 filibuster_reclearing=重啟議事阻撓貓關卡 all_selected_stages=所有選取的關卡 unknown_map_name=未知的地圖名稱 (ID:<@q>{id}) map_name={name} <@s>(ID:<@q>{id}) edit_map_chapters=選擇章節 clear_whole_chapters=通關整個章節 unclear_whole_chapters=取消通關整個章節 clear_specific_stages=通關特定關卡 unclear_specific_stages=取消通關特定關卡 select_clear_type=你要<@t>通關整個章節還是<@t>通關特定關卡?: select_unclear_type=你要<@t>取消通關整個章節還是<@t>取消通關特定關卡?: custom_star_count_per_chapter_yn=你要為每個章節設定自訂的星數/皇冠數嗎?({{y/n}}): modify_clear_amounts=正在將每個選取關卡的通關次數設定為 <@t>1。你要更改此設定嗎?({{y/n}}): clear_amount_chapter=為每個選取的章節設定不同的通關次數 clear_amount_all=為所有選取的章節設定相同的通關次數 clear_amount_stages=為每個選取的關卡設定不同的通關次數 select_clear_amount_type=輸入你要使用的通關次數設定模式: clear_amount_enter=輸入通關次數: custom_star_count_per_chapter=輸入星數/皇冠數(最大值 <@q>{max}): custom_star_count_per_chapter_unclear= >輸入要移除的星數/王冠數: ><@s><@t>1 = 整張地圖取消通關 ><@s><@t>2 = 從第 2、3、4 冠地圖中取消通關 ><@s><@t>3 = 從第 3、4 冠地圖中取消通關 ><@s><@t>4 = 從第 4 冠地圖中取消通關 >(最大值 <@q>{max}): current_sol_chapter=章節 <@t>{name} (ID:<@q>{id}) current_sol_star=星數/王冠:<@q>{star} current_sol_stage=關卡 <@q>{name} (ID:<@q>{id}) map_chapters_edited=<@su>已成功編輯章節 sol=傳奇故事 event=一般活動關卡 collab=合作活動關卡 select_map=選擇地圖 select_map_dialog= >選擇你要編輯的地圖 >你可以輸入一個數字範圍(例如 <@q>1-5)、個別數字(例如 <@q>1 3 5),或兩者混用(例如 <@q>1-3 5) >你也可以輸入地圖名稱或部分名稱(例如 <@t>{example})來進行搜尋/選擇 >你也可以輸入 <@q>all 來選擇所有章節 >輸入: no_map_found=<@e>找不到名稱為 <@s>{name} 的地圖 finished_selecting_maps=你完成地圖選擇了嗎?({{y/n}}): current_maps=目前的地圖: select_stage=選擇關卡 gauntlets=強襲關卡 collab_gauntlets=合作強襲關卡 uncanny=真傳奇故事 catamin_stages=喵力達關卡 behemoth_culling=超獸討伐關卡 legend_quest=傳奇尋寶記 towers=風雲貓咪塔 zero_legends=傳奇故事0 unclear_other_stages=你要覆寫章節中目前的進度嗎?({{y/n}}) <@t>n = 僅更改選取關卡的通關次數,<@t>y = 取消通關該章節中較後的已通關關卡: select_stage_progress=輸入要通關到哪一關為止(包含該關卡): zero_legends_warning=<@w>警告:如果你使用的遊戲版本還沒有「傳奇故事0」地圖,嘗試將其編輯為通關將會導致遊戲閃退! stages_select=輸入數字 {{range_input}} change_clear_amount_catamin=更改章節通關次數 clear_unclear_stage_catamin=通關 / 取消通關喵力達關卡 catamin_stage_clear_q=你想<@t>更改喵力達專用關卡的通關次數,還是只想<@t>通關或取消通關關卡?: select_map_from_names=選擇地圖 enter_clear_amount_catamin_map=輸入章節 <@t>{name} (ID:<@t>{id}) 的通關次數(<@t>0 = 尚未通關此章節,<@t>3 或以上代表該章節會消失): enter_clear_amount_catamin=輸入選取章節的通關次數(<@t>0 = 尚未通關該章節,<@t>3 或以上代表該章節會消失): catamin_stage_success=<@su>已成功編輯喵力達關卡 catamin_clear_amounts_q=你要<@t>個別編輯每個章節的通關次數,還是<@t>一次全部設定?: dojo_catclaw_championships=通關貓咪道場晉段測驗 finished=完成 edit_chapters_q=你要編輯什麼?: clear_whole_chapter=通關整個章節 clear_to_specific_stage=通關特定關卡 clear_whole_q=你要<@t>通關整個章節還是<@t>通關章節內特定關卡?: clear_all=通關所有章節 handle_individually=個別編輯每個章節 clear_chapters_q=你要<@t>通關所有選取的章節還是<@t>個別編輯每個章節的進度?: max_stars=輸入最大皇冠數(最大值:<@t>{max}): edit_map_all=所有章節統一通關次數 edit_chapters_q_all=你要<@t>為所有選取的章節設定相同的通關次數還是<@t>個別編輯每個章節的通關次數? edit_whole_chapter=為整個章節設定相同的通關次數 edit_specific_stages=編輯特定關卡的通關次數 edit_chapter_q=你要<@t>為章節內的所有關卡設定相同的通關次數還是<@t>編輯特定關卡的通關次數?: each_stage_individually=為每個選取的關卡設定不同通關次數 stage_all_at_once=為所有選取的關卡設定相同通關次數 set_clear_count_stage_q=你要<@t>為每個選取的關卡設定不同通關次數還是<@t>為所有選取的關卡設定相同通關次數?: unknown_stage_name=未知的關卡名稱 (索引:<@t>{index}) current_stage_map=<@t>{name} <@s>(索引:<@q>{index}) edit_progress_clear=編輯地圖進度 edit_progress_unclear=編輯地圖進度 (可取消通關) edit_clear_counts=編輯關卡通關次數 keep_selecting=繼續選擇 remove_selection=移除選擇 finish_selection=完成選擇 map_selection_q=你想要做什麼? ================================================ FILE: src/bcsfe/files/locales/tw/edits/medals.properties ================================================ medals=貓咪獎章 add_medals=新增獎章 remove_medals=移除獎章 medal_add_remove_dialog=你希望 <@t>新增獎章 或 <@t>移除獎章?: medal_string={medal_name}: <@q>{medal_req} select_medals=選擇獎章: medals_added=<@su>成功新增貓咪獎章 medals_removed=<@su>成功移除貓咪獎章 ================================================ FILE: src/bcsfe/files/locales/tw/edits/missions.properties ================================================ missions=貓咪任務 complete_reward=完成任務但不領取獎勵 complete_claim=完成任務並領取獎勵 uncomplete=取消完成任務 select_mission_claim=你要<@t>完成任務但不領取獎勵、<@t>完成任務並領取獎勵 <@q>(不會真的收到獎勵道具),還是<@t>取消完成任務(如果可行的話)? select_missions=選擇要編輯的任務: missions_edited=<@su>已成功編輯任務 ================================================ FILE: src/bcsfe/files/locales/tw/edits/playtime.properties ================================================ playtime_str=<@t>{hours} 小時, <@t>{minutes} 分鐘, <@t>{seconds} 秒 (<@t>{frames} 幀) playtime_current=目前的遊戲時數:{{playtime_str}} playtime_edited=已成功將遊戲時數編輯為 {{playtime_str}} playtime_hours_prompt=輸入要設定的遊戲時數<@t>小時數: playtime_minutes_prompt=輸入要設定的遊戲時數<@t>分鐘數: playtime_seconds_prompt=輸入要設定的遊戲時數<@t>秒數: playtime=遊戲時數 ================================================ FILE: src/bcsfe/files/locales/tw/edits/scheme_items.properties ================================================ scheme_items_edit_success=<@su>已成功編輯獸石/獸結晶 scheme_items_select_gain=選擇要獲取的獸石/獸結晶 scheme_items_select_remove=選擇要移除的獸石/獸結晶 gain_remove_scheme_items=你想要<@t>獲取還是<@t>移除獸石/獸結晶?: gain_scheme_items=獲取獸石/獸結晶 remove_scheme_items=移除獸石/獸結晶 ================================================ FILE: src/bcsfe/files/locales/tw/edits/special_skills.properties ================================================ special_skills_dialog=選擇要升級的基地能力 upgrade_individual_skill=為每個選取的能力個別輸入等級 upgrade_all_skills=輸入等級數值以套用至所有選取的能力 upgrade_skills_select_mod=選擇升級能力的選項: selected_skill=已選取 <@t>{name} selected_skill_upgrades={{selected_skill}}:<@t>{base_level}<@s>+{plus_level} selected_skill_upgraded=<@t>{name} 已升級至 <@t>{base_level}<@s>+{plus_level} skills_edited=<@su>已成功編輯特殊能力 ================================================ FILE: src/bcsfe/files/locales/tw/edits/talent_orbs.properties ================================================ total_current_orbs=目前本能玉總數:<@q>{total_orbs} total_current_orb_types=目前本能玉種類總數:<@q>{total_types} current_orbs=目前的本能玉: orb_select=選擇要編輯的本能玉: selected_orbs=已選取的本能玉: edit_orbs_individually=你要個別編輯每顆本能玉 (<@q>1) 還是一次全部編輯 (<@q>2)?: edit_orbs_all=輸入數值以套用至所有選取的本能玉(最大值 <@t>{max}): failed_to_load_orbs=無法載入本能玉 edit_orbs_help= >說明: >可用等級:{all_grades_str} >可用屬性:{all_attributes_str} >可用效果:{all_effects_str} ><@w>注意:並非所有等級和效果都適用於所有屬性。 >輸入範例: >    aku - 選擇所有惡魔本能玉 >    red s - 選擇所有紅色 S 級本能玉 >    alien d 0 - 選擇可提升攻擊力的 D 級異星本能玉。 >    c 1 - 選擇 C 級的傳奇故事強化本能玉 >如果你想選擇<@q>所有本能玉,請輸入: >    <@q>* >如果你想進行<@q>多重選擇,請使用<@q>逗號分隔,例如: >    s black 4,d 3,floating > ================================================ FILE: src/bcsfe/files/locales/tw/edits/treasures.properties ================================================ whole_chapters=整個章節 individual_stages=個別關卡 treasure_groups=寶物群組 / 寶物區域 treasure_dialog=你要編輯<@t>整個章節、<@t>個別關卡還是個別<@t>寶物群組的寶物?: treasures_edited=<@su>已成功編輯寶物 per_chapter=個別章節 all_selected_chapters=所有選取的章節 edit_per_chapter=你要一次編輯<@t>所有選取的章節,還是<@t>個別編輯每個章節的資料?: no_treasure=無寶物 (尚未獲得) custom_treasure_level=自訂寶物等級 (<@w>請確定你知道自己在做什麼再進行編輯!) treasure_level_dialog=輸入你要設定的寶物等級: custom_treasure_level_dialog=輸入你要設定的自訂寶物等級: select_stage_by_id=依 ID 選擇關卡 select_stage_by_name=依名稱選擇關卡 select_stage_dialog=你想要依 <@t>ID 還是 <@t>名稱 來選擇關卡?: select_stage_id=輸入你要選擇的關卡 ID {{range_input}} select_stages_name=選擇關卡: select_treasure_groups=選擇你要編輯的寶物群組: story_treasures=故事章節寶物 current_chapter=目前章節:<@t>{chapter_name} current_treasure_group=目前寶物群組:<@t>{treasure_group_name} group_individual=個別群組 group_all_at_once=所有選取的群組 select_treasure_groups_individual=你要<@t>個別編輯每個寶物群組的寶物等級,還是<@t>一次編輯所有選取的群組?: ================================================ FILE: src/bcsfe/files/locales/tw/edits/user_rank.properties ================================================ claim=領取 unclaim=取消領取 fix_claimed=修復領取狀態 claim_or_unclaim_ur=你想要<@t>領取、<@t>取消領取還是<@t>修復領取狀態 (取消領取任何高於目前等級排行的獎勵)等級排行獎勵?: select_ur=選擇等級獎勵 ur_claimed_success=<@su>成功領取等級排行獎勵 ur_unclaimed_success=<@su>成功取消領取等級排行獎勵 ur_string=等級排行:<@s>{rank}:{description} ur_fix_claimed_success=<@su>成功修復已領取的用戶等級獎勵狀態 ================================================ FILE: src/bcsfe/files/locales/tw/metadata.json ================================================ { "name": "繁體中文 (Traditional Chinese)", "authors": ["LinYuAn"] } ================================================ FILE: src/bcsfe/files/locales/vi/core/config.properties ================================================ # filename="config.properties" config=Cấu hình edit_config=Chỉnh sửa cấu hình default_value=(giá trị mặc định: <@q>{default_value}) current_value=(giá trị hiện tại: <@q>{current_value}) config_value_txt=<@s>{{current_value}} {{default_value}} config_dialog=Chọn một tùy chọn cấu hình để chỉnh sửa: update_to_beta_desc=Kiểm tra cập nhật cho phiên bản beta {{config_value_txt}} update_to_beta=Cập nhật lên phiên bản beta show_update_message_desc=Hiển thị thông báo khi có phiên bản mới {{config_value_txt}} show_update_message=Hiển thị thông báo cập nhật config_full=<@t>{key_desc} disable_maxes_desc=Tắt giá trị tối đa khi chỉnh sửa {{config_value_txt}} disable_maxes=Tắt giá trị tối đa max_backups_desc=Số lượng tối đa sao lưu save file giữ {{config_value_txt}} max_backups=Sao lưu save tối đa available_themes=Các chủ đề khả dụng: theme_desc=Chủ đề sử dụng {{config_value_txt}} theme=Chủ đề show_missing_locale_keys=Hiển thị các khóa ngôn ngữ thiếu 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}} 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}} reset_cat_data=Đặt lại dữ liệu cat khi xóa cat 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}} filter_current_cats=Lọc cat hiện tại khi chọn cat 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}} set_cat_current_forms=Đặt hình dạng hiện tại của cat khi mở khóa hình dạng 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}} strict_upgrade=Kiểm tra nâng cấp nghiêm ngặt 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}} separate_cat_edit_options=Tách tùy chọn chỉnh sửa cats 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}} strict_ban_prevention=Ngăn chặn cấm nghiêm ngặt 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}} max_request_timeout=Thời gian chờ yêu cầu tối đa game_data_repo_desc=Kho sử dụng cho dữ liệu trò chơi {{config_value_txt}} game_data_repo=Kho dữ liệu trò chơi game_data_repo_dialog=Nhập kho dữ liệu trò chơi để sử dụng: 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}} 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 clear_tutorial_on_load_desc=Xóa tutorial khi tải save file vào trình chỉnh sửa {{config_value_txt}} clear_tutorial_on_load=Xóa tutorial khi tải save 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}} remove_ban_message_on_load=Xóa thông báo cấm khi tải save unlock_cat_on_edit_desc=Mở khóa cat khi chỉnh sửa level, talents, form, v.v. {{config_value_txt}} unlock_cat_on_edit=Mở khóa cat khi chỉnh sửa 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}} use_file_dialog=Sử dụng hộp thoại tệp adb_path_desc=Đường dẫn đến executable adb {{config_value_txt}} adb_path=Đường dẫn ADB use_waydroid=Sử dụng shell waydroid thay vì adb use_waydroid_desc=Waydroid không hỗ trợ adb root, vì vậy sử dụng shell waydroid thay thế {{config_value_txt}} 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}} ignore_parse_error=Bỏ qua lỗi phân tích lưu string_config_dialog=Nhập giá trị mới cho <@q>{val}: enable_disable_dialog=Bạn muốn <@q>bật hay <@q>tắt tính năng này?: enable=Bật disable=Tắt enabled=Đã bật disabled=Đã tắt config_success=<@su>Đã cập nhật cấu hình thành công 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? 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}} use_pkexec_waydroid=Sử dụng binary pkexec để chạy các lệnh waydroid ================================================ FILE: src/bcsfe/files/locales/vi/core/files.properties ================================================ # filename="files.properties" another_path=Nhập đường dẫn thủ công select_files_dir=Chọn tệp trong thư mục: enter_path=Nhập đường dẫn tệp / vị trí: enter_path_dir=Nhập đường dẫn thư mục / vị trí: enter_path_default=Nhập đường dẫn tệp / vị trí (mặc định: <@t>{default}): current_files_dir=Các tệp hiện tại trong thư mục <@t>{dir}: other_dir=Nhập thư mục khác no_files_dir=<@e>Không có tệp trong thư mục path_not_exists=<@e>Đường dẫn không tồn tại ================================================ FILE: src/bcsfe/files/locales/vi/core/input.properties ================================================ # filename="input.properties" input_int=Nhập một số giữa <@q>{min} và <@q>{max}: select_edit=Chọn các tùy chọn cho <@t>{group_name}: input_int_default=Nhập một số giữa <@q>{min} và <@q>{max} (mặc định <@q>{default}): input_many=Nhập các số giữa <@q>{min} và <@q>{max} cách nhau bằng dấu cách: input_single=Nhập một số giữa <@q>{min} và <@q>{max}: input=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}) (giá trị tối đa: <@q>{max}): input_min=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}) (phạm vi: <@q>{min} - <@q>{max}): input_non_max=Nhập giá trị cho <@t>{name} (giá trị hiện tại: <@q>{value}): input_all=Nhập giá trị cho tất cả <@t>{name} (giá trị tối đa: <@q>{max}): value_changed=<@su>Đã thay đổi thành công <@s>{name} thành <@s>{value} value_gave=<@su>Đã cung cấp thành công <@s>{name} all_at_once=Chọn tất cả các tùy chọn cùng lúc invalid_input=<@e>Đầu vào không hợp lệ. Vui lòng thử lại. 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} features=Tính năng: go_back=Quay lại yes_key=y quit_key=q 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: select_features= >Để chọn một tính năng, nhập >- một <@q>số tương ứng với số bên trái >- <@t>văn bản để tìm kiếm tính năng >Bạn có thể nhấn <@t>enter để xem tất cả tính năng >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 >Đầu vào: individual=Cá nhân edit_all_at_once=Tất cả cùng lúc finish=Hoàn thành select_option=Chọn tùy chọn: ================================================ FILE: src/bcsfe/files/locales/vi/core/locale.properties ================================================ # filename="locale.properties" available_locales=Các ngôn ngữ khả dụng: locale_desc=Ngôn ngữ sử dụng {{config_value_txt}} locale=Ngôn ngữ locale_dialog=Chọn một ngôn ngữ: add_locale=Thêm ngôn ngữ remove_locale=Xóa ngôn ngữ locale_remove_dialog=Chọn các ngôn ngữ để xóa: enter_locale_git_repo=Nhập kho git của ngôn ngữ (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git): 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}}): locale_added=<@su>Đã thêm ngôn ngữ thành công checking_for_locale_updates=Đang kiểm tra cập nhật cho ngôn ngữ bên ngoài <@t>{locale_name}... 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}} 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> invalid_git_repo=<@e>Kho git không hợp lệ locale_cancelled=<@e>Đã hủy 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 locale_changed=<@su>Đã thay đổi ngôn ngữ thành công thành <@t>{locale_name}.\n{{restart_to_see_changes}} locale_removed=<@su>Đã xóa ngôn ngữ thành công <@t>{locale_name}.\n{{restart_to_see_changes}} no_external_locales=<@e>Không tìm thấy ngôn ngữ bên ngoài missing_locale_keys=Các khóa ngôn ngữ thiếu: extra_locale_keys=Các khóa ngôn ngữ thêm: locale_text= >Ngôn ngữ hiện tại: <@s>{locale_name} (Phiên bản: <@s>{locale_version}) >Được tạo bởi <@s>{locale_author} >Vị trí tệp ngôn ngữ: <@s>{locale_path} default_locale_text_authors= >Ngôn ngữ hiện tại: <@s>{name} >Được tạo bởi <@s>{authors} >Vị trí tệp ngôn ngữ: <@s>{path} ================================================ FILE: src/bcsfe/files/locales/vi/core/main.properties ================================================ # filename="main.properties" # Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files # color formatting # # <@p> = primary color # <@s> = secondary color # <@t> = tertiary color # <@q> = quaternary color # <@e> = error color # <@w> = warning color # <@su> = success color # # = close current color # When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes. # You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red. # If you want to write < or > or / in the text, escape them with a backslash (\) e.g. \< or \> or \/ # # <#rrggbb> = hex color # # = white # = black # = red # = green # = blue # = yellow # = magenta # = cyan # = dark yellow # = dark grey # = dark blue # = dark cyan # = dark magenta # = dark red # = dark green # = light grey # = orange downloading=<@su>Đang tải xuống <@s>{file_name} từ <@s>{pack_name} với phiên bản <@s>{version} 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. no_device_error=<@e>Không tìm thấy thiết bị kết nối 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. exit=Thoát 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. tkinter_not_found_enter_path_file=Vui lòng nhập đường dẫn/vị trí của tệp {initialfile}: tkinter_not_found_enter_path_file_save=Vui lòng nhập đường dẫn/vị trí để lưu tệp {initialfile}: tkinter_not_found_enter_path_dir=Vui lòng nhập đường dẫn/vị trí của thư mục {initialdir} thay thế: discord_url=https://discord.gg/DvmMgvn5ZB welcome= ><@t>Chào mừng đến với <@s>Trình chỉnh sửa save file Battle Cats! >Được tạo bởi <@s>fieryhenry > >Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python >Discord: <@s>{{discord_url}} - Vui lòng báo lỗi đến <@s>#bug-reports và gợi ý đến <@s>#suggestions >Ủng hộ: <@s>https://ko-fi.com/fieryhenry > >Vị trí tệp cấu hình: <@s>{config_path} > >{theme_text} > >{locale_text} > ><@q>Cảm ơn: >- <@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/ >- <@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 >- 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 >- 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}} > ><@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ở. > ><@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. >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. >Nếu save file bị hỏng, vui lòng vẫn báo cáo trong discord. >Tôi khuyên bạn nên sao lưu save file trước khi chỉnh sửa. report_message=Vui lòng báo cáo điều này đến <@s>#bug-reports trên discord: <@s>{{discord_url}} report_message_l=Vui lòng báo cáo điều này đến <@s>#bug-reports trên discord: <@s>{{discord_url}} try_again_message=Vui lòng thử lại. Nếu lỗi vẫn tiếp diễn {{report_message_l}} all=Tất cả error=<@e>Đã xảy ra lỗi (<@s>{error}) {{report_message_l}}\n{traceback} see_log=<@e>Vui lòng xem tệp nhật ký để biết thêm chi tiết. max=Tối đa none=Không có unknown=Không xác định leave=\n<@q>Cảm ơn bạn đã sử dụng Trình chỉnh sửa save file The Battle Cats! checking_for_changes=<@t>Đang kiểm tra thay đổi... no_changes=<@su>Không tìm thấy thay đổi. changes_found=<@su>Đã tìm thấy thay đổi. y/n=y/n 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. 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 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. cancel=Hủy update_external=Cập nhật nội dung bên ngoài updating_external_content=<@q>Đang cập nhật nội dung bên ngoài... downloading_map_names=<@q>Đang lấy tên bản đồ... (mã: <@t>{code}). Có thể mất một lúc... select_device=Chọn thiết bị: continue_q=Tiếp tục? ({{y/n}}): 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. 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. no_feature_with_name=<@e>Không tìm thấy tính năng với tên: <@s>{name} yes=Có ================================================ FILE: src/bcsfe/files/locales/vi/core/save.properties ================================================ # filename="save.properties" save_load_option=Chọn một tùy chọn để tải save file download_save=Tải xuống save file sử dụng Transfer Code và Confirmation Code select_save_file=Chọn save file từ tệp adb_pull_save=Kéo save file từ thiết bị sử dụng adb waydroid_pull_save=Kéo save file từ thiết bị waydroid load_save_data_json=Tải dữ liệu lưu từ json root_storage_pull_save=Kéo save file từ lưu trữ root save_save_dialog=Lưu save file save_downloaded=<@su>Save file đã tải xuống đến <@s>{path} save_json_dialog=Lưu dữ liệu lưu vào json load_from_documents=Tải save file từ thư mục tài liệu save_file_not_found=<@e>Save file không tìm thấy save_file_found=<@su>Đang tải save từ: <@t>{path}<@t> parse_save_error=<@e>Đã xảy ra lỗi khi phân tích save file của bạn: {error}\n{{report_message}} load_json_fail=<@e>Không thể tải dữ liệu lưu từ json ({error}) editor_version_mismatch=<@w>Phiên bản trình chỉnh sửa không khớp. Save file có thể không tương thích với trình chỉnh sửa này. Phiên bản Json: <@t>{json_version}, Phiên bản trình chỉnh sửa: <@t>{editor_version} save_management=Quản lý save file save_save=Lưu save file save_save_file=Lưu save file vào tệp cụ thể save_save_documents=Lưu save file vào thư mục tài liệu save_upload=Tải lên save file đến máy chủ và lấy Transfer Code và Confirmation Code unban_account=Gỡ cấm tài khoản / Sửa lỗi save file được sử dụng ở nơi khác adb_push_rerun=Sử dụng adb để đẩy save file đến thiết bị (Chạy lại trò chơi sau khi đẩy) adb_push=Sử dụng adb để đẩy save file đến thiết bị (Không chạy lại trò chơi sau khi đẩy) adb_push_success=<@su>Save file đã đẩy đến thiết bị adb_push_fail=<@e>Không thể đẩy save file đến thiết bị ({error}) adb_rerun_success=<@su>Đã chạy lại trò chơi thành công adb_rerun_fail=<@e>Không thể chạy lại trò chơi ({error}) waydroid_push_rerun=Đẩy save file đến thiết bị waydroid (Cũng chạy lại trò chơi sau khi đẩy) waydroid_push=Đẩy save file đến thiết bị waydroid (Không chạy lại trò chơi sau khi đẩy) waydroid_push_success=<@su>Save file đã đẩy đến thiết bị waydroid waydroid_push_fail=<@e>Không thể đẩy save file đến thiết bị waydroid ({error}) waydroid_rerun_success=<@su>Đã chạy lại trò chơi trên thiết bị waydroid thành công waydroid_rerun_fail=<@e>Không thể chạy lại trò chơi trên thiết bị waydroid ({error}) export_save=Xuất save file sang json save_success=<@su>Save file đã lưu đến <@s>{path} export_success=<@su>Dữ liệu lưu đã xuất đến <@s>{path} init_save=Đặt lại save file init_save_confirm=Bạn có chắc chắn muốn đặt lại save file không? ({{y/n}}): init_save_success=<@su>Đã đặt lại save file thành công adb_pulling=<@q>Đang kéo save file từ thiết bị với tên gói <@s>{package_name}bằng adb ... adb_pull_fail=<@e>Không thể kéo save file từ thiết bị với tên gói <@s>{package_name} ({error}) bằng adb waydroid_pulling=<@q>Đang kéo save file từ thiết bị với tên gói <@s>{package_name} bằng waydroid ... waydroid_pull_fail=<@e>Không thể kéo save file từ thiết bị với tên gói <@s>{package_name} ({error}) bằng waydroid storage_pulling=<@q>Đang kéo save file từ lưu trữ root với tên gói <@s>{package_name}... storage_pull_fail=<@e>Không thể kéo save file từ lưu trữ root với tên gói <@s>{package_name} ({error}) not_rooted_error=<@e>Thiết bị dường như không được root, hoặc trình chỉnh sửa không chạy với quyền root upload_items=Tải lên các vật phẩm được quản lý đến máy chủ upload_items_success=<@su>Đã tải lên các vật phẩm được quản lý thành công upload_items_fail=<@e>Không thể tải lên các vật phẩm được quản lý load_save=Tải save file load_save_success=<@su>Đã tải save file thành công account=Tài khoản save_before_exit=<@q>Lưu thay đổi mới nhất trước khi thoát? (<@s>y/<@s>n): save_temp_success=<@su>Đã khôi phục save file từ tệp tạm thành công save_temp_fail=<@e>Không thể khôi phục save file từ tệp tạm. Thay đổi lưu mới nhất bị mất ({error})\n{traceback} save_temp_not_found=<@e>Không thể khôi phục save file từ tệp tạm. Thay đổi lưu mới nhất bị mất (Tệp tạm không tìm thấy) cant_detect_cc=<@w>Không thể phát hiện Country Code từ save file. \nVui lòng nhập Country Code thủ công failed_to_load_save_gv=Save file đã tải nhưng một số giá trị không như mong đợi. Lỗi được ném để ngăn hỏng save file failed_to_load_save=Không thể tải save file failed_to_save_save=Không thể lưu save file game_version_dialog=Nhập phiên bản trò chơi (ví dụ <@t>12.2.1): invalid_game_version=<@e>Phiên bản trò chơi không hợp lệ country_code_set=<@su>Đã đặt Country Code thành công thành <@s>{cc} game_version_set=<@su>Đã đặt phiên bản trò chơi thành công thành <@s>{version} convert_region=Chuyển đổi Country Code (ví dụ en -\> jp) convert_version=Chuyển đổi phiên bản trò chơi (ví dụ 12.2.1 -\> 12.2.0) cc_warning=<@w>Cảnh báo: Điều này có thể gây vấn đề với save file của bạn\nCountry Code hiện tại: <@t>{current} gv_warning=<@w>Cảnh báo: Điều này có thể gây vấn đề với save file của bạn\nPhiên bản trò chơi hiện tại: <@t>{current} create_new_save_success=<@su>Đã tạo save file mới thành công create_new_save=Tạo save file mới create_new_save_warning=<@w>Cảnh báo: Nhiều tính năng trình chỉnh sửa sẽ không hoạt động với save file tự tạo, bạn cần tải nó vào trò chơi trước, sau đó tải lại vào trình chỉnh sửa\nĐiều này có thể thay đổi ở các phiên bản trình chỉnh sửa sau. parse_ignored_error=<@w>CẢNH BÁO: <@e>{error}<>\n<@w>Bỏ qua do cờ cấu hình <@s>Bỏ qua lỗi phân tích được đặt. Điều này có thể gây vấn đề! select_package_name=Chọn tên gói: adb_not_installed= ><@e>adb chưa được thêm vào biến môi trường PATH hoặc đường dẫn executable không đúng. Hãy thử chỉnh sửa đường dẫn adb trong cấu hình >Giá trị hiện tại: <@s>{path} >Lỗi: <@s>{error} waydroid_not_installed=<@e>Waydroid chưa được cài đặt, hoặc đã xảy ra lỗi: {error} root_push_success=<@su>Đã ghi save vào root storage thành công root_push_not_android_error=<@e>Root push chỉ khả dụng trên thiết bị Android root_rerun_fail=<@e>Thất bại khi chạy lại game. Lỗi: <@s>{error} root_push=Sử dụng root để ghi save trực tiếp vào game root_rerun_success=<@su>Đã chạy lại game thành công root_push_rerun=Sử dụng root để ghi save trực tiếp vào game (và chạy lại game) root_push_fail=<@e>Thất bại khi ghi save vào root storage. Lỗi: <@s>{error} ================================================ FILE: src/bcsfe/files/locales/vi/core/server.properties ================================================ # filename="server.properties" transfer_code=Transfer Code enter_transfer_code=Nhập Transfer Code: confirmation_code=Confirmation Code enter_confirmation_code=Nhập Confirmation Code: country_code=Country Code country_code_select=Chọn Country Code: 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. 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}}): response_text_display= >URL: <@q>{url} >Tiêu đề Yêu cầu: <@q>{request_headers} >Thân Yêu cầu: <@q>{request_body} > >Tiêu đề Phản hồi: <@q>{response_headers} >Thân Phản hồi: <@q>{response_body} 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})... upload_result= ><@su> >Transfer Code: <@s>{transfer_code} >Confirmation Code: <@s>{confirmation_code} > upload_fail=<@e>Không thể tải lên save file. {{try_again_message}} {{see_log}} unban_fail=<@e>Không thể gỡ cấm tài khoản. {{try_again_message}} {{see_log}} unban_success=<@su>Tài khoản đã gỡ cấm thành công. 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}}): 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ý. create_new_account_success=<@su>Tài khoản đã tạo thành công. create_new_account_fail=<@e>Không thể tạo tài khoản. {{try_again_message}} {{see_log}} uploading_save_file=<@q>Đang tải lên save file đến máy chủ... getting_codes=<@q>Đang lấy Transfer Code và Confirmation Code... getting_auth_token=<@q>Đang lấy mã xác thực tài khoản... refreshing_password=<@q>Đang làm mới mật khẩu tài khoản... getting_password=<@q>Đang lấy mật khẩu tài khoản... getting_save_key=<@q>Đang lấy khóa lưu tài khoản... 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}} 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}} 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. transfer_backup=<@su>Đã lưu save file chuyển giao sao lưu đến <@t>{path} transfer_backup_fail=<@e>Không thể lưu save file chuyển giao sao lưu đến <@t>{path} do {error} retry_auth_token=<@e>Không thể lấy mã xác thực, đang thử lại... downloading_compressed_data=<@su>Đang tải dữ liệu game nén từ <@s>{url} clear_game_data_q=Bạn có muốn xóa tất cả dữ liệu game đã tải xuống? ({{y/n}}): cleared_game_data=<@su>Đã xóa dữ liệu game thành công ================================================ FILE: src/bcsfe/files/locales/vi/core/theme.properties ================================================ # filename="theme.properties" theme_text= >Chủ đề hiện tại: <@s>{theme_name} (Phiên bản <@s>{theme_version}) >Được tạo bởi <@s>{theme_author} >Vị trí tệp chủ đề: <@s>{theme_path} default_theme_text= >Chủ đề hiện tại: <@s>Mặc định >Vị trí tệp chủ đề: <@s>{theme_path} checking_for_theme_updates=Đang kiểm tra cập nhật cho chủ đề bên ngoài <@t>{theme_name}... 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}} 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> theme_changed=<@su>Đã thay đổi chủ đề thành công thành <@t>{theme_name}.\n{{restart_to_see_changes}} theme_removed=<@su>Đã xóa chủ đề thành công <@t>{theme_name}.\n{{restart_to_see_changes}} no_external_themes=<@e>Không tìm thấy chủ đề bên ngoài theme_dialog=Chọn một chủ đề: add_theme=Thêm chủ đề remove_theme=Xóa chủ đề theme_remove_dialog=Chọn các chủ đề để xóa: enter_theme_git_repo=Nhập kho git của chủ đề (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git): 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}}): theme_added=<@su>Đã thêm chủ đề thành công ================================================ FILE: src/bcsfe/files/locales/vi/core/updater.properties ================================================ # filename="updater.properties" local_version=<@q>Phiên bản cục bộ: <@s>{local_version} latest_version=<@q>Phiên bản mới nhất: <@s>{latest_version} 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. update_available= ><@q>Có cập nhật khả dụng: <@s>{latest_version} >Bạn có muốn cập nhật không? <@t>({{y/n}}): update_success= ><@t>Cập nhật thành công >Vui lòng khởi động lại ứng dụng update_fail= ><@e>Cập nhật thất bại >Vui lòng cập nhật thủ công >Lệnh: <@s>pip install --upgrade bcsfe version_line={{local_version}} | {{latest_version}} disable_update_message=Bạn có muốn tắt thông báo cập nhật không? <@t>({{y/n}}): ================================================ FILE: src/bcsfe/files/locales/vi/edits/bannable_items.properties ================================================ # filename="bannable_items.properties" do_you_want_to_continue=Bạn có muốn tiếp tục không? ({{y/n}}): 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}} 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}} rare_ticket_warning= ><@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. >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. platinum_ticket_warning= ><@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. >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. select_an_option_to_continue=Chọn tùy chọn để tiếp tục chỉnh sửa {feature_name}: continue_editing=Tiếp tục chỉnh sửa {feature_name} go_to_safe_feature=Chuyển đến tính năng an toàn hơn {safer_feature_name} cancel_editing=Hủy chỉnh sửa {feature_name} rare_ticket_trade_enter=Nhập số Rare Tickets bạn muốn <@q>add (max value: <@q>{max}) (current amount: <@q>{current}): 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ó! rare_ticket_successfully_traded= ><@su>Đã trao {rare_ticket_count} Rare Tickets thành công. >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. rare_tickets_l=Rare Tickets rare_ticket_trade_l=Rare Ticket Trade 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! platinum_tickets_l=Platinum Tickets platinum_shards_l=Platinum Shards ================================================ FILE: src/bcsfe/files/locales/vi/edits/cats.properties ================================================ # filename="cats.properties" total_selected_cats=<@t>{total} cats hiện đang được chọn selected_cat=<@t>{name} (<@t>{id}) đã được chọn select_cats_rarity=Chọn cats dựa trên rarity select_cats_name=Chọn cats dựa trên name select_cats_obtainable=Chọn tất cả các cats có thể nhận được select_cats_not_obtainable=Chọn tất cả các cats không thể nhận được select_cats_gatya_banner=Chọn cats dựa trên gacha banner select_cats_all=Chọn tất cả cats select_cats=Chọn cats: and_mode_q=Bạn muốn lọc xuống lựa chọn hiện tại (<@t>1), thêm vào nó (<@t>2) hay thay thế nó (<@t>3)?: select_rarity=Chọn cat rarity: enter_name=Nhập cat name: select_name=Chọn cat name: select_gatya_banner=Nhập gacha banner ids {{range_input}} cats=Cats edit_cats=Chỉnh sửa cats enter_cat_ids=Bạn có thể tìm cat IDs tại đây: <@t>https://battlecats.miraheze.org/wiki/Cat_Release_Order\nNhập cat ids {{range_input}} select_cats_id=Chọn cats theo id no_cats_found_name=<@w>Không tìm thấy cats nào với name <@s>{name} select_cats_again=Chọn thêm cats unlock_cats=Unlock Cats|Get Cats remove_cats=Remove Cats upgrade_cats=Upgrade Cats true_form_cats=True Form Cats remove_true_form_cats=Remove Cat True Forms upgrade_talents_cats=Upgrade Cat Talents remove_talents_cats=Remove Cat Talents unlock_cat_guide=Claim Cat Guide remove_cat_guide=Unclaim Cat Guide finish_edit_cats=Kết thúc chỉnh sửa cats select_edit_cats_option=Chọn tùy chọn để chỉnh sửa cats: upgrade_success=<@su>Đã upgrade cats thành công upgrade_cats_select_mod=Chọn tùy chọn để upgrade cats: upgrade_individual=Nhập upgrade cho từng cat đã chọn selected_cat_upgrades={{selected_cat}}: <@t>{base_level}<@s>+{plus_level} selected_cat_upgraded=<@t>{name} (<@t>{id}) đã được upgrade lên <@t>{base_level}<@s>+{plus_level} upgrade_all=Nhập upgrade để áp dụng cho tất cả cats đã chọn upgrade_input= >Nhập upgrade level. Ví dụ: ><@t>10<@s>+20 = Base level 10, plus level 20 ><@t>10<@s>+ = Base level 10, giữ current plus level ><@t><@s>+20 = Giữ current base level, plus level 20 ><@t>10 = Base level 10, plus level 0 ><@t>5<@q>-10<@s>+20<@q>-30 = Random base level giữa 5 và 10, random plus level giữa 20 và 30 ><@t>5<@q>-10<@s>+ = Random base level giữa 5 và 10, giữ current plus level ><@t><@s>+20<@q>-30 = Giữ current base level, random plus level giữa 20 và 30 ><@t>5<@q>-10 = Random base level giữa 5 và 10, plus level 0 >Nhập: talents_success=<@su>Đã upgrade talents thành công talents_individual=Nhập talents cho từng cat đã chọn talents_all=Nhập talents để áp dụng cho tất cả cats đã chọn unlock_success=<@su>Đã unlock cats thành công remove_success=<@su>Đã remove cats thành công true_form_success=<@su>Đã true form cats thành công remove_true_form_success=<@su>Đã remove true forms thành công unlock_cat_guide_success=<@su>Đã claim cat guide entries thành công remove_cat_guide_success=<@su>Đã unclaim cat guide entries thành công force_true_form_cats=Dùng trước True Form Cats force_true_form_cats_warning= ><@w>Cảnh báo: Chỉ sử dụng nếu bạn biết cat có true form, nếu không sẽ dẫn đến true form bị glitch. >Sử dụng chính của tùy chọn này là khi game data của trình chỉnh sửa lỗi thời và true forms mới chưa được thêm. filter_current_q=Bạn muốn chỉ chọn từ cats mà bạn hiện đang unlock (<@t>1) hay tất cả cats (<@t>2)?: select_cats_currently_option=Chọn từ cats mà bạn hiện đang unlock (ví dụ khi chọn cats cho rarity cụ thể, chỉ chọn cats của rarity đó mà bạn hiện đang unlock) select_cats_all_option=Chọn từ tất cả cats unlock_remove_cats=Unlock Cats / Remove Cats true_form_remove_form_cats=True Form Cats / Remove Cat True Forms upgrade_talents_remove_talents_cats=Upgrade Talents / Remove Talents Cats unlock_remove_cat_guide=Claim / Unclaim Cat Guide Entries unlock_remove_q=Bạn muốn <@t>Unlock hay <@t>Remove cats?: true_form_remove_form_q=Bạn muốn <@t>True Form cats hay <@t>Remove Cat True Forms?: upgrade_talents_remove_talents_q=Bạn muốn <@t>Upgrade hay <@t>Remove cat talents?: unlock_cat_guide_remove_guide_q=Bạn muốn <@t>Claim hay <@t>Unclaim cat guide entries?: fourth_form_remove_form_cats=Ultra Form Cats / Remove Cat Ultra Forms (4th Forms) force_fourth_form_cats=Dùng trước Ultra Form Cats (4th Forms) fourth_form_success=<@su>Đã ultra form cats thành công remove_fourth_form_success=<@su>Đã remove ultra forms thành công fourth_form_cats=Ultra Form Cats remove_fourth_form_cats=Remove Cat Ultra Forms fourth_form_remove_form_q=Bạn muốn <@t>Ultra Form cats hay <@t>Remove Cat Ultra Forms?: force_fourth_form_cats_warning= ><@w>Cảnh báo: Chỉ sử dụng nếu bạn biết cat có ultra form, nếu không sẽ dẫn đến 4th form bị glitch. >Sử dụng chính của tùy chọn này là khi game data của trình chỉnh sửa lỗi thời và forms mới chưa được thêm. gatya_info_progress=Đang tải gacha info (<@t>{current}/<@t>{total}) unknown_banner=Unknown banner banner_txt={name} (<@s>{int}) filter_down_q_gatya=Bạn muốn remove duplicate và unknown banners khỏi list? ({{y/n}}): select_cats_non_gatya=Select Non-Gacha Cats finished_cats_selection=Bạn đã hoàn thành việc chọn cats chưa? ({{y/n}}): invalid_upgrade_plus=<@e>Cộng level không hợp lệ: <@s>{plus} talents=Talents no_talent_data=<@w>Không có dữ liệu talent cho cat này select_cats_not_unlocked=Chọn các cat chưa được mở khóa talents_remove_success=<@su>Đã xóa talent của cat thành công upgrade_talents_select_mod=Chọn tùy chọn để chỉnh sửa talent cat: select_cats_current=Chọn các cat đã mở khóa hiện tại invalid_upgrade_base_random=<@e>Phạm vi level cơ bản không hợp lệ: <@s>{min}-<@s>{max} max_upgrade=Cấp Nâng Tối Đa: <@t>{max_base}<@s>+{max_plus} upgrade_talent_cats=Nâng cấp talent cat invalid_upgrade_plus_random=<@e>Phạm vi cấp cộng không hợp lệ: <@s>{min}-<@s>{max} invalid_upgrade_base=<@e>Cấp cơ bản không hợp lệ: <@s>{base} talents_version_warning= ><@w>Cảnh báo: Dữ liệu game của trình chỉnh sửa không khớp với phiên bản game của file lưu này. Talents có thể không hoạt động như mong đợi. >Phiên bản Lưu: <@s>{save_version} >Phiên bản Dữ liệu Game: <@s>{data_version} >Nếu dữ liệu game đã lỗi thời, nó sẽ được cập nhật trong vài ngày tới. downloading_cat_names=<@su>Đang tải tên cat từ <@s>{url} item=<@t>{name} (<@t>{id}) need_x_more_space=<@e>Không đủ dung lượng storage. Cần thêm <@s>{needs} slots clear_storage=Clear storage select_cats_game_version=Chọn cats theo phiên bản game unrecognised_storage_item=<@e>Không nhận diện được storage item. Danh mục item: <@s>{item_type}. Item id: <@s>{id} special_skill=<@t>{name} (<@t>{id}) select_gv= >Nhập phiên bản game để lọc theo. Ví dụ: >- Lấy cats chỉ trong phiên bản <@t>11.5.0: <@t>11.5.0, >- Lấy cats chỉ trong các phiên bản <@t>12.4.0 và <@t>13.0.0: <@t>12.4.0 13.0.0 >- Lấy tất cả cats từ phiên bản <@t>12.4.0 đến <@t>13.0.0 (bao gồm): <@t>12.4.0-13.0.0 >Lưu ý rằng bất kỳ cats nào không xuất hiện trong menu upgrade đều không thể chọn ở đây vì game version của chúng được đặt là <@t>-1. >Nhập: display_storage=Hiển thị storage removed_items=Các items đã xóa: remove_items=Xóa cats / skills added_special_skills=Các special skills đã thêm: add_special_skills=Thêm special skills / base upgrades current_storage_items=Các storage items hiện tại: added_cats=Các cats đã thêm: no_valid_gvs_entered=<@w>Không nhập phiên bản game hợp lệ nào add_cats=Thêm cats too_many_skills_selected=<@e>Quá nhiều skills được chọn. Tối đa là <@s>{max}. Có <@s>{current} select_special_skills=Chọn special skills storage_is_empty=Storage trống available_storage=Dung lượng storage khả dụng: <@t>{slots} storage_success=<@su>Đã chỉnh sửa Cat Storage thành công too_many_cats_selected=<@e>Quá nhiều cats được chọn. Tối đa là <@s>{max}. Có <@s>{current} possible_gvs=Các phiên bản game có thể: cat_storage=Cat Storage cat=<@t>{name} (<@t>{id}) ================================================ FILE: src/bcsfe/files/locales/vi/edits/enemy.properties ================================================ # filename="enemy.properties" total_selected_enemies=<@t>{total} enemies hiện đang được chọn unlock_enemy_guide_success=<@su>Đã unlock enemy guide entries thành công remove_enemy_guide_success=<@su>Đã remove enemy guide entries thành công selected_enemy=<@t>{name} (<@t>{id}) đã được chọn select_enemies_valid=Chọn tất cả enemies trong enemy guide select_enemies_invalid=Chọn tất cả enemies không trong enemy guide select_enemies_all=Chọn tất cả enemies select_enemies_id=Chọn enemies theo ID select_enemies_name=Chọn enemies theo name select_enemies=Chọn enemies: 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}}: enter_enemy_name=Nhập enemy name: enemy_not_found_name=<@w>Không tìm thấy enemies nào với name <@s>{name} unlock_enemy_guide=Mở khóa enemy guide entries remove_enemy_guide=Xóa enemy guide entries enemy_guide=Enemy Guide edit_enemy_guide=Nhập tùy chọn để chỉnh sửa enemy guide entries: ================================================ FILE: src/bcsfe/files/locales/vi/edits/fixes.properties ================================================ # filename="fixes.properties" fix_gamatoto_crash=Fix gamatoto gây crashing game 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) fix_ototo_crash=Fix ototo gây crashing game fix_gamatoto_crash_success=<@su>Đã fix gamatoto không gây crash game thành công 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) fix_ototo_crash_success=<@su>Đã fix ototo không gây crash game thành công fixes=Fixes unlock_equip_menu=Mở khóa Equip Menu equip_menu_unlocked=<@su>Đã mở khóa equip menu thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/gambling.properties ================================================ reset_wildcat_slots=<@su>Đã reset Wildcat Slots thành công reset_gambling_events=Reset Wildcat Slots và Cat Scratcher Lottery reset_cat_scratcher=<@su>Đã reset Cat Scratcher Lottery thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/gamototo.properties ================================================ # filename="gamototo.properties" enter_raw_gamatoto_xp=Nhập Raw Gamatoto XP enter_gamatoto_level=Nhập Gamatoto Level edit_gamatoto_level_q=Nhập tùy chọn để chỉnh sửa gamatoto level: gamatoto_xp=Gamatoto XP gamatoto_level=Gamatoto Level gamatoto_level_success=<@su>Đã đặt gamatoto level thành công thành <@s>{level} (XP: <@s>{xp}) gamatoto_level_current=<@t>Gamatoto level hiện tại là <@q>{level} (XP: <@q>{xp}) gamatoto_xp_level=Gamatoto XP / Level current_gamatoto_helpers=Helpers hiện tại: gamatoto_helper=Helper: <@t>{name} (rarity: <@t>{rarity_name}) new_gamatoto_helpers=New Helpers: gamatoto_helpers=Gamatoto Helpers ototo_cat_cannon=Ototo Cat Cannon current_cannon_stats=Cannon Stats hiện tại: cannon_part=<@t><@q>{name}{buffer}(level <@s>{level}) development={buffer}(Development: <@q>{development}) cannon_stats={parts} foundation=Foundation style=Style effect=Effect improved_foundation=Improved Foundation improved_style=Improved Style unknown_stage=Unknown Stage (<@s>{stage}) selected_cannon=<@t>Selected cannon: <@q>{name} selected_cannon_stage=<@t>Cannon: <@q>{name} Current Stage: <@q>{stage} 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?: 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?: development_o=Development level_o=Levels select_development=Chọn development stage: select_cannon=Chọn Cannon cannon_level=Cannon Level cannon_success=<@su>Đã chỉnh sửa ototo cannons thành công cat_shrine=Cat Shrine shrine_level=Shrine Level shrine_xp=Shrine XP current_shrine_xp_level=<@t>Current XP: <@q>{xp} (Level: <@q>{level}) cat_shrine_choice_dialog=Bạn muốn chỉnh sửa cat shrine <@t>level hay cat shrine <@t>XP?: shrine_level_dialog=Nhập cat shrine level (max: <@q>{max_level}): shrine_xp_dialog=Nhập cat shrine XP (max: <@q>{max_xp}): cat_shrine_edited=<@su>Đã chỉnh sửa cat shrine thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/gatya.properties ================================================ # filename="gatya.properties" event_tickets=Event Tickets / Lucky Tickets downloading_gatya_data=Đang tải dữ liệu sự kiện gacha... download_gatya_data_success=<@su>Đã tải dữ liệu sự kiện gacha thành công download_gatya_data_fail=<@e>Không thể tải dữ liệu sự kiện gacha. Có lẽ thử lại save_gatya_error=<@e>Không thể lưu dữ liệu gatya do {error} gatya_by_id_q=Bạn có muốn chọn gacha banners theo <@t>ID hay <@t>tên?: by_name=Theo tên by_id=Theo ID ================================================ FILE: src/bcsfe/files/locales/vi/edits/gold_pass.properties ================================================ # filename="gold_pass.properties" 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): gold_pass=Gold Pass / Officer Club gold_pass_remove_success=<@su>Đã remove gold pass thành công gold_pass_get_success=<@su>Đã nhận gold pass thành công (id: <@t>{id}) officer_pass_fixed=<@su>Đã fix officer club khỏi crash thành công fix_officer_pass_crash=Fix Officer Club Crashing ================================================ FILE: src/bcsfe/files/locales/vi/edits/items.properties ================================================ # filename="items.properties" # Lưu ý rằng không phải tất cả các vật phẩm đều có ở đây catamins=Catamins catfruit=Catfruit base_materials=Base Materials inquiry_code=Inquiry Code rare_gatya_seed=Rare Gacha Seed normal_gatya_seed=Normal Gacha Seed event_gatya_seed=Event Gacha Seed unlocked_slots=Unlocked Slots|Equip Slots|Lineups password_refresh_token=Password Refresh Token challenge_score=Điểm Challenge dojo_score=Điểm Dojo items=Items user_rank_rewards=Claim phần thưởng User Rank (Không gửi phần thưởng) catfood=CatFood xp=XP normal_tickets=Normal Tickets|Basic Tickets|Silver Tickets rare_tickets=Rare Tickets|Gold Tickets platinum_tickets=Platinum Tickets legend_tickets=Legend Tickets 100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets 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 platinum_shards=Platinum Shards np=NP leadership=Leadership catseyes=Catseyes battle_items=Battle Items talent_orbs=Talent Orbs scheme_items=Scheme Items labyrinth_medals=Labyrinth Medals restart_pack=Restart Pack|Returner Mode engineers=Engineers gamototo=Gamatoto / Ototo special_skills=Special Skills / Base Abilities treasure_chests=Treasure Chests unknown_treasure_chest_name=Unknown Treasure Chest ({id}) rare_ticket_trade=Rare Ticket Trade rare_ticket_trade_feature_name=Rare Ticket Trade (Cho phép lấy vé không bị ban) other=Other gatya=Gacha levels=Levels / Story / Treasure cats_special_skills=Cats / Special Skills gatya_item_unknown_name=Unknown Item unknown_catamin_name=Unknown Catamin <@t>{id} unknown_catseye_name=Unknown Catseye <@t>{id} unknown_catfruit_name=Unknown Catfruit <@t>{id} unknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id} reset_golden_cat_cpus_success=<@su>Đã reset số lần sử dụng Golden Cat CPU thành công reset_golden_cat_cpus=Reset số lần sử dụng Golden Cat CPU ================================================ FILE: src/bcsfe/files/locales/vi/edits/map.properties ================================================ # filename="map.properties" tutorial_already_cleared=<@w>Bạn đã clear tutorial tutorial_cleared=<@su>Đã clear tutorial thành công clear_tutorial=Clear Tutorial clear_stages=Clear Stages unclear_stages=Unclear Stages clear_unclear_q=Bạn muốn <@t>clear hay <@t>unclear stages?: current_enigma_stages=Current Enigma Stages: enigma_stage=Enigma Stage <@q>{name} (id: <@q>{id}) unknown_enigma_name=Unknown Enigma Name (id: <@q>{id}) enigma_select=Chọn Enigma Stages để Add enigma_success=<@su>Đã add Enigma Stages thành công wipe_enigma=Bạn muốn wipe current enigma stages của bạn? ({{y/n}}): aku_realm_unlocked=<@su>Đã unlock Aku Realm thành công unlock_aku_realm=Unlock Aku Realm select_story_chapters=Chọn Story Chapters chapter_progress_txt=(ví dụ <@q>0 = không clear stage nào, <@q>1 = clear stage đầu tiên, <@q>2 = clear stage đầu tiên và thứ hai, ... <@q>{max} = clear tất cả stages) edit_chapter_progress_all=Nhập progress để đặt mỗi chapter thành {{chapter_progress_txt}}: edit_chapter_progress=Nhập progress để đặt <@t>{chapter_name} thành {{chapter_progress_txt}}: edit_stage_clear_count=Nhập số lần clear stage: story_cleared=<@su>Đã clear story thành công individual_chapters=Individual Chapters all_chapters=All Chapters individual_chapters_dialog=Bạn muốn chỉnh sửa clear progress của từng chapter <@t>individually? hay đặt <@t>all chapters thành cùng progress?: individual_clear_counts=Individual Clear Counts all_clear_counts=All Clear Counts individual_clear_counts_dialog=Bạn muốn chỉnh sửa clear count của từng stage <@t>individually? hay đặt <@t>all stages thành cùng clear count?: clear_story=Main Story Chapters|Clear Story clear=Clear unclear=Unclear outbreaks=Outbreaks / Zombie Stages clear_unclear_outbreaks=Bạn muốn <@t>clear hay <@t>unclear outbreaks?: clear_outbreaks_success=<@su>Đã clear outbreaks thành công unclear_outbreaks_success=<@su>Đã unclear outbreaks thành công no_valid_outbreaks=<@e>Lỗi: không tìm thấy outbreaks hợp lệ aku_chapters=Aku Realm Chapters aku_clear_success=<@su>Đã clear Aku Realm thành công aku_current_stage=Aku Realm Stage <@q>{name} (id: <@q>{id}) select_clear_type=Nhập tùy chọn để clear maps: clear_amount_all=Đặt cùng clear amount cho tất cả các chapters đã chọn clear_amount_stages=Đặt một clear amount khác nhau cho từng stage đã chọn select_clear_amount_type=Nhập clear amount setting mode bạn muốn sử dụng: clear_amount_enter=Nhập clear amount: custom_star_count_per_chapter=Nhập star/crown count (max <@q>{max}): custom_star_count_per_chapter_unclear= >Nhập star/crown để remove: ><@s><@t>1 = unclear từ toàn bộ map ><@s><@t>2 = unclear từ 2nd, 3rd và 4th crown/star map ><@s><@t>3 = unclear từ 3rd và 4th crown/star map ><@s><@t>4 = unclear từ 4th crown/star map >(max <@q>{max}): current_sol_chapter=Chapter <@q>{name} (id: <@q>{id}) current_sol_star=Star/Crown: <@q>{star} current_sol_stage=Stage <@q>{name} (id: <@q>{id}) map_chapters_edited=<@su>Đã chỉnh sửa chapters thành công sol=Stories of Legend event=Normal Event Stages collab=Collaboration Event Stages select_map=Chọn Map select_map_dialog= >Chọn các maps bạn muốn chỉnh sửa >Bạn có thể nhập phạm vi số (ví dụ <@q>1-5), số riêng lẻ (ví dụ <@q>1 3 5), hoặc kết hợp cả hai (ví dụ <@q>1-3 5) >Bạn cũng có thể nhập name / phần của name của map (ví dụ <@t>{example}) để tìm kiếm / chọn nó >Nhập: no_map_found=<@e>Không tìm thấy map với name <@s>{name} finished_selecting_maps=Bạn đã hoàn thành việc chọn maps chưa? ({{y/n}}): current_maps=Maps hiện tại: select_stage=Chọn Stage gauntlets=Gauntlets collab_gauntlets=Collaboration Gauntlets uncanny=Uncanny Legends behemoth_culling=Behemoth Culling legend_quest=Legend Quest towers=Towers zero_legends=Zero Legends unclear_other_stages=Bạn muốn ghi đè progress hiện tại của bạn trong chapter? ({{y/n}}) <@t>n = chỉ thay đổi clear times cho các stages đã chọn, <@t>y = unclear các stages sau trong chapter mà trước đó đã clear: select_stage_progress=Nhập stage để clear lên đến và bao gồm: zero_legends_warning=<@w>Cảnh báo: Nếu phiên bản game bạn đang sử dụng không có zero legends map, game sẽ crash nếu bạn cố gắng chỉnh sửa nó thành clear! stages_select=Nhập numbers {{range_input}} itf_timed_scores_dialog=Bạn có muốn chỉnh sửa điểm tính giờ cho <@t>toàn bộ chapter cùng lúc hay <@t>từng màn riêng lẻ? filibuster_reclearing=Bật lại Màn Filibuster unknown_map_name=Tên map không xác định (id: <@q>{id}) current_stage={chapter_name} <@t>{stage_name} modify_clear_amounts=Đặt số lần clear là <@t>1 cho mỗi màn đã chọn. Bạn có muốn thay đổi không? ({{y/n}}): select_unclear_type=Bạn muốn <@t>xóa trạng thái hoàn thành của toàn bộ chapter hay <@t>xóa trạng thái hoàn thành của các màn cụ thể?: clear_amount_chapter=Đặt số lần hoàn thành khác nhau cho mỗi chapter đã chọn itf_timed_scores_individual_dialog=Bạn có muốn chỉnh sửa điểm tính giờ của từng màn đã chọn <@t>riêng lẻ? hay đặt <@t>tất cả màn đã chọn cùng một điểm tính giờ?: unclear_specific_stages=Xóa trạng thái hoàn thành của Màn Cụ Thể catamin_stages=Màn Catamin unclear_whole_chapters=Xóa trạng thái hoàn thành của Toàn bộ Chapter filibuster_stage_reclearing_allowed=<@su>Đã bật lại màn Filibuster thành công. map_name={name} <@s>(id: <@q>{id}) edit_map_chapters=Chọn Chapter custom_star_count_per_chapter_yn=Bạn có muốn đặt số star/crown tùy chỉnh cho mỗi chapter không? ({{y/n}}): itf_timed_score_dialog=Nhập điểm tính giờ: clear_whole_chapters=Hoàn thành Toàn bộ Chapter all_selected_stages=Tất cả các Màn đã chọn clear_specific_stages=Hoàn thành Màn Cụ Thể itf_timed_scores=Điểm tính giờ Into the Future itf_timed_scores_edited=<@su>Đã chỉnh sửa điểm tính giờ Into The Future thành công enter_clear_amount_catamin=Nhập số lần hoàn thành để đặt cho các chapter đã chọn (<@t>0 = chưa hoàn thành chapter, <@t>3 hoặc nhiều hơn nghĩa là chapter biến mất): select_map_from_names=Chọn map change_clear_amount_catamin=Thay đổi số lượng hoàn thành chapter catamin_stage_clear_q=Bạn có muốn <@t>Thay đổi số lần bạn đã hoàn thành một chapter catamin, hay chỉ <@t>hoàn thành hoặc xóa hoàn thành các stages?: catamin_stage_success=<@su>Đã chỉnh sửa catamin stages thành công clear_unclear_stage_catamin=Hoàn thành / Xóa hoàn thành Catamin Stages enter_clear_amount_catamin_map=Nhập số lần hoàn thành để đặt cho chapter <@t>{name} (ID: <@t>{id}) (<@t>0 = chưa hoàn thành chapter này, <@t>3 hoặc nhiều hơn nghĩa là chapter biến mất): catamin_clear_amounts_q=Bạn có muốn chỉnh sửa số lần hoàn thành cho từng chapter <@t>riêng lẻ hay <@t>tất cả cùng lúc?: clear_enigma_stages=Clear Enigma Stages dojo_catclaw_championships=Clear Dojo Catclaw Championships add_enigma_stages=Thêm Enigma Stages ================================================ FILE: src/bcsfe/files/locales/vi/edits/medals.properties ================================================ # filename="medals.properties" medals=Meow Medals add_medals=Thêm Medals remove_medals=Xóa Medals medal_add_remove_dialog=Bạn muốn <@t>add medals hay <@t>remove medals?: medal_string={medal_name}: <@q>{medal_req} select_medals=Chọn medals: medals_added=<@su>Đã thêm meow medals thành công medals_removed=<@su>Đã xóa meow medals thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/missions.properties ================================================ # filename="missions.properties" missions=Catnip Challenges / Missions|Cat Missions complete_reward=Hoàn thành Missions và Không Nhận Phần Thưởng complete_claim=Hoàn thành Missions và Nhận Phần Thưởng uncomplete=Không Hoàn thành Mission 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ể? select_missions=Chọn missions để chỉnh sửa: missions_edited=<@su>Đã chỉnh sửa missions thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/playtime.properties ================================================ # filename="playtime.properties" playtime_str=<@t>{hours} giờ, <@t>{minutes} phút, <@t>{seconds} giây (<@t>{frames} frame) playtime_current=Thời gian chơi hiện tại: {{playtime_str}} playtime_edited=Đã chỉnh sửa thời gian chơi thành công thành {{playtime_str}} playtime_hours_prompt=Nhập số <@t>giờ để đặt thời gian chơi thành: playtime_minutes_prompt=Nhập số <@t>phút để đặt thời gian chơi thành: playtime_seconds_prompt=Nhập số <@t>giây để đặt thời gian chơi thành: playtime=Thời gian chơi ================================================ FILE: src/bcsfe/files/locales/vi/edits/scheme_items.properties ================================================ # filename="scheme_items.properties" scheme_items_edit_success=<@su>Đã chỉnh sửa scheme items thành công scheme_items_select_gain=Chọn scheme items để nhận scheme_items_select_remove=Chọn scheme items để xóa gain_remove_scheme_items=Bạn muốn <@t>nhận hay <@t>xóa scheme items?: gain_scheme_items=Nhận scheme items remove_scheme_items=Xóa scheme items ================================================ FILE: src/bcsfe/files/locales/vi/edits/special_skills.properties ================================================ # filename="special_skills.properties" special_skills_dialog=Chọn một base ability để nâng cấp upgrade_individual_skill=Nhập nâng cấp cho từng skill đã chọn upgrade_all_skills=Nhập nâng cấp để áp dụng cho tất cả skill đã chọn upgrade_skills_select_mod=Chọn tùy chọn để nâng cấp skills: selected_skill=<@t>{name} đã được chọn selected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+{plus_level} selected_skill_upgraded=<@t>{name} đã được nâng cấp lên <@t>{base_level}<@s>+{plus_level} skills_edited=<@su>Đã chỉnh sửa special skills thành công ================================================ FILE: src/bcsfe/files/locales/vi/edits/talent_orbs.properties ================================================ # filename="talent_orbs.properties" total_current_orbs=Tổng Orbs Hiện Tại: <@q>{total_orbs} total_current_orb_types=Tổng Loại Orbs Hiện Tại: <@q>{total_types} current_orbs=Orbs Hiện Tại: orb_select=Chọn talent orbs để chỉnh sửa: selected_orbs=Talent Orbs Đã Chọn: 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)?: edit_orbs_all=Nhập giá trị để chỉnh sửa tất cả orbs đã chọn thành (tối đa <@t>{max}): failed_to_load_orbs=Không thể tải talent orbs edit_orbs_help= >Trợ giúp: >Các grade khả dụng: {all_grades_str} >Các attribute khả dụng: {all_attributes_str} >Các effect khả dụng: {all_effects_str} ><@w>Lưu ý: Không phải tất cả grade và effect đều khả dụng cho mọi attribute. >Ví dụ đầu vào: > aku - chọn tất cả aku orbs > red s - chọn tất cả red orbs với s grade > alien d 0 - chọn alien orb với d grade tăng attack. > c 1 - chọn boost stories of legend orb với grade c >Nếu bạn muốn chọn <@q>tất cả orbs thì nhập: > <@q>* >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: > s black 4,d 3,floating ================================================ FILE: src/bcsfe/files/locales/vi/edits/treasures.properties ================================================ # filename="treasures.properties" whole_chapters=Toàn bộ chapters individual_stages=Từng stages riêng lẻ treasure_groups=Nhóm treasures / Sets 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?: treasures_edited=<@su>Đã chỉnh sửa treasures thành công per_chapter=Theo chapter all_selected_chapters=Tất cả chapters đã chọn no_treasure=Không có treasure 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ì!) treasure_level_dialog=Nhập level treasure bạn muốn đặt: custom_treasure_level_dialog=Nhập level treasure tùy chỉnh bạn muốn đặt: select_stage_by_id=Chọn stages theo IDs select_stage_by_name=Chọn stages theo tên select_stage_dialog=Bạn muốn chọn stages theo <@t>IDs hay <@t>tên?: select_stage_id=Nhập IDs stage bạn muốn chọn {{range_input}} select_stages_name=Chọn stages: select_treasure_groups=Chọn treasure groups bạn muốn chỉnh sửa: story_treasures=Story Treasures current_chapter=Chapter hiện tại: <@t>{chapter_name} current_treasure_group=Nhóm treasure hiện tại: <@t>{treasure_group_name} group_individual=Nhóm riêng lẻ group_all_at_once=Tất cả nhóm đã chọn 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?: 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ẻ?: ================================================ FILE: src/bcsfe/files/locales/vi/edits/user_rank.properties ================================================ # filename="user_rank.properties" claim=Nhận unclaim=Không Nhận fix_claimed=Sửa Đã Nhận 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?: select_ur=Chọn user rank rewards ur_claimed_success=<@su>Đã nhận user rank rewards thành công ur_unclaimed_success=<@su>Đã không nhận user rank rewards thành công ur_string=Rank: <@s>{rank}: {description} ur_fix_claimed_success=<@su>Đã sửa claimed user rank rewards thành công ================================================ FILE: src/bcsfe/files/locales/vi/metadata.json ================================================ { "authors": ["HungJoesifer"], "name": "Tiếng Việt (Vietnamese)" } ================================================ FILE: src/bcsfe/files/max_values.json ================================================ { "catfood": 45000, "xp": 99999999, "normal_tickets": 2999, "100_million_tickets": 9999, "rare_tickets": 299, "platinum_tickets": 9, "legend_tickets": 4, "np": 9999, "leadership": 9999, "battle_items": 9999, "catamins": 9999, "catseyes": 9999, "catfruit": { "old": 128, "new": 998 }, "base_materials": 9999, "labyrinth_medals": 9999, "talent_orbs": 998, "treasure_level": 9999, "stage_clear_count": 9999, "itf_timed_score": 9999, "event_tickets": 9999, "treasure_chests": 9999 } ================================================ FILE: src/bcsfe/files/themes/default.json ================================================ { "short_name": "default", "name": "Default", "description": "Default theme of the editor", "author": "fieryhenry", "version": "1.0.0", "colors": { "primary": "#FFFFFF", "secondary": "#FFFFFF", "quaternary": "#008000", "tertiary": "#00FFFF", "error": "#FF0000", "warning": "#FF0000", "success": "#00FF00" } } ================================================ FILE: src/bcsfe/files/themes/discord.json ================================================ { "short_name": "Discord Theme", "name": "Discord Theme", "description": "Discord-inspired dark mode theme", "author": "HungJoesifer", "version": "1.0.0", "colors": { "primary": "#F0F8FF", "secondary": "#A6B3F8", "tertiary": "#5865F2", "quaternary": "#FFFFFF", "error": "#ED4245", "warning": "#F1C40F", "success": "#57F287" } } ================================================ FILE: src/bcsfe/py.typed ================================================ ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_parse.py ================================================ from bcsfe import core def run(): saves_path = core.Path(__file__).parent().add("saves") for file in saves_path.get_files(): print(f"Testing {file.basename()}") data1 = file.read() save_1 = core.SaveFile(data1) data_2 = save_1.to_data() assert data1 == data_2 json_data_1 = save_1.to_dict() save_3 = core.SaveFile.from_dict(json_data_1) json_data_2 = save_3.to_dict() assert json_data_1 == json_data_2 data_3 = save_3.to_data() assert data1 == data_3 print(f"Tested {file.basename()} {save_1.game_version}")